Commit aa4942a2 authored by Clement Ho's avatar Clement Ho

Merge branch 'master' into 'bootstrap4'

# Conflicts:
#   app/helpers/issuables_helper.rb
#   app/views/projects/_home_panel.html.haml
#   app/views/projects/commits/_commit.html.haml
parents 3ae87eef 91a3a4b5
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.16-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6" image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.7-golang-1.9-git-2.17-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6"
.dedicated-runner: &dedicated-runner .dedicated-runner: &dedicated-runner
retry: 1 retry: 1
...@@ -6,7 +6,7 @@ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git ...@@ -6,7 +6,7 @@ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git
- gitlab-org - gitlab-org
.default-cache: &default-cache .default-cache: &default-cache
key: "ruby-2.3.6-with-yarn" key: "ruby-2.3.7-with-yarn"
paths: paths:
- vendor/ruby - vendor/ruby
- .yarn-cache/ - .yarn-cache/
...@@ -571,7 +571,7 @@ static-analysis: ...@@ -571,7 +571,7 @@ static-analysis:
script: script:
- scripts/static-analysis - scripts/static-analysis
cache: cache:
key: "ruby-2.3.6-with-yarn-and-rubocop" key: "ruby-2.3.7-with-yarn-and-rubocop"
paths: paths:
- vendor/ruby - vendor/ruby
- .yarn-cache/ - .yarn-cache/
......
...@@ -59,6 +59,8 @@ linters: ...@@ -59,6 +59,8 @@ linters:
# Reports when you define the same property twice in a single rule set. # Reports when you define the same property twice in a single rule set.
DuplicateProperty: DuplicateProperty:
enabled: true enabled: true
ignore_consecutive:
- cursor
# Separate rule, function, and mixin declarations with empty lines. # Separate rule, function, and mixin declarations with empty lines.
EmptyLineBetweenBlocks: EmptyLineBetweenBlocks:
......
...@@ -384,6 +384,7 @@ group :test do ...@@ -384,6 +384,7 @@ group :test do
gem 'email_spec', '~> 1.6.0' gem 'email_spec', '~> 1.6.0'
gem 'json-schema', '~> 2.8.0' gem 'json-schema', '~> 2.8.0'
gem 'webmock', '~> 2.3.2' gem 'webmock', '~> 2.3.2'
gem 'rails-controller-testing' if rails5? # Rails5 only gem.
gem 'test_after_commit', '~> 1.1' unless rails5? # Remove this gem when migrated to rails 5.0. It's been integrated to rails 5.0. gem 'test_after_commit', '~> 1.1' unless rails5? # Remove this gem when migrated to rails 5.0. It's been integrated to rails 5.0.
gem 'sham_rack', '~> 1.3.6' gem 'sham_rack', '~> 1.3.6'
gem 'concurrent-ruby', '~> 1.0.5' gem 'concurrent-ruby', '~> 1.0.5'
...@@ -421,7 +422,7 @@ group :ed25519 do ...@@ -421,7 +422,7 @@ group :ed25519 do
end end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 0.91.0', require: 'gitaly' gem 'gitaly-proto', '~> 0.94.0', require: 'gitaly'
gem 'grpc', '~> 1.10.0' gem 'grpc', '~> 1.10.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed # Locked until https://github.com/google/protobuf/issues/4210 is closed
...@@ -440,3 +441,5 @@ gem 'grape_logging', '~> 1.7' ...@@ -440,3 +441,5 @@ gem 'grape_logging', '~> 1.7'
# Asset synchronization # Asset synchronization
gem 'asset_sync', '~> 2.2.0' gem 'asset_sync', '~> 2.2.0'
gem 'goldiloader', '~> 2.0'
...@@ -290,7 +290,7 @@ GEM ...@@ -290,7 +290,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly-proto (0.91.0) gitaly-proto (0.94.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.0)
github-linguist (5.3.3) github-linguist (5.3.3)
...@@ -320,6 +320,9 @@ GEM ...@@ -320,6 +320,9 @@ GEM
rubyntlm (~> 0.5) rubyntlm (~> 0.5)
globalid (0.4.1) globalid (0.4.1)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
goldiloader (2.0.1)
activerecord (>= 4.2, < 5.2)
activesupport (>= 4.2, < 5.2)
gollum-grit_adapter (1.0.1) gollum-grit_adapter (1.0.1)
gitlab-grit (~> 2.7, >= 2.7.1) gitlab-grit (~> 2.7, >= 2.7.1)
gollum-lib (4.2.7) gollum-lib (4.2.7)
...@@ -587,7 +590,7 @@ GEM ...@@ -587,7 +590,7 @@ GEM
orm_adapter (0.5.0) orm_adapter (0.5.0)
os (0.9.6) os (0.9.6)
parallel (1.12.1) parallel (1.12.1)
parser (2.5.0.3) parser (2.5.0.5)
ast (~> 2.4.0) ast (~> 2.4.0)
parslet (1.5.0) parslet (1.5.0)
blankslate (~> 2.0) blankslate (~> 2.0)
...@@ -1061,12 +1064,13 @@ DEPENDENCIES ...@@ -1061,12 +1064,13 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3) gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.91.0) gitaly-proto (~> 0.94.0)
github-linguist (~> 5.3.3) github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2) gitlab-markup (~> 1.6.2)
gitlab-styles (~> 2.3) gitlab-styles (~> 2.3)
gitlab_omniauth-ldap (~> 2.0.4) gitlab_omniauth-ldap (~> 2.0.4)
goldiloader (~> 2.0)
gollum-lib (~> 4.2) gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.4) gollum-rugged_adapter (~> 0.4.4)
gon (~> 6.1.0) gon (~> 6.1.0)
......
...@@ -291,7 +291,7 @@ GEM ...@@ -291,7 +291,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly-proto (0.91.0) gitaly-proto (0.94.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.0)
github-linguist (5.3.3) github-linguist (5.3.3)
...@@ -587,7 +587,7 @@ GEM ...@@ -587,7 +587,7 @@ GEM
orm_adapter (0.5.0) orm_adapter (0.5.0)
os (0.9.6) os (0.9.6)
parallel (1.12.1) parallel (1.12.1)
parser (2.5.0.4) parser (2.5.0.5)
ast (~> 2.4.0) ast (~> 2.4.0)
parslet (1.5.0) parslet (1.5.0)
blankslate (~> 2.0) blankslate (~> 2.0)
...@@ -678,6 +678,10 @@ GEM ...@@ -678,6 +678,10 @@ GEM
bundler (>= 1.3.0) bundler (>= 1.3.0)
railties (= 5.0.6) railties (= 5.0.6)
sprockets-rails (>= 2.0.0) sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.2)
actionpack (~> 5.x, >= 5.0.1)
actionview (~> 5.x, >= 5.0.1)
activesupport (~> 5.x)
rails-deprecated_sanitizer (1.0.3) rails-deprecated_sanitizer (1.0.3)
activesupport (>= 4.2.0.alpha) activesupport (>= 4.2.0.alpha)
rails-dom-testing (2.0.3) rails-dom-testing (2.0.3)
...@@ -1062,7 +1066,7 @@ DEPENDENCIES ...@@ -1062,7 +1066,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3) gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.91.0) gitaly-proto (~> 0.94.0)
github-linguist (~> 5.3.3) github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2) gitlab-markup (~> 1.6.2)
...@@ -1145,6 +1149,7 @@ DEPENDENCIES ...@@ -1145,6 +1149,7 @@ DEPENDENCIES
rack-oauth2 (~> 1.2.1) rack-oauth2 (~> 1.2.1)
rack-proxy (~> 0.6.0) rack-proxy (~> 0.6.0)
rails (= 5.0.6) rails (= 5.0.6)
rails-controller-testing
rails-deprecated_sanitizer (~> 1.0.3) rails-deprecated_sanitizer (~> 1.0.3)
rails-i18n (~> 5.1) rails-i18n (~> 5.1)
rainbow (~> 2.2) rainbow (~> 2.2)
......
...@@ -4,7 +4,8 @@ import $ from 'jquery'; ...@@ -4,7 +4,8 @@ import $ from 'jquery';
import _ from 'underscore'; import _ from 'underscore';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { __ } from './locale'; import { __ } from './locale';
import { isInIssuePage, isInMRPage, isInEpicPage, hasVueMRDiscussionsCookie, updateTooltipTitle } from './lib/utils/common_utils'; import { updateTooltipTitle } from './lib/utils/common_utils';
import { isInVueNoteablePage } from './lib/utils/dom_utils';
import flash from './flash'; import flash from './flash';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
...@@ -243,7 +244,7 @@ class AwardsHandler { ...@@ -243,7 +244,7 @@ class AwardsHandler {
addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) { addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) {
const isMainAwardsBlock = votesBlock.closest('.js-noteable-awards').length; const isMainAwardsBlock = votesBlock.closest('.js-noteable-awards').length;
if (this.isInVueNoteablePage() && !isMainAwardsBlock) { if (isInVueNoteablePage() && !isMainAwardsBlock) {
const id = votesBlock.attr('id').replace('note_', ''); const id = votesBlock.attr('id').replace('note_', '');
this.hideMenuElement($('.emoji-menu')); this.hideMenuElement($('.emoji-menu'));
...@@ -295,16 +296,8 @@ class AwardsHandler { ...@@ -295,16 +296,8 @@ class AwardsHandler {
} }
} }
isVueMRDiscussions() {
return isInMRPage() && hasVueMRDiscussionsCookie() && !$('#diffs').is(':visible');
}
isInVueNoteablePage() {
return isInIssuePage() || isInEpicPage() || this.isVueMRDiscussions();
}
getVotesBlock() { getVotesBlock() {
if (this.isInVueNoteablePage()) { if (isInVueNoteablePage()) {
const $el = $('.js-add-award.is-active').closest('.note.timeline-entry'); const $el = $('.js-add-award.is-active').closest('.note.timeline-entry');
if ($el.length) { if ($el.length) {
......
<script>
import Icon from '~/vue_shared/components/icon.vue';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import Tooltip from '~/vue_shared/directives/tooltip';
export default {
name: 'Badge',
components: {
Icon,
LoadingIcon,
Tooltip,
},
directives: {
Tooltip,
},
props: {
imageUrl: {
type: String,
required: true,
},
linkUrl: {
type: String,
required: true,
},
},
data() {
return {
hasError: false,
isLoading: true,
numRetries: 0,
};
},
computed: {
imageUrlWithRetries() {
if (this.numRetries === 0) {
return this.imageUrl;
}
return `${this.imageUrl}#retries=${this.numRetries}`;
},
},
watch: {
imageUrl() {
this.hasError = false;
this.isLoading = true;
this.numRetries = 0;
},
},
methods: {
onError() {
this.isLoading = false;
this.hasError = true;
},
onLoad() {
this.isLoading = false;
},
reloadImage() {
this.hasError = false;
this.isLoading = true;
this.numRetries += 1;
},
},
};
</script>
<template>
<div>
<a
v-show="!isLoading && !hasError"
:href="linkUrl"
target="_blank"
rel="noopener noreferrer"
>
<img
class="project-badge"
:src="imageUrlWithRetries"
@load="onLoad"
@error="onError"
aria-hidden="true"
/>
</a>
<loading-icon
v-show="isLoading"
:inline="true"
/>
<div
v-show="hasError"
class="btn-group"
>
<div class="btn btn-default btn-xs disabled">
<icon
class="prepend-left-8 append-right-8"
name="doc_image"
:size="16"
aria-hidden="true"
/>
</div>
<div
class="btn btn-default btn-xs disabled"
>
<span class="prepend-left-8 append-right-8">{{ s__('Badges|No badge image') }}</span>
</div>
</div>
<button
v-show="hasError"
class="btn btn-transparent btn-xs text-primary"
type="button"
v-tooltip
:title="s__('Badges|Reload badge image')"
@click="reloadImage"
>
<icon
name="retry"
:size="16"
/>
</button>
</div>
</template>
<script>
import _ from 'underscore';
import { mapActions, mapState } from 'vuex';
import createFlash from '~/flash';
import { s__, sprintf } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import createEmptyBadge from '../empty_badge';
import Badge from './badge.vue';
const badgePreviewDelayInMilliseconds = 1500;
export default {
name: 'BadgeForm',
components: {
Badge,
LoadingButton,
LoadingIcon,
},
props: {
isEditing: {
type: Boolean,
required: true,
},
},
computed: {
...mapState([
'badgeInAddForm',
'badgeInEditForm',
'docsUrl',
'isRendering',
'isSaving',
'renderedBadge',
]),
badge() {
if (this.isEditing) {
return this.badgeInEditForm;
}
return this.badgeInAddForm;
},
canSubmit() {
return (
this.badge !== null &&
this.badge.imageUrl &&
this.badge.imageUrl.trim() !== '' &&
this.badge.linkUrl &&
this.badge.linkUrl.trim() !== '' &&
!this.isSaving
);
},
helpText() {
const placeholders = ['project_path', 'project_id', 'default_branch', 'commit_sha']
.map(placeholder => `<code>%{${placeholder}}</code>`)
.join(', ');
return sprintf(
s__('Badges|The %{docsLinkStart}variables%{docsLinkEnd} GitLab supports: %{placeholders}'),
{
docsLinkEnd: '</a>',
docsLinkStart: `<a href="${_.escape(this.docsUrl)}">`,
placeholders,
},
false,
);
},
renderedImageUrl() {
return this.renderedBadge ? this.renderedBadge.renderedImageUrl : '';
},
renderedLinkUrl() {
return this.renderedBadge ? this.renderedBadge.renderedLinkUrl : '';
},
imageUrl: {
get() {
return this.badge ? this.badge.imageUrl : '';
},
set(imageUrl) {
const badge = this.badge || createEmptyBadge();
this.updateBadgeInForm({
...badge,
imageUrl,
});
},
},
linkUrl: {
get() {
return this.badge ? this.badge.linkUrl : '';
},
set(linkUrl) {
const badge = this.badge || createEmptyBadge();
this.updateBadgeInForm({
...badge,
linkUrl,
});
},
},
submitButtonLabel() {
if (this.isEditing) {
return s__('Badges|Save changes');
}
return s__('Badges|Add badge');
},
},
methods: {
...mapActions(['addBadge', 'renderBadge', 'saveBadge', 'stopEditing', 'updateBadgeInForm']),
debouncedPreview: _.debounce(function preview() {
this.renderBadge();
}, badgePreviewDelayInMilliseconds),
onCancel() {
this.stopEditing();
},
onSubmit() {
if (!this.canSubmit) {
return Promise.resolve();
}
if (this.isEditing) {
return this.saveBadge()
.then(() => {
createFlash(s__('Badges|The badge was saved.'), 'notice');
})
.catch(error => {
createFlash(
s__('Badges|Saving the badge failed, please check the entered URLs and try again.'),
);
throw error;
});
}
return this.addBadge()
.then(() => {
createFlash(s__('Badges|A new badge was added.'), 'notice');
})
.catch(error => {
createFlash(
s__('Badges|Adding the badge failed, please check the entered URLs and try again.'),
);
throw error;
});
},
},
badgeImageUrlPlaceholder:
'https://example.gitlab.com/%{project_path}/badges/%{default_branch}/<badge>.svg',
badgeLinkUrlPlaceholder: 'https://example.gitlab.com/%{project_path}',
};
</script>
<template>
<form
class="prepend-top-default append-bottom-default"
@submit.prevent.stop="onSubmit"
>
<div class="form-group">
<label for="badge-link-url">{{ s__('Badges|Link') }}</label>
<input
id="badge-link-url"
type="text"
class="form-control"
v-model="linkUrl"
:placeholder="$options.badgeLinkUrlPlaceholder"
@input="debouncedPreview"
/>
<span
class="help-block"
v-html="helpText"
></span>
</div>
<div class="form-group">
<label for="badge-image-url">{{ s__('Badges|Badge image URL') }}</label>
<input
id="badge-image-url"
type="text"
class="form-control"
v-model="imageUrl"
:placeholder="$options.badgeImageUrlPlaceholder"
@input="debouncedPreview"
/>
<span
class="help-block"
v-html="helpText"
></span>
</div>
<div class="form-group">
<label for="badge-preview">{{ s__('Badges|Badge image preview') }}</label>
<badge
id="badge-preview"
v-show="renderedBadge && !isRendering"
:image-url="renderedImageUrl"
:link-url="renderedLinkUrl"
/>
<p v-show="isRendering">
<loading-icon
:inline="true"
/>
</p>
<p
v-show="!renderedBadge && !isRendering"
class="disabled-content"
>{{ s__('Badges|No image to preview') }}</p>
</div>
<div class="row-content-block">
<loading-button
type="submit"
container-class="btn btn-success"
:disabled="!canSubmit"
:loading="isSaving"
:label="submitButtonLabel"
/>
<button
class="btn btn-cancel"
type="button"
v-if="isEditing"
@click="onCancel"
>{{ __('Cancel') }}</button>
</div>
</form>
</template>
<script>
import { mapState } from 'vuex';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import BadgeListRow from './badge_list_row.vue';
import { GROUP_BADGE } from '../constants';
export default {
name: 'BadgeList',
components: {
BadgeListRow,
LoadingIcon,
},
computed: {
...mapState(['badges', 'isLoading', 'kind']),
hasNoBadges() {
return !this.isLoading && (!this.badges || !this.badges.length);
},
isGroupBadge() {
return this.kind === GROUP_BADGE;
},
},
};
</script>
<template>
<div class="panel panel-default">
<div class="panel-heading">
{{ s__('Badges|Your badges') }}
<span
v-show="!isLoading"
class="badge"
>{{ badges.length }}</span>
</div>
<loading-icon
v-show="isLoading"
class="panel-body"
size="2"
/>
<div
v-if="hasNoBadges"
class="panel-body"
>
<span v-if="isGroupBadge">{{ s__('Badges|This group has no badges') }}</span>
<span v-else>{{ s__('Badges|This project has no badges') }}</span>
</div>
<div
v-else
class="panel-body"
>
<badge-list-row
v-for="badge in badges"
:key="badge.id"
:badge="badge"
/>
</div>
</div>
</template>
<script>
import { mapActions, mapState } from 'vuex';
import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import { PROJECT_BADGE } from '../constants';
import Badge from './badge.vue';
export default {
name: 'BadgeListRow',
components: {
Badge,
Icon,
LoadingIcon,
},
props: {
badge: {
type: Object,
required: true,
},
},
computed: {
...mapState(['kind']),
badgeKindText() {
if (this.badge.kind === PROJECT_BADGE) {
return s__('Badges|Project Badge');
}
return s__('Badges|Group Badge');
},
canEditBadge() {
return this.badge.kind === this.kind;
},
},
methods: {
...mapActions(['editBadge', 'updateBadgeInModal']),
},
};
</script>
<template>
<div class="gl-responsive-table-row-layout gl-responsive-table-row">
<badge
class="table-section section-30"
:image-url="badge.renderedImageUrl"
:link-url="badge.renderedLinkUrl"
/>
<span class="table-section section-50 str-truncated">{{ badge.linkUrl }}</span>
<div class="table-section section-10">
<span class="badge">{{ badgeKindText }}</span>
</div>
<div class="table-section section-10 table-button-footer">
<div
v-if="canEditBadge"
class="table-action-buttons">
<button
class="btn btn-default append-right-8"
type="button"
:disabled="badge.isDeleting"
@click="editBadge(badge)"
>
<icon
name="pencil"
:size="16"
:aria-label="__('Edit')"
/>
</button>
<button
class="btn btn-danger"
type="button"
data-toggle="modal"
data-target="#delete-badge-modal"
:disabled="badge.isDeleting"
@click="updateBadgeInModal(badge)"
>
<icon
name="remove"
:size="16"
:aria-label="__('Delete')"
/>
</button>
<loading-icon
v-show="badge.isDeleting"
:inline="true"
/>
</div>
</div>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import GlModal from '~/vue_shared/components/gl_modal.vue';
import Badge from './badge.vue';
import BadgeForm from './badge_form.vue';
import BadgeList from './badge_list.vue';
export default {
name: 'BadgeSettings',
components: {
Badge,
BadgeForm,
BadgeList,
GlModal,
},
computed: {
...mapState(['badgeInModal', 'isEditing']),
deleteModalText() {
return s__(
'Badges|You are going to delete this badge. Deleted badges <strong>cannot</strong> be restored.',
);
},
},
methods: {
...mapActions(['deleteBadge']),
onSubmitModal() {
this.deleteBadge(this.badgeInModal)
.then(() => {
createFlash(s__('Badges|The badge was deleted.'), 'notice');
})
.catch(error => {
createFlash(s__('Badges|Deleting the badge failed, please try again.'));
throw error;
});
},
},
};
</script>
<template>
<div class="badge-settings">
<gl-modal
id="delete-badge-modal"
:header-title-text="s__('Badges|Delete badge?')"
footer-primary-button-variant="danger"
:footer-primary-button-text="s__('Badges|Delete badge')"
@submit="onSubmitModal">
<div class="well">
<badge
:image-url="badgeInModal ? badgeInModal.renderedImageUrl : ''"
:link-url="badgeInModal ? badgeInModal.renderedLinkUrl : ''"
/>
</div>
<p v-html="deleteModalText"></p>
</gl-modal>
<badge-form
v-show="isEditing"
:is-editing="true"
/>
<badge-form
v-show="!isEditing"
:is-editing="false"
/>
<badge-list v-show="!isEditing" />
</div>
</template>
export const GROUP_BADGE = 'group';
export const PROJECT_BADGE = 'project';
export default () => ({
imageUrl: '',
isDeleting: false,
linkUrl: '',
renderedImageUrl: '',
renderedLinkUrl: '',
});
import axios from '~/lib/utils/axios_utils';
import types from './mutation_types';
export const transformBackendBadge = badge => ({
id: badge.id,
imageUrl: badge.image_url,
kind: badge.kind,
linkUrl: badge.link_url,
renderedImageUrl: badge.rendered_image_url,
renderedLinkUrl: badge.rendered_link_url,
isDeleting: false,
});
export default {
requestNewBadge({ commit }) {
commit(types.REQUEST_NEW_BADGE);
},
receiveNewBadge({ commit }, newBadge) {
commit(types.RECEIVE_NEW_BADGE, newBadge);
},
receiveNewBadgeError({ commit }) {
commit(types.RECEIVE_NEW_BADGE_ERROR);
},
addBadge({ dispatch, state }) {
const newBadge = state.badgeInAddForm;
const endpoint = state.apiEndpointUrl;
dispatch('requestNewBadge');
return axios
.post(endpoint, {
image_url: newBadge.imageUrl,
link_url: newBadge.linkUrl,
})
.catch(error => {
dispatch('receiveNewBadgeError');
throw error;
})
.then(res => {
dispatch('receiveNewBadge', transformBackendBadge(res.data));
});
},
requestDeleteBadge({ commit }, badgeId) {
commit(types.REQUEST_DELETE_BADGE, badgeId);
},
receiveDeleteBadge({ commit }, badgeId) {
commit(types.RECEIVE_DELETE_BADGE, badgeId);
},
receiveDeleteBadgeError({ commit }, badgeId) {
commit(types.RECEIVE_DELETE_BADGE_ERROR, badgeId);
},
deleteBadge({ dispatch, state }, badge) {
const badgeId = badge.id;
dispatch('requestDeleteBadge', badgeId);
const endpoint = `${state.apiEndpointUrl}/${badgeId}`;
return axios
.delete(endpoint)
.catch(error => {
dispatch('receiveDeleteBadgeError', badgeId);
throw error;
})
.then(() => {
dispatch('receiveDeleteBadge', badgeId);
});
},
editBadge({ commit }, badge) {
commit(types.START_EDITING, badge);
},
requestLoadBadges({ commit }, data) {
commit(types.REQUEST_LOAD_BADGES, data);
},
receiveLoadBadges({ commit }, badges) {
commit(types.RECEIVE_LOAD_BADGES, badges);
},
receiveLoadBadgesError({ commit }) {
commit(types.RECEIVE_LOAD_BADGES_ERROR);
},
loadBadges({ dispatch, state }, data) {
dispatch('requestLoadBadges', data);
const endpoint = state.apiEndpointUrl;
return axios
.get(endpoint)
.catch(error => {
dispatch('receiveLoadBadgesError');
throw error;
})
.then(res => {
dispatch('receiveLoadBadges', res.data.map(transformBackendBadge));
});
},
requestRenderedBadge({ commit }) {
commit(types.REQUEST_RENDERED_BADGE);
},
receiveRenderedBadge({ commit }, renderedBadge) {
commit(types.RECEIVE_RENDERED_BADGE, renderedBadge);
},
receiveRenderedBadgeError({ commit }) {
commit(types.RECEIVE_RENDERED_BADGE_ERROR);
},
renderBadge({ dispatch, state }) {
const badge = state.isEditing ? state.badgeInEditForm : state.badgeInAddForm;
const { linkUrl, imageUrl } = badge;
if (!linkUrl || linkUrl.trim() === '' || !imageUrl || imageUrl.trim() === '') {
return Promise.resolve(badge);
}
dispatch('requestRenderedBadge');
const parameters = [
`link_url=${encodeURIComponent(linkUrl)}`,
`image_url=${encodeURIComponent(imageUrl)}`,
].join('&');
const renderEndpoint = `${state.apiEndpointUrl}/render?${parameters}`;
return axios
.get(renderEndpoint)
.catch(error => {
dispatch('receiveRenderedBadgeError');
throw error;
})
.then(res => {
dispatch('receiveRenderedBadge', transformBackendBadge(res.data));
});
},
requestUpdatedBadge({ commit }) {
commit(types.REQUEST_UPDATED_BADGE);
},
receiveUpdatedBadge({ commit }, updatedBadge) {
commit(types.RECEIVE_UPDATED_BADGE, updatedBadge);
},
receiveUpdatedBadgeError({ commit }) {
commit(types.RECEIVE_UPDATED_BADGE_ERROR);
},
saveBadge({ dispatch, state }) {
const badge = state.badgeInEditForm;
const endpoint = `${state.apiEndpointUrl}/${badge.id}`;
dispatch('requestUpdatedBadge');
return axios
.put(endpoint, {
image_url: badge.imageUrl,
link_url: badge.linkUrl,
})
.catch(error => {
dispatch('receiveUpdatedBadgeError');
throw error;
})
.then(res => {
dispatch('receiveUpdatedBadge', transformBackendBadge(res.data));
});
},
stopEditing({ commit }) {
commit(types.STOP_EDITING);
},
updateBadgeInForm({ commit }, badge) {
commit(types.UPDATE_BADGE_IN_FORM, badge);
},
updateBadgeInModal({ commit }, badge) {
commit(types.UPDATE_BADGE_IN_MODAL, badge);
},
};
import Vue from 'vue';
import Vuex from 'vuex';
import createState from './state';
import actions from './actions';
import mutations from './mutations';
Vue.use(Vuex);
export default new Vuex.Store({
state: createState(),
actions,
mutations,
});
export default {
RECEIVE_DELETE_BADGE: 'RECEIVE_DELETE_BADGE',
RECEIVE_DELETE_BADGE_ERROR: 'RECEIVE_DELETE_BADGE_ERROR',
RECEIVE_LOAD_BADGES: 'RECEIVE_LOAD_BADGES',
RECEIVE_LOAD_BADGES_ERROR: 'RECEIVE_LOAD_BADGES_ERROR',
RECEIVE_NEW_BADGE: 'RECEIVE_NEW_BADGE',
RECEIVE_NEW_BADGE_ERROR: 'RECEIVE_NEW_BADGE_ERROR',
RECEIVE_RENDERED_BADGE: 'RECEIVE_RENDERED_BADGE',
RECEIVE_RENDERED_BADGE_ERROR: 'RECEIVE_RENDERED_BADGE_ERROR',
RECEIVE_UPDATED_BADGE: 'RECEIVE_UPDATED_BADGE',
RECEIVE_UPDATED_BADGE_ERROR: 'RECEIVE_UPDATED_BADGE_ERROR',
REQUEST_DELETE_BADGE: 'REQUEST_DELETE_BADGE',
REQUEST_LOAD_BADGES: 'REQUEST_LOAD_BADGES',
REQUEST_NEW_BADGE: 'REQUEST_NEW_BADGE',
REQUEST_RENDERED_BADGE: 'REQUEST_RENDERED_BADGE',
REQUEST_UPDATED_BADGE: 'REQUEST_UPDATED_BADGE',
START_EDITING: 'START_EDITING',
STOP_EDITING: 'STOP_EDITING',
UPDATE_BADGE_IN_FORM: 'UPDATE_BADGE_IN_FORM',
UPDATE_BADGE_IN_MODAL: 'UPDATE_BADGE_IN_MODAL',
};
import types from './mutation_types';
import { PROJECT_BADGE } from '../constants';
const reorderBadges = badges =>
badges.sort((a, b) => {
if (a.kind !== b.kind) {
return a.kind === PROJECT_BADGE ? 1 : -1;
}
return a.id - b.id;
});
export default {
[types.RECEIVE_NEW_BADGE](state, newBadge) {
Object.assign(state, {
badgeInAddForm: null,
badges: reorderBadges(state.badges.concat(newBadge)),
isSaving: false,
renderedBadge: null,
});
},
[types.RECEIVE_NEW_BADGE_ERROR](state) {
Object.assign(state, {
isSaving: false,
});
},
[types.REQUEST_NEW_BADGE](state) {
Object.assign(state, {
isSaving: true,
});
},
[types.RECEIVE_UPDATED_BADGE](state, updatedBadge) {
const badges = state.badges.map(badge => {
if (badge.id === updatedBadge.id) {
return updatedBadge;
}
return badge;
});
Object.assign(state, {
badgeInEditForm: null,
badges,
isEditing: false,
isSaving: false,
renderedBadge: null,
});
},
[types.RECEIVE_UPDATED_BADGE_ERROR](state) {
Object.assign(state, {
isSaving: false,
});
},
[types.REQUEST_UPDATED_BADGE](state) {
Object.assign(state, {
isSaving: true,
});
},
[types.RECEIVE_LOAD_BADGES](state, badges) {
Object.assign(state, {
badges: reorderBadges(badges),
isLoading: false,
});
},
[types.RECEIVE_LOAD_BADGES_ERROR](state) {
Object.assign(state, {
isLoading: false,
});
},
[types.REQUEST_LOAD_BADGES](state, data) {
Object.assign(state, {
kind: data.kind, // project or group
apiEndpointUrl: data.apiEndpointUrl,
docsUrl: data.docsUrl,
isLoading: true,
});
},
[types.RECEIVE_DELETE_BADGE](state, badgeId) {
const badges = state.badges.filter(badge => badge.id !== badgeId);
Object.assign(state, {
badges,
});
},
[types.RECEIVE_DELETE_BADGE_ERROR](state, badgeId) {
const badges = state.badges.map(badge => {
if (badge.id === badgeId) {
return {
...badge,
isDeleting: false,
};
}
return badge;
});
Object.assign(state, {
badges,
});
},
[types.REQUEST_DELETE_BADGE](state, badgeId) {
const badges = state.badges.map(badge => {
if (badge.id === badgeId) {
return {
...badge,
isDeleting: true,
};
}
return badge;
});
Object.assign(state, {
badges,
});
},
[types.RECEIVE_RENDERED_BADGE](state, renderedBadge) {
Object.assign(state, { isRendering: false, renderedBadge });
},
[types.RECEIVE_RENDERED_BADGE_ERROR](state) {
Object.assign(state, { isRendering: false });
},
[types.REQUEST_RENDERED_BADGE](state) {
Object.assign(state, { isRendering: true });
},
[types.START_EDITING](state, badge) {
Object.assign(state, {
badgeInEditForm: { ...badge },
isEditing: true,
renderedBadge: { ...badge },
});
},
[types.STOP_EDITING](state) {
Object.assign(state, {
badgeInEditForm: null,
isEditing: false,
renderedBadge: null,
});
},
[types.UPDATE_BADGE_IN_FORM](state, badge) {
if (state.isEditing) {
Object.assign(state, {
badgeInEditForm: badge,
});
} else {
Object.assign(state, {
badgeInAddForm: badge,
});
}
},
[types.UPDATE_BADGE_IN_MODAL](state, badge) {
Object.assign(state, {
badgeInModal: badge,
});
},
};
export default () => ({
apiEndpointUrl: null,
badgeInAddForm: null,
badgeInEditForm: null,
badgeInModal: null,
badges: [],
docsUrl: null,
renderedBadge: null,
isEditing: false,
isLoading: false,
isRendering: false,
isSaving: false,
});
...@@ -94,7 +94,7 @@ export default class FileTemplateMediator { ...@@ -94,7 +94,7 @@ export default class FileTemplateMediator {
const hash = urlPieces[1]; const hash = urlPieces[1];
if (hash === 'preview') { if (hash === 'preview') {
this.hideTemplateSelectorMenu(); this.hideTemplateSelectorMenu();
} else if (hash === 'editor') { } else if (hash === 'editor' && !this.typeSelector.isHidden()) {
this.showTemplateSelectorMenu(); this.showTemplateSelectorMenu();
} }
}); });
......
...@@ -32,6 +32,10 @@ export default class FileTemplateSelector { ...@@ -32,6 +32,10 @@ export default class FileTemplateSelector {
} }
} }
isHidden() {
return this.$wrapper.hasClass('hidden');
}
getToggleText() { getToggleText() {
return this.$dropdownToggleText.text(); return this.$dropdownToggleText.text();
} }
......
...@@ -5,7 +5,7 @@ import Sortable from 'vendor/Sortable'; ...@@ -5,7 +5,7 @@ import Sortable from 'vendor/Sortable';
import Vue from 'vue'; import Vue from 'vue';
import AccessorUtilities from '../../lib/utils/accessor'; import AccessorUtilities from '../../lib/utils/accessor';
import boardList from './board_list.vue'; import boardList from './board_list.vue';
import boardBlankState from './board_blank_state'; import BoardBlankState from './board_blank_state.vue';
import './board_delete'; import './board_delete';
const Store = gl.issueBoards.BoardsStore; const Store = gl.issueBoards.BoardsStore;
...@@ -18,7 +18,7 @@ gl.issueBoards.Board = Vue.extend({ ...@@ -18,7 +18,7 @@ gl.issueBoards.Board = Vue.extend({
components: { components: {
boardList, boardList,
'board-delete': gl.issueBoards.BoardDelete, 'board-delete': gl.issueBoards.BoardDelete,
boardBlankState, BoardBlankState,
}, },
props: { props: {
list: Object, list: Object,
......
<script>
/* global ListLabel */ /* global ListLabel */
import _ from 'underscore'; import _ from 'underscore';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
const Store = gl.issueBoards.BoardsStore; const Store = gl.issueBoards.BoardsStore;
export default { export default {
template: `
<div class="board-blank-state">
<p>
Add the following default lists to your Issue Board with one click:
</p>
<ul class="board-blank-state-list">
<li v-for="label in predefinedLabels">
<span
class="label-color"
:style="{ backgroundColor: label.color }">
</span>
{{ label.title }}
</li>
</ul>
<p>
Starting out with the default set of lists will get you right on the way to making the most of your board.
</p>
<button
class="btn btn-create btn-inverted btn-block"
type="button"
@click.stop="addDefaultLists">
Add default lists
</button>
<button
class="btn btn-default btn-block"
type="button"
@click.stop="clearBlankState">
Nevermind, I'll use my own
</button>
</div>
`,
data() { data() {
return { return {
predefinedLabels: [ predefinedLabels: [
...@@ -89,3 +58,41 @@ export default { ...@@ -89,3 +58,41 @@ export default {
clearBlankState: Store.removeBlankState.bind(Store), clearBlankState: Store.removeBlankState.bind(Store),
}, },
}; };
</script>
<template>
<div class="board-blank-state">
<p>
Add the following default lists to your Issue Board with one click:
</p>
<ul class="board-blank-state-list">
<li
v-for="(label, index) in predefinedLabels"
:key="index"
>
<span
class="label-color"
:style="{ backgroundColor: label.color }">
</span>
{{ label.title }}
</li>
</ul>
<p>
Starting out with the default set of lists will get you
right on the way to making the most of your board.
</p>
<button
class="btn btn-create btn-inverted btn-block"
type="button"
@click.stop="addDefaultLists">
Add default lists
</button>
<button
class="btn btn-default btn-block"
type="button"
@click.stop="clearBlankState">
Nevermind, I'll use my own
</button>
</div>
</template>
...@@ -60,10 +60,6 @@ gl.issueBoards.BoardSidebar = Vue.extend({ ...@@ -60,10 +60,6 @@ gl.issueBoards.BoardSidebar = Vue.extend({
this.issue = this.detail.issue; this.issue = this.detail.issue;
this.list = this.detail.list; this.list = this.detail.list;
this.$nextTick(() => {
this.endpoint = this.$refs.assigneeDropdown.dataset.issueUpdate;
});
}, },
deep: true deep: true
}, },
...@@ -91,7 +87,7 @@ gl.issueBoards.BoardSidebar = Vue.extend({ ...@@ -91,7 +87,7 @@ gl.issueBoards.BoardSidebar = Vue.extend({
saveAssignees () { saveAssignees () {
this.loadingAssignees = true; this.loadingAssignees = true;
gl.issueBoards.BoardsStore.detail.issue.update(this.endpoint) gl.issueBoards.BoardsStore.detail.issue.update()
.then(() => { .then(() => {
this.loadingAssignees = false; this.loadingAssignees = false;
}) })
......
...@@ -68,15 +68,6 @@ gl.issueBoards.IssueCardInner = Vue.extend({ ...@@ -68,15 +68,6 @@ gl.issueBoards.IssueCardInner = Vue.extend({
return this.issue.assignees.length > this.numberOverLimit; return this.issue.assignees.length > this.numberOverLimit;
}, },
cardUrl() {
let baseUrl = this.issueLinkBase;
if (this.groupId && this.issue.project) {
baseUrl = this.issueLinkBase.replace(':project_path', this.issue.project.path);
}
return `${baseUrl}/${this.issue.iid}`;
},
issueId() { issueId() {
if (this.issue.iid) { if (this.issue.iid) {
return `#${this.issue.iid}`; return `#${this.issue.iid}`;
...@@ -153,13 +144,13 @@ gl.issueBoards.IssueCardInner = Vue.extend({ ...@@ -153,13 +144,13 @@ gl.issueBoards.IssueCardInner = Vue.extend({
/> />
<a <a
class="js-no-trigger" class="js-no-trigger"
:href="cardUrl" :href="issue.path"
:title="issue.title">{{ issue.title }}</a> :title="issue.title">{{ issue.title }}</a>
<span <span
class="card-number" class="card-number"
v-if="issueId" v-if="issueId"
> >
<template v-if="groupId && issue.project">{{issue.project.path}}</template>{{ issueId }} {{ issue.referencePath }}
</span> </span>
</h4> </h4>
<div class="card-assignee"> <div class="card-assignee">
......
import Vue from 'vue'; import Vue from 'vue';
import ModalStore from '../../stores/modal_store';
const ModalStore = gl.issueBoards.ModalStore; import modalMixin from '../../mixins/modal_mixins';
gl.issueBoards.ModalEmptyState = Vue.extend({ gl.issueBoards.ModalEmptyState = Vue.extend({
mixins: [gl.issueBoards.ModalMixins], mixins: [modalMixin],
data() { data() {
return ModalStore.store; return ModalStore.store;
}, },
......
...@@ -3,11 +3,11 @@ import Flash from '../../../flash'; ...@@ -3,11 +3,11 @@ import Flash from '../../../flash';
import { __ } from '../../../locale'; import { __ } from '../../../locale';
import './lists_dropdown'; import './lists_dropdown';
import { pluralize } from '../../../lib/utils/text_utility'; import { pluralize } from '../../../lib/utils/text_utility';
import ModalStore from '../../stores/modal_store';
const ModalStore = gl.issueBoards.ModalStore; import modalMixin from '../../mixins/modal_mixins';
gl.issueBoards.ModalFooter = Vue.extend({ gl.issueBoards.ModalFooter = Vue.extend({
mixins: [gl.issueBoards.ModalMixins], mixins: [modalMixin],
data() { data() {
return { return {
modal: ModalStore.store, modal: ModalStore.store,
......
import Vue from 'vue'; import Vue from 'vue';
import modalFilters from './filters'; import modalFilters from './filters';
import './tabs'; import './tabs';
import ModalStore from '../../stores/modal_store';
const ModalStore = gl.issueBoards.ModalStore; import modalMixin from '../../mixins/modal_mixins';
gl.issueBoards.ModalHeader = Vue.extend({ gl.issueBoards.ModalHeader = Vue.extend({
mixins: [gl.issueBoards.ModalMixins], mixins: [modalMixin],
props: { props: {
projectId: { projectId: {
type: Number, type: Number,
......
...@@ -7,8 +7,7 @@ import './header'; ...@@ -7,8 +7,7 @@ import './header';
import './list'; import './list';
import './footer'; import './footer';
import './empty_state'; import './empty_state';
import ModalStore from '../../stores/modal_store';
const ModalStore = gl.issueBoards.ModalStore;
gl.issueBoards.IssuesModal = Vue.extend({ gl.issueBoards.IssuesModal = Vue.extend({
props: { props: {
......
...@@ -2,8 +2,7 @@ ...@@ -2,8 +2,7 @@
import Vue from 'vue'; import Vue from 'vue';
import bp from '../../../breakpoints'; import bp from '../../../breakpoints';
import ModalStore from '../../stores/modal_store';
const ModalStore = gl.issueBoards.ModalStore;
gl.issueBoards.ModalList = Vue.extend({ gl.issueBoards.ModalList = Vue.extend({
props: { props: {
......
import Vue from 'vue'; import Vue from 'vue';
import ModalStore from '../../stores/modal_store';
const ModalStore = gl.issueBoards.ModalStore;
gl.issueBoards.ModalFooterListsDropdown = Vue.extend({ gl.issueBoards.ModalFooterListsDropdown = Vue.extend({
data() { data() {
......
import Vue from 'vue'; import Vue from 'vue';
import ModalStore from '../../stores/modal_store';
const ModalStore = gl.issueBoards.ModalStore; import modalMixin from '../../mixins/modal_mixins';
gl.issueBoards.ModalTabs = Vue.extend({ gl.issueBoards.ModalTabs = Vue.extend({
mixins: [gl.issueBoards.ModalMixins], mixins: [modalMixin],
data() { data() {
return ModalStore.store; return ModalStore.store;
}, },
......
...@@ -17,14 +17,10 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({ ...@@ -17,14 +17,10 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({
type: Object, type: Object,
required: true, required: true,
}, },
issueUpdate: {
type: String,
required: true,
},
}, },
computed: { computed: {
updateUrl() { updateUrl() {
return this.issueUpdate.replace(':project_path', this.issue.project.path); return this.issue.path;
}, },
}, },
methods: { methods: {
......
...@@ -6,6 +6,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager { ...@@ -6,6 +6,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
constructor(store, updateUrl = false, cantEdit = []) { constructor(store, updateUrl = false, cantEdit = []) {
super({ super({
page: 'boards', page: 'boards',
isGroupDecendent: true,
stateFiltersSelector: '.issues-state-filters', stateFiltersSelector: '.issues-state-filters',
}); });
......
...@@ -17,9 +17,9 @@ import './models/milestone'; ...@@ -17,9 +17,9 @@ import './models/milestone';
import './models/project'; import './models/project';
import './models/assignee'; import './models/assignee';
import './stores/boards_store'; import './stores/boards_store';
import './stores/modal_store'; import ModalStore from './stores/modal_store';
import BoardService from './services/board_service'; import BoardService from './services/board_service';
import './mixins/modal_mixins'; import modalMixin from './mixins/modal_mixins';
import './mixins/sortable_default_options'; import './mixins/sortable_default_options';
import './filters/due_date_filters'; import './filters/due_date_filters';
import './components/board'; import './components/board';
...@@ -31,7 +31,6 @@ import '~/vue_shared/vue_resource_interceptor'; // eslint-disable-line import/fi ...@@ -31,7 +31,6 @@ import '~/vue_shared/vue_resource_interceptor'; // eslint-disable-line import/fi
export default () => { export default () => {
const $boardApp = document.getElementById('board-app'); const $boardApp = document.getElementById('board-app');
const Store = gl.issueBoards.BoardsStore; const Store = gl.issueBoards.BoardsStore;
const ModalStore = gl.issueBoards.ModalStore;
window.gl = window.gl || {}; window.gl = window.gl || {};
...@@ -176,7 +175,7 @@ export default () => { ...@@ -176,7 +175,7 @@ export default () => {
gl.IssueBoardsModalAddBtn = new Vue({ gl.IssueBoardsModalAddBtn = new Vue({
el: document.getElementById('js-add-issues-btn'), el: document.getElementById('js-add-issues-btn'),
mixins: [gl.issueBoards.ModalMixins], mixins: [modalMixin],
data() { data() {
return { return {
modal: ModalStore.store, modal: ModalStore.store,
......
const ModalStore = gl.issueBoards.ModalStore; import ModalStore from '../stores/modal_store';
gl.issueBoards.ModalMixins = { export default {
methods: { methods: {
toggleModal(toggle) { toggleModal(toggle) {
ModalStore.store.showAddIssuesModal = toggle; ModalStore.store.showAddIssuesModal = toggle;
......
...@@ -23,6 +23,8 @@ class ListIssue { ...@@ -23,6 +23,8 @@ class ListIssue {
}; };
this.isLoading = {}; this.isLoading = {};
this.sidebarInfoEndpoint = obj.issue_sidebar_endpoint; this.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
this.referencePath = obj.reference_path;
this.path = obj.real_path;
this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint; this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
this.milestone_id = obj.milestone_id; this.milestone_id = obj.milestone_id;
this.project_id = obj.project_id; this.project_id = obj.project_id;
...@@ -98,7 +100,7 @@ class ListIssue { ...@@ -98,7 +100,7 @@ class ListIssue {
this.isLoading[key] = value; this.isLoading[key] = value;
} }
update (url) { update () {
const data = { const data = {
issue: { issue: {
milestone_id: this.milestone ? this.milestone.id : null, milestone_id: this.milestone ? this.milestone.id : null,
...@@ -113,7 +115,7 @@ class ListIssue { ...@@ -113,7 +115,7 @@ class ListIssue {
} }
const projectPath = this.project ? this.project.path : ''; const projectPath = this.project ? this.project.path : '';
return Vue.http.patch(url.replace(':project_path', projectPath), data); return Vue.http.patch(`${this.path}.json`, data);
} }
} }
......
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
class ModalStore { class ModalStore {
constructor() { constructor() {
this.store = { this.store = {
...@@ -95,4 +92,4 @@ class ModalStore { ...@@ -95,4 +92,4 @@ class ModalStore {
} }
} }
gl.issueBoards.ModalStore = new ModalStore(); export default new ModalStore();
...@@ -36,6 +36,7 @@ export default { ...@@ -36,6 +36,7 @@ export default {
> >
<a <a
v-tooltip v-tooltip
v-if="!file.binary"
:href="file.blamePath" :href="file.blamePath"
:title="__('Blame')" :title="__('Blame')"
class="btn btn-xs btn-transparent blame" class="btn btn-xs btn-transparent blame"
......
<script> <script>
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';
import timeAgoMixin from '~/vue_shared/mixins/timeago'; import timeAgoMixin from '~/vue_shared/mixins/timeago';
export default { export default {
components: { components: {
icon, icon,
},
directives: {
tooltip,
},
mixins: [timeAgoMixin],
props: {
file: {
type: Object,
required: true,
}, },
directives: { },
tooltip, };
},
mixins: [
timeAgoMixin,
],
props: {
file: {
type: Object,
required: true,
},
},
};
</script> </script>
<template> <template>
...@@ -50,7 +48,9 @@ ...@@ -50,7 +48,9 @@
<div class="text-right"> <div class="text-right">
{{ file.eol }} {{ file.eol }}
</div> </div>
<div class="text-right"> <div
class="text-right"
v-if="!file.binary">
{{ file.editorRow }}:{{ file.editorColumn }} {{ file.editorRow }}:{{ file.editorColumn }}
</div> </div>
<div class="text-right"> <div class="text-right">
......
...@@ -171,10 +171,10 @@ export default { ...@@ -171,10 +171,10 @@ export default {
id="ide" id="ide"
class="blob-viewer-container blob-editor-container" class="blob-viewer-container blob-editor-container"
> >
<div <div class="ide-mode-tabs clearfix">
class="ide-mode-tabs clearfix" <ul
v-if="!shouldHideEditor"> class="nav-links pull-left"
<ul class="nav-links pull-left"> v-if="!shouldHideEditor">
<li :class="editTabCSS"> <li :class="editTabCSS">
<a <a
href="javascript:void(0);" href="javascript:void(0);"
...@@ -210,9 +210,10 @@ export default { ...@@ -210,9 +210,10 @@ export default {
> >
</div> </div>
<content-viewer <content-viewer
v-if="!shouldHideEditor && file.viewMode === 'preview'" v-if="shouldHideEditor || file.viewMode === 'preview'"
:content="file.content || file.raw" :content="file.content || file.raw"
:path="file.path" :path="file.rawPath"
:file-size="file.size"
:project-path="file.projectId"/> :project-path="file.projectId"/>
</div> </div>
</template> </template>
...@@ -43,6 +43,7 @@ export default { ...@@ -43,6 +43,7 @@ export default {
raw: null, raw: null,
baseRaw: null, baseRaw: null,
html: data.html, html: data.html,
size: data.size,
}); });
}, },
[types.SET_FILE_RAW_DATA](state, { file, raw }) { [types.SET_FILE_RAW_DATA](state, { file, raw }) {
......
...@@ -40,6 +40,7 @@ export const dataStructure = () => ({ ...@@ -40,6 +40,7 @@ export const dataStructure = () => ({
eol: '', eol: '',
viewMode: 'edit', viewMode: 'edit',
previewMode: null, previewMode: null,
size: 0,
}); });
export const decorateData = entity => { export const decorateData = entity => {
......
...@@ -10,6 +10,7 @@ import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; ...@@ -10,6 +10,7 @@ import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import DropdownUtils from './filtered_search/dropdown_utils'; import DropdownUtils from './filtered_search/dropdown_utils';
import CreateLabelDropdown from './create_label'; import CreateLabelDropdown from './create_label';
import flash from './flash'; import flash from './flash';
import ModalStore from './boards/stores/modal_store';
export default class LabelsSelect { export default class LabelsSelect {
constructor(els, options = {}) { constructor(els, options = {}) {
...@@ -350,7 +351,7 @@ export default class LabelsSelect { ...@@ -350,7 +351,7 @@ export default class LabelsSelect {
} }
if ($dropdown.closest('.add-issues-modal').length) { if ($dropdown.closest('.add-issues-modal').length) {
boardsModel = gl.issueBoards.ModalStore.store.filter; boardsModel = ModalStore.store.filter;
} }
if (boardsModel) { if (boardsModel) {
......
/* eslint-disable import/prefer-default-export */ import $ from 'jquery';
import { isInIssuePage, isInMRPage, isInEpicPage, hasVueMRDiscussionsCookie } from './common_utils';
const isVueMRDiscussions = () => isInMRPage() && hasVueMRDiscussionsCookie() && !$('#diffs').is(':visible');
export const addClassIfElementExists = (element, className) => { export const addClassIfElementExists = (element, className) => {
if (element) { if (element) {
element.classList.add(className); element.classList.add(className);
} }
}; };
export const isInVueNoteablePage = () => isInIssuePage() || isInEpicPage() || isVueMRDiscussions();
...@@ -7,7 +7,8 @@ ...@@ -7,7 +7,8 @@
* @param {String} text * @param {String} text
* @returns {String} * @returns {String}
*/ */
export const addDelimiter = text => (text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') : text); export const addDelimiter = text =>
(text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') : text);
/** /**
* Returns '99+' for numbers bigger than 99. * Returns '99+' for numbers bigger than 99.
...@@ -22,7 +23,8 @@ export const highCountTrim = count => (count > 99 ? '99+' : count); ...@@ -22,7 +23,8 @@ export const highCountTrim = count => (count > 99 ? '99+' : count);
* @param {String} string * @param {String} string
* @requires {String} * @requires {String}
*/ */
export const humanize = string => string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1); export const humanize = string =>
string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
/** /**
* Adds an 's' to the end of the string when count is bigger than 0 * Adds an 's' to the end of the string when count is bigger than 0
...@@ -53,7 +55,7 @@ export const slugify = str => str.trim().toLowerCase(); ...@@ -53,7 +55,7 @@ export const slugify = str => str.trim().toLowerCase();
* @param {Number} maxLength * @param {Number} maxLength
* @returns {String} * @returns {String}
*/ */
export const truncate = (string, maxLength) => `${string.substr(0, (maxLength - 3))}...`; export const truncate = (string, maxLength) => `${string.substr(0, maxLength - 3)}...`;
/** /**
* Capitalizes first character * Capitalizes first character
...@@ -80,3 +82,15 @@ export const stripHtml = (string, replace = '') => string.replace(/<[^>]*>/g, re ...@@ -80,3 +82,15 @@ export const stripHtml = (string, replace = '') => string.replace(/<[^>]*>/g, re
* @param {*} string * @param {*} string
*/ */
export const convertToCamelCase = string => string.replace(/(_\w)/g, s => s[1].toUpperCase()); export const convertToCamelCase = string => string.replace(/(_\w)/g, s => s[1].toUpperCase());
/**
* Converts a sentence to lower case from the second word onwards
* e.g. Hello World => Hello world
*
* @param {*} string
*/
export const convertToSentenceCase = string => {
const splitWord = string.split(' ').map((word, index) => (index > 0 ? word.toLowerCase() : word));
return splitWord.join(' ');
};
...@@ -6,6 +6,7 @@ import $ from 'jquery'; ...@@ -6,6 +6,7 @@ import $ from 'jquery';
import _ from 'underscore'; import _ from 'underscore';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility'; import { timeFor } from './lib/utils/datetime_utility';
import ModalStore from './boards/stores/modal_store';
export default class MilestoneSelect { export default class MilestoneSelect {
constructor(currentProject, els, options = {}) { constructor(currentProject, els, options = {}) {
...@@ -164,7 +165,7 @@ export default class MilestoneSelect { ...@@ -164,7 +165,7 @@ export default class MilestoneSelect {
} }
if ($dropdown.closest('.add-issues-modal').length) { if ($dropdown.closest('.add-issues-modal').length) {
boardsStore = gl.issueBoards.ModalStore.store.filter; boardsStore = ModalStore.store.filter;
} }
if (boardsStore) { if (boardsStore) {
......
<script> <script>
import { scaleLinear, scaleTime } from 'd3-scale'; import { scaleLinear, scaleTime } from 'd3-scale';
import { axisLeft, axisBottom } from 'd3-axis'; import { axisLeft, axisBottom } from 'd3-axis';
import _ from 'underscore';
import { max, extent } from 'd3-array'; import { max, extent } from 'd3-array';
import { select } from 'd3-selection'; import { select } from 'd3-selection';
import GraphAxis from './graph/axis.vue';
import GraphLegend from './graph/legend.vue'; import GraphLegend from './graph/legend.vue';
import GraphFlag from './graph/flag.vue'; import GraphFlag from './graph/flag.vue';
import GraphDeployment from './graph/deployment.vue'; import GraphDeployment from './graph/deployment.vue';
...@@ -18,10 +20,11 @@ const d3 = { scaleLinear, scaleTime, axisLeft, axisBottom, max, extent, select } ...@@ -18,10 +20,11 @@ const d3 = { scaleLinear, scaleTime, axisLeft, axisBottom, max, extent, select }
export default { export default {
components: { components: {
GraphLegend, GraphAxis,
GraphFlag, GraphFlag,
GraphDeployment, GraphDeployment,
GraphPath, GraphPath,
GraphLegend,
}, },
mixins: [MonitoringMixin], mixins: [MonitoringMixin],
props: { props: {
...@@ -138,7 +141,7 @@ export default { ...@@ -138,7 +141,7 @@ export default {
this.legendTitle = query.label || 'Average'; this.legendTitle = query.label || 'Average';
this.graphWidth = this.$refs.baseSvg.clientWidth - this.margin.left - this.margin.right; this.graphWidth = this.$refs.baseSvg.clientWidth - this.margin.left - this.margin.right;
this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom; this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
this.baseGraphHeight = this.graphHeight; this.baseGraphHeight = this.graphHeight - 50;
this.baseGraphWidth = this.graphWidth; this.baseGraphWidth = this.graphWidth;
// pixel offsets inside the svg and outside are not 1:1 // pixel offsets inside the svg and outside are not 1:1
...@@ -177,10 +180,8 @@ export default { ...@@ -177,10 +180,8 @@ export default {
this.graphHeightOffset, this.graphHeightOffset,
); );
if (!this.showLegend) { if (_.findWhere(this.timeSeries, { renderCanary: true })) {
this.baseGraphHeight -= 50; this.timeSeries = this.timeSeries.map(series => ({ ...series, renderCanary: true }));
} else if (this.timeSeries.length > 3) {
this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20;
} }
const axisXScale = d3.scaleTime().range([0, this.graphWidth - 70]); const axisXScale = d3.scaleTime().range([0, this.graphWidth - 70]);
...@@ -251,17 +252,13 @@ export default { ...@@ -251,17 +252,13 @@ export default {
class="y-axis" class="y-axis"
transform="translate(70, 20)" transform="translate(70, 20)"
/> />
<graph-legend <graph-axis
:graph-width="graphWidth" :graph-width="graphWidth"
:graph-height="graphHeight" :graph-height="graphHeight"
:margin="margin" :margin="margin"
:measurements="measurements" :measurements="measurements"
:legend-title="legendTitle"
:y-axis-label="yAxisLabel" :y-axis-label="yAxisLabel"
:time-series="timeSeries"
:unit-of-display="unitOfDisplay" :unit-of-display="unitOfDisplay"
:current-data-index="currentDataIndex"
:show-legend-group="showLegend"
/> />
<svg <svg
class="graph-data" class="graph-data"
...@@ -306,5 +303,10 @@ export default { ...@@ -306,5 +303,10 @@ export default {
:deployment-flag-data="deploymentFlagData" :deployment-flag-data="deploymentFlagData"
/> />
</div> </div>
<graph-legend
v-if="showLegend"
:legend-title="legendTitle"
:time-series="timeSeries"
/>
</div> </div>
</template> </template>
<script>
import { convertToSentenceCase } from '~/lib/utils/text_utility';
import { s__ } from '~/locale';
export default {
props: {
graphWidth: {
type: Number,
required: true,
},
graphHeight: {
type: Number,
required: true,
},
margin: {
type: Object,
required: true,
},
measurements: {
type: Object,
required: true,
},
yAxisLabel: {
type: String,
required: true,
},
unitOfDisplay: {
type: String,
required: true,
},
},
data() {
return {
yLabelWidth: 0,
yLabelHeight: 0,
};
},
computed: {
textTransform() {
const yCoordinate =
(this.graphHeight -
this.margin.top +
this.measurements.axisLabelLineOffset) /
2 || 0;
return `translate(15, ${yCoordinate}) rotate(-90)`;
},
rectTransform() {
const yCoordinate =
(this.graphHeight -
this.margin.top +
this.measurements.axisLabelLineOffset) /
2 +
this.yLabelWidth / 2 || 0;
return `translate(0, ${yCoordinate}) rotate(-90)`;
},
xPosition() {
return (
(this.graphWidth + this.measurements.axisLabelLineOffset) / 2 -
this.margin.right || 0
);
},
yPosition() {
return (
this.graphHeight -
this.margin.top +
this.measurements.axisLabelLineOffset || 0
);
},
yAxisLabelSentenceCase() {
return `${convertToSentenceCase(this.yAxisLabel)} (${this.unitOfDisplay})`;
},
timeString() {
return s__('PrometheusDashboard|Time');
},
},
mounted() {
this.$nextTick(() => {
const bbox = this.$refs.ylabel.getBBox();
this.yLabelWidth = bbox.width + 10; // Added some padding
this.yLabelHeight = bbox.height + 5;
});
},
};
</script>
<template>
<g class="axis-label-container">
<line
class="label-x-axis-line"
stroke="#000000"
stroke-width="1"
x1="10"
:y1="yPosition"
:x2="graphWidth + 20"
:y2="yPosition"
/>
<line
class="label-y-axis-line"
stroke="#000000"
stroke-width="1"
x1="10"
y1="0"
:x2="10"
:y2="yPosition"
/>
<rect
class="rect-axis-text"
:transform="rectTransform"
:width="yLabelWidth"
:height="yLabelHeight"
/>
<text
class="label-axis-text y-label-text"
text-anchor="middle"
:transform="textTransform"
ref="ylabel"
>
{{ yAxisLabelSentenceCase }}
</text>
<rect
class="rect-axis-text"
:x="xPosition + 60"
:y="graphHeight - 80"
width="35"
height="50"
/>
<text
class="label-axis-text x-label-text"
:x="xPosition + 60"
:y="yPosition"
dy=".35em"
>
{{ timeString }}
</text>
</g>
</template>
<script> <script>
import { dateFormat, timeFormat } from '../../utils/date_time_formatters'; import { dateFormat, timeFormat } from '../../utils/date_time_formatters';
import { formatRelevantDigits } from '../../../lib/utils/number_utils'; import { formatRelevantDigits } from '../../../lib/utils/number_utils';
import icon from '../../../vue_shared/components/icon.vue'; import Icon from '../../../vue_shared/components/icon.vue';
import TrackLine from './track_line.vue';
export default { export default {
components: { components: {
icon, Icon,
TrackLine,
}, },
props: { props: {
currentXCoordinate: { currentXCoordinate: {
...@@ -107,11 +109,6 @@ export default { ...@@ -107,11 +109,6 @@ export default {
} }
return `series ${index + 1}`; return `series ${index + 1}`;
}, },
strokeDashArray(type) {
if (type === 'dashed') return '6, 3';
if (type === 'dotted') return '3, 3';
return null;
},
}, },
}; };
</script> </script>
...@@ -160,28 +157,13 @@ export default { ...@@ -160,28 +157,13 @@ export default {
</div> </div>
</div> </div>
<div class="popover-content"> <div class="popover-content">
<table> <table class="prometheus-table">
<tr <tr
v-for="(series, index) in timeSeries" v-for="(series, index) in timeSeries"
:key="index" :key="index"
> >
<td> <track-line :track="series"/>
<svg <td>{{ series.track }} {{ seriesMetricLabel(index, series) }}</td>
width="15"
height="6"
>
<line
:stroke="series.lineColor"
:stroke-dasharray="strokeDashArray(series.lineStyle)"
stroke-width="4"
x1="0"
x2="15"
y1="2"
y2="2"
/>
</svg>
</td>
<td>{{ seriesMetricLabel(index, series) }}</td>
<td> <td>
<strong>{{ seriesMetricValue(series) }}</strong> <strong>{{ seriesMetricValue(series) }}</strong>
</td> </td>
......
<script> <script>
import { formatRelevantDigits } from '../../../lib/utils/number_utils'; import TrackLine from './track_line.vue';
import TrackInfo from './track_info.vue';
export default { export default {
components: {
TrackLine,
TrackInfo,
},
props: { props: {
graphWidth: {
type: Number,
required: true,
},
graphHeight: {
type: Number,
required: true,
},
margin: {
type: Object,
required: true,
},
measurements: {
type: Object,
required: true,
},
legendTitle: { legendTitle: {
type: String, type: String,
required: true, required: true,
}, },
yAxisLabel: {
type: String,
required: true,
},
timeSeries: { timeSeries: {
type: Array, type: Array,
required: true, required: true,
}, },
unitOfDisplay: {
type: String,
required: true,
},
currentDataIndex: {
type: Number,
required: true,
},
showLegendGroup: {
type: Boolean,
required: false,
default: true,
},
},
data() {
return {
yLabelWidth: 0,
yLabelHeight: 0,
seriesXPosition: 0,
metricUsageXPosition: 0,
};
},
computed: {
textTransform() {
const yCoordinate =
(this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset) / 2 || 0;
return `translate(15, ${yCoordinate}) rotate(-90)`;
},
rectTransform() {
const yCoordinate =
(this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset) / 2 +
this.yLabelWidth / 2 || 0;
return `translate(0, ${yCoordinate}) rotate(-90)`;
},
xPosition() {
return (this.graphWidth + this.measurements.axisLabelLineOffset) / 2 - this.margin.right || 0;
},
yPosition() {
return this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset || 0;
},
},
mounted() {
this.$nextTick(() => {
const bbox = this.$refs.ylabel.getBBox();
this.metricUsageXPosition = 0;
this.seriesXPosition = 0;
if (this.$refs.legendTitleSvg != null) {
this.seriesXPosition = this.$refs.legendTitleSvg[0].getBBox().width;
}
if (this.$refs.seriesTitleSvg != null) {
this.metricUsageXPosition = this.$refs.seriesTitleSvg[0].getBBox().width;
}
this.yLabelWidth = bbox.width + 10; // Added some padding
this.yLabelHeight = bbox.height + 5;
});
}, },
methods: { methods: {
translateLegendGroup(index) { isStable(track) {
return `translate(0, ${12 * index})`; return {
}, 'prometheus-table-row-highlight': track.trackName !== 'Canary' && track.renderCanary,
formatMetricUsage(series) { };
const value =
series.values[this.currentDataIndex] && series.values[this.currentDataIndex].value;
if (isNaN(value)) {
return '-';
}
return `${formatRelevantDigits(value)} ${this.unitOfDisplay}`;
},
createSeriesString(index, series) {
if (series.metricTag) {
return `${series.metricTag} ${this.formatMetricUsage(series)}`;
}
return `${this.legendTitle} series ${index + 1} ${this.formatMetricUsage(series)}`;
},
strokeDashArray(type) {
if (type === 'dashed') return '6, 3';
if (type === 'dotted') return '3, 3';
return null;
}, },
}, },
}; };
</script> </script>
<template> <template>
<g class="axis-label-container"> <div class="prometheus-graph-legends prepend-left-10">
<line <table class="prometheus-table">
class="label-x-axis-line" <tr
stroke="#000000"
stroke-width="1"
x1="10"
:y1="yPosition"
:x2="graphWidth + 20"
:y2="yPosition"
/>
<line
class="label-y-axis-line"
stroke="#000000"
stroke-width="1"
x1="10"
y1="0"
:x2="10"
:y2="yPosition"
/>
<rect
class="rect-axis-text"
:transform="rectTransform"
:width="yLabelWidth"
:height="yLabelHeight"
/>
<text
class="label-axis-text y-label-text"
text-anchor="middle"
:transform="textTransform"
ref="ylabel"
>
{{ yAxisLabel }}
</text>
<rect
class="rect-axis-text"
:x="xPosition + 60"
:y="graphHeight - 80"
width="35"
height="50"
/>
<text
class="label-axis-text x-label-text"
:x="xPosition + 60"
:y="yPosition"
dy=".35em"
>
Time
</text>
<template v-if="showLegendGroup">
<g
class="legend-group"
v-for="(series, index) in timeSeries" v-for="(series, index) in timeSeries"
:key="index" :key="index"
:transform="translateLegendGroup(index)" v-if="series.shouldRenderLegend"
:class="isStable(series)"
> >
<line <td>
:stroke="series.lineColor" <strong v-if="series.renderCanary">{{ series.trackName }}</strong>
:stroke-width="measurements.legends.height" </td>
:stroke-dasharray="strokeDashArray(series.lineStyle)" <track-line :track="series" />
:x1="measurements.legends.offsetX" <td
:x2="measurements.legends.offsetX + measurements.legends.width"
:y1="graphHeight - measurements.legends.offsetY"
:y2="graphHeight - measurements.legends.offsetY"
/>
<text
v-if="timeSeries.length > 1"
class="legend-metric-title"
ref="legendTitleSvg"
x="38"
:y="graphHeight - 30"
>
{{ createSeriesString(index, series) }}
</text>
<text
v-else
class="legend-metric-title" class="legend-metric-title"
ref="legendTitleSvg" v-if="timeSeries.length > 1">
x="38" <track-info
:y="graphHeight - 30" :track="series"
> v-if="series.metricTag" />
{{ legendTitle }} {{ formatMetricUsage(series) }} <track-info
</text> v-else
</g> :track="series">
</template> <strong>{{ legendTitle }}</strong> series {{ index + 1 }}
</g> </track-info>
</td>
<td v-else>
<track-info :track="series">
<strong>{{ legendTitle }}</strong>
</track-info>
</td>
<template v-for="(track, trackIndex) in series.tracksLegend">
<track-line
:track="track"
:key="`track-line-${trackIndex}`"/>
<td :key="`track-info-${trackIndex}`">
<track-info
class="legend-metric-title"
:track="track" />
</td>
</template>
</tr>
</table>
</div>
</template> </template>
<script>
import { formatRelevantDigits } from '~/lib/utils/number_utils';
export default {
name: 'TrackInfo',
props: {
track: {
type: Object,
required: true,
},
},
computed: {
summaryMetrics() {
return `Avg: ${formatRelevantDigits(this.track.average)} · Max: ${formatRelevantDigits(
this.track.max,
)}`;
},
},
};
</script>
<template>
<span>
<slot>
<strong> {{ track.metricTag }} </strong>
</slot>
{{ summaryMetrics }}
</span>
</template>
<script>
export default {
name: 'TrackLine',
props: {
track: {
type: Object,
required: true,
},
},
computed: {
stylizedLine() {
if (this.track.lineStyle === 'dashed') return '6, 3';
if (this.track.lineStyle === 'dotted') return '3, 3';
return null;
},
},
};
</script>
<template>
<td>
<svg
width="15"
height="6">
<line
:stroke-dasharray="stylizedLine"
:stroke="track.lineColor"
stroke-width="4"
:x1="0"
:x2="15"
:y1="2"
:y2="2"
/>
</svg>
</td>
</template>
import _ from 'underscore'; import _ from 'underscore';
function sortMetrics(metrics) { function sortMetrics(metrics) {
return _.chain(metrics).sortBy('weight').sortBy('title').value(); return _.chain(metrics).sortBy('title').sortBy('weight').value();
} }
function normalizeMetrics(metrics) { function normalizeMetrics(metrics) {
......
import _ from 'underscore'; import _ from 'underscore';
import { scaleLinear, scaleTime } from 'd3-scale'; import { scaleLinear, scaleTime } from 'd3-scale';
import { line, area, curveLinear } from 'd3-shape'; import { line, area, curveLinear } from 'd3-shape';
import { extent, max } from 'd3-array'; import { extent, max, sum } from 'd3-array';
import { timeMinute } from 'd3-time'; import { timeMinute } from 'd3-time';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
const d3 = { scaleLinear, scaleTime, line, area, curveLinear, extent, max, timeMinute };
const d3 = {
scaleLinear,
scaleTime,
line,
area,
curveLinear,
extent,
max,
timeMinute,
sum,
};
const defaultColorPalette = { const defaultColorPalette = {
blue: ['#1f78d1', '#8fbce8'], blue: ['#1f78d1', '#8fbce8'],
...@@ -20,6 +31,8 @@ const defaultStyleOrder = ['solid', 'dashed', 'dotted']; ...@@ -20,6 +31,8 @@ const defaultStyleOrder = ['solid', 'dashed', 'dotted'];
function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom, yDom, lineStyle) { function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom, yDom, lineStyle) {
let usedColors = []; let usedColors = [];
let renderCanary = false;
const timeSeriesParsed = [];
function pickColor(name) { function pickColor(name) {
let pick; let pick;
...@@ -38,16 +51,23 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom ...@@ -38,16 +51,23 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
return defaultColorPalette[pick]; return defaultColorPalette[pick];
} }
return query.result.map((timeSeries, timeSeriesNumber) => { query.result.forEach((timeSeries, timeSeriesNumber) => {
let metricTag = ''; let metricTag = '';
let lineColor = ''; let lineColor = '';
let areaColor = ''; let areaColor = '';
let shouldRenderLegend = true;
const timeSeriesValues = timeSeries.values.map(d => d.value);
const maximumValue = d3.max(timeSeriesValues);
const accum = d3.sum(timeSeriesValues);
const trackName = capitalizeFirstCharacter(query.track ? query.track : 'Stable');
if (trackName === 'Canary') {
renderCanary = true;
}
const timeSeriesScaleX = d3.scaleTime() const timeSeriesScaleX = d3.scaleTime().range([0, graphWidth - 70]);
.range([0, graphWidth - 70]);
const timeSeriesScaleY = d3.scaleLinear() const timeSeriesScaleY = d3.scaleLinear().range([graphHeight - graphHeightOffset, 0]);
.range([graphHeight - graphHeightOffset, 0]);
timeSeriesScaleX.domain(xDom); timeSeriesScaleX.domain(xDom);
timeSeriesScaleX.ticks(d3.timeMinute, 60); timeSeriesScaleX.ticks(d3.timeMinute, 60);
...@@ -55,13 +75,15 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom ...@@ -55,13 +75,15 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
const defined = d => !isNaN(d.value) && d.value != null; const defined = d => !isNaN(d.value) && d.value != null;
const lineFunction = d3.line() const lineFunction = d3
.line()
.defined(defined) .defined(defined)
.curve(d3.curveLinear) // d3 v4 uses curbe instead of interpolate .curve(d3.curveLinear) // d3 v4 uses curbe instead of interpolate
.x(d => timeSeriesScaleX(d.time)) .x(d => timeSeriesScaleX(d.time))
.y(d => timeSeriesScaleY(d.value)); .y(d => timeSeriesScaleY(d.value));
const areaFunction = d3.area() const areaFunction = d3
.area()
.defined(defined) .defined(defined)
.curve(d3.curveLinear) .curve(d3.curveLinear)
.x(d => timeSeriesScaleX(d.time)) .x(d => timeSeriesScaleX(d.time))
...@@ -69,38 +91,62 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom ...@@ -69,38 +91,62 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
.y1(d => timeSeriesScaleY(d.value)); .y1(d => timeSeriesScaleY(d.value));
const timeSeriesMetricLabel = timeSeries.metric[Object.keys(timeSeries.metric)[0]]; const timeSeriesMetricLabel = timeSeries.metric[Object.keys(timeSeries.metric)[0]];
const seriesCustomizationData = query.series != null && const seriesCustomizationData =
_.findWhere(query.series[0].when, { value: timeSeriesMetricLabel }); query.series != null && _.findWhere(query.series[0].when, { value: timeSeriesMetricLabel });
if (seriesCustomizationData) { if (seriesCustomizationData) {
metricTag = seriesCustomizationData.value || timeSeriesMetricLabel; metricTag = seriesCustomizationData.value || timeSeriesMetricLabel;
[lineColor, areaColor] = pickColor(seriesCustomizationData.color); [lineColor, areaColor] = pickColor(seriesCustomizationData.color);
shouldRenderLegend = false;
} else { } else {
metricTag = timeSeriesMetricLabel || query.label || `series ${timeSeriesNumber + 1}`; metricTag = timeSeriesMetricLabel || query.label || `series ${timeSeriesNumber + 1}`;
[lineColor, areaColor] = pickColor(); [lineColor, areaColor] = pickColor();
if (timeSeriesParsed.length > 1) {
shouldRenderLegend = false;
}
} }
if (query.track) { if (!shouldRenderLegend) {
metricTag += ` - ${query.track}`; if (!timeSeriesParsed[0].tracksLegend) {
timeSeriesParsed[0].tracksLegend = [];
}
timeSeriesParsed[0].tracksLegend.push({
max: maximumValue,
average: accum / timeSeries.values.length,
lineStyle,
lineColor,
metricTag,
});
} }
return { timeSeriesParsed.push({
linePath: lineFunction(timeSeries.values), linePath: lineFunction(timeSeries.values),
areaPath: areaFunction(timeSeries.values), areaPath: areaFunction(timeSeries.values),
timeSeriesScaleX, timeSeriesScaleX,
values: timeSeries.values, values: timeSeries.values,
max: maximumValue,
average: accum / timeSeries.values.length,
lineStyle, lineStyle,
lineColor, lineColor,
areaColor, areaColor,
metricTag, metricTag,
}; trackName,
shouldRenderLegend,
renderCanary,
});
}); });
return timeSeriesParsed;
} }
export default function createTimeSeries(queries, graphWidth, graphHeight, graphHeightOffset) { export default function createTimeSeries(queries, graphWidth, graphHeight, graphHeightOffset) {
const allValues = queries.reduce((allQueryResults, query) => allQueryResults.concat( const allValues = queries.reduce(
query.result.reduce((allResults, result) => allResults.concat(result.values), []), (allQueryResults, query) =>
), []); allQueryResults.concat(
query.result.reduce((allResults, result) => allResults.concat(result.values), []),
),
[],
);
const xDom = d3.extent(allValues, d => d.time); const xDom = d3.extent(allValues, d => d.time);
const yDom = [0, d3.max(allValues.map(d => d.value))]; const yDom = [0, d3.max(allValues.map(d => d.value))];
......
...@@ -13,8 +13,11 @@ export default function initMrNotes() { ...@@ -13,8 +13,11 @@ export default function initMrNotes() {
data() { data() {
const notesDataset = document.getElementById('js-vue-mr-discussions') const notesDataset = document.getElementById('js-vue-mr-discussions')
.dataset; .dataset;
const noteableData = JSON.parse(notesDataset.noteableData);
noteableData.noteableType = notesDataset.noteableType;
return { return {
noteableData: JSON.parse(notesDataset.noteableData), noteableData,
currentUserData: JSON.parse(notesDataset.currentUserData), currentUserData: JSON.parse(notesDataset.currentUserData),
notesData: JSON.parse(notesDataset.notesData), notesData: JSON.parse(notesDataset.notesData),
}; };
......
...@@ -49,16 +49,7 @@ export default { ...@@ -49,16 +49,7 @@ export default {
computed: { computed: {
...mapGetters(['notes', 'getNotesDataByProp', 'discussionCount']), ...mapGetters(['notes', 'getNotesDataByProp', 'discussionCount']),
noteableType() { noteableType() {
// FIXME -- @fatihacet Get this from JSON data. return this.noteableData.noteableType;
const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE, EPIC_NOTEABLE_TYPE } = constants;
if (this.noteableData.noteableType === EPIC_NOTEABLE_TYPE) {
return EPIC_NOTEABLE_TYPE;
}
return this.noteableData.merge_params
? MERGE_REQUEST_NOTEABLE_TYPE
: ISSUE_NOTEABLE_TYPE;
}, },
allNotes() { allNotes() {
if (this.isLoading) { if (this.isLoading) {
......
...@@ -14,3 +14,9 @@ export const EPIC_NOTEABLE_TYPE = 'epic'; ...@@ -14,3 +14,9 @@ export const EPIC_NOTEABLE_TYPE = 'epic';
export const MERGE_REQUEST_NOTEABLE_TYPE = 'merge_request'; export const MERGE_REQUEST_NOTEABLE_TYPE = 'merge_request';
export const UNRESOLVE_NOTE_METHOD_NAME = 'delete'; export const UNRESOLVE_NOTE_METHOD_NAME = 'delete';
export const RESOLVE_NOTE_METHOD_NAME = 'post'; export const RESOLVE_NOTE_METHOD_NAME = 'post';
export const NOTEABLE_TYPE_MAPPING = {
Issue: ISSUE_NOTEABLE_TYPE,
MergeRequest: MERGE_REQUEST_NOTEABLE_TYPE,
Epic: EPIC_NOTEABLE_TYPE,
};
...@@ -9,16 +9,7 @@ export default { ...@@ -9,16 +9,7 @@ export default {
}, },
computed: { computed: {
noteableType() { noteableType() {
switch (this.note.noteable_type) { return constants.NOTEABLE_TYPE_MAPPING[this.note.noteable_type];
case 'MergeRequest':
return constants.MERGE_REQUEST_NOTEABLE_TYPE;
case 'Issue':
return constants.ISSUE_NOTEABLE_TYPE;
case 'Epic':
return constants.EPIC_NOTEABLE_TYPE;
default:
return '';
}
}, },
}, },
}; };
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import { GROUP_BADGE } from '~/badges/constants';
import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => {
mountBadgeSettings(GROUP_BADGE);
});
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import { PROJECT_BADGE } from '~/badges/constants';
import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => {
mountBadgeSettings(PROJECT_BADGE);
});
import initForm from '../form';
document.addEventListener('DOMContentLoaded', initForm);
/* eslint-disable no-new */
import ProtectedTagCreate from '~/protected_tags/protected_tag_create';
import ProtectedTagEditList from '~/protected_tags/protected_tag_edit_list';
import initSettingsPanels from '~/settings_panels';
import initDeployKeys from '~/deploy_keys';
import ProtectedBranchCreate from '~/protected_branches/protected_branch_create';
import ProtectedBranchEditList from '~/protected_branches/protected_branch_edit_list';
import DueDateSelectors from '~/due_date_select';
export default () => {
new ProtectedTagCreate();
new ProtectedTagEditList();
initDeployKeys();
initSettingsPanels();
new ProtectedBranchCreate(); // eslint-disable-line no-new
new ProtectedBranchEditList(); // eslint-disable-line no-new
new DueDateSelectors();
};
/* eslint-disable no-new */ import initForm from '../form';
import ProtectedTagCreate from '~/protected_tags/protected_tag_create'; document.addEventListener('DOMContentLoaded', initForm);
import ProtectedTagEditList from '~/protected_tags/protected_tag_edit_list';
import initSettingsPanels from '~/settings_panels';
import initDeployKeys from '~/deploy_keys';
import ProtectedBranchCreate from '~/protected_branches/protected_branch_create';
import ProtectedBranchEditList from '~/protected_branches/protected_branch_edit_list';
document.addEventListener('DOMContentLoaded', () => {
new ProtectedTagCreate();
new ProtectedTagEditList();
initDeployKeys();
initSettingsPanels();
new ProtectedBranchCreate(); // eslint-disable-line no-new
new ProtectedBranchEditList(); // eslint-disable-line no-new
});
import Vue from 'vue';
import BadgeSettings from '~/badges/components/badge_settings.vue';
import store from '~/badges/store';
export default kind => {
const badgeSettingsElement = document.getElementById('badge-settings');
store.dispatch('loadBadges', {
kind,
apiEndpointUrl: badgeSettingsElement.dataset.apiEndpointUrl,
docsUrl: badgeSettingsElement.dataset.docsUrl,
});
return new Vue({
el: badgeSettingsElement,
store,
components: {
BadgeSettings,
},
render(createElement) {
return createElement(BadgeSettings);
},
});
};
<script> <script>
import tooltip from '../../../vue_shared/directives/tooltip'; import $ from 'jquery';
import icon from '../../../vue_shared/components/icon.vue'; import tooltip from '../../../vue_shared/directives/tooltip';
import { dasherize } from '../../../lib/utils/text_utility'; import Icon from '../../../vue_shared/components/icon.vue';
/** import { dasherize } from '../../../lib/utils/text_utility';
* Renders either a cancel, retry or play icon pointing to the given path. import eventHub from '../../event_hub';
* TODO: Remove UJS from here and use an async request instead. /**
*/ * Renders either a cancel, retry or play icon pointing to the given path.
export default { */
components: { export default {
icon, components: {
}, Icon,
},
directives: { directives: {
tooltip, tooltip,
}, },
props: { props: {
tooltipText: { tooltipText: {
type: String, type: String,
required: true, required: true,
}, },
link: { link: {
type: String, type: String,
required: true, required: true,
}, },
actionMethod: { actionIcon: {
type: String, type: String,
required: true, required: true,
}, },
actionIcon: { buttonDisabled: {
type: String, type: String,
required: true, required: false,
}, default: null,
},
},
computed: {
cssClass() {
const actionIconDash = dasherize(this.actionIcon);
return `${actionIconDash} js-icon-${actionIconDash}`;
},
isDisabled() {
return this.buttonDisabled === this.link;
}, },
},
computed: { methods: {
cssClass() { onClickAction() {
const actionIconDash = dasherize(this.actionIcon); $(this.$el).tooltip('hide');
return `${actionIconDash} js-icon-${actionIconDash}`; eventHub.$emit('graphAction', this.link);
},
}, },
}; },
};
</script> </script>
<template> <template>
<a <button
type="button"
@click="onClickAction"
v-tooltip v-tooltip
:data-method="actionMethod"
:title="tooltipText" :title="tooltipText"
:href="link" class="btn btn-blank btn-transparent ci-action-icon-container ci-action-icon-wrapper"
class="ci-action-icon-container ci-action-icon-wrapper"
:class="cssClass" :class="cssClass"
data-container="body" data-container="body"
:disabled="isDisabled"
> >
<icon :name="actionIcon" /> <icon :name="actionIcon" />
</a> </button>
</template> </template>
<script> <script>
import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import stageColumnComponent from './stage_column_component.vue'; import StageColumnComponent from './stage_column_component.vue';
export default { export default {
components: { components: {
stageColumnComponent, StageColumnComponent,
loadingIcon, LoadingIcon,
}, },
props: { props: {
isLoading: { isLoading: {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
pipeline: { pipeline: {
type: Object, type: Object,
required: true, required: true,
}, },
actionDisabled: {
type: String,
required: false,
default: null,
}, },
},
computed: { computed: {
graph() { graph() {
return this.pipeline.details && this.pipeline.details.stages; return this.pipeline.details && this.pipeline.details.stages;
},
}, },
},
methods: { methods: {
capitalizeStageName(name) { capitalizeStageName(name) {
return name.charAt(0).toUpperCase() + name.slice(1); return name.charAt(0).toUpperCase() + name.slice(1);
}, },
isFirstColumn(index) { isFirstColumn(index) {
return index === 0; return index === 0;
}, },
stageConnectorClass(index, stage) { stageConnectorClass(index, stage) {
let className; let className;
// If it's the first stage column and only has one job // If it's the first stage column and only has one job
if (index === 0 && stage.groups.length === 1) { if (index === 0 && stage.groups.length === 1) {
className = 'no-margin'; className = 'no-margin';
} else if (index > 0) { } else if (index > 0) {
// If it is not the first column // If it is not the first column
className = 'left-margin'; className = 'left-margin';
} }
return className; return className;
},
}, },
}; },
};
</script> </script>
<template> <template>
<div class="build-content middle-block js-pipeline-graph"> <div class="build-content middle-block js-pipeline-graph">
...@@ -70,6 +75,7 @@ ...@@ -70,6 +75,7 @@
:key="stage.name" :key="stage.name"
:stage-connector-class="stageConnectorClass(index, stage)" :stage-connector-class="stageConnectorClass(index, stage)"
:is-first-column="isFirstColumn(index)" :is-first-column="isFirstColumn(index)"
:action-disabled="actionDisabled"
/> />
</ul> </ul>
</div> </div>
......
<script> <script>
import actionComponent from './action_component.vue'; import ActionComponent from './action_component.vue';
import dropdownActionComponent from './dropdown_action_component.vue'; import DropdownActionComponent from './dropdown_action_component.vue';
import jobNameComponent from './job_name_component.vue'; import JobNameComponent from './job_name_component.vue';
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
/** /**
* Renders the badge for the pipeline graph and the job's dropdown. * Renders the badge for the pipeline graph and the job's dropdown.
* *
* The following object should be provided as `job`: * The following object should be provided as `job`:
* *
* { * {
* "id": 4256, * "id": 4256,
* "name": "test", * "name": "test",
* "status": { * "status": {
* "icon": "icon_status_success", * "icon": "icon_status_success",
* "text": "passed", * "text": "passed",
* "label": "passed", * "label": "passed",
* "group": "success", * "group": "success",
* "details_path": "/root/ci-mock/builds/4256", * "tooltip": "passed",
* "action": { * "details_path": "/root/ci-mock/builds/4256",
* "icon": "retry", * "action": {
* "title": "Retry", * "icon": "retry",
* "path": "/root/ci-mock/builds/4256/retry", * "title": "Retry",
* "method": "post" * "path": "/root/ci-mock/builds/4256/retry",
* } * "method": "post"
* } * }
* } * }
*/ * }
*/
export default {
components: { export default {
actionComponent, components: {
dropdownActionComponent, ActionComponent,
jobNameComponent, DropdownActionComponent,
JobNameComponent,
},
directives: {
tooltip,
},
props: {
job: {
type: Object,
required: true,
}, },
directives: { cssClassJobName: {
tooltip, type: String,
required: false,
default: '',
}, },
props: {
job: { isDropdown: {
type: Object, type: Boolean,
required: true, required: false,
}, default: false,
},
cssClassJobName: {
type: String, actionDisabled: {
required: false, type: String,
default: '', required: false,
}, default: null,
},
isDropdown: { },
type: Boolean,
required: false, computed: {
default: false, status() {
}, return this.job && this.job.status ? this.job.status : {};
},
tooltipText() {
const textBuilder = [];
if (this.job.name) {
textBuilder.push(this.job.name);
}
if (this.job.name && this.status.tooltip) {
textBuilder.push('-');
}
if (this.status.tooltip) {
textBuilder.push(`${this.job.status.tooltip}`);
}
return textBuilder.join(' ');
}, },
computed: { /**
status() { * Verifies if the provided job has an action path
return this.job && this.job.status ? this.job.status : {}; *
}, * @return {Boolean}
*/
tooltipText() { hasAction() {
const textBuilder = []; return this.job.status && this.job.status.action && this.job.status.action.path;
if (this.job.name) {
textBuilder.push(this.job.name);
}
if (this.job.name && this.status.label) {
textBuilder.push('-');
}
if (this.status.label) {
textBuilder.push(`${this.job.status.label}`);
}
return textBuilder.join(' ');
},
/**
* Verifies if the provided job has an action path
*
* @return {Boolean}
*/
hasAction() {
return this.job.status && this.job.status.action && this.job.status.action.path;
},
}, },
}; },
};
</script> </script>
<template> <template>
<div class="ci-job-component"> <div class="ci-job-component">
...@@ -100,6 +107,7 @@ ...@@ -100,6 +107,7 @@
:title="tooltipText" :title="tooltipText"
:class="cssClassJobName" :class="cssClassJobName"
data-container="body" data-container="body"
data-html="true"
class="js-pipeline-graph-job-link" class="js-pipeline-graph-job-link"
> >
...@@ -115,6 +123,7 @@ ...@@ -115,6 +123,7 @@
class="js-job-component-tooltip" class="js-job-component-tooltip"
:title="tooltipText" :title="tooltipText"
:class="cssClassJobName" :class="cssClassJobName"
data-html="true"
data-container="body" data-container="body"
> >
...@@ -129,7 +138,7 @@ ...@@ -129,7 +138,7 @@
:tooltip-text="status.action.title" :tooltip-text="status.action.title"
:link="status.action.path" :link="status.action.path"
:action-icon="status.action.icon" :action-icon="status.action.icon"
:action-method="status.action.method" :button-disabled="actionDisabled"
/> />
<dropdown-action-component <dropdown-action-component
......
<script> <script>
import jobComponent from './job_component.vue'; import JobComponent from './job_component.vue';
import dropdownJobComponent from './dropdown_job_component.vue'; import DropdownJobComponent from './dropdown_job_component.vue';
export default { export default {
components: { components: {
jobComponent, JobComponent,
dropdownJobComponent, DropdownJobComponent,
},
props: {
title: {
type: String,
required: true,
}, },
props: {
title: {
type: String,
required: true,
},
jobs: { jobs: {
type: Array, type: Array,
required: true, required: true,
}, },
isFirstColumn: { isFirstColumn: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false, default: false,
}, },
stageConnectorClass: { stageConnectorClass: {
type: String, type: String,
required: false, required: false,
default: '', default: '',
}, },
actionDisabled: {
type: String,
required: false,
default: null,
}, },
},
methods: { methods: {
firstJob(list) { firstJob(list) {
return list[0]; return list[0];
}, },
jobId(job) { jobId(job) {
return `ci-badge-${job.name}`; return `ci-badge-${job.name}`;
}, },
buildConnnectorClass(index) { buildConnnectorClass(index) {
return index === 0 && !this.isFirstColumn ? 'left-connector' : ''; return index === 0 && !this.isFirstColumn ? 'left-connector' : '';
},
}, },
}; },
};
</script> </script>
<template> <template>
<li <li
...@@ -69,6 +74,7 @@ ...@@ -69,6 +74,7 @@
v-if="job.size === 1" v-if="job.size === 1"
:job="job" :job="job"
css-class-job-name="build-content" css-class-job-name="build-content"
:action-disabled="actionDisabled"
/> />
<dropdown-job-component <dropdown-job-component
......
...@@ -25,13 +25,36 @@ export default () => { ...@@ -25,13 +25,36 @@ export default () => {
data() { data() {
return { return {
mediator, mediator,
actionDisabled: null,
}; };
}, },
created() {
eventHub.$on('graphAction', this.postAction);
},
beforeDestroy() {
eventHub.$off('graphAction', this.postAction);
},
methods: {
postAction(action) {
this.actionDisabled = action;
this.mediator.service.postAction(action)
.then(() => {
this.mediator.refreshPipeline();
this.actionDisabled = null;
})
.catch(() => {
this.actionDisabled = null;
Flash(__('An error occurred while making the request.'));
});
},
},
render(createElement) { render(createElement) {
return createElement('pipeline-graph', { return createElement('pipeline-graph', {
props: { props: {
isLoading: this.mediator.state.isLoading, isLoading: this.mediator.state.isLoading,
pipeline: this.mediator.store.state.pipeline, pipeline: this.mediator.store.state.pipeline,
actionDisabled: this.actionDisabled,
}, },
}); });
}, },
......
...@@ -52,8 +52,11 @@ export default class pipelinesMediator { ...@@ -52,8 +52,11 @@ export default class pipelinesMediator {
} }
refreshPipeline() { refreshPipeline() {
this.service.getPipeline() this.poll.stop();
return this.service.getPipeline()
.then(response => this.successCallback(response)) .then(response => this.successCallback(response))
.catch(() => this.errorCallback()); .catch(() => this.errorCallback())
.finally(() => this.poll.restart());
} }
} }
<script>
import _ from 'underscore';
import axios from '~/lib/utils/axios_utils';
import GlModal from '~/vue_shared/components/gl_modal.vue';
import { s__, sprintf } from '~/locale';
import Flash from '~/flash';
export default {
components: {
GlModal,
},
props: {
actionUrl: {
type: String,
required: true,
},
rootUrl: {
type: String,
required: true,
},
initialUsername: {
type: String,
required: true,
},
},
data() {
return {
isRequestPending: false,
username: this.initialUsername,
newUsername: this.initialUsername,
};
},
computed: {
path() {
return sprintf(s__('Profiles|Current path: %{path}'), {
path: `${this.rootUrl}${this.username}`,
});
},
modalText() {
return sprintf(
s__(`Profiles|
You are going to change the username %{currentUsernameBold} to %{newUsernameBold}.
Profile and projects will be redirected to the %{newUsername} namespace but this redirect will expire once the %{currentUsername} namespace is registered by another user or group.
Please update your Git repository remotes as soon as possible.`),
{
currentUsernameBold: `<strong>${_.escape(this.username)}</strong>`,
newUsernameBold: `<strong>${_.escape(this.newUsername)}</strong>`,
currentUsername: _.escape(this.username),
newUsername: _.escape(this.newUsername),
},
false,
);
},
},
methods: {
onConfirm() {
this.isRequestPending = true;
const username = this.newUsername;
const putData = {
user: {
username,
},
};
return axios
.put(this.actionUrl, putData)
.then(result => {
Flash(result.data.message, 'notice');
this.username = username;
this.isRequestPending = false;
})
.catch(error => {
Flash(error.response.data.message);
this.isRequestPending = false;
throw error;
});
},
},
modalId: 'username-change-confirmation-modal',
inputId: 'username-change-input',
buttonText: s__('Profiles|Update username'),
};
</script>
<template>
<div>
<div class="form-group">
<label :for="$options.inputId">{{ s__('Profiles|Path') }}</label>
<div class="input-group">
<div class="input-group-addon">{{ rootUrl }}</div>
<input
:id="$options.inputId"
class="form-control"
required="required"
v-model="newUsername"
:disabled="isRequestPending"
/>
</div>
<p class="help-block">
{{ path }}
</p>
</div>
<button
:data-target="`#${$options.modalId}`"
class="btn btn-warning"
type="button"
data-toggle="modal"
:disabled="isRequestPending || newUsername === username"
>
{{ $options.buttonText }}
</button>
<gl-modal
:id="$options.modalId"
:header-title-text="s__('Profiles|Change username') + '?'"
footer-primary-button-variant="warning"
:footer-primary-button-text="$options.buttonText"
@submit="onConfirm"
>
<span v-html="modalText"></span>
</gl-modal>
</div>
</template>
import Vue from 'vue'; import Vue from 'vue';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import UpdateUsername from './components/update_username.vue';
import deleteAccountModal from './components/delete_account_modal.vue'; import deleteAccountModal from './components/delete_account_modal.vue';
export default () => { export default () => {
Vue.use(Translate); Vue.use(Translate);
const updateUsernameElement = document.getElementById('update-username');
// eslint-disable-next-line no-new
new Vue({
el: updateUsernameElement,
components: {
UpdateUsername,
},
render(createElement) {
return createElement('update-username', {
props: { ...updateUsernameElement.dataset },
});
},
});
const deleteAccountButton = document.getElementById('delete-account-button'); const deleteAccountButton = document.getElementById('delete-account-button');
const deleteAccountModalEl = document.getElementById('delete-account-modal'); const deleteAccountModalEl = document.getElementById('delete-account-modal');
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
......
...@@ -233,21 +233,21 @@ export default class SearchAutocomplete { ...@@ -233,21 +233,21 @@ export default class SearchAutocomplete {
const issueItems = [ const issueItems = [
{ {
text: 'Issues assigned to me', text: 'Issues assigned to me',
url: `${issuesPath}/?assignee_username=${userName}`, url: `${issuesPath}/?assignee_id=${userId}`,
}, },
{ {
text: "Issues I've created", text: "Issues I've created",
url: `${issuesPath}/?author_username=${userName}`, url: `${issuesPath}/?author_id=${userId}`,
}, },
]; ];
const mergeRequestItems = [ const mergeRequestItems = [
{ {
text: 'Merge requests assigned to me', text: 'Merge requests assigned to me',
url: `${mrPath}/?assignee_username=${userName}`, url: `${mrPath}/?assignee_id=${userId}`,
}, },
{ {
text: "Merge requests I've created", text: "Merge requests I've created",
url: `${mrPath}/?author_username=${userName}`, url: `${mrPath}/?author_id=${userId}`,
}, },
]; ];
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import $ from 'jquery'; import $ from 'jquery';
import _ from 'underscore'; import _ from 'underscore';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import ModalStore from './boards/stores/modal_store';
// TODO: remove eventHub hack after code splitting refactor // TODO: remove eventHub hack after code splitting refactor
window.emitSidebarEvent = window.emitSidebarEvent || $.noop; window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
...@@ -441,7 +442,7 @@ function UsersSelect(currentUser, els, options = {}) { ...@@ -441,7 +442,7 @@ function UsersSelect(currentUser, els, options = {}) {
return; return;
} }
if ($el.closest('.add-issues-modal').length) { if ($el.closest('.add-issues-modal').length) {
gl.issueBoards.ModalStore.store.filter[$dropdown.data('fieldName')] = user.id; ModalStore.store.filter[$dropdown.data('fieldName')] = user.id;
} else if (handleClick) { } else if (handleClick) {
e.preventDefault(); e.preventDefault();
handleClick(user, isMarking); handleClick(user, isMarking);
......
<script> <script>
/* eslint-disable vue/require-default-prop */ /* eslint-disable vue/require-default-prop */
import pipelineStage from '~/pipelines/components/stage.vue'; import PipelineStage from '~/pipelines/components/stage.vue';
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';
export default { export default {
name: 'MRWidgetPipeline', name: 'MRWidgetPipeline',
components: { components: {
pipelineStage, PipelineStage,
ciIcon, CiIcon,
icon, Icon,
},
props: {
pipeline: {
type: Object,
required: true,
}, },
props: { // This prop needs to be camelCase, html attributes are case insensive
pipeline: { // https://vuejs.org/v2/guide/components.html#camelCase-vs-kebab-case
type: Object, hasCi: {
required: true, type: Boolean,
}, required: false,
// This prop needs to be camelCase, html attributes are case insensive
// https://vuejs.org/v2/guide/components.html#camelCase-vs-kebab-case
hasCi: {
type: Boolean,
required: false,
},
ciStatus: {
type: String,
required: false,
},
}, },
computed: { ciStatus: {
hasPipeline() { type: String,
return this.pipeline && Object.keys(this.pipeline).length > 0; required: false,
},
hasCIError() {
return this.hasCi && !this.ciStatus;
},
status() {
return this.pipeline.details &&
this.pipeline.details.status ? this.pipeline.details.status : {};
},
hasStages() {
return this.pipeline.details &&
this.pipeline.details.stages &&
this.pipeline.details.stages.length;
},
}, },
}; },
computed: {
hasPipeline() {
return this.pipeline && Object.keys(this.pipeline).length > 0;
},
hasCIError() {
return this.hasCi && !this.ciStatus;
},
status() {
return this.pipeline.details && this.pipeline.details.status
? this.pipeline.details.status
: {};
},
hasStages() {
return (
this.pipeline.details && this.pipeline.details.stages && this.pipeline.details.stages.length
);
},
hasCommitInfo() {
return this.pipeline.commit && Object.keys(this.pipeline.commit).length > 0;
},
},
};
</script> </script>
<template> <template>
<div <div
v-if="hasPipeline || hasCIError" v-if="hasPipeline || hasCIError"
class="mr-widget-heading"> class="mr-widget-heading"
>
<div class="ci-widget media"> <div class="ci-widget media">
<template v-if="hasCIError"> <template v-if="hasCIError">
<div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10"> <div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10">
...@@ -77,13 +82,17 @@ ...@@ -77,13 +82,17 @@
#{{ pipeline.id }} #{{ pipeline.id }}
</a> </a>
{{ pipeline.details.status.label }} for {{ pipeline.details.status.label }}
<a <template v-if="hasCommitInfo">
:href="pipeline.commit.commit_path" for
class="commit-sha js-commit-link"
> <a
{{ pipeline.commit.short_id }}</a>. :href="pipeline.commit.commit_path"
class="commit-sha js-commit-link"
>
{{ pipeline.commit.short_id }}</a>.
</template>
<span class="mr-widget-pipeline-graph"> <span class="mr-widget-pipeline-graph">
<span <span
......
<script> <script>
import { viewerInformationForPath } from './lib/viewer_utils'; import { viewerInformationForPath } from './lib/viewer_utils';
import MarkdownViewer from './viewers/markdown_viewer.vue'; import MarkdownViewer from './viewers/markdown_viewer.vue';
import ImageViewer from './viewers/image_viewer.vue';
import DownloadViewer from './viewers/download_viewer.vue';
export default { export default {
props: { props: {
content: { content: {
type: String, type: String,
required: true, default: '',
}, },
path: { path: {
type: String, type: String,
required: true, required: true,
}, },
fileSize: {
type: Number,
required: false,
default: 0,
},
projectPath: { projectPath: {
type: String, type: String,
required: false, required: false,
...@@ -20,12 +27,18 @@ export default { ...@@ -20,12 +27,18 @@ export default {
}, },
computed: { computed: {
viewer() { viewer() {
if (!this.path) return null;
const previewInfo = viewerInformationForPath(this.path); const previewInfo = viewerInformationForPath(this.path);
if (!previewInfo) return DownloadViewer;
switch (previewInfo.id) { switch (previewInfo.id) {
case 'markdown': case 'markdown':
return MarkdownViewer; return MarkdownViewer;
case 'image':
return ImageViewer;
default: default:
return null; return DownloadViewer;
} }
}, },
}, },
...@@ -36,6 +49,8 @@ export default { ...@@ -36,6 +49,8 @@ export default {
<div class="preview-container"> <div class="preview-container">
<component <component
:is="viewer" :is="viewer"
:path="path"
:file-size="fileSize"
:project-path="projectPath" :project-path="projectPath"
:content="content" :content="content"
/> />
......
const viewers = { const viewers = {
image: {
id: 'image',
},
markdown: { markdown: {
id: 'markdown', id: 'markdown',
previewTitle: 'Preview Markdown', previewTitle: 'Preview Markdown',
...@@ -7,6 +10,12 @@ const viewers = { ...@@ -7,6 +10,12 @@ const viewers = {
const fileNameViewers = {}; const fileNameViewers = {};
const fileExtensionViewers = { const fileExtensionViewers = {
jpg: 'image',
jpeg: 'image',
gif: 'image',
png: 'image',
bmp: 'image',
ico: 'image',
md: 'markdown', md: 'markdown',
markdown: 'markdown', markdown: 'markdown',
}; };
......
<script>
import Icon from '../../icon.vue';
import { numberToHumanSize } from '../../../../lib/utils/number_utils';
export default {
components: {
Icon,
},
props: {
path: {
type: String,
required: true,
},
fileSize: {
type: Number,
required: false,
default: 0,
},
},
computed: {
fileSizeReadable() {
return numberToHumanSize(this.fileSize);
},
fileName() {
return this.path.split('/').pop();
},
},
};
</script>
<template>
<div class="file-container">
<div class="file-content">
<p class="prepend-top-10 file-info">
{{ fileName }} ({{ fileSizeReadable }})
</p>
<a
:href="path"
class="btn btn-default"
rel="nofollow"
download
target="_blank">
<icon
name="download"
css-classes="pull-left append-right-8"
:size="16"
/>
{{ __('Download') }}
</a>
</div>
</div>
</template>
<script>
import { numberToHumanSize } from '../../../../lib/utils/number_utils';
export default {
props: {
path: {
type: String,
required: true,
},
fileSize: {
type: Number,
required: false,
default: 0,
},
},
data() {
return {
width: 0,
height: 0,
isZoomable: false,
isZoomed: false,
};
},
computed: {
fileSizeReadable() {
return numberToHumanSize(this.fileSize);
},
},
methods: {
onImgLoad() {
const contentImg = this.$refs.contentImg;
this.isZoomable =
contentImg.naturalWidth > contentImg.width || contentImg.naturalHeight > contentImg.height;
this.width = contentImg.naturalWidth;
this.height = contentImg.naturalHeight;
},
onImgClick() {
if (this.isZoomable) this.isZoomed = !this.isZoomed;
},
},
};
</script>
<template>
<div class="file-container">
<div class="file-content image_file">
<img
ref="contentImg"
:class="{ 'isZoomable': isZoomable, 'isZoomed': isZoomed }"
:src="path"
:alt="path"
@load="onImgLoad"
@click="onImgClick"/>
<p class="file-info prepend-top-10">
<template v-if="fileSize>0">
{{ fileSizeReadable }}
</template>
<template v-if="fileSize>0 && width && height">
-
</template>
<template v-if="width && height">
{{ width }} x {{ height }}
</template>
</p>
</div>
</div>
</template>
<script> <script>
const buttonVariants = [ const buttonVariants = ['danger', 'primary', 'success', 'warning'];
'danger',
'primary',
'success',
'warning',
];
export default { export default {
name: 'GlModal', name: 'GlModal',
props: { props: {
id: { id: {
type: String, type: String,
required: false, required: false,
default: null, default: null,
},
headerTitleText: {
type: String,
required: false,
default: '',
},
footerPrimaryButtonVariant: {
type: String,
required: false,
default: 'primary',
validator: value => buttonVariants.indexOf(value) !== -1,
},
footerPrimaryButtonText: {
type: String,
required: false,
default: '',
},
}, },
headerTitleText: {
type: String,
required: false,
default: '',
},
footerPrimaryButtonVariant: {
type: String,
required: false,
default: 'primary',
validator: value => buttonVariants.includes(value),
},
footerPrimaryButtonText: {
type: String,
required: false,
default: '',
},
},
methods: { methods: {
emitCancel(event) { emitCancel(event) {
this.$emit('cancel', event); this.$emit('cancel', event);
}, },
emitSubmit(event) { emitSubmit(event) {
this.$emit('submit', event); this.$emit('submit', event);
},
}, },
}; },
};
</script> </script>
<template> <template>
...@@ -60,7 +55,7 @@ ...@@ -60,7 +55,7 @@
<slot name="header"> <slot name="header">
<button <button
type="button" type="button"
class="close" class="close js-modal-close-action"
data-dismiss="modal" data-dismiss="modal"
:aria-label="s__('Modal|Close')" :aria-label="s__('Modal|Close')"
@click="emitCancel($event)" @click="emitCancel($event)"
...@@ -83,7 +78,7 @@ ...@@ -83,7 +78,7 @@
<slot name="footer"> <slot name="footer">
<button <button
type="button" type="button"
class="btn" class="btn js-modal-cancel-action"
data-dismiss="modal" data-dismiss="modal"
@click="emitCancel($event)" @click="emitCancel($event)"
> >
...@@ -91,7 +86,7 @@ ...@@ -91,7 +86,7 @@
</button> </button>
<button <button
type="button" type="button"
class="btn" class="btn js-modal-primary-action"
:class="`btn-${footerPrimaryButtonVariant}`" :class="`btn-${footerPrimaryButtonVariant}`"
data-dismiss="modal" data-dismiss="modal"
@click="emitSubmit($event)" @click="emitSubmit($event)"
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
width: 100%; width: 100%;
} }
$image-widths: 80 250 306 394 430; $image-widths: 80 130 250 306 394 430;
@each $width in $image-widths { @each $width in $image-widths {
&.svg-#{$width} { &.svg-#{$width} {
img, img,
......
...@@ -24,6 +24,10 @@ ...@@ -24,6 +24,10 @@
color: $list-text-disabled-color; color: $list-text-disabled-color;
} }
&:not(.ui-sort-disabled):hover {
background: $row-hover;
}
&.unstyled { &.unstyled {
&:hover { &:hover {
background: none; background: none;
...@@ -34,14 +38,15 @@ ...@@ -34,14 +38,15 @@
background-color: $list-warning-row-bg; background-color: $list-warning-row-bg;
border-color: $list-warning-row-border; border-color: $list-warning-row-border;
color: $list-warning-row-color; color: $list-warning-row-color;
}
&.smoke { background-color: $gray-light; } &:hover {
background: $list-warning-row-bg;
}
&:not(.ui-sort-disabled):hover {
background: $row-hover;
} }
&.smoke { background-color: $gray-light; }
&:last-child { &:last-child {
border-bottom: 0; border-bottom: 0;
......
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
.table-section { .table-section {
white-space: nowrap; white-space: nowrap;
$section-widths: 10 15 20 25 30 40 100; $section-widths: 10 15 20 25 30 40 50 100;
@each $width in $section-widths { @each $width in $section-widths {
&.section-#{$width} { &.section-#{$width} {
flex: 0 0 #{$width + '%'}; flex: 0 0 #{$width + '%'};
......
...@@ -289,6 +289,11 @@ body { ...@@ -289,6 +289,11 @@ body {
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
&.with-button {
line-height: 34px;
}
} }
.page-title-empty { .page-title-empty {
......
...@@ -767,3 +767,8 @@ $border-color-settings: #e1e1e1; ...@@ -767,3 +767,8 @@ $border-color-settings: #e1e1e1;
Modals Modals
*/ */
$modal-body-height: 134px; $modal-body-height: 134px;
/*
Prometheus
*/
$prometheus-table-row-highlight-color: $theme-gray-100;
...@@ -391,7 +391,7 @@ ...@@ -391,7 +391,7 @@
} }
&:hover { &:hover {
background-color: $row-hover; background-color: $dropdown-item-hover-bg;
} }
.icon-retry { .icon-retry {
......
...@@ -107,7 +107,6 @@ ...@@ -107,7 +107,6 @@
} }
} }
.commits-compare-switch { .commits-compare-switch {
float: left; float: left;
margin-right: 9px; margin-right: 9px;
...@@ -179,7 +178,7 @@ ...@@ -179,7 +178,7 @@
.commit-detail { .commit-detail {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: flex-start; align-items: center;
flex-grow: 1; flex-grow: 1;
.merge-request-branches & { .merge-request-branches & {
...@@ -200,37 +199,63 @@ ...@@ -200,37 +199,63 @@
} }
.ci-status-link { .ci-status-link {
display: inline-block; display: inline-flex;
position: relative;
top: 2px;
} }
.btn-clipboard, > .ci-status-link,
.btn-transparent { > .btn,
padding-left: 0; > .commit-sha-group {
padding-right: 0; margin-left: $gl-padding-8;
} }
}
.commit-sha-group {
display: inline-flex;
.label,
.btn { .btn {
&:not(:first-child) { padding: $gl-vert-padding $gl-btn-padding;
margin-left: $gl-padding; border: 1px $border-color solid;
} font-size: $gl-font-size;
line-height: $line-height-base;
border-radius: 0;
display: flex;
align-items: center;
}
.label-monospace {
@extend .monospace;
user-select: text;
color: $gl-text-color;
background-color: $gray-light;
} }
.commit-sha { .btn svg {
font-size: 14px; top: auto;
font-weight: $gl-font-weight-bold; fill: $gl-text-color-secondary;
} }
.ci-status-icon { .fa-clipboard {
position: relative; color: $gl-text-color-secondary;
top: 2px; }
:first-child {
border-bottom-left-radius: $border-radius-default;
border-top-left-radius: $border-radius-default;
}
:not(:first-child) {
border-left: 0;
}
:last-child {
border-bottom-right-radius: $border-radius-default;
border-top-right-radius: $border-radius-default;
} }
} }
.commit, .commit,
.generic_commit_status { .generic_commit_status {
a, a,
button { button {
color: $gl-text-color; color: $gl-text-color;
...@@ -303,10 +328,8 @@ ...@@ -303,10 +328,8 @@
} }
} }
.gpg-status-box { .gpg-status-box {
padding: 2px 10px; padding: 2px 10px;
margin-right: $gl-padding;
&:empty { &:empty {
display: none; display: none;
......
...@@ -273,21 +273,6 @@ ...@@ -273,21 +273,6 @@
line-height: 1.2; line-height: 1.2;
} }
table {
border-collapse: collapse;
padding: 0;
margin: 0;
}
td {
vertical-align: middle;
+ td {
padding-left: 5px;
vertical-align: top;
}
}
.deploy-meta-content { .deploy-meta-content {
border-bottom: 1px solid $white-dark; border-bottom: 1px solid $white-dark;
...@@ -323,6 +308,26 @@ ...@@ -323,6 +308,26 @@
} }
} }
.prometheus-table {
border-collapse: collapse;
padding: 0;
margin: 0;
td {
vertical-align: middle;
+ td {
padding-left: 5px;
vertical-align: top;
}
}
.legend-metric-title {
font-size: 12px;
vertical-align: middle;
}
}
.prometheus-svg-container { .prometheus-svg-container {
position: relative; position: relative;
height: 0; height: 0;
...@@ -330,8 +335,7 @@ ...@@ -330,8 +335,7 @@
padding: 0; padding: 0;
padding-bottom: 100%; padding-bottom: 100%;
.text-metric-usage, .text-metric-usage {
.legend-metric-title {
fill: $black; fill: $black;
font-weight: $gl-font-weight-normal; font-weight: $gl-font-weight-normal;
font-size: 12px; font-size: 12px;
...@@ -374,10 +378,6 @@ ...@@ -374,10 +378,6 @@
} }
} }
.text-metric-title {
font-size: 12px;
}
.y-label-text, .y-label-text,
.x-label-text { .x-label-text {
fill: $gray-darkest; fill: $gray-darkest;
...@@ -414,3 +414,7 @@ ...@@ -414,3 +414,7 @@
} }
} }
} }
.prometheus-table-row-highlight {
background-color: $prometheus-table-row-highlight-color;
}
.pages-domain-list {
&-item {
position: relative;
display: flex;
align-items: center;
.domain-status {
display: inline-flex;
left: $gl-padding;
position: absolute;
}
.domain-name {
flex-grow: 1;
}
}
&.has-verification-status > li {
padding-left: 3 * $gl-padding;
}
}
.status-badge {
display: inline-flex;
margin-bottom: $gl-padding-8;
// Most of the following settings "stolen" from btn-sm
// Border radius is overwritten for both
.label,
.btn {
padding: $gl-padding-4 $gl-padding-8;
font-size: $gl-font-size;
line-height: $gl-btn-line-height;
border-radius: 0;
display: flex;
align-items: center;
}
.btn svg {
top: auto;
}
:first-child {
border-bottom-left-radius: $border-radius-default;
border-top-left-radius: $border-radius-default;
}
:not(:first-child) {
border-left: 0;
}
:last-child {
border-bottom-right-radius: $border-radius-default;
border-top-right-radius: $border-radius-default;
}
}
...@@ -495,17 +495,17 @@ ...@@ -495,17 +495,17 @@
svg { svg {
fill: $gl-text-color-secondary; fill: $gl-text-color-secondary;
position: relative; position: relative;
left: 5px; left: 1px;
top: 2px; top: -1px;
width: 18px; width: 16px;
height: 18px; height: 16px;
} }
&.play { &.play {
svg { svg {
width: #{$ci-action-icon-size - 8}; width: 16px;
height: #{$ci-action-icon-size - 8}; height: 16px;
left: 8px; left: 3px;
} }
} }
} }
......
...@@ -210,13 +210,8 @@ ...@@ -210,13 +210,8 @@
} }
.created-personal-access-token-container { .created-personal-access-token-container {
#created-personal-access-token {
width: 90%;
display: inline;
}
.btn-clipboard { .btn-clipboard {
margin-left: 5px; border: 1px solid $border-color;
} }
} }
......
...@@ -1143,3 +1143,11 @@ pre.light-well { ...@@ -1143,3 +1143,11 @@ pre.light-well {
white-space: pre-wrap; white-space: pre-wrap;
} }
} }
.project-badge {
opacity: 0.9;
&:hover {
opacity: 1;
}
}
...@@ -312,6 +312,45 @@ ...@@ -312,6 +312,45 @@
height: 100%; height: 100%;
overflow: auto; overflow: auto;
.file-container {
background-color: $gray-darker;
display: flex;
height: 100%;
align-items: center;
justify-content: center;
text-align: center;
.file-content {
padding: $gl-padding;
max-width: 100%;
max-height: 100%;
img {
max-width: 90%;
max-height: 90%;
}
.isZoomable {
cursor: pointer;
cursor: zoom-in;
&.isZoomed {
cursor: pointer;
cursor: zoom-out;
max-width: none;
max-height: none;
margin-right: $gl-padding;
}
}
}
.file-info {
font-size: $label-font-size;
color: $diff-image-info-color;
}
}
.md-previewer { .md-previewer {
padding: $gl-padding; padding: $gl-padding;
} }
......
...@@ -284,3 +284,23 @@ ...@@ -284,3 +284,23 @@
.deprecated-service { .deprecated-service {
cursor: default; cursor: default;
} }
.personal-access-tokens-never-expires-label {
color: $note-disabled-comment-color;
}
.created-deploy-token-container {
.deploy-token-field {
width: 90%;
display: inline;
}
.btn-clipboard {
margin-left: 5px;
}
.deploy-token-help-block {
display: block;
margin-bottom: 0;
}
}
...@@ -96,7 +96,8 @@ module Boards ...@@ -96,7 +96,8 @@ module Boards
resource.as_json( resource.as_json(
only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position], only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position],
labels: true, labels: true,
sidebar_endpoints: true, issue_endpoints: true,
include_full_project_path: board.group_board?,
include: { include: {
project: { only: [:id, :path] }, project: { only: [:id, :path] },
assignees: { only: [:id, :name, :username], methods: [:avatar_url] }, assignees: { only: [:id, :name, :username], methods: [:avatar_url] },
......
...@@ -10,7 +10,7 @@ module AuthenticatesWithTwoFactor ...@@ -10,7 +10,7 @@ module AuthenticatesWithTwoFactor
# This action comes from DeviseController, but because we call `sign_in` # This action comes from DeviseController, but because we call `sign_in`
# manually, not skipping this action would cause a "You are already signed # manually, not skipping this action would cause a "You are already signed
# in." error message to be shown upon successful login. # in." error message to be shown upon successful login.
skip_before_action :require_no_authentication, only: [:create] skip_before_action :require_no_authentication, only: [:create], raise: false
end end
# Store the user's ID in the session for later retrieval and render the # Store the user's ID in the session for later retrieval and render the
......
...@@ -2,9 +2,17 @@ class DashboardController < Dashboard::ApplicationController ...@@ -2,9 +2,17 @@ class DashboardController < Dashboard::ApplicationController
include IssuesAction include IssuesAction
include MergeRequestsAction include MergeRequestsAction
FILTER_PARAMS = [
:author_id,
:assignee_id,
:milestone_title,
:label_name
].freeze
before_action :event_filter, only: :activity before_action :event_filter, only: :activity
before_action :projects, only: [:issues, :merge_requests] before_action :projects, only: [:issues, :merge_requests]
before_action :set_show_full_reference, only: [:issues, :merge_requests] before_action :set_show_full_reference, only: [:issues, :merge_requests]
before_action :check_filters_presence!, only: [:issues, :merge_requests]
respond_to :html respond_to :html
...@@ -39,4 +47,15 @@ class DashboardController < Dashboard::ApplicationController ...@@ -39,4 +47,15 @@ class DashboardController < Dashboard::ApplicationController
def set_show_full_reference def set_show_full_reference
@show_full_reference = true @show_full_reference = true
end end
def check_filters_presence!
@no_filters_set = FILTER_PARAMS.none? { |k| params.key?(k) }
return unless @no_filters_set
respond_to do |format|
format.html
format.atom { head :bad_request }
end
end
end end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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