Commit 844e1980 authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'ce-to-ee-2018-10-04' into 'master'

CE upstream - 2018-10-04 10:35 UTC

Closes gitlab-ce#14249

See merge request gitlab-org/gitlab-ee!7795
parents c9672994 e1c583f8
{
"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 @@ ...@@ -6,7 +6,8 @@
/doc/ @axil @marcia /doc/ @axil @marcia
# Frontend maintainers should see everything in `app/assets/` # 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/` # Someone from the database team should review changes in `db/`
db/ @abrandl @NikolayS db/ @abrandl @NikolayS
......
...@@ -10,24 +10,6 @@ ...@@ -10,24 +10,6 @@
Capybara/CurrentPathExpectation: Capybara/CurrentPathExpectation:
Enabled: false 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 # Offense count: 167
# Cop supports --auto-correct. # Cop supports --auto-correct.
Layout/EmptyLinesAroundArguments: Layout/EmptyLinesAroundArguments:
...@@ -53,20 +35,6 @@ Layout/IndentArray: ...@@ -53,20 +35,6 @@ Layout/IndentArray:
Layout/IndentHash: Layout/IndentHash:
Enabled: false 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 # Offense count: 93
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle. # Configuration parameters: EnforcedStyle.
...@@ -74,15 +42,6 @@ Layout/SpaceBeforeFirstArg: ...@@ -74,15 +42,6 @@ Layout/SpaceBeforeFirstArg:
Layout/SpaceInLambdaLiteral: Layout/SpaceInLambdaLiteral:
Enabled: false 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 # Offense count: 327
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters. # Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters.
...@@ -96,14 +55,6 @@ Layout/SpaceInsideBlockBraces: ...@@ -96,14 +55,6 @@ Layout/SpaceInsideBlockBraces:
Layout/SpaceInsideParens: Layout/SpaceInsideParens:
Enabled: false 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 # Offense count: 26
Lint/DuplicateMethods: Lint/DuplicateMethods:
Exclude: Exclude:
...@@ -135,31 +86,11 @@ Lint/InterpolationCheck: ...@@ -135,31 +86,11 @@ Lint/InterpolationCheck:
Lint/MissingCopEnableDirective: Lint/MissingCopEnableDirective:
Enabled: false 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 # Offense count: 1
Lint/ReturnInVoidContext: Lint/ReturnInVoidContext:
Exclude: Exclude:
- 'app/models/project.rb' - '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 # Offense count: 9
Lint/UriEscapeUnescape: Lint/UriEscapeUnescape:
Exclude: Exclude:
...@@ -199,16 +130,6 @@ Naming/HeredocDelimiterCase: ...@@ -199,16 +130,6 @@ Naming/HeredocDelimiterCase:
Naming/HeredocDelimiterNaming: Naming/HeredocDelimiterNaming:
Enabled: false 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 # Offense count: 3821
# Configuration parameters: Prefixes. # Configuration parameters: Prefixes.
......
...@@ -94,7 +94,7 @@ GEM ...@@ -94,7 +94,7 @@ GEM
bindata (2.4.3) bindata (2.4.3)
binding_of_caller (0.7.2) binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
bootsnap (1.3.1) bootsnap (1.3.2)
msgpack (~> 1.0) msgpack (~> 1.0)
bootstrap_form (2.7.0) bootstrap_form (2.7.0)
brakeman (4.2.1) brakeman (4.2.1)
...@@ -148,7 +148,7 @@ GEM ...@@ -148,7 +148,7 @@ GEM
creole (0.5.0) creole (0.5.0)
css_parser (1.5.0) css_parser (1.5.0)
addressable addressable
daemons (1.2.3) daemons (1.2.6)
database_cleaner (1.5.3) database_cleaner (1.5.3)
debug_inspector (0.0.2) debug_inspector (0.0.2)
debugger-ruby_core_source (1.3.8) debugger-ruby_core_source (1.3.8)
...@@ -208,7 +208,7 @@ GEM ...@@ -208,7 +208,7 @@ GEM
escape_utils (1.1.1) escape_utils (1.1.1)
et-orbi (1.0.3) et-orbi (1.0.3)
tzinfo tzinfo
eventmachine (1.0.8) eventmachine (1.2.7)
excon (0.62.0) excon (0.62.0)
execjs (2.6.0) execjs (2.6.0)
expression_parser (0.9.0) expression_parser (0.9.0)
...@@ -516,7 +516,7 @@ GEM ...@@ -516,7 +516,7 @@ GEM
mime-types-data (3.2016.0521) mime-types-data (3.2016.0521)
mimemagic (0.3.0) mimemagic (0.3.0)
mini_magick (4.8.0) mini_magick (4.8.0)
mini_mime (1.0.0) mini_mime (1.0.1)
mini_portile2 (2.3.0) mini_portile2 (2.3.0)
minitest (5.7.0) minitest (5.7.0)
mousetrap-rails (1.4.6) mousetrap-rails (1.4.6)
...@@ -653,9 +653,9 @@ GEM ...@@ -653,9 +653,9 @@ GEM
pry-byebug (3.4.3) pry-byebug (3.4.3)
byebug (>= 9.0, < 9.1) byebug (>= 9.0, < 9.1)
pry (~> 0.10) pry (~> 0.10)
pry-rails (0.3.5) pry-rails (0.3.6)
pry (>= 0.9.10) pry (>= 0.10.4)
public_suffix (3.0.2) public_suffix (3.0.3)
pyu-ruby-sasl (0.0.3.3) pyu-ruby-sasl (0.0.3.3)
rack (1.6.10) rack (1.6.10)
rack-accept (0.4.5) rack-accept (0.4.5)
...@@ -882,7 +882,7 @@ GEM ...@@ -882,7 +882,7 @@ GEM
simplecov-html (~> 0.10.0) simplecov-html (~> 0.10.0)
simplecov-html (0.10.0) simplecov-html (0.10.0)
slack-notifier (1.5.1) slack-notifier (1.5.1)
spring (2.0.1) spring (2.0.2)
activesupport (>= 4.2) activesupport (>= 4.2)
spring-commands-rspec (1.0.4) spring-commands-rspec (1.0.4)
spring (>= 0.9.1) spring (>= 0.9.1)
...@@ -912,7 +912,7 @@ GEM ...@@ -912,7 +912,7 @@ GEM
test_after_commit (1.1.0) test_after_commit (1.1.0)
activerecord (>= 3.2) activerecord (>= 3.2)
text (1.3.1) text (1.3.1)
thin (1.7.0) thin (1.7.2)
daemons (~> 1.0, >= 1.0.9) daemons (~> 1.0, >= 1.0.9)
eventmachine (~> 1.0, >= 1.0.4) eventmachine (~> 1.0, >= 1.0.4)
rack (>= 1, < 3) rack (>= 1, < 3)
...@@ -971,7 +971,7 @@ GEM ...@@ -971,7 +971,7 @@ GEM
addressable (>= 2.3.6) addressable (>= 2.3.6)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff hashdiff
webpack-rails (0.9.10) webpack-rails (0.9.11)
railties (>= 3.2.0) railties (>= 3.2.0)
wikicloth (0.8.1) wikicloth (0.8.1)
builder builder
......
...@@ -97,7 +97,7 @@ GEM ...@@ -97,7 +97,7 @@ GEM
bindata (2.4.3) bindata (2.4.3)
binding_of_caller (0.7.2) binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
bootsnap (1.3.1) bootsnap (1.3.2)
msgpack (~> 1.0) msgpack (~> 1.0)
bootstrap_form (2.7.0) bootstrap_form (2.7.0)
brakeman (4.2.1) brakeman (4.2.1)
...@@ -151,7 +151,7 @@ GEM ...@@ -151,7 +151,7 @@ GEM
creole (0.5.0) creole (0.5.0)
css_parser (1.5.0) css_parser (1.5.0)
addressable addressable
daemons (1.2.3) daemons (1.2.6)
database_cleaner (1.5.3) database_cleaner (1.5.3)
debug_inspector (0.0.2) debug_inspector (0.0.2)
debugger-ruby_core_source (1.3.8) debugger-ruby_core_source (1.3.8)
...@@ -211,7 +211,7 @@ GEM ...@@ -211,7 +211,7 @@ GEM
escape_utils (1.1.1) escape_utils (1.1.1)
et-orbi (1.0.3) et-orbi (1.0.3)
tzinfo tzinfo
eventmachine (1.0.8) eventmachine (1.2.7)
excon (0.62.0) excon (0.62.0)
execjs (2.6.0) execjs (2.6.0)
expression_parser (0.9.0) expression_parser (0.9.0)
...@@ -434,7 +434,7 @@ GEM ...@@ -434,7 +434,7 @@ GEM
json (~> 1.8) json (~> 1.8)
multi_xml (>= 0.5.2) multi_xml (>= 0.5.2)
httpclient (2.8.3) httpclient (2.8.3)
i18n (1.0.1) i18n (1.1.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
icalendar (2.4.1) icalendar (2.4.1)
ice_nine (0.11.2) ice_nine (0.11.2)
...@@ -519,7 +519,7 @@ GEM ...@@ -519,7 +519,7 @@ GEM
mime-types-data (3.2016.0521) mime-types-data (3.2016.0521)
mimemagic (0.3.0) mimemagic (0.3.0)
mini_magick (4.8.0) mini_magick (4.8.0)
mini_mime (1.0.0) mini_mime (1.0.1)
mini_portile2 (2.3.0) mini_portile2 (2.3.0)
minitest (5.7.0) minitest (5.7.0)
mousetrap-rails (1.4.6) mousetrap-rails (1.4.6)
...@@ -657,9 +657,9 @@ GEM ...@@ -657,9 +657,9 @@ GEM
pry-byebug (3.4.3) pry-byebug (3.4.3)
byebug (>= 9.0, < 9.1) byebug (>= 9.0, < 9.1)
pry (~> 0.10) pry (~> 0.10)
pry-rails (0.3.5) pry-rails (0.3.6)
pry (>= 0.9.10) pry (>= 0.10.4)
public_suffix (3.0.2) public_suffix (3.0.3)
pyu-ruby-sasl (0.0.3.3) pyu-ruby-sasl (0.0.3.3)
rack (2.0.5) rack (2.0.5)
rack-accept (0.4.5) rack-accept (0.4.5)
...@@ -890,7 +890,7 @@ GEM ...@@ -890,7 +890,7 @@ GEM
simplecov-html (~> 0.10.0) simplecov-html (~> 0.10.0)
simplecov-html (0.10.0) simplecov-html (0.10.0)
slack-notifier (1.5.1) slack-notifier (1.5.1)
spring (2.0.1) spring (2.0.2)
activesupport (>= 4.2) activesupport (>= 4.2)
spring-commands-rspec (1.0.4) spring-commands-rspec (1.0.4)
spring (>= 0.9.1) spring (>= 0.9.1)
...@@ -918,7 +918,7 @@ GEM ...@@ -918,7 +918,7 @@ GEM
temple (0.8.0) temple (0.8.0)
test-prof (0.2.5) test-prof (0.2.5)
text (1.3.1) text (1.3.1)
thin (1.7.0) thin (1.7.2)
daemons (~> 1.0, >= 1.0.9) daemons (~> 1.0, >= 1.0.9)
eventmachine (~> 1.0, >= 1.0.4) eventmachine (~> 1.0, >= 1.0.4)
rack (>= 1, < 3) rack (>= 1, < 3)
...@@ -977,7 +977,7 @@ GEM ...@@ -977,7 +977,7 @@ GEM
addressable (>= 2.3.6) addressable (>= 2.3.6)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff hashdiff
webpack-rails (0.9.10) webpack-rails (0.9.11)
railties (>= 3.2.0) railties (>= 3.2.0)
websocket-driver (0.6.5) websocket-driver (0.6.5)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
......
...@@ -23,6 +23,7 @@ const Api = { ...@@ -23,6 +23,7 @@ const Api = {
dockerfilePath: '/api/:version/templates/dockerfiles/:key', dockerfilePath: '/api/:version/templates/dockerfiles/:key',
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key', issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
usersPath: '/api/:version/users.json', usersPath: '/api/:version/users.json',
userStatusPath: '/api/:version/user/status',
commitPath: '/api/:version/projects/:id/repository/commits', commitPath: '/api/:version/projects/:id/repository/commits',
commitPipelinesPath: '/:project_id/commit/:sha/pipelines', commitPipelinesPath: '/:project_id/commit/:sha/pipelines',
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch', branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
...@@ -268,6 +269,15 @@ const Api = { ...@@ -268,6 +269,15 @@ const Api = {
}); });
}, },
postUserStatus({ emoji, message }) {
const url = Api.buildUrl(this.userStatusPath);
return axios.put(url, {
emoji,
message,
});
},
templates(key, params = {}) { templates(key, params = {}) {
const url = Api.buildUrl(this.templatesPath).replace(':key', key); const url = Api.buildUrl(this.templatesPath).replace(':key', key);
......
...@@ -42,10 +42,11 @@ export class AwardsHandler { ...@@ -42,10 +42,11 @@ export class AwardsHandler {
} }
bindEvents() { bindEvents() {
const $parentEl = this.targetContainerEl ? $(this.targetContainerEl) : $(document);
// If the user shows intent let's pre-build the menu // If the user shows intent let's pre-build the menu
this.registerEventListener( this.registerEventListener(
'one', 'one',
$(document), $parentEl,
'mouseenter focus', 'mouseenter focus',
this.toggleButtonSelector, this.toggleButtonSelector,
'mouseenter focus', 'mouseenter focus',
...@@ -58,7 +59,7 @@ export class AwardsHandler { ...@@ -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.stopPropagation();
e.preventDefault(); e.preventDefault();
this.showEmojiMenu($(e.currentTarget)); this.showEmojiMenu($(e.currentTarget));
...@@ -76,7 +77,7 @@ export class AwardsHandler { ...@@ -76,7 +77,7 @@ export class AwardsHandler {
}); });
const emojiButtonSelector = `.js-awards-block .js-emoji-btn, .${this.menuClass} .js-emoji-btn`; 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(); e.preventDefault();
const $target = $(e.currentTarget); const $target = $(e.currentTarget);
const $glEmojiElement = $target.find('gl-emoji'); const $glEmojiElement = $target.find('gl-emoji');
...@@ -168,7 +169,8 @@ export class AwardsHandler { ...@@ -168,7 +169,8 @@ export class AwardsHandler {
</div> </div>
`; `;
document.body.insertAdjacentHTML('beforeend', emojiMenuMarkup); const targetEl = this.targetContainerEl ? this.targetContainerEl : document.body;
targetEl.insertAdjacentHTML('beforeend', emojiMenuMarkup);
this.addRemainingEmojiMenuCategories(); this.addRemainingEmojiMenuCategories();
this.setupSearch(); this.setupSearch();
...@@ -250,6 +252,12 @@ export class AwardsHandler { ...@@ -250,6 +252,12 @@ export class AwardsHandler {
} }
positionMenu($menu, $addBtn) { positionMenu($menu, $addBtn) {
if (this.targetContainerEl) {
return $menu.css({
top: `${$addBtn.outerHeight()}px`,
});
}
const position = $addBtn.data('position'); const position = $addBtn.data('position');
// The menu could potentially be off-screen or in a hidden overflow element // The menu could potentially be off-screen or in a hidden overflow element
// So we position the element absolute in the body // So we position the element absolute in the body
...@@ -424,9 +432,7 @@ export class AwardsHandler { ...@@ -424,9 +432,7 @@ export class AwardsHandler {
users = origTitle.trim().split(FROM_SENTENCE_REGEX); users = origTitle.trim().split(FROM_SENTENCE_REGEX);
} }
users.unshift('You'); users.unshift('You');
return awardBlock return awardBlock.attr('title', this.toSentence(users)).tooltip('_fixTitle');
.attr('title', this.toSentence(users))
.tooltip('_fixTitle');
} }
createAwardButtonForVotesBlock(votesBlock, emojiName) { createAwardButtonForVotesBlock(votesBlock, emojiName) {
...@@ -609,13 +615,11 @@ export class AwardsHandler { ...@@ -609,13 +615,11 @@ export class AwardsHandler {
let awardsHandlerPromise = null; let awardsHandlerPromise = null;
export default function loadAwardsHandler(reload = false) { export default function loadAwardsHandler(reload = false) {
if (!awardsHandlerPromise || reload) { if (!awardsHandlerPromise || reload) {
awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji').then( awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji').then(Emoji => {
Emoji => {
const awardsHandler = new AwardsHandler(Emoji); const awardsHandler = new AwardsHandler(Emoji);
awardsHandler.bindEvents(); awardsHandler.bindEvents();
return awardsHandler; return awardsHandler;
}, });
);
} }
return awardsHandlerPromise; return awardsHandlerPromise;
} }
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import { Button } from '@gitlab-org/gitlab-ui';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
import ProjectSelect from './project_select.vue'; import ProjectSelect from './project_select.vue';
import ListIssue from '../models/issue'; import ListIssue from '../models/issue';
...@@ -10,6 +11,7 @@ export default { ...@@ -10,6 +11,7 @@ export default {
name: 'BoardNewIssue', name: 'BoardNewIssue',
components: { components: {
ProjectSelect, ProjectSelect,
'gl-button': Button,
}, },
props: { props: {
groupId: { groupId: {
...@@ -126,21 +128,23 @@ export default { ...@@ -126,21 +128,23 @@ export default {
:group-id="groupId" :group-id="groupId"
/> />
<div class="clearfix prepend-top-10"> <div class="clearfix prepend-top-10">
<button <gl-button
ref="submit-button" ref="submit-button"
:disabled="disabled" :disabled="disabled"
class="btn btn-success float-left" class="float-left"
variant="success"
type="submit" type="submit"
> >
Submit issue Submit issue
</button> </gl-button>
<button <gl-button
class="btn btn-default float-right" class="float-right"
type="button" type="button"
variant="default"
@click="cancel" @click="cancel"
> >
Cancel Cancel
</button> </gl-button>
</div> </div>
</form> </form>
</div> </div>
......
...@@ -5,22 +5,22 @@ import { __ } from '~/locale'; ...@@ -5,22 +5,22 @@ import { __ } from '~/locale';
import createFlash from '~/flash'; import createFlash from '~/flash';
import eventHub from '../../notes/event_hub'; import eventHub from '../../notes/event_hub';
import CompareVersions from './compare_versions.vue'; import CompareVersions from './compare_versions.vue';
import ChangedFiles from './changed_files.vue';
import DiffFile from './diff_file.vue'; import DiffFile from './diff_file.vue';
import NoChanges from './no_changes.vue'; import NoChanges from './no_changes.vue';
import HiddenFilesWarning from './hidden_files_warning.vue'; import HiddenFilesWarning from './hidden_files_warning.vue';
import CommitWidget from './commit_widget.vue'; import CommitWidget from './commit_widget.vue';
import TreeList from './tree_list.vue';
export default { export default {
name: 'DiffsApp', name: 'DiffsApp',
components: { components: {
Icon, Icon,
CompareVersions, CompareVersions,
ChangedFiles,
DiffFile, DiffFile,
NoChanges, NoChanges,
HiddenFilesWarning, HiddenFilesWarning,
CommitWidget, CommitWidget,
TreeList,
}, },
props: { props: {
endpoint: { endpoint: {
...@@ -58,6 +58,7 @@ export default { ...@@ -58,6 +58,7 @@ export default {
plainDiffPath: state => state.diffs.plainDiffPath, plainDiffPath: state => state.diffs.plainDiffPath,
emailPatchPath: state => state.diffs.emailPatchPath, emailPatchPath: state => state.diffs.emailPatchPath,
}), }),
...mapState('diffs', ['showTreeList']),
...mapGetters('diffs', ['isParallelView']), ...mapGetters('diffs', ['isParallelView']),
...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']), ...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']),
targetBranch() { targetBranch() {
...@@ -88,6 +89,9 @@ export default { ...@@ -88,6 +89,9 @@ export default {
canCurrentUserFork() { canCurrentUserFork() {
return this.currentUser.canFork === true && this.currentUser.canCreateMergeRequest; return this.currentUser.canFork === true && this.currentUser.canCreateMergeRequest;
}, },
showCompareVersions() {
return this.mergeRequestDiffs && this.mergeRequestDiff;
},
}, },
watch: { watch: {
diffViewType() { diffViewType() {
...@@ -102,6 +106,8 @@ export default { ...@@ -102,6 +106,8 @@ export default {
this.adjustView(); this.adjustView();
}, },
isLoading: 'adjustView',
showTreeList: 'adjustView',
}, },
mounted() { mounted() {
this.setBaseConfig({ endpoint: this.endpoint, projectPath: this.projectPath }); this.setBaseConfig({ endpoint: this.endpoint, projectPath: this.projectPath });
...@@ -152,10 +158,11 @@ export default { ...@@ -152,10 +158,11 @@ export default {
} }
}, },
adjustView() { adjustView() {
if (this.shouldShow && this.isParallelView) { if (this.shouldShow) {
window.mrTabs.expandViewContainer(); this.$nextTick(() => {
} else {
window.mrTabs.resetViewContainer(); window.mrTabs.resetViewContainer();
window.mrTabs.expandViewContainer(this.showTreeList);
});
} }
}, },
}, },
...@@ -177,7 +184,7 @@ export default { ...@@ -177,7 +184,7 @@ export default {
class="diffs tab-pane" class="diffs tab-pane"
> >
<compare-versions <compare-versions
v-if="!commit && mergeRequestDiffs.length > 1" v-if="showCompareVersions"
:merge-request-diffs="mergeRequestDiffs" :merge-request-diffs="mergeRequestDiffs"
:merge-request-diff="mergeRequestDiff" :merge-request-diff="mergeRequestDiff"
:start-version="startVersion" :start-version="startVersion"
...@@ -215,13 +222,16 @@ export default { ...@@ -215,13 +222,16 @@ export default {
:commit="commit" :commit="commit"
/> />
<changed-files <div class="files d-flex prepend-top-default">
:diff-files="diffFiles" <div
/> v-show="showTreeList"
class="diff-tree-list"
>
<tree-list />
</div>
<div <div
v-if="diffFiles.length > 0" v-if="diffFiles.length > 0"
class="files" class="diff-files-holder"
> >
<diff-file <diff-file
v-for="file in diffFiles" v-for="file in diffFiles"
...@@ -233,4 +243,5 @@ export default { ...@@ -233,4 +243,5 @@ export default {
<no-changes v-else /> <no-changes v-else />
</div> </div>
</div> </div>
</div>
</template> </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> <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'; import CompareVersionsDropdown from './compare_versions_dropdown.vue';
export default { export default {
components: { components: {
CompareVersionsDropdown, CompareVersionsDropdown,
Icon,
},
directives: {
Tooltip,
}, },
props: { props: {
mergeRequestDiffs: { mergeRequestDiffs: {
...@@ -26,16 +35,65 @@ export default { ...@@ -26,16 +35,65 @@ export default {
}, },
}, },
computed: { computed: {
...mapState('diffs', ['commit', 'showTreeList']),
...mapGetters('diffs', ['isInlineView', 'isParallelView', 'areAllFilesCollapsed']),
comparableDiffs() { comparableDiffs() {
return this.mergeRequestDiffs.slice(1); 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> </script>
<template> <template>
<div class="mr-version-controls"> <div class="mr-version-controls">
<div class="mr-version-menus-container content-block"> <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 Changes between
<compare-versions-dropdown <compare-versions-dropdown
:other-versions="mergeRequestDiffs" :other-versions="mergeRequestDiffs"
...@@ -51,5 +109,45 @@ export default { ...@@ -51,5 +109,45 @@ export default {
class="mr-version-compare-dropdown" class="mr-version-compare-dropdown"
/> />
</div> </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> </div>
</template> </template>
...@@ -108,7 +108,7 @@ export default { ...@@ -108,7 +108,7 @@ export default {
<template> <template>
<span class="dropdown inline"> <span class="dropdown inline">
<a <a
class="dropdown-toggle btn btn-default" class="dropdown-menu-toggle btn btn-default w-100"
data-toggle="dropdown" data-toggle="dropdown"
aria-expanded="false" aria-expanded="false"
> >
...@@ -118,6 +118,7 @@ export default { ...@@ -118,6 +118,7 @@ export default {
<Icon <Icon
:size="12" :size="12"
name="angle-down" name="angle-down"
class="position-absolute"
/> />
</a> </a>
<div class="dropdown-menu dropdown-select dropdown-menu-selectable"> <div class="dropdown-menu dropdown-select dropdown-menu-selectable">
...@@ -163,3 +164,10 @@ export default { ...@@ -163,3 +164,10 @@ export default {
</div> </div>
</span> </span>
</template> </template>
<style>
.dropdown {
min-width: 0;
max-height: 170px;
}
</style>
<script> <script>
import { mapActions, mapGetters } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore'; import _ from 'underscore';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import createFlash from '~/flash'; import createFlash from '~/flash';
...@@ -28,6 +28,7 @@ export default { ...@@ -28,6 +28,7 @@ export default {
}; };
}, },
computed: { computed: {
...mapState('diffs', ['currentDiffFileId']),
...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']), ...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']),
isCollapsed() { isCollapsed() {
return this.file.collapsed || false; return this.file.collapsed || false;
...@@ -101,6 +102,9 @@ export default { ...@@ -101,6 +102,9 @@ export default {
<template> <template>
<div <div
:id="file.fileHash" :id="file.fileHash"
:class="{
'is-active': currentDiffFileId === file.fileHash
}"
class="diff-file file-holder" class="diff-file file-holder"
> >
<diff-file-header <diff-file-header
...@@ -168,3 +172,20 @@ export default { ...@@ -168,3 +172,20 @@ export default {
</div> </div>
</div> </div>
</template> </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 { ...@@ -166,18 +166,16 @@ export default {
:title="diffFile.oldPath" :title="diffFile.oldPath"
class="file-title-name" class="file-title-name"
data-container="body" data-container="body"
> v-html="diffFile.oldPathHtml"
{{ diffFile.oldPath }} ></strong>
</strong>
<strong <strong
v-tooltip v-tooltip
:title="diffFile.newPath" :title="diffFile.newPath"
class="file-title-name" class="file-title-name"
data-container="body" data-container="body"
> v-html="diffFile.newPathHtml"
{{ diffFile.newPath }} ></strong>
</strong>
</span> </span>
<strong <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; ...@@ -29,3 +29,5 @@ export const LENGTH_OF_AVATAR_TOOLTIP = 17;
export const LINES_TO_BE_RENDERED_DIRECTLY = 100; export const LINES_TO_BE_RENDERED_DIRECTLY = 100;
export const MAX_LINES_TO_BE_RENDERED = 2000; 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 { ...@@ -12,6 +12,7 @@ import {
PARALLEL_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE,
INLINE_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE,
DIFF_VIEW_COOKIE_NAME, DIFF_VIEW_COOKIE_NAME,
MR_TREE_SHOW_KEY,
} from '../constants'; } from '../constants';
export const setBaseConfig = ({ commit }, options) => { export const setBaseConfig = ({ commit }, options) => {
...@@ -195,5 +196,23 @@ export const saveDiffDiscussion = ({ dispatch }, { note, formData }) => { ...@@ -195,5 +196,23 @@ export const saveDiffDiscussion = ({ dispatch }, { note, formData }) => {
.catch(() => createFlash(s__('MergeRequests|Saving the comment failed'))); .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 // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
...@@ -110,5 +110,9 @@ export const shouldRenderInlineCommentRow = state => line => { ...@@ -110,5 +110,9 @@ export const shouldRenderInlineCommentRow = state => line => {
export const getDiffFileByHash = state => fileHash => export const getDiffFileByHash = state => fileHash =>
state.diffFiles.find(file => file.fileHash === 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 // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { getParameterValues } from '~/lib/utils/url_utility'; 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 viewTypeFromQueryString = getParameterValues('view')[0];
const viewTypeFromCookie = Cookies.get(DIFF_VIEW_COOKIE_NAME); const viewTypeFromCookie = Cookies.get(DIFF_VIEW_COOKIE_NAME);
const defaultViewType = INLINE_DIFF_VIEW_TYPE; const defaultViewType = INLINE_DIFF_VIEW_TYPE;
const storedTreeShow = localStorage.getItem(MR_TREE_SHOW_KEY);
export default () => ({ export default () => ({
isLoading: true, isLoading: true,
...@@ -17,4 +18,8 @@ export default () => ({ ...@@ -17,4 +18,8 @@ export default () => ({
mergeRequestDiff: null, mergeRequestDiff: null,
diffLineCommentForms: {}, diffLineCommentForms: {},
diffViewType: viewTypeFromQueryString || viewTypeFromCookie || defaultViewType, 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'; ...@@ -11,3 +11,6 @@ export const EXPAND_ALL_FILES = 'EXPAND_ALL_FILES';
export const RENDER_FILE = 'RENDER_FILE'; export const RENDER_FILE = 'RENDER_FILE';
export const SET_LINE_DISCUSSIONS_FOR_FILE = 'SET_LINE_DISCUSSIONS_FOR_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 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 Vue from 'vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { sortTree } from '~/ide/stores/utils';
import { import {
findDiffFile, findDiffFile,
addLineReferences, addLineReferences,
...@@ -7,6 +8,7 @@ import { ...@@ -7,6 +8,7 @@ import {
addContextLines, addContextLines,
prepareDiffData, prepareDiffData,
isDiscussionApplicableToLine, isDiscussionApplicableToLine,
generateTreeList,
} from './utils'; } from './utils';
import * as types from './mutation_types'; import * as types from './mutation_types';
...@@ -23,9 +25,12 @@ export default { ...@@ -23,9 +25,12 @@ export default {
[types.SET_DIFF_DATA](state, data) { [types.SET_DIFF_DATA](state, data) {
const diffData = convertObjectPropsToCamelCase(data, { deep: true }); const diffData = convertObjectPropsToCamelCase(data, { deep: true });
prepareDiffData(diffData); prepareDiffData(diffData);
const { tree, treeEntries } = generateTreeList(diffData.diffFiles);
Object.assign(state, { Object.assign(state, {
...diffData, ...diffData,
tree: sortTree(tree),
treeEntries,
}); });
}, },
...@@ -163,4 +168,13 @@ export default { ...@@ -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) { ...@@ -244,6 +244,7 @@ export function getDiffPositionByLineCode(diffFiles) {
oldLine, oldLine,
newLine, newLine,
lineCode, lineCode,
positionType: 'text',
}; };
} }
}); });
...@@ -259,11 +260,57 @@ export function isDiscussionApplicableToLine({ discussion, diffPosition, latestD ...@@ -259,11 +260,57 @@ export function isDiscussionApplicableToLine({ discussion, diffPosition, latestD
const { lineCode, ...diffPositionCopy } = diffPosition; const { lineCode, ...diffPositionCopy } = diffPosition;
if (discussion.original_position && discussion.position) { if (discussion.original_position && discussion.position) {
const originalRefs = convertObjectPropsToCamelCase(discussion.original_position.formatter); const originalRefs = convertObjectPropsToCamelCase(discussion.original_position);
const refs = convertObjectPropsToCamelCase(discussion.position.formatter); const refs = convertObjectPropsToCamelCase(discussion.position);
return _.isEqual(refs, diffPositionCopy) || _.isEqual(originalRefs, diffPositionCopy); return _.isEqual(refs, diffPositionCopy) || _.isEqual(originalRefs, diffPositionCopy);
} }
return latestDiff && discussion.active && lineCode === discussion.line_code; 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 @@ ...@@ -2,12 +2,14 @@
/** /**
* Renders the Monitoring (Metrics) link in environments table. * Renders the Monitoring (Metrics) link in environments table.
*/ */
import { Button } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
components: { components: {
Icon, Icon,
'gl-button': Button,
}, },
directives: { directives: {
tooltip, tooltip,
...@@ -26,15 +28,16 @@ export default { ...@@ -26,15 +28,16 @@ export default {
}; };
</script> </script>
<template> <template>
<a <gl-button
v-tooltip v-tooltip
:href="monitoringUrl" :href="monitoringUrl"
:title="title" :title="title"
:aria-label="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" data-container="body"
rel="noopener noreferrer nofollow" rel="noopener noreferrer nofollow"
variant="default"
> >
<icon name="chart" /> <icon name="chart" />
</a> </gl-button>
</template> </template>
import $ from 'jquery'; import $ from 'jquery';
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import { highCountTrim } from '~/lib/utils/text_utility'; 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. * Updates todo counter when todos are toggled.
...@@ -17,3 +21,54 @@ export default function initTodoToggle() { ...@@ -17,3 +21,54 @@ export default function initTodoToggle() {
$todoPendingCount.toggleClass('hidden', parsedCount === 0); $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'; ...@@ -3,7 +3,7 @@ import $ from 'jquery';
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import { __ } from '~/locale'; import { __ } from '~/locale';
import FileIcon from '~/vue_shared/components/file_icon.vue'; 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 { export default {
components: { components: {
......
<script> <script>
import fuzzaldrinPlus from 'fuzzaldrin-plus'; import fuzzaldrinPlus from 'fuzzaldrin-plus';
import FileIcon from '../../../vue_shared/components/file_icon.vue'; 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; const MAX_PATH_LENGTH = 60;
......
...@@ -3,8 +3,8 @@ import { mapGetters } from 'vuex'; ...@@ -3,8 +3,8 @@ import { mapGetters } from 'vuex';
import { n__, __, sprintf } from '~/locale'; import { n__, __, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue'; 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 NewDropdown from './new_dropdown/index.vue';
import ChangedFileIcon from './changed_file_icon.vue';
import MrFileIcon from './mr_file_icon.vue'; import MrFileIcon from './mr_file_icon.vue';
export default { export default {
......
...@@ -3,8 +3,8 @@ import { mapActions } from 'vuex'; ...@@ -3,8 +3,8 @@ import { mapActions } from 'vuex';
import FileIcon from '~/vue_shared/components/file_icon.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue';
import Icon from '~/vue_shared/components/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 FileStatusIcon from './repo_file_status_icon.vue';
import ChangedFileIcon from './changed_file_icon.vue';
export default { export default {
components: { components: {
......
...@@ -24,7 +24,6 @@ export default class Job extends LogOutputBehaviours { ...@@ -24,7 +24,6 @@ export default class Job extends LogOutputBehaviours {
this.$document = $(document); this.$document = $(document);
this.$window = $(window); this.$window = $(window);
this.logBytes = 0; this.logBytes = 0;
this.updateDropdown = this.updateDropdown.bind(this);
this.$buildTrace = $('#build-trace'); this.$buildTrace = $('#build-trace');
this.$buildRefreshAnimation = $('.js-build-refresh'); this.$buildRefreshAnimation = $('.js-build-refresh');
...@@ -35,18 +34,12 @@ export default class Job extends LogOutputBehaviours { ...@@ -35,18 +34,12 @@ export default class Job extends LogOutputBehaviours {
clearTimeout(this.timeout); clearTimeout(this.timeout);
this.initSidebar(); this.initSidebar();
this.populateJobs(this.buildStage);
this.updateStageDropdownText(this.buildStage);
this.sidebarOnResize(); this.sidebarOnResize();
this.$document this.$document
.off('click', '.js-sidebar-build-toggle') .off('click', '.js-sidebar-build-toggle')
.on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this)); .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.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100);
this.$window this.$window
...@@ -194,20 +187,4 @@ export default class Job extends LogOutputBehaviours { ...@@ -194,20 +187,4 @@ export default class Job extends LogOutputBehaviours {
if (this.shouldHideSidebarForViewport()) this.toggleSidebar(); 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 @@ ...@@ -46,7 +46,7 @@
v-if="mergeRequest" v-if="mergeRequest"
:href="mergeRequest.path" :href="mergeRequest.path"
class="js-link-commit link-commit" class="js-link-commit link-commit"
>{{ mergeRequest.iid }}</a> >!{{ mergeRequest.iid }}</a>
</p> </p>
<p class="build-light-text append-bottom-0"> <p class="build-light-text append-bottom-0">
......
...@@ -87,8 +87,8 @@ ...@@ -87,8 +87,8 @@
<shared-runner <shared-runner
v-if="shouldRenderSharedRunnerLimitWarning" v-if="shouldRenderSharedRunnerLimitWarning"
class="js-shared-runner-limit" class="js-shared-runner-limit"
:quota-used="job.runner.quota.used" :quota-used="job.runners.quota.used"
:quota-limit="job.runner.quota.limit" :quota-limit="job.runners.quota.limit"
:runners-path="runnerHelpUrl" :runners-path="runnerHelpUrl"
/> />
......
<script> <script>
import _ from 'underscore';
import CiIcon from '~/vue_shared/components/ci_icon.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
...@@ -16,26 +17,39 @@ ...@@ -16,26 +17,39 @@
type: Array, type: Array,
required: true, required: true,
}, },
jobId: {
type: Number,
required: true,
},
},
methods: {
isJobActive(currentJobId) {
return this.jobId === currentJobId;
},
tooltipText(job) {
return `${_.escape(job.name)} - ${job.status.tooltip}`;
},
}, },
}; };
</script> </script>
<template> <template>
<div class="builds-container"> <div class="js-jobs-container builds-container">
<div <div
v-for="job in jobs"
:key="job.id"
class="build-job" class="build-job"
:class="{ retried: job.retried, active: isJobActive(job.id) }"
> >
<a <a
v-for="job in jobs"
:key="job.id"
v-tooltip v-tooltip
:href="job.path" :href="job.status.details_path"
:title="job.tooltip" :title="tooltipText(job)"
:class="{ active: job.active, retried: job.retried }" data-container="body"
> >
<icon <icon
v-if="job.active" v-if="isJobActive(job.id)"
name="arrow-right" name="arrow-right"
class="js-arrow-right" class="js-arrow-right icon-arrow-right"
/> />
<ci-icon :status="job.status" /> <ci-icon :status="job.status" />
......
<script> <script>
import _ from 'underscore'; import _ from 'underscore';
import { mapActions, mapState } from 'vuex';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
...@@ -7,26 +8,22 @@ ...@@ -7,26 +8,22 @@
import ArtifactsBlock from './artifacts_block.vue'; import ArtifactsBlock from './artifacts_block.vue';
import TriggerBlock from './trigger_block.vue'; import TriggerBlock from './trigger_block.vue';
import CommitBlock from './commit_block.vue'; import CommitBlock from './commit_block.vue';
import StagesDropdown from './stages_dropdown.vue';
import JobsContainer from './jobs_container.vue';
export default { export default {
name: 'SidebarDetailsBlock', name: 'JobSidebar',
components: { components: {
ArtifactsBlock, ArtifactsBlock,
CommitBlock, CommitBlock,
DetailRow, DetailRow,
Icon, Icon,
TriggerBlock, TriggerBlock,
StagesDropdown,
JobsContainer,
}, },
mixins: [timeagoMixin], mixins: [timeagoMixin],
props: { props: {
job: {
type: Object,
required: true,
},
isLoading: {
type: Boolean,
required: true,
},
runnerHelpUrl: { runnerHelpUrl: {
type: String, type: String,
required: false, required: false,
...@@ -39,9 +36,7 @@ ...@@ -39,9 +36,7 @@
}, },
}, },
computed: { computed: {
shouldRenderContent() { ...mapState(['job', 'isLoading', 'stages', 'jobs']),
return !this.isLoading && Object.keys(this.job).length > 0;
},
coverage() { coverage() {
return `${this.job.coverage}%`; return `${this.job.coverage}%`;
}, },
...@@ -97,20 +92,31 @@ ...@@ -97,20 +92,31 @@
}, },
hasStages() { hasStages() {
return ( return (
this.job && (this.job &&
this.job.pipeline && this.job.pipeline &&
this.job.pipeline.stages && this.job.pipeline.stages &&
this.job.pipeline.stages.length > 0 this.job.pipeline.stages.length > 0) ||
) || false; false
);
}, },
commit() { commit() {
return this.job.pipeline.commit || {}; return this.job.pipeline.commit || {};
}, },
}, },
methods: {
...mapActions(['fetchJobsForStage']),
},
}; };
</script> </script>
<template> <template>
<div> <aside
class="right-sidebar right-sidebar-expanded build-sidebar"
data-offset-top="101"
data-spy="affix"
>
<div class="sidebar-container">
<div class="blocks-container">
<template v-if="!isLoading">
<div class="block"> <div class="block">
<strong class="inline prepend-top-8"> <strong class="inline prepend-top-8">
{{ job.name }} {{ job.name }}
...@@ -137,7 +143,8 @@ ...@@ -137,7 +143,8 @@
<button <button
:aria-label="__('Toggle Sidebar')" :aria-label="__('Toggle Sidebar')"
type="button" type="button"
class="btn btn-blank gutter-toggle float-right d-block d-md-none js-sidebar-build-toggle" class="btn btn-blank gutter-toggle
float-right d-block d-md-none js-sidebar-build-toggle"
> >
<i <i
aria-hidden="true" aria-hidden="true"
...@@ -146,7 +153,6 @@ ...@@ -146,7 +153,6 @@
></i> ></i>
</button> </button>
</div> </div>
<template v-if="shouldRenderContent">
<div <div
v-if="job.retry_path || job.new_issue_path" v-if="job.retry_path || job.new_issue_path"
class="block retry-link" class="block retry-link"
...@@ -168,7 +174,7 @@ ...@@ -168,7 +174,7 @@
{{ __('Retry') }} {{ __('Retry') }}
</a> </a>
</div> </div>
<div :class="{block : renderBlock }"> <div :class="{ block : renderBlock }">
<p <p
v-if="job.merge_request" v-if="job.merge_request"
class="build-detail-row js-job-mr" class="build-detail-row js-job-mr"
...@@ -266,11 +272,26 @@ ...@@ -266,11 +272,26 @@
:commit="commit" :commit="commit"
:merge-request="job.merge_request" :merge-request="job.merge_request"
/> />
<stages-dropdown
:stages="stages"
:pipeline="job.pipeline"
@requestSidebarStageDropdown="fetchJobsForStage"
/>
</template> </template>
<gl-loading-icon <gl-loading-icon
v-if="isLoading" v-else
:size="2" :size="2"
class="prepend-top-10" class="prepend-top-10"
/> />
</div> </div>
<jobs-container
v-if="!isLoading && jobs.length"
:jobs="jobs"
:job-id="job.id"
/>
</div>
</aside>
</template> </template>
<script> <script>
import _ from 'underscore';
import CiIcon from '~/vue_shared/components/ci_icon.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { __ } from '~/locale';
import { sprintf, __ } from '~/locale';
export default { export default {
components: { components: {
...@@ -10,30 +10,14 @@ ...@@ -10,30 +10,14 @@
Icon, Icon,
}, },
props: { props: {
pipelineId: { pipeline: {
type: Number, type: Object,
required: true,
},
pipelinePath: {
type: String,
required: true,
},
pipelineRef: {
type: String,
required: true,
},
pipelineRefPath: {
type: String,
required: true, required: true,
}, },
stages: { stages: {
type: Array, type: Array,
required: true, required: true,
}, },
pipelineStatus: {
type: Object,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -41,51 +25,68 @@ ...@@ -41,51 +25,68 @@
}; };
}, },
computed: { computed: {
pipelineLink() { hasRef() {
return sprintf(__('Pipeline %{pipelineLinkStart} #%{pipelineId} %{pipelineLinkEnd} from %{pipelineLinkRefStart} %{pipelineRef} %{pipelineLinkRefEnd}'), { return !_.isEmpty(this.pipeline.ref);
pipelineLinkStart: `<a href=${this.pipelinePath} class="js-pipeline-path link-commit">`, },
pipelineId: this.pipelineId, },
pipelineLinkEnd: '</a>', watch: {
pipelineLinkRefStart: `<a href=${this.pipelineRefPath} class="link-commit ref-name">`, // When the component is initially mounted it may start with an empty stages array.
pipelineRef: this.pipelineRef, // Once the prop is updated, we set the first stage as the selected one
pipelineLinkRefEnd: '</a>', stages(newVal) {
}, false); if (newVal.length) {
this.selectedStage = newVal[0].name;
}
}, },
}, },
methods: { methods: {
onStageClick(stage) { 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.$emit('requestSidebarStageDropdown', stage);
this.selectedStage = stage.name;
}, },
}, },
}; };
</script> </script>
<template> <template>
<div class="block-last"> <div class="block-last dropdown">
<ci-icon :status="pipelineStatus" /> <ci-icon
:status="pipeline.details.status"
class="vertical-align-middle"
/>
<p v-html="pipelineLink"></p> {{ __('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>
<div class="dropdown">
<button <button
type="button" type="button"
data-toggle="dropdown" data-toggle="dropdown"
class="js-selected-stage dropdown-menu-toggle prepend-top-8"
> >
{{ selectedStage }} {{ selectedStage }}
<icon name="chevron-down" /> <i class="fa fa-chevron-down" ></i>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li <li
v-for="(stage, index) in stages" v-for="stage in stages"
:key="index" :key="stage.name"
> >
<button <button
type="button" type="button"
class="stage-item" class="js-stage-item stage-item"
@click="onStageClick(stage)" @click="onStageClick(stage)"
> >
{{ stage.name }} {{ stage.name }}
...@@ -93,5 +94,4 @@ ...@@ -93,5 +94,4 @@
</li> </li>
</ul> </ul>
</div> </div>
</div>
</template> </template>
import { mapState } from 'vuex'; import _ from 'underscore';
import { mapState, mapActions } from 'vuex';
import Vue from 'vue'; import Vue from 'vue';
import Job from '../job'; import Job from '../job';
import JobApp from './components/job_app.vue'; import JobApp from './components/job_app.vue';
import DetailsBlock from './components/sidebar_details_block.vue'; import Sidebar from './components/sidebar.vue';
import createStore from './store'; import createStore from './store';
export default () => { export default () => {
...@@ -13,6 +14,7 @@ export default () => { ...@@ -13,6 +14,7 @@ export default () => {
const store = createStore(); const store = createStore();
store.dispatch('setJobEndpoint', dataset.endpoint); store.dispatch('setJobEndpoint', dataset.endpoint);
store.dispatch('fetchJob'); store.dispatch('fetchJob');
// Header // Header
...@@ -44,17 +46,25 @@ export default () => { ...@@ -44,17 +46,25 @@ export default () => {
new Vue({ new Vue({
el: detailsBlockElement, el: detailsBlockElement,
components: { components: {
DetailsBlock, Sidebar,
}, },
store,
computed: { computed: {
...mapState(['job', 'isLoading']), ...mapState(['job']),
},
watch: {
job(newVal, oldVal) {
if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) {
this.fetchStages();
}
},
}, },
methods: {
...mapActions(['fetchStages']),
},
store,
render(createElement) { render(createElement) {
return createElement('details-block', { return createElement('sidebar', {
props: { props: {
isLoading: this.isLoading,
job: this.job,
runnerHelpUrl: dataset.runnerHelpUrl, runnerHelpUrl: dataset.runnerHelpUrl,
terminalPath: detailsBlockDataset.terminalPath, terminalPath: detailsBlockDataset.terminalPath,
}, },
......
...@@ -62,7 +62,9 @@ export const fetchJob = ({ state, dispatch }) => { ...@@ -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 }) => { export const receiveJobError = ({ commit }) => {
commit(types.RECEIVE_JOB_ERROR); commit(types.RECEIVE_JOB_ERROR);
flash(__('An error occurred while fetching the job.')); flash(__('An error occurred while fetching the job.'));
...@@ -137,8 +139,11 @@ export const fetchStages = ({ state, dispatch }) => { ...@@ -137,8 +139,11 @@ export const fetchStages = ({ state, dispatch }) => {
dispatch('requestStages'); dispatch('requestStages');
axios axios
.get(state.stagesEndpoint) .get(state.job.pipeline.path)
.then(({ data }) => dispatch('receiveStagesSuccess', data)) .then(({ data }) => {
dispatch('receiveStagesSuccess', data.details.stages);
dispatch('fetchJobsForStage', data.details.stages[0]);
})
.catch(() => dispatch('receiveStagesError')); .catch(() => dispatch('receiveStagesError'));
}; };
export const receiveStagesSuccess = ({ commit }, data) => export const receiveStagesSuccess = ({ commit }, data) =>
...@@ -152,16 +157,23 @@ export const receiveStagesError = ({ commit }) => { ...@@ -152,16 +157,23 @@ export const receiveStagesError = ({ commit }) => {
* Jobs list on sidebar - depend on stages dropdown * Jobs list on sidebar - depend on stages dropdown
*/ */
export const requestJobsForStage = ({ commit }) => commit(types.REQUEST_JOBS_FOR_STAGE); 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 // On stage click, set selected stage + fetch job
export const fetchJobsForStage = ({ state, dispatch }, stage) => { export const fetchJobsForStage = ({ dispatch }, stage) => {
dispatch('setSelectedStage', stage);
dispatch('requestJobsForStage'); dispatch('requestJobsForStage');
axios axios
.get(state.stageJobsEndpoint) .get(stage.dropdown_path, {
.then(({ data }) => dispatch('receiveJobsForStageSuccess', data)) 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')); .catch(() => dispatch('receiveJobsForStageError'));
}; };
export const receiveJobsForStageSuccess = ({ commit }, data) => export const receiveJobsForStageSuccess = ({ commit }, data) =>
......
...@@ -51,7 +51,7 @@ export const isJobStuck = state => ...@@ -51,7 +51,7 @@ export const isJobStuck = state =>
// ee-only start // ee-only start
export const shouldRenderSharedRunnerLimitWarning = state => export const shouldRenderSharedRunnerLimitWarning = state =>
state.job.runner && state.job.runner.quota && state.job.ruuner.quota.used; state.job.runners && state.job.runners.quota && state.job.runners.quota.used;
// ee-only end // ee-only end
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
......
...@@ -88,6 +88,7 @@ export const handleLocationHash = () => { ...@@ -88,6 +88,7 @@ export const handleLocationHash = () => {
const fixedDiffStats = document.querySelector('.js-diff-files-changed'); const fixedDiffStats = document.querySelector('.js-diff-files-changed');
const fixedNav = document.querySelector('.navbar-gitlab'); const fixedNav = document.querySelector('.navbar-gitlab');
const performanceBar = document.querySelector('#js-peek'); const performanceBar = document.querySelector('#js-peek');
const topPadding = 8;
let adjustment = 0; let adjustment = 0;
if (fixedNav) adjustment -= fixedNav.offsetHeight; if (fixedNav) adjustment -= fixedNav.offsetHeight;
...@@ -108,6 +109,10 @@ export const handleLocationHash = () => { ...@@ -108,6 +109,10 @@ export const handleLocationHash = () => {
adjustment -= performanceBar.offsetHeight; adjustment -= performanceBar.offsetHeight;
} }
if (isInMRPage()) {
adjustment -= topPadding;
}
window.scrollBy(0, adjustment); window.scrollBy(0, adjustment);
}; };
...@@ -381,8 +386,11 @@ export const objectToQueryString = (params = {}) => ...@@ -381,8 +386,11 @@ export const objectToQueryString = (params = {}) =>
.map(param => `${param}=${params[param]}`) .map(param => `${param}=${params[param]}`)
.join('&'); .join('&');
export const buildUrlWithCurrentLocation = param => export const buildUrlWithCurrentLocation = param => {
(param ? `${window.location.pathname}${param}` : window.location.pathname); if (param) return `${window.location.pathname}${param}`;
return window.location.pathname;
};
/** /**
* Based on the current location and the string parameters provided * Based on the current location and the string parameters provided
......
...@@ -194,9 +194,7 @@ export default class MergeRequestTabs { ...@@ -194,9 +194,7 @@ export default class MergeRequestTabs {
if (bp.getBreakpointSize() !== 'lg') { if (bp.getBreakpointSize() !== 'lg') {
this.shrinkView(); this.shrinkView();
} }
if (this.diffViewType() === 'parallel') {
this.expandViewContainer(); this.expandViewContainer();
}
this.destroyPipelinesView(); this.destroyPipelinesView();
this.commitsTab.classList.remove('active'); this.commitsTab.classList.remove('active');
} else if (action === 'pipelines') { } else if (action === 'pipelines') {
...@@ -355,7 +353,7 @@ export default class MergeRequestTabs { ...@@ -355,7 +353,7 @@ export default class MergeRequestTabs {
localTimeAgo($('.js-timeago', 'div#diffs')); localTimeAgo($('.js-timeago', 'div#diffs'));
syntaxHighlight($('#diffs .js-syntax-highlight')); syntaxHighlight($('#diffs .js-syntax-highlight'));
if (this.diffViewType() === 'parallel' && this.isDiffAction(this.currentAction)) { if (this.isDiffAction(this.currentAction)) {
this.expandViewContainer(); this.expandViewContainer();
} }
this.diffsLoaded = true; this.diffsLoaded = true;
...@@ -408,19 +406,23 @@ export default class MergeRequestTabs { ...@@ -408,19 +406,23 @@ export default class MergeRequestTabs {
} }
diffViewType() { diffViewType() {
return $('.inline-parallel-buttons a.active').data('viewType'); return $('.inline-parallel-buttons button.active').data('viewType');
} }
isDiffAction(action) { isDiffAction(action) {
return action === 'diffs' || action === 'new/diffs'; return action === 'diffs' || action === 'new/diffs';
} }
expandViewContainer() { expandViewContainer(removeLimited = true) {
const $wrapper = $('.content-wrapper .container-fluid').not('.breadcrumbs'); const $wrapper = $('.content-wrapper .container-fluid').not('.breadcrumbs');
if (this.fixedLayoutPref === null) { if (this.fixedLayoutPref === null) {
this.fixedLayoutPref = $wrapper.hasClass('container-limited'); this.fixedLayoutPref = $wrapper.hasClass('container-limited');
} }
if (this.diffViewType() === 'parallel' || removeLimited) {
$wrapper.removeClass('container-limited'); $wrapper.removeClass('container-limited');
} else {
$wrapper.addClass('container-limited');
}
} }
resetViewContainer() { 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 { ...@@ -191,6 +191,7 @@ export default {
if (note.placeholderType === SYSTEM_NOTE) { if (note.placeholderType === SYSTEM_NOTE) {
return placeholderSystemNote; return placeholderSystemNote;
} }
return placeholderNote; return placeholderNote;
} }
...@@ -201,7 +202,7 @@ export default { ...@@ -201,7 +202,7 @@ export default {
return noteableNote; return noteableNote;
}, },
componentData(note) { componentData(note) {
return note.isPlaceholderNote ? this.discussion.notes[0] : note; return note.isPlaceholderNote ? note.notes[0] : note;
}, },
toggleDiscussionHandler() { toggleDiscussionHandler() {
this.toggleDiscussion({ discussionId: this.discussion.id }); this.toggleDiscussion({ discussionId: this.discussion.id });
......
...@@ -126,8 +126,8 @@ export const unresolvedDiscussionsIdsByDiff = (state, getters) => ...@@ -126,8 +126,8 @@ export const unresolvedDiscussionsIdsByDiff = (state, getters) =>
const filenameComparison = a.diff_file.file_path.localeCompare(b.diff_file.file_path); const filenameComparison = a.diff_file.file_path.localeCompare(b.diff_file.file_path);
// Get the line numbers, to compare within the same file // Get the line numbers, to compare within the same file
const aLines = [a.position.formatter.new_line, a.position.formatter.old_line]; const aLines = [a.position.new_line, a.position.old_line];
const bLines = [b.position.formatter.new_line, b.position.formatter.old_line]; const bLines = [b.position.new_line, b.position.old_line];
return filenameComparison < 0 || return filenameComparison < 0 ||
(filenameComparison === 0 && (filenameComparison === 0 &&
......
...@@ -11,7 +11,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -11,7 +11,7 @@ document.addEventListener('DOMContentLoaded', () => {
const statusEmojiField = document.getElementById('js-status-emoji-field'); const statusEmojiField = document.getElementById('js-status-emoji-field');
const statusMessageField = document.getElementById('js-status-message-field'); const statusMessageField = document.getElementById('js-status-message-field');
const toggleNoEmojiPlaceholder = (isVisible) => { const toggleNoEmojiPlaceholder = isVisible => {
const placeholderElement = document.getElementById('js-no-emoji-placeholder'); const placeholderElement = document.getElementById('js-no-emoji-placeholder');
placeholderElement.classList.toggle('hidden', !isVisible); placeholderElement.classList.toggle('hidden', !isVisible);
}; };
...@@ -69,5 +69,5 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -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'; ...@@ -3,7 +3,7 @@ import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { pluralize } from '~/lib/utils/text_utility'; import { pluralize } from '~/lib/utils/text_utility';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import { getCommitIconMap } from '../utils'; import { getCommitIconMap } from '~/ide/utils';
export default { export default {
components: { components: {
...@@ -32,6 +32,11 @@ export default { ...@@ -32,6 +32,11 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
size: {
type: Number,
required: false,
default: 12,
},
}, },
computed: { computed: {
changedIcon() { changedIcon() {
...@@ -42,7 +47,7 @@ export default { ...@@ -42,7 +47,7 @@ export default {
return `${getCommitIconMap(this.file).icon}${suffix}`; return `${getCommitIconMap(this.file).icon}${suffix}`;
}, },
changedIconClass() { changedIconClass() {
return `ide-${this.changedIcon} float-left`; return `${this.changedIcon} float-left d-block`;
}, },
tooltipTitle() { tooltipTitle() {
if (!this.showTooltip) return undefined; if (!this.showTooltip) return undefined;
...@@ -78,13 +83,30 @@ export default { ...@@ -78,13 +83,30 @@ export default {
:title="tooltipTitle" :title="tooltipTitle"
data-container="body" data-container="body"
data-placement="right" data-placement="right"
class="ide-file-changed-icon" class="file-changed-icon ml-auto"
> >
<icon <icon
v-if="showIcon" v-if="showIcon"
:name="changedIcon" :name="changedIcon"
:size="12" :size="size"
:css-classes="changedIconClass" :css-classes="changedIconClass"
/> />
</span> </span>
</template> </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> <script>
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
export default { export default {
name: 'FileRow', name: 'FileRow',
components: { components: {
FileIcon, FileIcon,
Icon, Icon,
ChangedFileIcon,
}, },
props: { props: {
file: { file: {
...@@ -22,6 +24,16 @@ export default { ...@@ -22,6 +24,16 @@ export default {
required: false, required: false,
default: null, default: null,
}, },
hideExtraOnTree: {
type: Boolean,
required: false,
default: false,
},
showChangedIcon: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
return { return {
...@@ -65,6 +77,9 @@ export default { ...@@ -65,6 +77,9 @@ export default {
toggleTreeOpen(path) { toggleTreeOpen(path) {
this.$emit('toggleTreeOpen', path); this.$emit('toggleTreeOpen', path);
}, },
clickedFile(path) {
this.$emit('clickFile', path);
},
clickFile() { clickFile() {
// Manual Action if a tree is selected/opened // Manual Action if a tree is selected/opened
if (this.isTree && this.hasUrlAtCurrentRoute()) { if (this.isTree && this.hasUrlAtCurrentRoute()) {
...@@ -72,6 +87,8 @@ export default { ...@@ -72,6 +87,8 @@ export default {
} }
if (this.$router) this.$router.push(`/project${this.file.url}`); if (this.$router) this.$router.push(`/project${this.file.url}`);
if (this.isBlob) this.clickedFile(this.file.path);
}, },
scrollIntoView(isInit = false) { scrollIntoView(isInit = false) {
const block = isInit && this.isTree ? 'center' : 'nearest'; const block = isInit && this.isTree ? 'center' : 'nearest';
...@@ -126,17 +143,24 @@ export default { ...@@ -126,17 +143,24 @@ export default {
class="file-row-name str-truncated" class="file-row-name str-truncated"
> >
<file-icon <file-icon
v-if="!showChangedIcon || file.type === 'tree'"
:file-name="file.name" :file-name="file.name"
:loading="file.loading" :loading="file.loading"
:folder="isTree" :folder="isTree"
:opened="file.opened" :opened="file.opened"
:size="16" :size="16"
/> />
<changed-file-icon
v-else
:file="file"
:size="16"
class="append-right-5"
/>
{{ file.name }} {{ file.name }}
</span> </span>
<component <component
:is="extraComponent" :is="extraComponent"
v-if="extraComponent" v-if="extraComponent && !(hideExtraOnTree && file.type === 'tree')"
:file="file" :file="file"
:mouse-over="mouseOver" :mouse-over="mouseOver"
/> />
...@@ -148,8 +172,11 @@ export default { ...@@ -148,8 +172,11 @@ export default {
:key="childFile.key" :key="childFile.key"
:file="childFile" :file="childFile"
:level="level + 1" :level="level + 1"
:hide-extra-on-tree="hideExtraOnTree"
:extra-component="extraComponent" :extra-component="extraComponent"
:show-changed-icon="showChangedIcon"
@toggleTreeOpen="toggleTreeOpen" @toggleTreeOpen="toggleTreeOpen"
@clickFile="clickedFile"
/> />
</template> </template>
</div> </div>
......
...@@ -529,9 +529,10 @@ ...@@ -529,9 +529,10 @@
} }
.header-user { .header-user {
.dropdown-menu { &.show .dropdown-menu {
width: auto; width: auto;
min-width: unset; min-width: unset;
max-height: 323px;
margin-top: 4px; margin-top: 4px;
color: $gl-text-color; color: $gl-text-color;
left: auto; left: auto;
...@@ -542,6 +543,18 @@ ...@@ -542,6 +543,18 @@
.user-name { .user-name {
display: block; 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 { svg {
...@@ -573,3 +586,24 @@ ...@@ -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 @@ ...@@ -356,3 +356,59 @@
border-radius: 50%; 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%); ...@@ -322,7 +322,8 @@ $diff-jagged-border-gradient-color: darken($white-normal, 8%);
$monospace-font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono', $monospace-font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono',
'Courier New', 'andale mono', 'lucida console', monospace; 'Courier New', 'andale mono', 'lucida console', monospace;
$regular-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, $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 * Dropdowns
...@@ -666,5 +667,4 @@ Modals ...@@ -666,5 +667,4 @@ Modals
$modal-body-height: 134px; $modal-body-height: 134px;
$modal-border-color: #e9ecef; $modal-border-color: #e9ecef;
$priority-label-empty-state-width: 114px; $priority-label-empty-state-width: 114px;
...@@ -517,21 +517,6 @@ $ide-commit-header-height: 48px; ...@@ -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 { .multi-file-commit-list-collapsed {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
...@@ -1399,14 +1384,6 @@ $ide-commit-header-height: 48px; ...@@ -1399,14 +1384,6 @@ $ide-commit-header-height: 48px;
color: $theme-gray-700; color: $theme-gray-700;
} }
.ide-file-changed-icon {
margin-left: auto;
> svg {
display: block;
}
}
.file-row:hover, .file-row:hover,
.file-row:focus { .file-row:focus {
.ide-new-btn { .ide-new-btn {
......
...@@ -262,23 +262,6 @@ ...@@ -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 { .builds-container {
background-color: $white-light; background-color: $white-light;
border-top: 1px solid $border-color; border-top: 1px solid $border-color;
...@@ -315,15 +298,11 @@ ...@@ -315,15 +298,11 @@
position: absolute; position: absolute;
left: 15px; left: 15px;
top: 20px; top: 20px;
display: none; display: block;
} }
&.active { &.active {
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
.icon-arrow-right {
display: block;
}
} }
&.retried { &.retried {
......
...@@ -571,8 +571,6 @@ ...@@ -571,8 +571,6 @@
} }
.files { .files {
margin-top: 1px;
.diff-file:last-child { .diff-file:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
...@@ -987,3 +985,63 @@ ...@@ -987,3 +985,63 @@
.discussion-body .image .frame { .discussion-body .image .frame {
position: relative; 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 @@ ...@@ -723,6 +723,17 @@
align-items: center; align-items: center;
padding: 16px; padding: 16px;
z-index: 199; z-index: 199;
white-space: nowrap;
.dropdown-menu-toggle {
width: auto;
max-width: 170px;
svg {
top: 10px;
right: 8px;
}
}
} }
.content-block { .content-block {
......
...@@ -519,59 +519,7 @@ ul.notes { ...@@ -519,59 +519,7 @@ ul.notes {
} }
.note-action-button { .note-action-button {
line-height: 1; @include emoji-menu-toggle-button;
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;
}
}
} }
.discussion-toggle-button { .discussion-toggle-button {
......
...@@ -81,14 +81,14 @@ ...@@ -81,14 +81,14 @@
// Middle dot divider between each element in a list of items. // Middle dot divider between each element in a list of items.
.middle-dot-divider { .middle-dot-divider {
&::after { &::after {
content: "\00B7"; // Middle Dot content: '\00B7'; // Middle Dot
padding: 0 6px; padding: 0 6px;
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
} }
&:last-child { &:last-child {
&::after { &::after {
content: ""; content: '';
padding: 0; padding: 0;
} }
} }
...@@ -191,7 +191,6 @@ ...@@ -191,7 +191,6 @@
@include media-breakpoint-down(xs) { @include media-breakpoint-down(xs) {
width: auto; width: auto;
} }
} }
.profile-crop-image-container { .profile-crop-image-container {
...@@ -215,7 +214,6 @@ ...@@ -215,7 +214,6 @@
} }
} }
.user-profile { .user-profile {
.cover-controls a { .cover-controls a {
margin-left: 5px; margin-left: 5px;
...@@ -415,7 +413,7 @@ table.u2f-registrations { ...@@ -415,7 +413,7 @@ table.u2f-registrations {
} }
&.unverified { &.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 { ...@@ -428,7 +426,7 @@ table.u2f-registrations {
} }
.emoji-menu-toggle-button { .emoji-menu-toggle-button {
@extend .note-action-button; @include emoji-menu-toggle-button;
.no-emoji-placeholder { .no-emoji-placeholder {
position: relative; position: relative;
......
...@@ -831,6 +831,14 @@ ...@@ -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 { pre.light-well {
border-color: $well-light-border; border-color: $well-light-border;
} }
......
...@@ -13,8 +13,4 @@ module ClustersHelper ...@@ -13,8 +13,4 @@ module ClustersHelper
render 'projects/clusters/gcp_signup_offer_banner' render 'projects/clusters/gcp_signup_offer_banner'
end end
end end
def rbac_clusters_feature_enabled?
Feature.enabled?(:rbac_clusters)
end
end end
...@@ -13,6 +13,7 @@ module RepositoryLanguagesHelper ...@@ -13,6 +13,7 @@ module RepositoryLanguagesHelper
content_tag :div, nil, content_tag :div, nil,
class: "progress-bar has-tooltip", class: "progress-bar has-tooltip",
style: "width: #{lang.share}%; background-color:#{lang.color}", 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
end end
...@@ -640,6 +640,10 @@ module Ci ...@@ -640,6 +640,10 @@ module Ci
end end
end end
def default_branch?
ref == project.default_branch
end
private private
def ci_yaml_from_repo def ci_yaml_from_repo
......
...@@ -64,10 +64,10 @@ class InstanceConfiguration ...@@ -64,10 +64,10 @@ class InstanceConfiguration
end end
def ssh_algorithm_md5(ssh_file_content) def ssh_algorithm_md5(ssh_file_content)
OpenSSL::Digest::MD5.hexdigest(ssh_file_content).scan(/../).join(':') Gitlab::SSHPublicKey.new(ssh_file_content).fingerprint
end end
def ssh_algorithm_sha256(ssh_file_content) def ssh_algorithm_sha256(ssh_file_content)
OpenSSL::Digest::SHA256.hexdigest(ssh_file_content) Gitlab::SSHPublicKey.new(ssh_file_content).fingerprint('SHA256')
end end
end end
...@@ -84,7 +84,7 @@ class DiffFileEntity < Grape::Entity ...@@ -84,7 +84,7 @@ class DiffFileEntity < Grape::Entity
end end
expose :old_path_html do |diff_file| 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 old_path
end end
......
...@@ -5,7 +5,14 @@ ...@@ -5,7 +5,14 @@
.user-name.bold .user-name.bold
= current_user.name = current_user.name
= current_user.to_reference = 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 %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) - if current_user_menu?(:profile)
%li %li
= link_to s_("CurrentUser|Profile"), current_user, class: 'profile-link', data: { user: current_user.username } = link_to s_("CurrentUser|Profile"), current_user, class: 'profile-link', data: { user: current_user.username }
......
...@@ -74,3 +74,6 @@ ...@@ -74,3 +74,6 @@
%span.sr-only= _('Toggle navigation') %span.sr-only= _('Toggle navigation')
= sprite_icon('ellipsis_h', size: 12, css_class: 'more-icon js-navbar-toggle-right') = 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') = 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 : '' } }
...@@ -61,5 +61,5 @@ ...@@ -61,5 +61,5 @@
.option-description .option-description
Allows you to immediately clone this project’s repository. Skip this if you plan to push up an existing repository. Allows you to immediately clone this project’s repository. Skip this if you plan to push up an existing repository.
= 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: "" }
= link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "cancel" } = link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "cancel" }
...@@ -61,7 +61,6 @@ ...@@ -61,7 +61,6 @@
%p.form-text.text-muted %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 } = 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-group
.form-check .form-check
= provider_gcp_field.check_box :legacy_abac, { class: 'form-check-input' }, false, true = provider_gcp_field.check_box :legacy_abac, { class: 'form-check-input' }, false, true
......
...@@ -37,7 +37,6 @@ ...@@ -37,7 +37,6 @@
= platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)') = 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') = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
- if rbac_clusters_feature_enabled?
.form-group .form-group
.form-check .form-check
= platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac' = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac'
......
...@@ -25,7 +25,6 @@ ...@@ -25,7 +25,6 @@
= platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold' = 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') = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
- if rbac_clusters_feature_enabled?
.form-group .form-group
.form-check .form-check
= platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input' }, 'rbac', 'abac' = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input' }, 'rbac', 'abac'
......
...@@ -26,7 +26,6 @@ ...@@ -26,7 +26,6 @@
= platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold' = 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') = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
- if rbac_clusters_feature_enabled?
.form-group .form-group
.form-check .form-check
= platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac' = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac'
......
...@@ -49,15 +49,16 @@ ...@@ -49,15 +49,16 @@
.environments-container .environments-container
- if @deployments.blank? - if @deployments.blank?
.blank-state-row .empty-state
.blank-state-center .text-content
%h2.blank-state-title %h4.state-title
You don't have any deployments right now. You don't have any deployments right now.
%p.blank-state-text %p.blank-state-text
Define environments in the deploy stage(s) in Define environments in the deploy stage(s) in
%code .gitlab-ci.yml %code .gitlab-ci.yml
to track deployments here. 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 - else
.table-holder .table-holder
.ci-table.environments{ role: 'grid' } .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,56 +9,6 @@ ...@@ -9,56 +9,6 @@
%div{ class: container_class } %div{ class: container_class }
.build-page.js-build-page .build-page.js-build-page
#js-build-header-vue #js-build-header-vue
- if @build.stuck?
- unless @build.any_runners_online?
.bs-callout.bs-callout-warning.js-build-stuck
%p
- if @project.any_runners?
This job is stuck, because the project doesn't have any runners online assigned to it.
- elsif @build.tags.any?
This job is stuck, because you don't have any active runners online with any of these tags assigned to them:
- @build.tags.each do |tag|
%span.badge.badge-primary
= tag
- else
This job is stuck, because you don't have any active runners that can run this job.
%br
Go to
= link_to project_runners_path(@build.project, anchor: 'js-runners-settings') do
Runners page
= render "shared_runner_limit_warning", project: @build.project
- if @build.starts_environment?
.prepend-top-default.js-environment-container
.environment-information
- if @build.outdated_deployment?
= ci_icon_for_status('success_with_warnings')
- else
= ci_icon_for_status(@build.status)
- environment = environment_for_build(@build.project, @build)
- if @build.success? && @build.last_deployment.present?
- if @build.last_deployment.last?
This job is the most recent deployment to #{environment_link_for_build(@build.project, @build)}.
- else
This job is an out-of-date deployment to #{environment_link_for_build(@build.project, @build)}.
View the most recent deployment #{deployment_link(environment.last_deployment)}.
- elsif @build.complete? && !@build.success?
The deployment of this job to #{environment_link_for_build(@build.project, @build)} did not succeed.
- else
This job is creating a deployment to #{environment_link_for_build(@build.project, @build)}
- if environment.try(:last_deployment)
and will overwrite the #{deployment_link(environment.last_deployment, text: 'latest deployment')}
- if @build.erased?
.prepend-top-default.js-build-erased
.erased.alert.alert-warning
- if @build.erased_by_user?
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)}
- if @build.running? || @build.has_trace? - if @build.running? || @build.has_trace?
.build-trace-container.prepend-top-default .build-trace-container.prepend-top-default
...@@ -93,7 +43,7 @@ ...@@ -93,7 +43,7 @@
= render 'shared/builds/build_output' = render 'shared/builds/build_output'
= 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 } .js-build-options{ data: javascript_build_options }
......
...@@ -10,10 +10,8 @@ ...@@ -10,10 +10,8 @@
= template.description = template.description
.controls.d-flex.align-items-center .controls.d-flex.align-items-center
%label.btn.btn-success.template-button.choose-template.append-right-10.append-bottom-0{ for: template.name } %label.btn.btn-success.template-button.choose-template.append-right-10.append-bottom-0{ for: template.name }
%input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, 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" } }
data: { track_label: "create_from_template", track_property: "template_use", track_event: "click_button" } }
%span %span
= _("Use template") = _("Use template")
%a.btn.btn-default{ href: template.preview, rel: 'noopener noreferrer', target: '_blank', %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 } }
data: { track_label: "create_from_template", track_property: "template_preview", track_event: "click_button", track_value: template.name } }
= _("Preview") = _("Preview")
...@@ -3,8 +3,7 @@ ...@@ -3,8 +3,7 @@
- restricted = restricted_visibility_levels.include?(level) - restricted = restricted_visibility_levels.include?(level)
- disabled = disallowed || restricted - disabled = disallowed || restricted
.form-check{ class: [('disabled' if disabled), ('restricted' if restricted)] } .form-check{ class: [('disabled' if disabled), ('restricted' if restricted)] }
= form.radio_button model_method, level, checked: (selected_level == level), disabled: disabled, class: 'form-check-input', = 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}" }
data: { track_label: "blank_project", track_event: "activate_form_input", track_property: "#{model_method}", track_value: "#{level}" }
= form.label "#{model_method}_#{level}", class: 'form-check-label' do = form.label "#{model_method}_#{level}", class: 'form-check-label' do
= visibility_level_icon(level) = visibility_level_icon(level)
.option-title .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: 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
---
title: Fix showing diff file header for renamed files
merge_request: 22089
author:
type: fixed
---
title: Fix rendering placeholder notes
merge_request: 22078
author:
type: fixed
---
title: Added tree of changed files to merge request diffs
merge_request: 21833
author:
type: added
...@@ -585,3 +585,17 @@ ...@@ -585,3 +585,17 @@
and are therefore exempt. and are therefore exempt.
:versions: [] :versions: []
:when: 2018-08-30 12:06:35.668181000 Z :when: 2018-08-30 12:06:35.668181000 Z
- - :approve
- caniuse-lite
- :who: Mike Greiling
:why: CC-BY-4.0 license. Tool only used during build process, code is not present
in compiled/distributed product so attribution not needed.
:versions: []
:when: 2018-10-02 19:23:11.221660000 Z
- - :approve
- node-releases
- :who: Mike Greiling
:why: CC-BY-4.0 license. Tool only used during build process, code is not present
in compiled/distributed product so attribution not needed.
:versions: []
:when: 2018-10-02 19:23:54.840151000 Z
...@@ -23,6 +23,13 @@ webpackConfig.optimization.splitChunks = false; ...@@ -23,6 +23,13 @@ webpackConfig.optimization.splitChunks = false;
// use quicker sourcemap option // use quicker sourcemap option
webpackConfig.devtool = 'cheap-inline-source-map'; webpackConfig.devtool = 'cheap-inline-source-map';
// set BABEL_ENV to indicate when we're running code coverage
webpackConfig.plugins.push(
new webpack.DefinePlugin({
'process.env.BABEL_ENV': JSON.stringify(process.env.BABEL_ENV || process.env.NODE_ENV || null),
})
);
const specFilters = argumentsParser const specFilters = argumentsParser
.option( .option(
'-f, --filter-spec [filter]', '-f, --filter-spec [filter]',
...@@ -87,7 +94,7 @@ module.exports = function(config) { ...@@ -87,7 +94,7 @@ module.exports = function(config) {
basePath: ROOT_PATH, basePath: ROOT_PATH,
browsers: ['ChromeHeadlessCustom'], browsers: ['ChromeHeadlessCustom'],
client: { client: {
color: !process.env.CI color: !process.env.CI,
}, },
customLaunchers: { customLaunchers: {
ChromeHeadlessCustom: { ChromeHeadlessCustom: {
......
...@@ -434,9 +434,11 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -434,9 +434,11 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end end
collection do collection do
post :bulk_update post :bulk_update
post :export_csv
get :service_desk ## EE-specific ## EE-specific START
post :export_csv
get :service_desk
## EE-specific END
end end
resources :issue_links, only: [:index, :create, :destroy], as: 'links', path: 'links' resources :issue_links, only: [:index, :create, :destroy], as: 'links', path: 'links'
......
...@@ -61,13 +61,13 @@ scope(constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }) d ...@@ -61,13 +61,13 @@ scope(constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }) d
get :contributed, as: :contributed_projects get :contributed, as: :contributed_projects
get :snippets get :snippets
get :exists get :exists
get :pipelines_quota
get :activity get :activity
get '/', to: redirect('%{username}'), as: nil get '/', to: redirect('%{username}'), as: nil
## EE-specific ## EE-specific START
get :available_templates, format: :js get :available_templates, format: :js
## EE-specific get :pipelines_quota
## EE-specific END
end end
# Compatibility with old routing # Compatibility with old routing
......
require 'thread'
class RenameReservedProjectNames < ActiveRecord::Migration class RenameReservedProjectNames < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
......
require 'thread'
class RenameMoreReservedProjectNames < ActiveRecord::Migration class RenameMoreReservedProjectNames < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
......
...@@ -22,9 +22,8 @@ However, it is not possible to resume an interrupted tar pipe: if ...@@ -22,9 +22,8 @@ However, it is not possible to resume an interrupted tar pipe: if
that happens then all data must be copied again. that happens then all data must be copied again.
``` ```
# As the git user sudo -u git sh -c 'tar -C /var/opt/gitlab/git-data/repositories -cf - -- . |\
tar -C /var/opt/gitlab/git-data/repositories -cf - -- . |\ tar -C /mnt/gitlab/repositories -xf -'
tar -C /mnt/gitlab/repositories -xf -
``` ```
If you want to see progress, replace `-xf` with `-xvf`. If you want to see progress, replace `-xf` with `-xvf`.
...@@ -36,9 +35,8 @@ You can also use a tar pipe to copy data to another server. If your ...@@ -36,9 +35,8 @@ You can also use a tar pipe to copy data to another server. If your
can pipe the data through SSH. can pipe the data through SSH.
``` ```
# As the git user sudo -u git sh -c 'tar -C /var/opt/gitlab/git-data/repositories -cf - -- . |\
tar -C /var/opt/gitlab/git-data/repositories -cf - -- . |\ ssh git@newserver tar -C /mnt/gitlab/repositories -xf -'
ssh git@newserver tar -C /mnt/gitlab/repositories -xf -
``` ```
If you want to compress the data before it goes over the network If you want to compress the data before it goes over the network
...@@ -53,9 +51,8 @@ is either already installed on your system or easily installable ...@@ -53,9 +51,8 @@ is either already installed on your system or easily installable
via apt, yum etc. via apt, yum etc.
``` ```
# As the 'git' user sudo -u git sh -c 'rsync -a --delete /var/opt/gitlab/git-data/repositories/. \
rsync -a --delete /var/opt/gitlab/git-data/repositories/. \ /mnt/gitlab/repositories'
/mnt/gitlab/repositories
``` ```
The `/.` in the command above is very important, without it you can The `/.` in the command above is very important, without it you can
...@@ -68,9 +65,8 @@ If the 'git' user on your source system has SSH access to the target ...@@ -68,9 +65,8 @@ If the 'git' user on your source system has SSH access to the target
server you can send the repositories over the network with rsync. server you can send the repositories over the network with rsync.
``` ```
# As the 'git' user sudo -u git sh -c 'rsync -a --delete /var/opt/gitlab/git-data/repositories/. \
rsync -a --delete /var/opt/gitlab/git-data/repositories/. \ git@newserver:/mnt/gitlab/repositories'
git@newserver:/mnt/gitlab/repositories
``` ```
## Thousands of Git repositories: use one rsync per repository ## Thousands of Git repositories: use one rsync per repository
...@@ -125,7 +121,7 @@ sudo -u git -H sh -c 'bundle exec rake gitlab:list_repos > /home/git/transfer-lo ...@@ -125,7 +121,7 @@ sudo -u git -H sh -c 'bundle exec rake gitlab:list_repos > /home/git/transfer-lo
Now we can start the transfer. The command below is idempotent, and Now we can start the transfer. The command below is idempotent, and
the number of jobs done by GNU Parallel should converge to zero. If it the number of jobs done by GNU Parallel should converge to zero. If it
does not some repositories listed in all-repos-1234.txt may have been does not, some repositories listed in `all-repos-1234.txt` may have been
deleted/renamed before they could be copied. deleted/renamed before they could be copied.
``` ```
...@@ -155,8 +151,8 @@ cat /home/git/transfer-logs/* | sort | uniq -u |\ ...@@ -155,8 +151,8 @@ cat /home/git/transfer-logs/* | sort | uniq -u |\
Suppose you have already done one sync that started after 2015-10-1 12:00 UTC. Suppose you have already done one sync that started after 2015-10-1 12:00 UTC.
Then you might only want to sync repositories that were changed via GitLab Then you might only want to sync repositories that were changed via GitLab
_after_ that time. You can use the 'SINCE' variable to tell 'rake _after_ that time. You can use the `SINCE` variable to tell `rake
gitlab:list_repos' to only print repositories with recent activity. gitlab:list_repos` to only print repositories with recent activity.
``` ```
# Omnibus # Omnibus
......
...@@ -79,6 +79,7 @@ POST /projects/:id/repository/commits ...@@ -79,6 +79,7 @@ POST /projects/:id/repository/commits
| `actions[]` | array | yes | An array of action hashes to commit as a batch. See the next table for what attributes it can take. | | `actions[]` | array | yes | An array of action hashes to commit as a batch. See the next table for what attributes it can take. |
| `author_email` | string | no | Specify the commit author's email address | | `author_email` | string | no | Specify the commit author's email address |
| `author_name` | string | no | Specify the commit author's name | | `author_name` | string | no | Specify the commit author's name |
| `stats` | boolean | no | Include commit stats. Default is true |
| `actions[]` Attribute | Type | Required | Description | | `actions[]` Attribute | Type | Required | Description |
......
...@@ -440,6 +440,30 @@ no longer be valid as soon as the deployment job finishes. This means that ...@@ -440,6 +440,30 @@ no longer be valid as soon as the deployment job finishes. This means that
Kubernetes can run the application, but in case it should be restarted or Kubernetes can run the application, but in case it should be restarted or
executed somewhere else, it cannot be accessed again. executed somewhere else, it cannot be accessed again.
> [Introduced][ce-21955] in GitLab 11.4
Database initialization and migrations for PostgreSQL can be configured to run
within the application pod by setting the project variables `DB_INITIALIZE` and
`DB_MIGRATE` respectively.
If present, `DB_INITIALIZE` will be run as a shell command within an application pod as a helm
post-install hook. Note that this means that if any deploy succeeds,
`DB_INITIALIZE` will not be processed thereafter.
If present, `DB_MIGRATE` will be run as a shell command within an application pod as
a helm pre-upgrade hook.
For example, in a Rails application:
* `DB_INITIALIZE` can be set to `cd /app && RAILS_ENV=production
bin/setup`
* `DB_MIGRATE` can be set to `cd /app && RAILS_ENV=production bin/update`
NOTE: **Note:**
The `/app` path is the directory of your project inside the docker image
as [configured by
Herokuish](https://github.com/gliderlabs/herokuish#paths)
> [Introduced][ce-19507] in GitLab 11.0. > [Introduced][ce-19507] in GitLab 11.0.
For internal and private projects a [GitLab Deploy Token](../../user/project/deploy_tokens/index.md###gitlab-deploy-token) For internal and private projects a [GitLab Deploy Token](../../user/project/deploy_tokens/index.md###gitlab-deploy-token)
...@@ -581,6 +605,8 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac ...@@ -581,6 +605,8 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac
| `BUILDPACK_URL` | The buildpack's full URL. It can point to either Git repositories or a tarball URL. For Git repositories, it is possible to point to a specific `ref`, for example `https://github.com/heroku/heroku-buildpack-ruby.git#v142` | | `BUILDPACK_URL` | The buildpack's full URL. It can point to either Git repositories or a tarball URL. For Git repositories, it is possible to point to a specific `ref`, for example `https://github.com/heroku/heroku-buildpack-ruby.git#v142` |
| `SAST_CONFIDENCE_LEVEL` | The minimum confidence level of security issues you want to be reported; `1` for Low, `2` for Medium, `3` for High; defaults to `3`.| | `SAST_CONFIDENCE_LEVEL` | The minimum confidence level of security issues you want to be reported; `1` for Low, `2` for Medium, `3` for High; defaults to `3`.|
| `DEP_SCAN_DISABLE_REMOTE_CHECKS` | Whether remote Dependency Scanning checks are disabled; defaults to `"false"`. Set to `"true"` to disable checks that send data to GitLab central servers. [Read more about remote checks](https://gitlab.com/gitlab-org/security-products/dependency-scanning#remote-checks).| | `DEP_SCAN_DISABLE_REMOTE_CHECKS` | Whether remote Dependency Scanning checks are disabled; defaults to `"false"`. Set to `"true"` to disable checks that send data to GitLab central servers. [Read more about remote checks](https://gitlab.com/gitlab-org/security-products/dependency-scanning#remote-checks).|
| `DB_INITIALIZE` | From GitLab 11.4, this variable can be used to specify the command to run to initialize the application's PostgreSQL database. It runs inside the application pod. |
| `DB_MIGRATE` | From GitLab 11.4, this variable can be used to specify the command to run to migrate the application's PostgreSQL database. It runs inside the application pod. |
| `STAGING_ENABLED` | From GitLab 10.8, this variable can be used to define a [deploy policy for staging and production environments](#deploy-policy-for-staging-and-production-environments). | | `STAGING_ENABLED` | From GitLab 10.8, this variable can be used to define a [deploy policy for staging and production environments](#deploy-policy-for-staging-and-production-environments). |
| `CANARY_ENABLED` | From GitLab 11.0, this variable can be used to define a [deploy policy for canary environments](#deploy-policy-for-canary-environments). | | `CANARY_ENABLED` | From GitLab 11.0, this variable can be used to define a [deploy policy for canary environments](#deploy-policy-for-canary-environments). |
| `INCREMENTAL_ROLLOUT_ENABLED`| From GitLab 10.8, this variable can be used to enable an [incremental rollout](#incremental-rollout-to-production) of your application for the production environment. | | `INCREMENTAL_ROLLOUT_ENABLED`| From GitLab 10.8, this variable can be used to enable an [incremental rollout](#incremental-rollout-to-production) of your application for the production environment. |
...@@ -834,4 +860,5 @@ curl --data "value=true" --header "PRIVATE-TOKEN: personal_access_token" https:/ ...@@ -834,4 +860,5 @@ curl --data "value=true" --header "PRIVATE-TOKEN: personal_access_token" https:/
[postgresql]: https://www.postgresql.org/ [postgresql]: https://www.postgresql.org/
[Auto DevOps template]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml [Auto DevOps template]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
[ee]: https://about.gitlab.com/pricing/ [ee]: https://about.gitlab.com/pricing/
[ce-21955]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21955
[ce-19507]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19507 [ce-19507]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19507
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