Commit f3131210 authored by GitLab Bot's avatar GitLab Bot

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2018-10-04

# Conflicts:
#	app/assets/javascripts/jobs/components/job_app.vue
#	app/assets/javascripts/jobs/job_details_bundle.js
#	app/assets/javascripts/jobs/store/getters.js
#	app/views/projects/_import_project_pane.html.haml
#	app/views/projects/_new_project_fields.html.haml
#	app/views/projects/jobs/show.html.haml
#	app/views/projects/project_templates/_built_in_templates.html.haml
#	app/views/shared/_visibility_radios.html.haml
#	config/routes/project.rb
#	config/routes/user.rb
#	lib/api/commits.rb
#	locale/gitlab.pot
#	spec/javascripts/jobs/components/job_app_spec.js

[ci skip]
parents b9f35f4e 630c9a1f
{
"presets": [["latest", { "es2015": { "modules": false } }], "stage-2"],
"env": {
"karma": {
"plugins": ["rewire"]
},
"coverage": {
"plugins": [
[
"istanbul",
{
"exclude": ["spec/javascripts/**/*", "app/assets/javascripts/locale/**/app.js"]
}
],
[
"transform-define",
{
"process.env.BABEL_ENV": "coverage"
}
],
"rewire"
]
}
}
}
const BABEL_ENV = process.env.BABEL_ENV || process.env.NODE_ENV || null;
const presets = [
[
'@babel/preset-env',
{
modules: false,
targets: {
ie: '11',
},
},
],
];
// include stage 3 proposals
const plugins = [
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-syntax-import-meta',
'@babel/plugin-proposal-class-properties',
'@babel/plugin-proposal-json-strings',
];
// add code coverage tooling if necessary
if (BABEL_ENV === 'coverage') {
plugins.push([
'babel-plugin-istanbul',
{
exclude: ['spec/javascripts/**/*', 'app/assets/javascripts/locale/**/app.js'],
},
]);
}
// add rewire support when running tests
if (BABEL_ENV === 'karma' || BABEL_ENV === 'coverage') {
plugins.push('babel-plugin-rewire');
}
module.exports = { presets, plugins };
......@@ -6,7 +6,8 @@
/doc/ @axil @marcia
# Frontend maintainers should see everything in `app/assets/`
app/assets/ @annabeldunstone @ClemMakesApps @fatihacet @filipa @iamphill @mikegreiling @timzallmann
app/assets/ @ClemMakesApps @fatihacet @filipa @iamphill @mikegreiling @timzallmann
*.scss @annabeldunstone @ClemMakesApps @fatihacet @filipa @iamphill @mikegreiling @timzallmann
# Someone from the database team should review changes in `db/`
db/ @abrandl @NikolayS
......
......@@ -10,24 +10,6 @@
Capybara/CurrentPathExpectation:
Enabled: false
# Offense count: 23
FactoryBot/DynamicAttributeDefinedStatically:
Exclude:
- 'spec/factories/broadcast_messages.rb'
- 'spec/factories/ci/builds.rb'
- 'spec/factories/ci/runners.rb'
- 'spec/factories/clusters/applications/helm.rb'
- 'spec/factories/clusters/platforms/kubernetes.rb'
- 'spec/factories/emails.rb'
- 'spec/factories/gpg_keys.rb'
- 'spec/factories/group_members.rb'
- 'spec/factories/merge_requests.rb'
- 'spec/factories/notes.rb'
- 'spec/factories/oauth_access_grants.rb'
- 'spec/factories/project_members.rb'
- 'spec/factories/todos.rb'
- 'spec/factories/uploads.rb'
# Offense count: 167
# Cop supports --auto-correct.
Layout/EmptyLinesAroundArguments:
......@@ -53,20 +35,6 @@ Layout/IndentArray:
Layout/IndentHash:
Enabled: false
# Offense count: 11
# Cop supports --auto-correct.
# Configuration parameters: AllowForAlignment.
Layout/SpaceBeforeFirstArg:
Exclude:
- 'config/routes/project.rb'
- 'db/migrate/20170506185517_add_foreign_key_pipeline_schedules_and_pipelines.rb'
- 'features/steps/project/source/browse_files.rb'
- 'features/steps/project/source/markdown_render.rb'
- 'lib/api/runners.rb'
- 'spec/features/search/user_uses_search_filters_spec.rb'
- 'spec/routing/project_routing_spec.rb'
- 'spec/services/system_note_service_spec.rb'
# Offense count: 93
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
......@@ -74,15 +42,6 @@ Layout/SpaceBeforeFirstArg:
Layout/SpaceInLambdaLiteral:
Enabled: false
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBrackets.
# SupportedStyles: space, no_space, compact
# SupportedStylesForEmptyBrackets: space, no_space
Layout/SpaceInsideArrayLiteralBrackets:
Exclude:
- 'spec/lib/gitlab/import_export/relation_factory_spec.rb'
# Offense count: 327
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters.
......@@ -96,14 +55,6 @@ Layout/SpaceInsideBlockBraces:
Layout/SpaceInsideParens:
Enabled: false
# Offense count: 14
# Cop supports --auto-correct.
Layout/SpaceInsidePercentLiteralDelimiters:
Exclude:
- 'lib/gitlab/git_access.rb'
- 'lib/gitlab/health_checks/fs_shards_check.rb'
- 'spec/lib/gitlab/health_checks/fs_shards_check_spec.rb'
# Offense count: 26
Lint/DuplicateMethods:
Exclude:
......@@ -135,31 +86,11 @@ Lint/InterpolationCheck:
Lint/MissingCopEnableDirective:
Enabled: false
# Offense count: 2
Lint/NestedPercentLiteral:
Exclude:
- 'lib/gitlab/git/repository.rb'
- 'spec/support/shared_examples/email_format_shared_examples.rb'
# Offense count: 1
Lint/ReturnInVoidContext:
Exclude:
- 'app/models/project.rb'
# Offense count: 1
# Configuration parameters: IgnoreImplicitReferences.
Lint/ShadowedArgument:
Exclude:
- 'lib/gitlab/database/sha_attribute.rb'
# Offense count: 3
# Cop supports --auto-correct.
Lint/UnneededRequireStatement:
Exclude:
- 'db/post_migrate/20161221153951_rename_reserved_project_names.rb'
- 'db/post_migrate/20170313133418_rename_more_reserved_project_names.rb'
- 'lib/declarative_policy.rb'
# Offense count: 9
Lint/UriEscapeUnescape:
Exclude:
......@@ -199,16 +130,6 @@ Naming/HeredocDelimiterCase:
Naming/HeredocDelimiterNaming:
Enabled: false
# Offense count: 1
Performance/UnfreezeString:
Exclude:
- 'features/steps/project/commits/commits.rb'
# Offense count: 1
# Cop supports --auto-correct.
Performance/UriDefaultParser:
Exclude:
- 'lib/gitlab/url_sanitizer.rb'
# Offense count: 3821
# Configuration parameters: Prefixes.
......
......@@ -94,7 +94,7 @@ GEM
bindata (2.4.3)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
bootsnap (1.3.1)
bootsnap (1.3.2)
msgpack (~> 1.0)
bootstrap_form (2.7.0)
brakeman (4.2.1)
......@@ -148,7 +148,7 @@ GEM
creole (0.5.0)
css_parser (1.5.0)
addressable
daemons (1.2.3)
daemons (1.2.6)
database_cleaner (1.5.3)
debug_inspector (0.0.2)
debugger-ruby_core_source (1.3.8)
......@@ -208,7 +208,7 @@ GEM
escape_utils (1.1.1)
et-orbi (1.0.3)
tzinfo
eventmachine (1.0.8)
eventmachine (1.2.7)
excon (0.62.0)
execjs (2.6.0)
expression_parser (0.9.0)
......@@ -516,7 +516,7 @@ GEM
mime-types-data (3.2016.0521)
mimemagic (0.3.0)
mini_magick (4.8.0)
mini_mime (1.0.0)
mini_mime (1.0.1)
mini_portile2 (2.3.0)
minitest (5.7.0)
mousetrap-rails (1.4.6)
......@@ -653,9 +653,9 @@ GEM
pry-byebug (3.4.3)
byebug (>= 9.0, < 9.1)
pry (~> 0.10)
pry-rails (0.3.5)
pry (>= 0.9.10)
public_suffix (3.0.2)
pry-rails (0.3.6)
pry (>= 0.10.4)
public_suffix (3.0.3)
pyu-ruby-sasl (0.0.3.3)
rack (1.6.10)
rack-accept (0.4.5)
......@@ -882,7 +882,7 @@ GEM
simplecov-html (~> 0.10.0)
simplecov-html (0.10.0)
slack-notifier (1.5.1)
spring (2.0.1)
spring (2.0.2)
activesupport (>= 4.2)
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
......@@ -912,7 +912,7 @@ GEM
test_after_commit (1.1.0)
activerecord (>= 3.2)
text (1.3.1)
thin (1.7.0)
thin (1.7.2)
daemons (~> 1.0, >= 1.0.9)
eventmachine (~> 1.0, >= 1.0.4)
rack (>= 1, < 3)
......@@ -971,7 +971,7 @@ GEM
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff
webpack-rails (0.9.10)
webpack-rails (0.9.11)
railties (>= 3.2.0)
wikicloth (0.8.1)
builder
......
......@@ -97,7 +97,7 @@ GEM
bindata (2.4.3)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
bootsnap (1.3.1)
bootsnap (1.3.2)
msgpack (~> 1.0)
bootstrap_form (2.7.0)
brakeman (4.2.1)
......@@ -151,7 +151,7 @@ GEM
creole (0.5.0)
css_parser (1.5.0)
addressable
daemons (1.2.3)
daemons (1.2.6)
database_cleaner (1.5.3)
debug_inspector (0.0.2)
debugger-ruby_core_source (1.3.8)
......@@ -211,7 +211,7 @@ GEM
escape_utils (1.1.1)
et-orbi (1.0.3)
tzinfo
eventmachine (1.0.8)
eventmachine (1.2.7)
excon (0.62.0)
execjs (2.6.0)
expression_parser (0.9.0)
......@@ -434,7 +434,7 @@ GEM
json (~> 1.8)
multi_xml (>= 0.5.2)
httpclient (2.8.3)
i18n (1.0.1)
i18n (1.1.0)
concurrent-ruby (~> 1.0)
icalendar (2.4.1)
ice_nine (0.11.2)
......@@ -519,7 +519,7 @@ GEM
mime-types-data (3.2016.0521)
mimemagic (0.3.0)
mini_magick (4.8.0)
mini_mime (1.0.0)
mini_mime (1.0.1)
mini_portile2 (2.3.0)
minitest (5.7.0)
mousetrap-rails (1.4.6)
......@@ -657,9 +657,9 @@ GEM
pry-byebug (3.4.3)
byebug (>= 9.0, < 9.1)
pry (~> 0.10)
pry-rails (0.3.5)
pry (>= 0.9.10)
public_suffix (3.0.2)
pry-rails (0.3.6)
pry (>= 0.10.4)
public_suffix (3.0.3)
pyu-ruby-sasl (0.0.3.3)
rack (2.0.5)
rack-accept (0.4.5)
......@@ -890,7 +890,7 @@ GEM
simplecov-html (~> 0.10.0)
simplecov-html (0.10.0)
slack-notifier (1.5.1)
spring (2.0.1)
spring (2.0.2)
activesupport (>= 4.2)
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
......@@ -918,7 +918,7 @@ GEM
temple (0.8.0)
test-prof (0.2.5)
text (1.3.1)
thin (1.7.0)
thin (1.7.2)
daemons (~> 1.0, >= 1.0.9)
eventmachine (~> 1.0, >= 1.0.4)
rack (>= 1, < 3)
......@@ -977,7 +977,7 @@ GEM
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff
webpack-rails (0.9.10)
webpack-rails (0.9.11)
railties (>= 3.2.0)
websocket-driver (0.6.5)
websocket-extensions (>= 0.1.0)
......
......@@ -23,6 +23,7 @@ const Api = {
dockerfilePath: '/api/:version/templates/dockerfiles/:key',
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
usersPath: '/api/:version/users.json',
userStatusPath: '/api/:version/user/status',
commitPath: '/api/:version/projects/:id/repository/commits',
commitPipelinesPath: '/:project_id/commit/:sha/pipelines',
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
......@@ -268,6 +269,15 @@ const Api = {
});
},
postUserStatus({ emoji, message }) {
const url = Api.buildUrl(this.userStatusPath);
return axios.put(url, {
emoji,
message,
});
},
templates(key, params = {}) {
const url = Api.buildUrl(this.templatesPath).replace(':key', key);
......
......@@ -42,10 +42,11 @@ export class AwardsHandler {
}
bindEvents() {
const $parentEl = this.targetContainerEl ? $(this.targetContainerEl) : $(document);
// If the user shows intent let's pre-build the menu
this.registerEventListener(
'one',
$(document),
$parentEl,
'mouseenter focus',
this.toggleButtonSelector,
'mouseenter focus',
......@@ -58,7 +59,7 @@ export class AwardsHandler {
}
},
);
this.registerEventListener('on', $(document), 'click', this.toggleButtonSelector, e => {
this.registerEventListener('on', $parentEl, 'click', this.toggleButtonSelector, e => {
e.stopPropagation();
e.preventDefault();
this.showEmojiMenu($(e.currentTarget));
......@@ -76,7 +77,7 @@ export class AwardsHandler {
});
const emojiButtonSelector = `.js-awards-block .js-emoji-btn, .${this.menuClass} .js-emoji-btn`;
this.registerEventListener('on', $(document), 'click', emojiButtonSelector, e => {
this.registerEventListener('on', $parentEl, 'click', emojiButtonSelector, e => {
e.preventDefault();
const $target = $(e.currentTarget);
const $glEmojiElement = $target.find('gl-emoji');
......@@ -168,7 +169,8 @@ export class AwardsHandler {
</div>
`;
document.body.insertAdjacentHTML('beforeend', emojiMenuMarkup);
const targetEl = this.targetContainerEl ? this.targetContainerEl : document.body;
targetEl.insertAdjacentHTML('beforeend', emojiMenuMarkup);
this.addRemainingEmojiMenuCategories();
this.setupSearch();
......@@ -250,6 +252,12 @@ export class AwardsHandler {
}
positionMenu($menu, $addBtn) {
if (this.targetContainerEl) {
return $menu.css({
top: `${$addBtn.outerHeight()}px`,
});
}
const position = $addBtn.data('position');
// The menu could potentially be off-screen or in a hidden overflow element
// So we position the element absolute in the body
......@@ -424,9 +432,7 @@ export class AwardsHandler {
users = origTitle.trim().split(FROM_SENTENCE_REGEX);
}
users.unshift('You');
return awardBlock
.attr('title', this.toSentence(users))
.tooltip('_fixTitle');
return awardBlock.attr('title', this.toSentence(users)).tooltip('_fixTitle');
}
createAwardButtonForVotesBlock(votesBlock, emojiName) {
......@@ -609,13 +615,11 @@ export class AwardsHandler {
let awardsHandlerPromise = null;
export default function loadAwardsHandler(reload = false) {
if (!awardsHandlerPromise || reload) {
awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji').then(
Emoji => {
const awardsHandler = new AwardsHandler(Emoji);
awardsHandler.bindEvents();
return awardsHandler;
},
);
awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji').then(Emoji => {
const awardsHandler = new AwardsHandler(Emoji);
awardsHandler.bindEvents();
return awardsHandler;
});
}
return awardsHandlerPromise;
}
<script>
import $ from 'jquery';
import { Button } from '@gitlab-org/gitlab-ui';
import eventHub from '../eventhub';
import ProjectSelect from './project_select.vue';
import ListIssue from '../models/issue';
......@@ -10,6 +11,7 @@ export default {
name: 'BoardNewIssue',
components: {
ProjectSelect,
'gl-button': Button,
},
props: {
groupId: {
......@@ -126,21 +128,23 @@ export default {
:group-id="groupId"
/>
<div class="clearfix prepend-top-10">
<button
<gl-button
ref="submit-button"
:disabled="disabled"
class="btn btn-success float-left"
class="float-left"
variant="success"
type="submit"
>
Submit issue
</button>
<button
class="btn btn-default float-right"
</gl-button>
<gl-button
class="float-right"
type="button"
variant="default"
@click="cancel"
>
Cancel
</button>
</gl-button>
</div>
</form>
</div>
......
......@@ -5,22 +5,22 @@ import { __ } from '~/locale';
import createFlash from '~/flash';
import eventHub from '../../notes/event_hub';
import CompareVersions from './compare_versions.vue';
import ChangedFiles from './changed_files.vue';
import DiffFile from './diff_file.vue';
import NoChanges from './no_changes.vue';
import HiddenFilesWarning from './hidden_files_warning.vue';
import CommitWidget from './commit_widget.vue';
import TreeList from './tree_list.vue';
export default {
name: 'DiffsApp',
components: {
Icon,
CompareVersions,
ChangedFiles,
DiffFile,
NoChanges,
HiddenFilesWarning,
CommitWidget,
TreeList,
},
props: {
endpoint: {
......@@ -58,6 +58,7 @@ export default {
plainDiffPath: state => state.diffs.plainDiffPath,
emailPatchPath: state => state.diffs.emailPatchPath,
}),
...mapState('diffs', ['showTreeList']),
...mapGetters('diffs', ['isParallelView']),
...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']),
targetBranch() {
......@@ -88,6 +89,9 @@ export default {
canCurrentUserFork() {
return this.currentUser.canFork === true && this.currentUser.canCreateMergeRequest;
},
showCompareVersions() {
return this.mergeRequestDiffs && this.mergeRequestDiff;
},
},
watch: {
diffViewType() {
......@@ -102,6 +106,8 @@ export default {
this.adjustView();
},
isLoading: 'adjustView',
showTreeList: 'adjustView',
},
mounted() {
this.setBaseConfig({ endpoint: this.endpoint, projectPath: this.projectPath });
......@@ -152,10 +158,11 @@ export default {
}
},
adjustView() {
if (this.shouldShow && this.isParallelView) {
window.mrTabs.expandViewContainer();
} else {
window.mrTabs.resetViewContainer();
if (this.shouldShow) {
this.$nextTick(() => {
window.mrTabs.resetViewContainer();
window.mrTabs.expandViewContainer(this.showTreeList);
});
}
},
},
......@@ -177,7 +184,7 @@ export default {
class="diffs tab-pane"
>
<compare-versions
v-if="!commit && mergeRequestDiffs.length > 1"
v-if="showCompareVersions"
:merge-request-diffs="mergeRequestDiffs"
:merge-request-diff="mergeRequestDiff"
:start-version="startVersion"
......@@ -215,22 +222,26 @@ export default {
:commit="commit"
/>
<changed-files
:diff-files="diffFiles"
/>
<div
v-if="diffFiles.length > 0"
class="files"
>
<diff-file
v-for="file in diffFiles"
:key="file.newPath"
:file="file"
:can-current-user-fork="canCurrentUserFork"
/>
<div class="files d-flex prepend-top-default">
<div
v-show="showTreeList"
class="diff-tree-list"
>
<tree-list />
</div>
<div
v-if="diffFiles.length > 0"
class="diff-files-holder"
>
<diff-file
v-for="file in diffFiles"
:key="file.newPath"
:file="file"
:can-current-user-fork="canCurrentUserFork"
/>
</div>
<no-changes v-else />
</div>
<no-changes v-else />
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
import { pluralize } from '~/lib/utils/text_utility';
import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility';
import { contentTop } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import ChangedFilesDropdown from './changed_files_dropdown.vue';
import changedFilesMixin from '../mixins/changed_files';
export default {
components: {
Icon,
ChangedFilesDropdown,
ClipboardButton,
},
mixins: [changedFilesMixin],
data() {
return {
isStuck: false,
maxWidth: 'auto',
offsetTop: 0,
};
},
computed: {
...mapGetters('diffs', ['isInlineView', 'isParallelView', 'areAllFilesCollapsed']),
sumAddedLines() {
return this.sumValues('addedLines');
},
sumRemovedLines() {
return this.sumValues('removedLines');
},
whitespaceVisible() {
return !getParameterValues('w')[0];
},
toggleWhitespaceText() {
if (this.whitespaceVisible) {
return __('Hide whitespace changes');
}
return __('Show whitespace changes');
},
toggleWhitespacePath() {
if (this.whitespaceVisible) {
return mergeUrlParams({ w: 1 }, window.location.href);
}
return mergeUrlParams({ w: 0 }, window.location.href);
},
top() {
return `${this.offsetTop}px`;
},
},
created() {
document.addEventListener('scroll', this.handleScroll);
this.offsetTop = contentTop();
},
beforeDestroy() {
document.removeEventListener('scroll', this.handleScroll);
},
methods: {
...mapActions('diffs', ['setInlineDiffViewType', 'setParallelDiffViewType', 'expandAllFiles']),
pluralize,
handleScroll() {
if (!this.updating) {
this.$nextTick(this.updateIsStuck);
this.updating = true;
}
},
updateIsStuck() {
if (!this.$refs.wrapper) {
return;
}
const scrollPosition = window.scrollY;
this.isStuck = scrollPosition + this.offsetTop >= this.$refs.placeholder.offsetTop;
this.updating = false;
},
sumValues(key) {
return this.diffFiles.reduce((total, file) => total + file[key], 0);
},
},
};
</script>
<template>
<span>
<div ref="placeholder"></div>
<div
ref="wrapper"
:style="{ top }"
:class="{'is-stuck': isStuck}"
class="content-block oneline-block diff-files-changed diff-files-changed-merge-request
files-changed js-diff-files-changed"
>
<div class="files-changed-inner">
<div
class="inline-parallel-buttons d-none d-md-block"
>
<a
v-if="areAllFilesCollapsed"
class="btn btn-default"
@click="expandAllFiles"
>
{{ __('Expand all') }}
</a>
<a
:href="toggleWhitespacePath"
class="btn btn-default"
>
{{ toggleWhitespaceText }}
</a>
<div class="btn-group">
<button
id="inline-diff-btn"
:class="{ active: isInlineView }"
type="button"
class="btn js-inline-diff-button"
data-view-type="inline"
@click="setInlineDiffViewType"
>
{{ __('Inline') }}
</button>
<button
id="parallel-diff-btn"
:class="{ active: isParallelView }"
type="button"
class="btn js-parallel-diff-button"
data-view-type="parallel"
@click="setParallelDiffViewType"
>
{{ __('Side-by-side') }}
</button>
</div>
</div>
<div class="commit-stat-summary dropdown">
<changed-files-dropdown
:diff-files="diffFiles"
/>
<span
class="js-diff-stats-additions-deletions-expanded
diff-stats-additions-deletions-expanded"
>
with
<strong class="cgreen">
{{ pluralize(`${sumAddedLines} addition`, sumAddedLines) }}
</strong>
and
<strong class="cred">
{{ pluralize(`${sumRemovedLines} deletion`, sumRemovedLines) }}
</strong>
</span>
<div
class="js-diff-stats-additions-deletions-collapsed
diff-stats-additions-deletions-collapsed float-right d-sm-none"
>
<strong class="cgreen">
+{{ sumAddedLines }}
</strong>
<strong class="cred">
-{{ sumRemovedLines }}
</strong>
</div>
</div>
</div>
</div>
</span>
</template>
<script>
import Icon from '~/vue_shared/components/icon.vue';
import changedFilesMixin from '../mixins/changed_files';
export default {
components: {
Icon,
},
mixins: [changedFilesMixin],
data() {
return {
searchText: '',
};
},
computed: {
filteredDiffFiles() {
return this.diffFiles.filter(file =>
file.filePath.toLowerCase().includes(this.searchText.toLowerCase()),
);
},
},
methods: {
clearSearch() {
this.searchText = '';
},
},
};
</script>
<template>
<span>
Showing
<button
class="diff-stats-summary-toggler"
data-toggle="dropdown"
type="button"
aria-expanded="false"
>
<span>
{{ n__('%d changed file', '%d changed files', diffFiles.length) }}
</span>
<icon
class="caret-icon"
name="chevron-down"
/>
</button>
<div class="dropdown-menu diff-file-changes">
<div class="dropdown-input">
<input
v-model="searchText"
type="search"
class="dropdown-input-field"
placeholder="Search files"
autocomplete="off"
/>
<i
v-if="searchText.length === 0"
aria-hidden="true"
data-hidden="true"
class="fa fa-search dropdown-input-search">
</i>
<i
v-else
role="button"
class="fa fa-times dropdown-input-search"
@click.stop.prevent="clearSearch"
></i>
</div>
<div class="dropdown-content">
<ul>
<li
v-for="diffFile in filteredDiffFiles"
:key="diffFile.name"
>
<a
:href="`#${diffFile.fileHash}`"
:title="diffFile.newPath"
class="diff-changed-file"
>
<icon
:name="fileChangedIcon(diffFile)"
:size="16"
:class="fileChangedClass(diffFile)"
class="diff-file-changed-icon append-right-8"
/>
<span class="diff-changed-file-content append-right-8">
<strong
v-if="diffFile.blob && diffFile.blob.name"
class="diff-changed-file-name"
>
{{ diffFile.blob.name }}
</strong>
<strong
v-else
class="diff-changed-blank-file-name"
>
{{ s__('Diffs|No file name available') }}
</strong>
<span class="diff-changed-file-path prepend-top-5">
{{ truncatedDiffPath(diffFile.blob.path) }}
</span>
</span>
<span class="diff-changed-stats">
<span class="cgreen">
+{{ diffFile.addedLines }}
</span>
<span class="cred">
-{{ diffFile.removedLines }}
</span>
</span>
</a>
</li>
<li
v-show="filteredDiffFiles.length === 0"
class="dropdown-menu-empty-item"
>
<a>
{{ __('No files found') }}
</a>
</li>
</ul>
</div>
</div>
</span>
</template>
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import Tooltip from '@gitlab-org/gitlab-ui/dist/directives/tooltip';
import { __ } from '~/locale';
import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility';
import Icon from '~/vue_shared/components/icon.vue';
import CompareVersionsDropdown from './compare_versions_dropdown.vue';
export default {
components: {
CompareVersionsDropdown,
Icon,
},
directives: {
Tooltip,
},
props: {
mergeRequestDiffs: {
......@@ -26,30 +35,119 @@ export default {
},
},
computed: {
...mapState('diffs', ['commit', 'showTreeList']),
...mapGetters('diffs', ['isInlineView', 'isParallelView', 'areAllFilesCollapsed']),
comparableDiffs() {
return this.mergeRequestDiffs.slice(1);
},
isWhitespaceVisible() {
return !getParameterValues('w')[0];
},
toggleWhitespaceText() {
if (this.isWhitespaceVisible) {
return __('Hide whitespace changes');
}
return __('Show whitespace changes');
},
toggleWhitespacePath() {
if (this.isWhitespaceVisible) {
return mergeUrlParams({ w: 1 }, window.location.href);
}
return mergeUrlParams({ w: 0 }, window.location.href);
},
showDropdowns() {
return !this.commit && this.mergeRequestDiffs.length;
},
},
methods: {
...mapActions('diffs', [
'setInlineDiffViewType',
'setParallelDiffViewType',
'expandAllFiles',
'toggleShowTreeList',
]),
},
};
</script>
<template>
<div class="mr-version-controls">
<div class="mr-version-menus-container content-block">
Changes between
<compare-versions-dropdown
:other-versions="mergeRequestDiffs"
:merge-request-version="mergeRequestDiff"
:show-commit-count="true"
class="mr-version-dropdown"
/>
and
<compare-versions-dropdown
:other-versions="comparableDiffs"
:start-version="startVersion"
:target-branch="targetBranch"
class="mr-version-compare-dropdown"
/>
<div
class="mr-version-menus-container content-block"
>
<button
v-tooltip.hover
type="button"
class="btn btn-default append-right-8 js-toggle-tree-list"
:class="{
active: showTreeList
}"
:title="__('Toggle file browser')"
@click="toggleShowTreeList"
>
<icon
name="hamburger"
/>
</button>
<div
v-if="showDropdowns"
class="d-flex align-items-center compare-versions-container"
>
Changes between
<compare-versions-dropdown
:other-versions="mergeRequestDiffs"
:merge-request-version="mergeRequestDiff"
:show-commit-count="true"
class="mr-version-dropdown"
/>
and
<compare-versions-dropdown
:other-versions="comparableDiffs"
:start-version="startVersion"
:target-branch="targetBranch"
class="mr-version-compare-dropdown"
/>
</div>
<div
class="inline-parallel-buttons d-none d-md-flex ml-auto"
>
<a
v-if="areAllFilesCollapsed"
class="btn btn-default"
@click="expandAllFiles"
>
{{ __('Expand all') }}
</a>
<a
:href="toggleWhitespacePath"
class="btn btn-default"
>
{{ toggleWhitespaceText }}
</a>
<div class="btn-group prepend-left-8">
<button
id="inline-diff-btn"
:class="{ active: isInlineView }"
type="button"
class="btn js-inline-diff-button"
data-view-type="inline"
@click="setInlineDiffViewType"
>
{{ __('Inline') }}
</button>
<button
id="parallel-diff-btn"
:class="{ active: isParallelView }"
type="button"
class="btn js-parallel-diff-button"
data-view-type="parallel"
@click="setParallelDiffViewType"
>
{{ __('Side-by-side') }}
</button>
</div>
</div>
</div>
</div>
</template>
......@@ -108,7 +108,7 @@ export default {
<template>
<span class="dropdown inline">
<a
class="dropdown-toggle btn btn-default"
class="dropdown-menu-toggle btn btn-default w-100"
data-toggle="dropdown"
aria-expanded="false"
>
......@@ -118,6 +118,7 @@ export default {
<Icon
:size="12"
name="angle-down"
class="position-absolute"
/>
</a>
<div class="dropdown-menu dropdown-select dropdown-menu-selectable">
......@@ -163,3 +164,10 @@ export default {
</div>
</span>
</template>
<style>
.dropdown {
min-width: 0;
max-height: 170px;
}
</style>
<script>
import { mapActions, mapGetters } from 'vuex';
import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore';
import { __, sprintf } from '~/locale';
import createFlash from '~/flash';
......@@ -28,6 +28,7 @@ export default {
};
},
computed: {
...mapState('diffs', ['currentDiffFileId']),
...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']),
isCollapsed() {
return this.file.collapsed || false;
......@@ -101,6 +102,9 @@ export default {
<template>
<div
:id="file.fileHash"
:class="{
'is-active': currentDiffFileId === file.fileHash
}"
class="diff-file file-holder"
>
<diff-file-header
......@@ -168,3 +172,20 @@ export default {
</div>
</div>
</template>
<style>
@keyframes shadow-fade {
from {
box-shadow: 0 0 4px #919191;
}
to {
box-shadow: 0 0 0 #dfdfdf;
}
}
.diff-file.is-active {
box-shadow: 0 0 0 #dfdfdf;
animation: shadow-fade 1.2s 0.1s 1;
}
</style>
......@@ -166,18 +166,16 @@ export default {
:title="diffFile.oldPath"
class="file-title-name"
data-container="body"
>
{{ diffFile.oldPath }}
</strong>
v-html="diffFile.oldPathHtml"
></strong>
<strong
v-tooltip
:title="diffFile.newPath"
class="file-title-name"
data-container="body"
>
{{ diffFile.newPath }}
</strong>
v-html="diffFile.newPathHtml"
></strong>
</span>
<strong
......
<script>
export default {
props: {
file: {
type: Object,
required: true,
},
},
};
</script>
<template>
<span
v-once
class="file-row-stats"
>
<span class="cgreen">
+{{ file.addedLines }}
</span>
<span class="cred">
-{{ file.removedLines }}
</span>
</span>
</template>
<style>
.file-row-stats {
font-size: 12px;
}
</style>
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import FileRow from '~/vue_shared/components/file_row.vue';
import FileRowStats from './file_row_stats.vue';
export default {
components: {
Icon,
FileRow,
},
data() {
return {
search: '',
};
},
computed: {
...mapState('diffs', ['tree', 'addedLines', 'removedLines']),
...mapGetters('diffs', ['allBlobs', 'diffFilesLength']),
filteredTreeList() {
const search = this.search.toLowerCase().trim();
if (search === '') return this.tree;
return this.allBlobs.filter(f => f.name.toLowerCase().indexOf(search) >= 0);
},
},
methods: {
...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile']),
clearSearch() {
this.search = '';
},
},
FileRowStats,
};
</script>
<template>
<div class="tree-list-holder d-flex flex-column">
<div class="append-bottom-8 position-relative tree-list-search">
<icon
name="search"
class="position-absolute tree-list-icon"
/>
<input
v-model="search"
:placeholder="s__('MergeRequest|Filter files')"
type="search"
class="form-control"
/>
<button
v-show="search"
:aria-label="__('Clear search')"
type="button"
class="position-absolute tree-list-icon tree-list-clear-icon border-0 p-0"
@click="clearSearch"
>
<icon
name="close"
/>
</button>
</div>
<div
class="tree-list-scroll"
>
<template v-if="filteredTreeList.length">
<file-row
v-for="file in filteredTreeList"
:key="file.key"
:file="file"
:level="0"
:hide-extra-on-tree="true"
:extra-component="$options.FileRowStats"
:show-changed-icon="true"
@toggleTreeOpen="toggleTreeOpen"
@clickFile="scrollToFile"
/>
</template>
<p
v-else
class="prepend-top-20 append-bottom-20 text-center"
>
{{ s__('MergeRequest|No files found') }}
</p>
</div>
<div
v-once
class="pt-3 pb-3 text-center"
>
{{ n__('%d changed file', '%d changed files', diffFilesLength) }}
<div>
<span class="cgreen">
{{ n__('%d addition', '%d additions', addedLines) }}
</span>
<span class="cred">
{{ n__('%d deleted', '%d deletions', removedLines) }}
</span>
</div>
</div>
</div>
</template>
......@@ -29,3 +29,5 @@ export const LENGTH_OF_AVATAR_TOOLTIP = 17;
export const LINES_TO_BE_RENDERED_DIRECTLY = 100;
export const MAX_LINES_TO_BE_RENDERED = 2000;
export const MR_TREE_SHOW_KEY = 'mr_tree_show';
export default {
props: {
diffFiles: {
type: Array,
required: true,
},
},
methods: {
fileChangedIcon(diffFile) {
if (diffFile.deletedFile) {
return 'file-deletion';
} else if (diffFile.newFile) {
return 'file-addition';
}
return 'file-modified';
},
fileChangedClass(diffFile) {
if (diffFile.deletedFile) {
return 'cred';
} else if (diffFile.newFile) {
return 'cgreen';
}
return '';
},
truncatedDiffPath(path) {
const maxLength = 60;
if (path.length > maxLength) {
const start = path.length - maxLength;
const end = start + maxLength;
return `...${path.slice(start, end)}`;
}
return path;
},
},
};
......@@ -12,6 +12,7 @@ import {
PARALLEL_DIFF_VIEW_TYPE,
INLINE_DIFF_VIEW_TYPE,
DIFF_VIEW_COOKIE_NAME,
MR_TREE_SHOW_KEY,
} from '../constants';
export const setBaseConfig = ({ commit }, options) => {
......@@ -195,5 +196,23 @@ export const saveDiffDiscussion = ({ dispatch }, { note, formData }) => {
.catch(() => createFlash(s__('MergeRequests|Saving the comment failed')));
};
export const toggleTreeOpen = ({ commit }, path) => {
commit(types.TOGGLE_FOLDER_OPEN, path);
};
export const scrollToFile = ({ state, commit }, path) => {
const { fileHash } = state.treeEntries[path];
document.location.hash = fileHash;
commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash);
setTimeout(() => commit(types.UPDATE_CURRENT_DIFF_FILE_ID, ''), 1000);
};
export const toggleShowTreeList = ({ commit, state }) => {
commit(types.TOGGLE_SHOW_TREE_LIST);
localStorage.setItem(MR_TREE_SHOW_KEY, state.showTreeList);
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
......@@ -110,5 +110,9 @@ export const shouldRenderInlineCommentRow = state => line => {
export const getDiffFileByHash = state => fileHash =>
state.diffFiles.find(file => file.fileHash === fileHash);
export const allBlobs = state => Object.values(state.treeEntries).filter(f => f.type === 'blob');
export const diffFilesLength = state => state.diffFiles.length;
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import Cookies from 'js-cookie';
import { getParameterValues } from '~/lib/utils/url_utility';
import { INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME } from '../../constants';
import { INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME, MR_TREE_SHOW_KEY } from '../../constants';
const viewTypeFromQueryString = getParameterValues('view')[0];
const viewTypeFromCookie = Cookies.get(DIFF_VIEW_COOKIE_NAME);
const defaultViewType = INLINE_DIFF_VIEW_TYPE;
const storedTreeShow = localStorage.getItem(MR_TREE_SHOW_KEY);
export default () => ({
isLoading: true,
......@@ -17,4 +18,8 @@ export default () => ({
mergeRequestDiff: null,
diffLineCommentForms: {},
diffViewType: viewTypeFromQueryString || viewTypeFromCookie || defaultViewType,
tree: [],
treeEntries: {},
showTreeList: storedTreeShow === null ? true : storedTreeShow === 'true',
currentDiffFileId: '',
});
......@@ -11,3 +11,6 @@ export const EXPAND_ALL_FILES = 'EXPAND_ALL_FILES';
export const RENDER_FILE = 'RENDER_FILE';
export const SET_LINE_DISCUSSIONS_FOR_FILE = 'SET_LINE_DISCUSSIONS_FOR_FILE';
export const REMOVE_LINE_DISCUSSIONS_FOR_FILE = 'REMOVE_LINE_DISCUSSIONS_FOR_FILE';
export const TOGGLE_FOLDER_OPEN = 'TOGGLE_FOLDER_OPEN';
export const TOGGLE_SHOW_TREE_LIST = 'TOGGLE_SHOW_TREE_LIST';
export const UPDATE_CURRENT_DIFF_FILE_ID = 'UPDATE_CURRENT_DIFF_FILE_ID';
import Vue from 'vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { sortTree } from '~/ide/stores/utils';
import {
findDiffFile,
addLineReferences,
......@@ -7,6 +8,7 @@ import {
addContextLines,
prepareDiffData,
isDiscussionApplicableToLine,
generateTreeList,
} from './utils';
import * as types from './mutation_types';
......@@ -23,9 +25,12 @@ export default {
[types.SET_DIFF_DATA](state, data) {
const diffData = convertObjectPropsToCamelCase(data, { deep: true });
prepareDiffData(diffData);
const { tree, treeEntries } = generateTreeList(diffData.diffFiles);
Object.assign(state, {
...diffData,
tree: sortTree(tree),
treeEntries,
});
},
......@@ -163,4 +168,13 @@ export default {
}
}
},
[types.TOGGLE_FOLDER_OPEN](state, path) {
state.treeEntries[path].opened = !state.treeEntries[path].opened;
},
[types.TOGGLE_SHOW_TREE_LIST](state) {
state.showTreeList = !state.showTreeList;
},
[types.UPDATE_CURRENT_DIFF_FILE_ID](state, fileId) {
state.currentDiffFileId = fileId;
},
};
......@@ -244,6 +244,7 @@ export function getDiffPositionByLineCode(diffFiles) {
oldLine,
newLine,
lineCode,
positionType: 'text',
};
}
});
......@@ -259,11 +260,57 @@ export function isDiscussionApplicableToLine({ discussion, diffPosition, latestD
const { lineCode, ...diffPositionCopy } = diffPosition;
if (discussion.original_position && discussion.position) {
const originalRefs = convertObjectPropsToCamelCase(discussion.original_position.formatter);
const refs = convertObjectPropsToCamelCase(discussion.position.formatter);
const originalRefs = convertObjectPropsToCamelCase(discussion.original_position);
const refs = convertObjectPropsToCamelCase(discussion.position);
return _.isEqual(refs, diffPositionCopy) || _.isEqual(originalRefs, diffPositionCopy);
}
return latestDiff && discussion.active && lineCode === discussion.line_code;
}
export const generateTreeList = files =>
files.reduce(
(acc, file) => {
const { fileHash, addedLines, removedLines, newFile, deletedFile, newPath } = file;
const split = newPath.split('/');
split.forEach((name, i) => {
const parent = acc.treeEntries[split.slice(0, i).join('/')];
const path = `${parent ? `${parent.path}/` : ''}${name}`;
if (!acc.treeEntries[path]) {
const type = path === newPath ? 'blob' : 'tree';
acc.treeEntries[path] = {
key: path,
path,
name,
type,
tree: [],
};
const entry = acc.treeEntries[path];
if (type === 'blob') {
Object.assign(entry, {
changed: true,
tempFile: newFile,
deleted: deletedFile,
fileHash,
addedLines,
removedLines,
});
} else {
Object.assign(entry, {
opened: true,
});
}
(parent ? parent.tree : acc.tree).push(entry);
}
});
return acc;
},
{ treeEntries: {}, tree: [] },
);
......@@ -2,12 +2,14 @@
/**
* Renders the Monitoring (Metrics) link in environments table.
*/
import { Button } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
components: {
Icon,
'gl-button': Button,
},
directives: {
tooltip,
......@@ -26,15 +28,16 @@ export default {
};
</script>
<template>
<a
<gl-button
v-tooltip
:href="monitoringUrl"
:title="title"
:aria-label="title"
class="btn monitoring-url d-none d-sm-none d-md-block"
class="monitoring-url d-none d-sm-none d-md-block"
data-container="body"
rel="noopener noreferrer nofollow"
variant="default"
>
<icon name="chart" />
</a>
</gl-button>
</template>
import $ from 'jquery';
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import { highCountTrim } from '~/lib/utils/text_utility';
import SetStatusModalTrigger from './set_status_modal/set_status_modal_trigger.vue';
import SetStatusModalWrapper from './set_status_modal/set_status_modal_wrapper.vue';
/**
* Updates todo counter when todos are toggled.
......@@ -17,3 +21,54 @@ export default function initTodoToggle() {
$todoPendingCount.toggleClass('hidden', parsedCount === 0);
});
}
document.addEventListener('DOMContentLoaded', () => {
const setStatusModalTriggerEl = document.querySelector('.js-set-status-modal-trigger');
const setStatusModalWrapperEl = document.querySelector('.js-set-status-modal-wrapper');
if (setStatusModalTriggerEl || setStatusModalWrapperEl) {
Vue.use(Translate);
// eslint-disable-next-line no-new
new Vue({
el: setStatusModalTriggerEl,
data() {
const { hasStatus } = this.$options.el.dataset;
return {
hasStatus: hasStatus === 'true',
};
},
render(createElement) {
return createElement(SetStatusModalTrigger, {
props: {
hasStatus: this.hasStatus,
},
});
},
});
// eslint-disable-next-line no-new
new Vue({
el: setStatusModalWrapperEl,
data() {
const { currentEmoji, currentMessage } = this.$options.el.dataset;
return {
currentEmoji,
currentMessage,
};
},
render(createElement) {
const { currentEmoji, currentMessage } = this;
return createElement(SetStatusModalWrapper, {
props: {
currentEmoji,
currentMessage,
},
});
},
});
}
});
......@@ -3,7 +3,7 @@ import $ from 'jquery';
import { mapActions } from 'vuex';
import { __ } from '~/locale';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import ChangedFileIcon from '../changed_file_icon.vue';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
export default {
components: {
......
<script>
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import FileIcon from '../../../vue_shared/components/file_icon.vue';
import ChangedFileIcon from '../changed_file_icon.vue';
import ChangedFileIcon from '../../../vue_shared/components/changed_file_icon.vue';
const MAX_PATH_LENGTH = 60;
......
......@@ -3,8 +3,8 @@ import { mapGetters } from 'vuex';
import { n__, __, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
import NewDropdown from './new_dropdown/index.vue';
import ChangedFileIcon from './changed_file_icon.vue';
import MrFileIcon from './mr_file_icon.vue';
export default {
......
......@@ -3,8 +3,8 @@ import { mapActions } from 'vuex';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
import FileStatusIcon from './repo_file_status_icon.vue';
import ChangedFileIcon from './changed_file_icon.vue';
export default {
components: {
......
......@@ -24,7 +24,6 @@ export default class Job extends LogOutputBehaviours {
this.$document = $(document);
this.$window = $(window);
this.logBytes = 0;
this.updateDropdown = this.updateDropdown.bind(this);
this.$buildTrace = $('#build-trace');
this.$buildRefreshAnimation = $('.js-build-refresh');
......@@ -35,18 +34,12 @@ export default class Job extends LogOutputBehaviours {
clearTimeout(this.timeout);
this.initSidebar();
this.populateJobs(this.buildStage);
this.updateStageDropdownText(this.buildStage);
this.sidebarOnResize();
this.$document
.off('click', '.js-sidebar-build-toggle')
.on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this));
this.$document
.off('click', '.stage-item')
.on('click', '.stage-item', this.updateDropdown);
this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100);
this.$window
......@@ -194,20 +187,4 @@ export default class Job extends LogOutputBehaviours {
if (this.shouldHideSidebarForViewport()) this.toggleSidebar();
}
// eslint-disable-next-line class-methods-use-this
populateJobs(stage) {
$('.build-job').hide();
$(`.build-job[data-stage="${stage}"]`).show();
}
// eslint-disable-next-line class-methods-use-this
updateStageDropdownText(stage) {
$('.stage-selection').text(stage);
}
updateDropdown(e) {
e.preventDefault();
const stage = e.currentTarget.text;
this.updateStageDropdownText(stage);
this.populateJobs(stage);
}
}
......@@ -46,7 +46,7 @@
v-if="mergeRequest"
:href="mergeRequest.path"
class="js-link-commit link-commit"
>{{ mergeRequest.iid }}</a>
>!{{ mergeRequest.iid }}</a>
</p>
<p class="build-light-text append-bottom-0">
......
......@@ -2,9 +2,12 @@
import { mapGetters, mapState } from 'vuex';
import CiHeader from '~/vue_shared/components/header_ci_component.vue';
import Callout from '~/vue_shared/components/callout.vue';
<<<<<<< HEAD
// ee-only start
import SharedRunner from 'ee/jobs/components/shared_runner_limit_block.vue';
// ee-only end
=======
>>>>>>> upstream/master
import EnvironmentsBlock from './environments_block.vue';
import ErasedBlock from './erased_block.vue';
import StuckBlock from './stuck_block.vue';
......@@ -17,7 +20,10 @@
EnvironmentsBlock,
ErasedBlock,
StuckBlock,
<<<<<<< HEAD
SharedRunner,
=======
>>>>>>> upstream/master
},
props: {
runnerHelpUrl: {
......@@ -35,7 +41,10 @@
'jobHasStarted',
'hasEnvironment',
'isJobStuck',
<<<<<<< HEAD
'shouldRenderSharedRunnerLimitWarning',
=======
>>>>>>> upstream/master
]),
},
};
......@@ -80,6 +89,7 @@
:runners-path="runnerHelpUrl"
/>
<<<<<<< HEAD
<shared-runner
v-if="shouldRenderSharedRunnerLimitWarning"
class="js-shared-runner-limit"
......@@ -88,6 +98,8 @@
:runners-path="runnerHelpUrl"
/>
=======
>>>>>>> upstream/master
<environments-block
v-if="hasEnvironment"
:deployment-status="job.deployment_status"
......
<script>
import _ from 'underscore';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
......@@ -16,26 +17,39 @@
type: Array,
required: true,
},
jobId: {
type: Number,
required: true,
},
},
methods: {
isJobActive(currentJobId) {
return this.jobId === currentJobId;
},
tooltipText(job) {
return `${_.escape(job.name)} - ${job.status.tooltip}`;
},
},
};
</script>
<template>
<div class="builds-container">
<div class="js-jobs-container builds-container">
<div
v-for="job in jobs"
:key="job.id"
class="build-job"
:class="{ retried: job.retried, active: isJobActive(job.id) }"
>
<a
v-for="job in jobs"
:key="job.id"
v-tooltip
:href="job.path"
:title="job.tooltip"
:class="{ active: job.active, retried: job.retried }"
:href="job.status.details_path"
:title="tooltipText(job)"
data-container="body"
>
<icon
v-if="job.active"
v-if="isJobActive(job.id)"
name="arrow-right"
class="js-arrow-right"
class="js-arrow-right icon-arrow-right"
/>
<ci-icon :status="job.status" />
......
<script>
import _ from 'underscore';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
import { sprintf, __ } from '~/locale';
import { __ } from '~/locale';
export default {
components: {
......@@ -10,30 +10,14 @@
Icon,
},
props: {
pipelineId: {
type: Number,
required: true,
},
pipelinePath: {
type: String,
required: true,
},
pipelineRef: {
type: String,
required: true,
},
pipelineRefPath: {
type: String,
pipeline: {
type: Object,
required: true,
},
stages: {
type: Array,
required: true,
},
pipelineStatus: {
type: Object,
required: true,
},
},
data() {
return {
......@@ -41,57 +25,73 @@
};
},
computed: {
pipelineLink() {
return sprintf(__('Pipeline %{pipelineLinkStart} #%{pipelineId} %{pipelineLinkEnd} from %{pipelineLinkRefStart} %{pipelineRef} %{pipelineLinkRefEnd}'), {
pipelineLinkStart: `<a href=${this.pipelinePath} class="js-pipeline-path link-commit">`,
pipelineId: this.pipelineId,
pipelineLinkEnd: '</a>',
pipelineLinkRefStart: `<a href=${this.pipelineRefPath} class="link-commit ref-name">`,
pipelineRef: this.pipelineRef,
pipelineLinkRefEnd: '</a>',
}, false);
hasRef() {
return !_.isEmpty(this.pipeline.ref);
},
},
watch: {
// When the component is initially mounted it may start with an empty stages array.
// Once the prop is updated, we set the first stage as the selected one
stages(newVal) {
if (newVal.length) {
this.selectedStage = newVal[0].name;
}
},
},
methods: {
onStageClick(stage) {
// todo: consider moving into store
this.selectedStage = stage.name;
// update dropdown with jobs
// jobs container is a new component.
this.$emit('requestSidebarStageDropdown', stage);
this.selectedStage = stage.name;
},
},
};
</script>
<template>
<div class="block-last">
<ci-icon :status="pipelineStatus" />
<div class="block-last dropdown">
<ci-icon
:status="pipeline.details.status"
class="vertical-align-middle"
/>
{{ __('Pipeline') }}
<a
:href="pipeline.path"
class="js-pipeline-path link-commit"
>
#{{ pipeline.id }}
</a>
<template v-if="hasRef">
{{ __('from') }}
<a
:href="pipeline.ref.path"
class="link-commit ref-name"
>
{{ pipeline.ref.name }}
</a>
</template>
<p v-html="pipelineLink"></p>
<button
type="button"
data-toggle="dropdown"
class="js-selected-stage dropdown-menu-toggle prepend-top-8"
>
{{ selectedStage }}
<i class="fa fa-chevron-down" ></i>
</button>
<div class="dropdown">
<button
type="button"
data-toggle="dropdown"
<ul class="dropdown-menu">
<li
v-for="stage in stages"
:key="stage.name"
>
{{ selectedStage }}
<icon name="chevron-down" />
</button>
<ul class="dropdown-menu">
<li
v-for="(stage, index) in stages"
:key="index"
<button
type="button"
class="js-stage-item stage-item"
@click="onStageClick(stage)"
>
<button
type="button"
class="stage-item"
@click="onStageClick(stage)"
>
{{ stage.name }}
</button>
</li>
</ul>
</div>
{{ stage.name }}
</button>
</li>
</ul>
</div>
</template>
import { mapState } from 'vuex';
import _ from 'underscore';
import { mapState, mapActions } from 'vuex';
import Vue from 'vue';
import Job from '../job';
import JobApp from './components/job_app.vue';
<<<<<<< HEAD
import DetailsBlock from './components/sidebar_details_block.vue';
=======
import Sidebar from './components/sidebar.vue';
>>>>>>> upstream/master
import createStore from './store';
export default () => {
......@@ -13,6 +18,7 @@ export default () => {
const store = createStore();
store.dispatch('setJobEndpoint', dataset.endpoint);
store.dispatch('fetchJob');
// Header
......@@ -44,17 +50,25 @@ export default () => {
new Vue({
el: detailsBlockElement,
components: {
DetailsBlock,
Sidebar,
},
store,
computed: {
...mapState(['job', 'isLoading']),
...mapState(['job']),
},
watch: {
job(newVal, oldVal) {
if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) {
this.fetchStages();
}
},
},
methods: {
...mapActions(['fetchStages']),
},
store,
render(createElement) {
return createElement('details-block', {
return createElement('sidebar', {
props: {
isLoading: this.isLoading,
job: this.job,
runnerHelpUrl: dataset.runnerHelpUrl,
terminalPath: detailsBlockDataset.terminalPath,
},
......
......@@ -62,7 +62,9 @@ export const fetchJob = ({ state, dispatch }) => {
});
};
export const receiveJobSuccess = ({ commit }, data) => commit(types.RECEIVE_JOB_SUCCESS, data);
export const receiveJobSuccess = ({ commit }, data) => {
commit(types.RECEIVE_JOB_SUCCESS, data);
};
export const receiveJobError = ({ commit }) => {
commit(types.RECEIVE_JOB_ERROR);
flash(__('An error occurred while fetching the job.'));
......@@ -137,8 +139,11 @@ export const fetchStages = ({ state, dispatch }) => {
dispatch('requestStages');
axios
.get(state.stagesEndpoint)
.then(({ data }) => dispatch('receiveStagesSuccess', data))
.get(state.job.pipeline.path)
.then(({ data }) => {
dispatch('receiveStagesSuccess', data.details.stages);
dispatch('fetchJobsForStage', data.details.stages[0]);
})
.catch(() => dispatch('receiveStagesError'));
};
export const receiveStagesSuccess = ({ commit }, data) =>
......@@ -152,16 +157,23 @@ export const receiveStagesError = ({ commit }) => {
* Jobs list on sidebar - depend on stages dropdown
*/
export const requestJobsForStage = ({ commit }) => commit(types.REQUEST_JOBS_FOR_STAGE);
export const setSelectedStage = ({ commit }, stage) => commit(types.SET_SELECTED_STAGE, stage);
// On stage click, set selected stage + fetch job
export const fetchJobsForStage = ({ state, dispatch }, stage) => {
dispatch('setSelectedStage', stage);
export const fetchJobsForStage = ({ dispatch }, stage) => {
dispatch('requestJobsForStage');
axios
.get(state.stageJobsEndpoint)
.then(({ data }) => dispatch('receiveJobsForStageSuccess', data))
.get(stage.dropdown_path, {
params: {
retried: 1,
},
})
.then(({ data }) => {
const retriedJobs = data.retried.map(job => Object.assign({}, job, { retried: true }));
const jobs = data.latest_statuses.concat(retriedJobs);
dispatch('receiveJobsForStageSuccess', jobs);
})
.catch(() => dispatch('receiveJobsForStageError'));
};
export const receiveJobsForStageSuccess = ({ commit }, data) =>
......
......@@ -38,10 +38,13 @@ export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status);
export const isJobStuck = state =>
state.job.status.group === 'pending' && state.job.runners && state.job.runners.available === false;
<<<<<<< HEAD
// ee-only start
export const shouldRenderSharedRunnerLimitWarning = state =>
state.job.runner && state.job.runner.quota && state.job.ruuner.quota.used;
// ee-only end
=======
>>>>>>> upstream/master
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
......@@ -88,6 +88,7 @@ export const handleLocationHash = () => {
const fixedDiffStats = document.querySelector('.js-diff-files-changed');
const fixedNav = document.querySelector('.navbar-gitlab');
const performanceBar = document.querySelector('#js-peek');
const topPadding = 8;
let adjustment = 0;
if (fixedNav) adjustment -= fixedNav.offsetHeight;
......@@ -108,6 +109,10 @@ export const handleLocationHash = () => {
adjustment -= performanceBar.offsetHeight;
}
if (isInMRPage()) {
adjustment -= topPadding;
}
window.scrollBy(0, adjustment);
};
......@@ -381,8 +386,11 @@ export const objectToQueryString = (params = {}) =>
.map(param => `${param}=${params[param]}`)
.join('&');
export const buildUrlWithCurrentLocation = param =>
(param ? `${window.location.pathname}${param}` : window.location.pathname);
export const buildUrlWithCurrentLocation = param => {
if (param) return `${window.location.pathname}${param}`;
return window.location.pathname;
};
/**
* Based on the current location and the string parameters provided
......
......@@ -194,9 +194,7 @@ export default class MergeRequestTabs {
if (bp.getBreakpointSize() !== 'lg') {
this.shrinkView();
}
if (this.diffViewType() === 'parallel') {
this.expandViewContainer();
}
this.expandViewContainer();
this.destroyPipelinesView();
this.commitsTab.classList.remove('active');
} else if (action === 'pipelines') {
......@@ -355,7 +353,7 @@ export default class MergeRequestTabs {
localTimeAgo($('.js-timeago', 'div#diffs'));
syntaxHighlight($('#diffs .js-syntax-highlight'));
if (this.diffViewType() === 'parallel' && this.isDiffAction(this.currentAction)) {
if (this.isDiffAction(this.currentAction)) {
this.expandViewContainer();
}
this.diffsLoaded = true;
......@@ -408,19 +406,23 @@ export default class MergeRequestTabs {
}
diffViewType() {
return $('.inline-parallel-buttons a.active').data('viewType');
return $('.inline-parallel-buttons button.active').data('viewType');
}
isDiffAction(action) {
return action === 'diffs' || action === 'new/diffs';
}
expandViewContainer() {
expandViewContainer(removeLimited = true) {
const $wrapper = $('.content-wrapper .container-fluid').not('.breadcrumbs');
if (this.fixedLayoutPref === null) {
this.fixedLayoutPref = $wrapper.hasClass('container-limited');
}
$wrapper.removeClass('container-limited');
if (this.diffViewType() === 'parallel' || removeLimited) {
$wrapper.removeClass('container-limited');
} else {
$wrapper.addClass('container-limited');
}
}
resetViewContainer() {
......
<script>
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
ClipboardButton,
Icon,
},
props: {
diffFile: {
type: Object,
required: true,
},
},
computed: {
titleTag() {
return this.diffFile.discussionPath ? 'a' : 'span';
},
},
};
</script>
<template>
<div class="file-header-content">
<div
v-if="diffFile.submodule"
>
<span>
<icon name="archive" />
<strong
class="file-title-name"
v-html="diffFile.submoduleLink"
></strong>
<clipboard-button
:text="diffFile.submoduleLink"
title="Copy file path to clipboard"
css-class="btn-default btn-transparent btn-clipboard"
/>
</span>
</div>
<template v-else>
<component
:is="titleTag"
ref="titleWrapper"
:href="diffFile.discussionPath"
>
<span v-html="diffFile.blobIcon"></span>
<span v-if="diffFile.renamedFile">
<strong
:title="diffFile.oldPath"
class="file-title-name has-tooltip"
data-container="body"
>
{{ diffFile.oldPath }}
</strong>
&rarr;
<strong
:title="diffFile.newPath"
class="file-title-name has-tooltip"
data-container="body"
>
{{ diffFile.newPath }}
</strong>
</span>
<strong
v-else
:title="diffFile.oldPath"
class="file-title-name has-tooltip"
data-container="body"
>
{{ diffFile.filePath }}
<span v-if="diffFile.deletedFile">
deleted
</span>
</strong>
</component>
<clipboard-button
:text="diffFile.filePath"
title="Copy file path to clipboard"
css-class="btn-default btn-transparent btn-clipboard"
/>
<small
v-if="diffFile.modeChanged"
ref="fileMode"
>
{{ diffFile.aMode }}{{ diffFile.bMode }}
</small>
</template>
</div>
</template>
......@@ -191,6 +191,7 @@ export default {
if (note.placeholderType === SYSTEM_NOTE) {
return placeholderSystemNote;
}
return placeholderNote;
}
......@@ -201,7 +202,7 @@ export default {
return noteableNote;
},
componentData(note) {
return note.isPlaceholderNote ? this.discussion.notes[0] : note;
return note.isPlaceholderNote ? note.notes[0] : note;
},
toggleDiscussionHandler() {
this.toggleDiscussion({ discussionId: this.discussion.id });
......
......@@ -126,8 +126,8 @@ export const unresolvedDiscussionsIdsByDiff = (state, getters) =>
const filenameComparison = a.diff_file.file_path.localeCompare(b.diff_file.file_path);
// Get the line numbers, to compare within the same file
const aLines = [a.position.formatter.new_line, a.position.formatter.old_line];
const bLines = [b.position.formatter.new_line, b.position.formatter.old_line];
const aLines = [a.position.new_line, a.position.old_line];
const bLines = [b.position.new_line, b.position.old_line];
return filenameComparison < 0 ||
(filenameComparison === 0 &&
......
......@@ -11,7 +11,7 @@ document.addEventListener('DOMContentLoaded', () => {
const statusEmojiField = document.getElementById('js-status-emoji-field');
const statusMessageField = document.getElementById('js-status-message-field');
const toggleNoEmojiPlaceholder = (isVisible) => {
const toggleNoEmojiPlaceholder = isVisible => {
const placeholderElement = document.getElementById('js-no-emoji-placeholder');
placeholderElement.classList.toggle('hidden', !isVisible);
};
......@@ -69,5 +69,5 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
})
.catch(() => createFlash('Failed to load emoji list!'));
.catch(() => createFlash('Failed to load emoji list.'));
});
import { AwardsHandler } from '~/awards_handler';
class EmojiMenuInModal extends AwardsHandler {
constructor(emoji, toggleButtonSelector, menuClass, selectEmojiCallback, targetContainerEl) {
super(emoji);
this.selectEmojiCallback = selectEmojiCallback;
this.toggleButtonSelector = toggleButtonSelector;
this.menuClass = menuClass;
this.targetContainerEl = targetContainerEl;
this.bindEvents();
}
postEmoji($emojiButton, awardUrl, selectedEmoji, callback) {
this.selectEmojiCallback(selectedEmoji, this.emoji.glEmojiTag(selectedEmoji));
callback();
}
}
export default EmojiMenuInModal;
import Vue from 'vue';
export default new Vue();
<script>
import { s__ } from '~/locale';
import eventHub from './event_hub';
export default {
props: {
hasStatus: {
type: Boolean,
required: true,
},
},
computed: {
buttonText() {
return this.hasStatus ? s__('SetStatusModal|Edit status') : s__('SetStatusModal|Set status');
},
},
methods: {
openModal() {
eventHub.$emit('openModal');
},
},
};
</script>
<template>
<button
type="button"
class="btn menu-item"
@click="openModal"
>
{{ buttonText }}
</button>
</template>
<script>
import $ from 'jquery';
import createFlash from '~/flash';
import Icon from '~/vue_shared/components/icon.vue';
import GfmAutoComplete from '~/gfm_auto_complete';
import { __, s__ } from '~/locale';
import Api from '~/api';
import eventHub from './event_hub';
import EmojiMenuInModal from './emoji_menu_in_modal';
const emojiMenuClass = 'js-modal-status-emoji-menu';
export default {
components: {
Icon,
},
props: {
currentEmoji: {
type: String,
required: true,
},
currentMessage: {
type: String,
required: true,
},
},
data() {
return {
defaultEmojiTag: '',
emoji: this.currentEmoji,
emojiMenu: null,
emojiTag: '',
isEmojiMenuVisible: false,
message: this.currentMessage,
modalId: 'set-user-status-modal',
noEmoji: true,
};
},
computed: {
isDirty() {
return this.message.length || this.emoji.length;
},
},
mounted() {
eventHub.$on('openModal', this.openModal);
},
beforeDestroy() {
this.emojiMenu.destroy();
},
methods: {
openModal() {
this.$root.$emit('bv::show::modal', this.modalId);
},
closeModal() {
this.$root.$emit('bv::hide::modal', this.modalId);
},
setupEmojiListAndAutocomplete() {
const toggleEmojiMenuButtonSelector = '#set-user-status-modal .js-toggle-emoji-menu';
const emojiAutocomplete = new GfmAutoComplete();
emojiAutocomplete.setup($(this.$refs.statusMessageField), { emojis: true });
import(/* webpackChunkName: 'emoji' */ '~/emoji')
.then(Emoji => {
if (this.emoji) {
this.emojiTag = Emoji.glEmojiTag(this.emoji);
}
this.noEmoji = this.emoji === '';
this.defaultEmojiTag = Emoji.glEmojiTag('speech_balloon');
this.emojiMenu = new EmojiMenuInModal(
Emoji,
toggleEmojiMenuButtonSelector,
emojiMenuClass,
this.setEmoji,
this.$refs.userStatusForm,
);
})
.catch(() => createFlash(__('Failed to load emoji list.')));
},
showEmojiMenu() {
this.isEmojiMenuVisible = true;
this.emojiMenu.showEmojiMenu($(this.$refs.toggleEmojiMenuButton));
},
hideEmojiMenu() {
if (!this.isEmojiMenuVisible) {
return;
}
this.isEmojiMenuVisible = false;
this.emojiMenu.hideMenuElement($(`.${emojiMenuClass}`));
},
setDefaultEmoji() {
const { emojiTag } = this;
const hasStatusMessage = this.message;
if (hasStatusMessage && emojiTag) {
return;
}
if (hasStatusMessage) {
this.noEmoji = false;
this.emojiTag = this.defaultEmojiTag;
} else if (emojiTag === this.defaultEmojiTag) {
this.noEmoji = true;
this.clearEmoji();
}
},
setEmoji(emoji, emojiTag) {
this.emoji = emoji;
this.noEmoji = false;
this.clearEmoji();
this.emojiTag = emojiTag;
},
clearEmoji() {
if (this.emojiTag) {
this.emojiTag = '';
}
},
clearStatusInputs() {
this.emoji = '';
this.message = '';
this.noEmoji = true;
this.clearEmoji();
this.hideEmojiMenu();
},
removeStatus() {
this.clearStatusInputs();
this.setStatus();
},
setStatus() {
const { emoji, message } = this;
Api.postUserStatus({
emoji,
message,
})
.then(this.onUpdateSuccess)
.catch(this.onUpdateFail);
},
onUpdateSuccess() {
this.closeModal();
window.location.reload();
},
onUpdateFail() {
createFlash(
s__("SetStatusModal|Sorry, we weren't able to set your status. Please try again later."),
);
this.closeModal();
},
},
};
</script>
<template>
<gl-ui-modal
:title="s__('SetStatusModal|Set a status')"
:modal-id="modalId"
:ok-title="s__('SetStatusModal|Set status')"
:cancel-title="s__('SetStatusModal|Remove status')"
ok-variant="success"
class="set-user-status-modal"
@shown="setupEmojiListAndAutocomplete"
@hide="hideEmojiMenu"
@ok="setStatus"
@cancel="removeStatus"
>
<div>
<input
v-model="emoji"
class="js-status-emoji-field"
type="hidden"
name="user[status][emoji]"
/>
<div
ref="userStatusForm"
class="form-group position-relative m-0"
>
<div class="input-group">
<span class="input-group-btn">
<button
ref="toggleEmojiMenuButton"
v-gl-tooltip.bottom
:title="s__('SetStatusModal|Add status emoji')"
:aria-label="s__('SetStatusModal|Add status emoji')"
name="button"
type="button"
class="js-toggle-emoji-menu emoji-menu-toggle-button btn"
@click="showEmojiMenu"
>
<span v-html="emojiTag"></span>
<span
v-show="noEmoji"
class="js-no-emoji-placeholder no-emoji-placeholder position-relative"
>
<icon
name="emoji_slightly_smiling_face"
css-classes="award-control-icon-neutral"
/>
<icon
name="emoji_smiley"
css-classes="award-control-icon-positive"
/>
<icon
name="emoji_smile"
css-classes="award-control-icon-super-positive"
/>
</span>
</button>
</span>
<input
ref="statusMessageField"
v-model="message"
:placeholder="s__('SetStatusModal|What\'s your status?')"
type="text"
class="form-control form-control input-lg js-status-message-field"
name="user[status][message]"
@keyup="setDefaultEmoji"
@keyup.enter.prevent
@click="hideEmojiMenu"
/>
<span
v-show="isDirty"
class="input-group-btn"
>
<button
v-gl-tooltip.bottom
:title="s__('SetStatusModal|Clear status')"
:aria-label="s__('SetStatusModal|Clear status')"
name="button"
type="button"
class="js-clear-user-status-button clear-user-status btn"
@click="clearStatusInputs()"
>
<icon name="close" />
</button>
</span>
</div>
</div>
</div>
</gl-ui-modal>
</template>
......@@ -3,7 +3,7 @@ import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import { pluralize } from '~/lib/utils/text_utility';
import { __, sprintf } from '~/locale';
import { getCommitIconMap } from '../utils';
import { getCommitIconMap } from '~/ide/utils';
export default {
components: {
......@@ -32,6 +32,11 @@ export default {
required: false,
default: false,
},
size: {
type: Number,
required: false,
default: 12,
},
},
computed: {
changedIcon() {
......@@ -42,7 +47,7 @@ export default {
return `${getCommitIconMap(this.file).icon}${suffix}`;
},
changedIconClass() {
return `ide-${this.changedIcon} float-left`;
return `${this.changedIcon} float-left d-block`;
},
tooltipTitle() {
if (!this.showTooltip) return undefined;
......@@ -78,13 +83,30 @@ export default {
:title="tooltipTitle"
data-container="body"
data-placement="right"
class="ide-file-changed-icon"
class="file-changed-icon ml-auto"
>
<icon
v-if="showIcon"
:name="changedIcon"
:size="12"
:size="size"
:css-classes="changedIconClass"
/>
</span>
</template>
<style>
.file-addition,
.file-addition-solid {
color: #1aaa55;
}
.file-modified,
.file-modified-solid {
color: #fc9403;
}
.file-deletion,
.file-deletion-solid {
color: #db3b21;
}
</style>
<script>
import Icon from '~/vue_shared/components/icon.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
export default {
name: 'FileRow',
components: {
FileIcon,
Icon,
ChangedFileIcon,
},
props: {
file: {
......@@ -22,6 +24,16 @@ export default {
required: false,
default: null,
},
hideExtraOnTree: {
type: Boolean,
required: false,
default: false,
},
showChangedIcon: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
......@@ -65,6 +77,9 @@ export default {
toggleTreeOpen(path) {
this.$emit('toggleTreeOpen', path);
},
clickedFile(path) {
this.$emit('clickFile', path);
},
clickFile() {
// Manual Action if a tree is selected/opened
if (this.isTree && this.hasUrlAtCurrentRoute()) {
......@@ -72,6 +87,8 @@ export default {
}
if (this.$router) this.$router.push(`/project${this.file.url}`);
if (this.isBlob) this.clickedFile(this.file.path);
},
scrollIntoView(isInit = false) {
const block = isInit && this.isTree ? 'center' : 'nearest';
......@@ -126,17 +143,24 @@ export default {
class="file-row-name str-truncated"
>
<file-icon
v-if="!showChangedIcon || file.type === 'tree'"
:file-name="file.name"
:loading="file.loading"
:folder="isTree"
:opened="file.opened"
:size="16"
/>
<changed-file-icon
v-else
:file="file"
:size="16"
class="append-right-5"
/>
{{ file.name }}
</span>
<component
:is="extraComponent"
v-if="extraComponent"
v-if="extraComponent && !(hideExtraOnTree && file.type === 'tree')"
:file="file"
:mouse-over="mouseOver"
/>
......@@ -148,8 +172,11 @@ export default {
:key="childFile.key"
:file="childFile"
:level="level + 1"
:hide-extra-on-tree="hideExtraOnTree"
:extra-component="extraComponent"
:show-changed-icon="showChangedIcon"
@toggleTreeOpen="toggleTreeOpen"
@clickFile="clickedFile"
/>
</template>
</div>
......
......@@ -529,9 +529,10 @@
}
.header-user {
.dropdown-menu {
&.show .dropdown-menu {
width: auto;
min-width: unset;
max-height: 323px;
margin-top: 4px;
color: $gl-text-color;
left: auto;
......@@ -542,6 +543,18 @@
.user-name {
display: block;
}
.user-status-emoji {
margin-right: 0;
display: block;
vertical-align: text-top;
max-width: 148px;
font-size: 12px;
gl-emoji {
font-size: $gl-font-size;
}
}
}
svg {
......@@ -573,3 +586,24 @@
}
}
}
.set-user-status-modal {
.modal-body {
min-height: unset;
}
.input-lg {
max-width: unset;
}
.no-emoji-placeholder,
.clear-user-status {
svg {
fill: $gl-text-color-secondary;
}
}
.emoji-menu-toggle-button {
@include emoji-menu-toggle-button;
}
}
......@@ -356,3 +356,59 @@
border-radius: 50%;
}
}
@mixin emoji-menu-toggle-button {
line-height: 1;
padding: 0;
min-width: 16px;
color: $gray-darkest;
fill: $gray-darkest;
.fa {
position: relative;
font-size: 16px;
}
svg {
@include btn-svg;
margin: 0;
}
.award-control-icon-positive,
.award-control-icon-super-positive {
position: absolute;
top: 0;
left: 0;
opacity: 0;
}
&:hover,
&.is-active {
.danger-highlight {
color: $red-500;
}
.link-highlight {
color: $blue-600;
fill: $blue-600;
}
.award-control-icon-neutral {
opacity: 0;
}
.award-control-icon-positive {
opacity: 1;
}
}
&.is-active {
.award-control-icon-positive {
opacity: 0;
}
.award-control-icon-super-positive {
opacity: 1;
}
}
}
......@@ -322,7 +322,8 @@ $diff-jagged-border-gradient-color: darken($white-normal, 8%);
$monospace-font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono',
'Courier New', 'andale mono', 'lucida console', monospace;
$regular-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell,
'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
/*
* Dropdowns
......@@ -666,5 +667,4 @@ Modals
$modal-body-height: 134px;
$modal-border-color: #e9ecef;
$priority-label-empty-state-width: 114px;
......@@ -517,21 +517,6 @@ $ide-commit-header-height: 48px;
}
}
.ide-file-addition,
.ide-file-addition-solid {
color: $green-500;
}
.ide-file-modified,
.ide-file-modified-solid {
color: $orange-500;
}
.ide-file-deletion,
.ide-file-deletion-solid {
color: $red-500;
}
.multi-file-commit-list-collapsed {
display: flex;
flex-direction: column;
......@@ -1399,14 +1384,6 @@ $ide-commit-header-height: 48px;
color: $theme-gray-700;
}
.ide-file-changed-icon {
margin-left: auto;
> svg {
display: block;
}
}
.file-row:hover,
.file-row:focus {
.ide-new-btn {
......
......@@ -262,23 +262,6 @@
}
}
.build-dropdown {
margin: $gl-padding 0;
padding: 0;
.dropdown-menu-toggle {
margin-top: #{$gl-padding / 2};
}
svg {
position: relative;
top: 3px;
margin-right: 3px;
width: 14px;
height: 14px;
}
}
.builds-container {
background-color: $white-light;
border-top: 1px solid $border-color;
......@@ -315,15 +298,11 @@
position: absolute;
left: 15px;
top: 20px;
display: none;
display: block;
}
&.active {
font-weight: $gl-font-weight-bold;
.icon-arrow-right {
display: block;
}
}
&.retried {
......
......@@ -571,8 +571,6 @@
}
.files {
margin-top: 1px;
.diff-file:last-child {
margin-bottom: 0;
}
......@@ -987,3 +985,63 @@
.discussion-body .image .frame {
position: relative;
}
.diff-tree-list {
width: 320px;
}
.diff-files-holder {
flex: 1;
min-width: 0;
}
.compare-versions-container {
min-width: 0;
}
.tree-list-holder {
position: sticky;
top: 100px;
max-height: calc(100vh - 100px);
padding-right: $gl-padding;
.file-row {
margin-left: 0;
margin-right: 0;
}
.with-performance-bar & {
top: 135px;
}
}
.tree-list-scroll {
max-height: 100%;
padding-top: $grid-size;
padding-bottom: $grid-size;
border-top: 1px solid $border-color;
border-bottom: 1px solid $border-color;
overflow-y: scroll;
overflow-x: auto;
}
.tree-list-search .form-control {
padding-left: 30px;
}
.tree-list-icon {
top: 50%;
left: 10px;
transform: translateY(-50%);
&,
svg {
fill: $gl-text-color-tertiary;
}
}
.tree-list-clear-icon {
right: 10px;
left: auto;
line-height: 0;
}
......@@ -723,6 +723,17 @@
align-items: center;
padding: 16px;
z-index: 199;
white-space: nowrap;
.dropdown-menu-toggle {
width: auto;
max-width: 170px;
svg {
top: 10px;
right: 8px;
}
}
}
.content-block {
......
......@@ -519,59 +519,7 @@ ul.notes {
}
.note-action-button {
line-height: 1;
padding: 0;
min-width: 16px;
color: $gray-darkest;
fill: $gray-darkest;
.fa {
position: relative;
font-size: 16px;
}
svg {
@include btn-svg;
margin: 0;
}
.award-control-icon-positive,
.award-control-icon-super-positive {
position: absolute;
top: 0;
left: 0;
opacity: 0;
}
&:hover,
&.is-active {
.danger-highlight {
color: $red-500;
}
.link-highlight {
color: $blue-600;
fill: $blue-600;
}
.award-control-icon-neutral {
opacity: 0;
}
.award-control-icon-positive {
opacity: 1;
}
}
&.is-active {
.award-control-icon-positive {
opacity: 0;
}
.award-control-icon-super-positive {
opacity: 1;
}
}
@include emoji-menu-toggle-button;
}
.discussion-toggle-button {
......
......@@ -81,14 +81,14 @@
// Middle dot divider between each element in a list of items.
.middle-dot-divider {
&::after {
content: "\00B7"; // Middle Dot
content: '\00B7'; // Middle Dot
padding: 0 6px;
font-weight: $gl-font-weight-bold;
}
&:last-child {
&::after {
content: "";
content: '';
padding: 0;
}
}
......@@ -191,7 +191,6 @@
@include media-breakpoint-down(xs) {
width: auto;
}
}
.profile-crop-image-container {
......@@ -215,7 +214,6 @@
}
}
.user-profile {
.cover-controls a {
margin-left: 5px;
......@@ -415,7 +413,7 @@ table.u2f-registrations {
}
&.unverified {
@include status-color($gray-dark, color("gray"), $common-gray-dark);
@include status-color($gray-dark, color('gray'), $common-gray-dark);
}
}
}
......@@ -428,7 +426,7 @@ table.u2f-registrations {
}
.emoji-menu-toggle-button {
@extend .note-action-button;
@include emoji-menu-toggle-button;
.no-emoji-placeholder {
position: relative;
......
......@@ -831,6 +831,14 @@
}
}
.repository-language-bar-tooltip-language {
font-weight: $gl-font-weight-bold;
}
.repository-language-bar-tooltip-share {
color: $theme-gray-400;
}
pre.light-well {
border-color: $well-light-border;
}
......
......@@ -69,8 +69,9 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end
end
def reset_runners_token
def reset_registration_token
@application_setting.reset_runners_registration_token!
flash[:notice] = 'New runners registration token has been generated!'
redirect_to admin_runners_path
end
......
......@@ -10,6 +10,13 @@ module Groups
define_secret_variables
end
def reset_registration_token
@group.reset_runners_token!
flash[:notice] = 'New runners registration token has been generated!'
redirect_to group_settings_ci_cd_path
end
private
def define_secret_variables
......
......@@ -38,6 +38,13 @@ module Projects
end
end
def reset_registration_token
@project.reset_runners_token!
flash[:notice] = 'New runners registration token has been generated!'
redirect_to namespace_project_settings_ci_cd_path
end
private
def update_params
......
......@@ -13,8 +13,4 @@ module ClustersHelper
render 'projects/clusters/gcp_signup_offer_banner'
end
end
def rbac_clusters_feature_enabled?
Feature.enabled?(:rbac_clusters)
end
end
......@@ -13,6 +13,7 @@ module RepositoryLanguagesHelper
content_tag :div, nil,
class: "progress-bar has-tooltip",
style: "width: #{lang.share}%; background-color:#{lang.color}",
title: lang.name
data: { html: true },
title: "<span class=\"repository-language-bar-tooltip-language\">#{escape_javascript(lang.name)}</span>&nbsp;<span class=\"repository-language-bar-tooltip-share\">#{lang.share.round(1)}%</span>"
end
end
......@@ -640,6 +640,10 @@ module Ci
end
end
def default_branch?
ref == project.default_branch
end
private
def ci_yaml_from_repo
......
......@@ -64,10 +64,10 @@ class InstanceConfiguration
end
def ssh_algorithm_md5(ssh_file_content)
OpenSSL::Digest::MD5.hexdigest(ssh_file_content).scan(/../).join(':')
Gitlab::SSHPublicKey.new(ssh_file_content).fingerprint
end
def ssh_algorithm_sha256(ssh_file_content)
OpenSSL::Digest::SHA256.hexdigest(ssh_file_content)
Gitlab::SSHPublicKey.new(ssh_file_content).fingerprint('SHA256')
end
end
......@@ -84,7 +84,7 @@ class DiffFileEntity < Grape::Entity
end
expose :old_path_html do |diff_file|
old_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
old_path, _ = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
old_path
end
......
......@@ -2,106 +2,103 @@
- @no_container = true
%div{ class: container_class }
.bs-callout
%p
= (_"A 'Runner' is a process which runs a job. You can set up as many Runners as you need.")
%br
= _('Runners can be placed on separate users, servers, even on your local machine.')
%br
.row
.col-sm-6
.bs-callout
%p
= (_"A 'Runner' is a process which runs a job. You can set up as many Runners as you need.")
%br
= _('Runners can be placed on separate users, servers, even on your local machine.')
%br
%div
%span= _('Each Runner can be in one of the following states:')
%ul
%li
%span.badge.badge-success shared
\-
= _('Runner runs jobs from all unassigned projects')
%li
%span.badge.badge-success group
\-
= _('Runner runs jobs from all unassigned projects in its group')
%li
%span.badge.badge-info specific
\-
= _('Runner runs jobs from assigned projects')
%li
%span.badge.badge-warning locked
\-
= _('Runner cannot be assigned to other projects')
%li
%span.badge.badge-danger paused
\-
= _('Runner will not receive any new jobs')
%div
%span= _('Each Runner can be in one of the following states:')
%ul
%li
%span.badge.badge-success shared
\-
= _('Runner runs jobs from all unassigned projects')
%li
%span.badge.badge-success group
\-
= _('Runner runs jobs from all unassigned projects in its group')
%li
%span.badge.badge-info specific
\-
= _('Runner runs jobs from assigned projects')
%li
%span.badge.badge-warning locked
\-
= _('Runner cannot be assigned to other projects')
%li
%span.badge.badge-danger paused
\-
= _('Runner will not receive any new jobs')
.bs-callout.clearfix
.float-left
%p
= _('You can reset runners registration token by pressing a button below.')
.prepend-top-10
= button_to _('Reset runners registration token'), reset_runners_token_admin_application_settings_path,
method: :put, class: 'btn btn-default',
data: { confirm: _('Are you sure you want to reset registration token?') }
.col-sm-6
.bs-callout
= render partial: 'ci/runner/how_to_setup_runner',
locals: { registration_token: Gitlab::CurrentSettings.runners_registration_token,
type: 'shared',
reset_token_url: reset_registration_token_admin_application_settings_path }
= render partial: 'ci/runner/how_to_setup_shared_runner',
locals: { registration_token: Gitlab::CurrentSettings.runners_registration_token }
.row
.col-sm-9
= form_tag admin_runners_path, id: 'runners-search', method: :get, class: 'filter-form js-filter-form' do
.filtered-search-wrapper
.filtered-search-box
= dropdown_tag(custom_icon('icon_history'),
options: { wrapper_class: 'filtered-search-history-dropdown-wrapper',
toggle_class: 'filtered-search-history-dropdown-toggle-button',
dropdown_class: 'filtered-search-history-dropdown',
content_class: 'filtered-search-history-dropdown-content',
title: _('Recent searches') }) do
.js-filtered-search-history-dropdown{ data: { full_path: admin_runners_path } }
.filtered-search-box-input-container.droplab-dropdown
.scroll-container
%ul.tokens-container.list-unstyled
%li.input-token
%input.form-control.filtered-search{ { id: 'filtered-search-runners', placeholder: _('Search or filter results...') } }
#js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { action: 'submit' } }
= button_tag class: %w[btn btn-link] do
= sprite_icon('search')
%span
= _('Press Enter or click to search')
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
= button_tag class: %w[btn btn-link] do
-# Encapsulate static class name `{{icon}}` inside #{} to bypass
-# haml lint's ClassAttributeWithStaticValue
%svg
%use{ 'xlink:href': "#{'{{icon}}'}" }
%span.js-filter-hint
{{hint}}
%span.js-filter-tag.dropdown-light-content
{{tag}}
.bs-callout
%p
= _('Runners currently online: %{active_runners_count}') % { active_runners_count: @active_runners_count }
#js-dropdown-admin-runner-status.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
- Ci::Runner::AVAILABLE_STATUSES.each do |status|
%li.filter-dropdown-item{ data: { value: status } }
= button_tag class: %w[btn btn-link] do
= status.titleize
.row-content-block.second-block
= form_tag admin_runners_path, id: 'runners-search', method: :get, class: 'filter-form js-filter-form' do
.filtered-search-wrapper
.filtered-search-box
= dropdown_tag(custom_icon('icon_history'),
options: { wrapper_class: 'filtered-search-history-dropdown-wrapper',
toggle_class: 'filtered-search-history-dropdown-toggle-button',
dropdown_class: 'filtered-search-history-dropdown',
content_class: 'filtered-search-history-dropdown-content',
title: _('Recent searches') }) do
.js-filtered-search-history-dropdown{ data: { full_path: admin_runners_path } }
.filtered-search-box-input-container.droplab-dropdown
.scroll-container
%ul.tokens-container.list-unstyled
%li.input-token
%input.form-control.filtered-search{ { id: 'filtered-search-runners', placeholder: _('Search or filter results...') } }
#js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { action: 'submit' } }
= button_tag class: %w[btn btn-link] do
= sprite_icon('search')
%span
= _('Press Enter or click to search')
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
= button_tag class: %w[btn btn-link] do
-# Encapsulate static class name `{{icon}}` inside #{} to bypass
-# haml lint's ClassAttributeWithStaticValue
%svg
%use{ 'xlink:href': "#{'{{icon}}'}" }
%span.js-filter-hint
{{hint}}
%span.js-filter-tag.dropdown-light-content
{{tag}}
#js-dropdown-admin-runner-type.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
- Ci::Runner::AVAILABLE_TYPES.each do |runner_type|
%li.filter-dropdown-item{ data: { value: runner_type } }
= button_tag class: %w[btn btn-link] do
= runner_type.titleize
#js-dropdown-admin-runner-status.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
- Ci::Runner::AVAILABLE_STATUSES.each do |status|
%li.filter-dropdown-item{ data: { value: status } }
= button_tag class: %w[btn btn-link] do
= status.titleize
= button_tag class: %w[clear-search hidden] do
= icon('times')
.filter-dropdown-container
= render 'sort_dropdown'
#js-dropdown-admin-runner-type.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
- Ci::Runner::AVAILABLE_TYPES.each do |runner_type|
%li.filter-dropdown-item{ data: { value: runner_type } }
= button_tag class: %w[btn btn-link] do
= runner_type.titleize
= button_tag class: %w[clear-search hidden] do
= icon('times')
.filter-dropdown-container
= render 'sort_dropdown'
.col-sm-3.text-right-lg
= _('Runners currently online: %{active_runners_count}') % { active_runners_count: @active_runners_count }
- if @runners.any?
.runners-content.content-list
......
......@@ -13,5 +13,9 @@
= _("Use the following registration token during setup:")
%code#registration_token= registration_token
= clipboard_button(target: '#registration_token', title: _("Copy token to clipboard"), class: "btn-transparent btn-clipboard")
.prepend-top-10.append-bottom-10
= button_to _("Reset runners registration token"), reset_token_url,
method: :put, class: 'btn btn-default',
data: { confirm: _("Are you sure you want to reset registration token?") }
%li
= _("Start the Runner!")
.bs-callout.help-callout
= render partial: 'ci/runner/how_to_setup_runner',
locals: { registration_token: registration_token, type: 'shared' }
.bs-callout.help-callout
.append-bottom-10
%h4= _('Set up a specific Runner automatically')
%p
- link_to_help_page = link_to(_('Learn more about Kubernetes'),
help_page_path('user/project/clusters/index'),
target: '_blank',
rel: 'noopener noreferrer')
= _('You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page }
%ol
%li
= _('Click the button below to begin the install process by navigating to the Kubernetes page')
%li
= _('Select an existing Kubernetes cluster or create a new one')
%li
= _('From the Kubernetes cluster details view, install Runner from the applications list')
= link_to _('Install Runner on Kubernetes'),
project_clusters_path(@project),
class: 'btn btn-info'
%hr
= render partial: 'ci/runner/how_to_setup_runner',
locals: { registration_token: registration_token, type: 'specific' }
......@@ -11,7 +11,9 @@
-# https://gitlab.com/gitlab-org/gitlab-ce/issues/45894
- if can?(current_user, :admin_pipeline, @group)
= render partial: 'ci/runner/how_to_setup_runner',
locals: { registration_token: @group.runners_token, type: 'group' }
locals: { registration_token: @group.runners_token,
type: 'group',
reset_token_url: reset_registration_token_group_settings_ci_cd_path }
- if @group.runners.empty?
%h4.underlined-title
......
......@@ -5,7 +5,14 @@
.user-name.bold
= current_user.name
= current_user.to_reference
- if current_user.status
.user-status-emoji.str-truncated.has-tooltip{ title: current_user.status.message_html, data: { html: 'true', placement: 'bottom' } }
= emoji_icon current_user.status.emoji
= current_user.status.message_html.html_safe
%li.divider
- if can?(current_user, :update_user_status, current_user)
%li
.js-set-status-modal-trigger{ data: { has_status: current_user.status.present? ? 'true' : 'false' } }
- if current_user_menu?(:profile)
%li
= link_to s_("CurrentUser|Profile"), current_user, class: 'profile-link', data: { user: current_user.username }
......
......@@ -74,3 +74,6 @@
%span.sr-only= _('Toggle navigation')
= sprite_icon('ellipsis_h', size: 12, css_class: 'more-icon js-navbar-toggle-right')
= sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left')
- if can?(current_user, :update_user_status, current_user)
.js-set-status-modal-wrapper{ data: { current_emoji: current_user.status.present? ? current_user.status.emoji : '', current_message: current_user.status.present? ? current_user.status.message : '' } }
......@@ -25,7 +25,12 @@
= render 'bitbucket_import_modal'
- if bitbucket_server_import_enabled?
%div
<<<<<<< HEAD
= link_to status_import_bitbucket_server_path, class: "btn import_bitbucket", data: { track_label: "#{track_label}", track_event: "click_button", track_property: "bitbucket_server" } do
=======
= link_to status_import_bitbucket_server_path, class: "btn import_bitbucket",
data: { track_label: "#{track_label}", track_event: "click_button", track_property: "bitbucket_server" } do
>>>>>>> upstream/master
= icon('bitbucket-square', text: 'Bitbucket Server')
%div
- if gitlab_import_enabled?
......@@ -54,7 +59,11 @@
- if git_import_enabled?
%div
<<<<<<< HEAD
%button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active', track_label: "#{track_label}" , track_event: "click_button", track_property: "repo_url" } }
=======
%button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active', data: { toggle_open_class: 'active', track_label: "#{track_label}" , track_event: "click_button", track_property: "repo_url" } } }
>>>>>>> upstream/master
= icon('git', text: 'Repo by URL')
- if manifest_import_enabled?
......
......@@ -61,5 +61,9 @@
.option-description
Allows you to immediately clone this project’s repository. Skip this if you plan to push up an existing repository.
<<<<<<< HEAD
= f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4, data: { track_label: "#{track_label}", track_event: "click_button", track_property: "create_project", track_value: "" }
=======
= f.submit 'Create project', class: "btn btn-success project-submit", tabindex: 4, data: { track_label: "#{track_label}", track_event: "click_button", track_property: "create_project", track_value: "" }
>>>>>>> upstream/master
= link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "cancel" }
......@@ -61,15 +61,14 @@
%p.form-text.text-muted
= s_('ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}.').html_safe % { help_link_start_machine_type: help_link_start % { url: machine_type_link_url }, help_link_start_pricing: help_link_start % { url: pricing_link_url }, help_link_end: help_link_end }
- if rbac_clusters_feature_enabled?
.form-group
.form-check
= provider_gcp_field.check_box :legacy_abac, { class: 'form-check-input' }, false, true
= provider_gcp_field.label :legacy_abac, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold'
.form-text.text-muted
= s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
= s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
= link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-experimental-support'), target: '_blank'
.form-group
.form-check
= provider_gcp_field.check_box :legacy_abac, { class: 'form-check-input' }, false, true
= provider_gcp_field.label :legacy_abac, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold'
.form-text.text-muted
= s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
= s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
= link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-experimental-support'), target: '_blank'
.form-group
= field.submit s_('ClusterIntegration|Create Kubernetes cluster'), class: 'js-gke-cluster-creation-submit btn btn-success', disabled: true
......@@ -37,14 +37,13 @@
= platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
= platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
- if rbac_clusters_feature_enabled?
.form-group
.form-check
= platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac'
= platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold'
.form-text.text-muted
= s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
= s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
.form-group
.form-check
= platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac'
= platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold'
.form-text.text-muted
= s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
= s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
.form-group
= field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success'
......@@ -25,15 +25,14 @@
= platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold'
= platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
- if rbac_clusters_feature_enabled?
.form-group
.form-check
= platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input' }, 'rbac', 'abac'
= platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold'
.form-text.text-muted
= s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
= s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
= link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-experimental-support'), target: '_blank'
.form-group
.form-check
= platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input' }, 'rbac', 'abac'
= platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold'
.form-text.text-muted
= s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
= s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
= link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-experimental-support'), target: '_blank'
.form-group
= field.submit s_('ClusterIntegration|Add Kubernetes cluster'), class: 'btn btn-success'
......@@ -26,14 +26,13 @@
= platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold'
= platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
- if rbac_clusters_feature_enabled?
.form-group
.form-check
= platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac'
= platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold'
.form-text.text-muted
= s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
= s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
.form-group
.form-check
= platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac'
= platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold'
.form-text.text-muted
= s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
= s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
.form-group
= field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success'
......@@ -49,15 +49,16 @@
.environments-container
- if @deployments.blank?
.blank-state-row
.blank-state-center
%h2.blank-state-title
.empty-state
.text-content
%h4.state-title
You don't have any deployments right now.
%p.blank-state-text
Define environments in the deploy stage(s) in
%code .gitlab-ci.yml
to track deployments here.
= link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success"
.text-center
= link_to _("Read more"), help_page_path("ci/environments"), class: "btn btn-success"
- else
.table-holder
.ci-table.environments{ role: 'grid' }
......
%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } }
.sidebar-container
.blocks-container
#js-details-block-vue{ data: { terminal_path: can?(current_user, :create_build_terminal, @build) && @build.has_terminal? ? terminal_project_job_path(@project, @build) : nil } }
- if @build.pipeline.stages_count > 1
.block-last.dropdown.build-dropdown
%div
%span{ class: "ci-status-icon-#{@build.pipeline.status}" }
= ci_icon_for_status(@build.pipeline.status)
Pipeline
= link_to "##{@build.pipeline.id}", project_pipeline_path(@project, @build.pipeline), class: 'link-commit'
from
= link_to "#{@build.pipeline.ref}", project_ref_path(@project, @build.pipeline.ref), class: 'link-commit ref-name'
%button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span.stage-selection More
= icon('chevron-down')
%ul.dropdown-menu
- @build.pipeline.legacy_stages.each do |stage|
%li
%a.stage-item= stage.name
.builds-container
- HasStatus::ORDERED_STATUSES.each do |build_status|
- builds.select{|build| build.status == build_status}.each do |build|
.build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } }
- tooltip = sanitize(build.tooltip_message.dup)
= link_to(project_job_path(@project, build), data: { toggle: 'tooltip', title: tooltip, container: 'body' }) do
= sprite_icon('arrow-right', size:16, css_class: 'icon-arrow-right')
%span{ class: "ci-status-icon-#{build.status}" }
= ci_icon_for_status(build.status)
%span
- if build.name
= build.name
- else
= build.id
- if build.retried?
= sprite_icon('retry', size:16, css_class: 'icon-retry')
......@@ -9,6 +9,7 @@
%div{ class: container_class }
.build-page.js-build-page
#js-build-header-vue
<<<<<<< HEAD
- if @build.stuck?
- unless @build.any_runners_online?
.bs-callout.bs-callout-warning.js-build-stuck
......@@ -59,6 +60,8 @@
Job has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)}
- else
Job has been erased #{time_ago_with_tooltip(@build.erased_at)}
=======
>>>>>>> upstream/master
- if @build.running? || @build.has_trace?
.build-trace-container.prepend-top-default
......@@ -95,7 +98,7 @@
- else
= render "empty_states"
= render "sidebar", builds: @builds
#js-details-block-vue{ data: { terminal_path: can?(current_user, :create_build_terminal, @build) && @build.has_terminal? ? terminal_project_job_path(@project, @build) : nil } }
.js-build-options{ data: javascript_build_options }
......
......@@ -10,10 +10,17 @@
= template.description
.controls.d-flex.align-items-center
%label.btn.btn-success.template-button.choose-template.append-right-10.append-bottom-0{ for: template.name }
<<<<<<< HEAD
%input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name,
data: { track_label: "create_from_template", track_property: "template_use", track_event: "click_button" } }
%span
= _("Use template")
%a.btn.btn-default{ href: template.preview, rel: 'noopener noreferrer', target: '_blank',
data: { track_label: "create_from_template", track_property: "template_preview", track_event: "click_button", track_value: template.name } }
=======
%input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name, data: { track_label: "create_from_template", track_property: "template_use", track_event: "click_button" } }
%span
= _("Use template")
%a.btn.btn-default{ href: template.preview, rel: 'noopener noreferrer', target: '_blank', data: { track_label: "create_from_template", track_property: "template_preview", track_event: "click_button", track_value: template.name } }
>>>>>>> upstream/master
= _("Preview")
%h3
= _('Specific Runners')
= render partial: 'ci/runner/how_to_setup_specific_runner',
locals: { registration_token: @project.runners_token }
.bs-callout.help-callout
.append-bottom-10
%h4= _('Set up a specific Runner automatically')
%p
- link_to_help_page = link_to(_('Learn more about Kubernetes'),
help_page_path('user/project/clusters/index'),
target: '_blank',
rel: 'noopener noreferrer')
= _('You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page }
%ol
%li
= _('Click the button below to begin the install process by navigating to the Kubernetes page')
%li
= _('Select an existing Kubernetes cluster or create a new one')
%li
= _('From the Kubernetes cluster details view, install Runner from the applications list')
= link_to _('Install Runner on Kubernetes'),
project_clusters_path(@project),
class: 'btn btn-info'
%hr
= render partial: 'ci/runner/how_to_setup_runner',
locals: { registration_token: @project.runners_token,
type: 'specific',
reset_token_url: reset_registration_token_namespace_project_settings_ci_cd_path }
- if @project_runners.any?
%h4.underlined-title Runners activated for this project
......
......@@ -3,16 +3,6 @@
= form_for @project, url: project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings') do |f|
= form_errors(@project)
%fieldset.builds-feature
.form-group.append-bottom-default.js-secret-runner-token
= f.label :runners_token, _("Runner token"), class: 'label-bold'
.form-control.js-secret-value-placeholder
= '*' * 20
= f.text_field :runners_token, class: "form-control hide js-secret-value", placeholder: 'xEeFCaDAB89'
%p.form-text.text-muted= _("The secure token used by the Runner to checkout the project")
%button.btn.btn-info.prepend-top-10.js-secret-value-reveal-button{ type: 'button', data: { secret_reveal_status: 'false' } }
= _('Reveal value')
%hr
.form-group
%h5.prepend-top-0
= _("Git strategy for pipelines")
......
......@@ -12,7 +12,7 @@
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
= _("Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report.")
= _("Customize your pipeline configuration, view your pipeline status and coverage report.")
.settings-content
= render 'form'
......
......@@ -3,8 +3,12 @@
- restricted = restricted_visibility_levels.include?(level)
- disabled = disallowed || restricted
.form-check{ class: [('disabled' if disabled), ('restricted' if restricted)] }
<<<<<<< HEAD
= form.radio_button model_method, level, checked: (selected_level == level), disabled: disabled, class: 'form-check-input',
data: { track_label: "blank_project", track_event: "activate_form_input", track_property: "#{model_method}", track_value: "#{level}" }
=======
= form.radio_button model_method, level, checked: (selected_level == level), disabled: disabled, class: 'form-check-input', data: { track_label: "blank_project", track_event: "activate_form_input", track_property: "#{model_method}", track_value: "#{level}" }
>>>>>>> upstream/master
= form.label "#{model_method}_#{level}", class: 'form-check-label' do
= visibility_level_icon(level)
.option-title
......
---
title: Show percentage of language detection on the language bar
merge_request: 22056
author: Johann Hubert Sonntagbauer
type: added
---
title: Instance Configuration page now displays correct SSH fingerprints
merge_request: 22081
author:
type: fixed
---
title: Simplify runner registration token resetting
merge_request: 21658
author:
type: changed
---
title: Support db migration and initialization for Auto DevOps
merge_request: 21955
author:
type: added
---
title: Set user status from within user menu
merge_request: 21643
author:
type: added
---
title: Remove 'rbac_clusters' feature flag
merge_request: 22096
author:
type: changed
---
title: Includes commit stats in POST project commits API
merge_request: 21968
author: Jacopo Beschi @jacopo-beschi
type: fixed
---
title: Fix loading issue on some merge request discussion
merge_request: 21982
author:
type: fixed
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment