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
This commit is contained in:
commit
aa4942a213
496 changed files with 17538 additions and 10503 deletions
|
@ -1,4 +1,4 @@
|
||||||
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
|
||||||
- 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:
|
||||||
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/
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
2.3.6
|
2.3.7
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
0.93.0
|
0.94.0
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
7.1.1
|
7.1.2
|
||||||
|
|
5
Gemfile
5
Gemfile
|
@ -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
|
||||||
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'
|
||||||
|
|
||||||
# Asset synchronization
|
# Asset synchronization
|
||||||
gem 'asset_sync', '~> 2.2.0'
|
gem 'asset_sync', '~> 2.2.0'
|
||||||
|
|
||||||
|
gem 'goldiloader', '~> 2.0'
|
||||||
|
|
10
Gemfile.lock
10
Gemfile.lock
|
@ -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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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';
|
||||||
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 {
|
||||||
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 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
|
|
121
app/assets/javascripts/badges/components/badge.vue
Normal file
121
app/assets/javascripts/badges/components/badge.vue
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
<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>
|
219
app/assets/javascripts/badges/components/badge_form.vue
Normal file
219
app/assets/javascripts/badges/components/badge_form.vue
Normal file
|
@ -0,0 +1,219 @@
|
||||||
|
<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>
|
57
app/assets/javascripts/badges/components/badge_list.vue
Normal file
57
app/assets/javascripts/badges/components/badge_list.vue
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
<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>
|
89
app/assets/javascripts/badges/components/badge_list_row.vue
Normal file
89
app/assets/javascripts/badges/components/badge_list_row.vue
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
<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>
|
70
app/assets/javascripts/badges/components/badge_settings.vue
Normal file
70
app/assets/javascripts/badges/components/badge_settings.vue
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
<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>
|
2
app/assets/javascripts/badges/constants.js
Normal file
2
app/assets/javascripts/badges/constants.js
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export const GROUP_BADGE = 'group';
|
||||||
|
export const PROJECT_BADGE = 'project';
|
7
app/assets/javascripts/badges/empty_badge.js
Normal file
7
app/assets/javascripts/badges/empty_badge.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export default () => ({
|
||||||
|
imageUrl: '',
|
||||||
|
isDeleting: false,
|
||||||
|
linkUrl: '',
|
||||||
|
renderedImageUrl: '',
|
||||||
|
renderedLinkUrl: '',
|
||||||
|
});
|
167
app/assets/javascripts/badges/store/actions.js
Normal file
167
app/assets/javascripts/badges/store/actions.js
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
};
|
13
app/assets/javascripts/badges/store/index.js
Normal file
13
app/assets/javascripts/badges/store/index.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
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,
|
||||||
|
});
|
21
app/assets/javascripts/badges/store/mutation_types.js
Normal file
21
app/assets/javascripts/badges/store/mutation_types.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
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',
|
||||||
|
};
|
158
app/assets/javascripts/badges/store/mutations.js
Normal file
158
app/assets/javascripts/badges/store/mutations.js
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
13
app/assets/javascripts/badges/store/state.js
Normal file
13
app/assets/javascripts/badges/store/state.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
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 {
|
||||||
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 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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';
|
||||||
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({
|
||||||
components: {
|
components: {
|
||||||
boardList,
|
boardList,
|
||||||
'board-delete': gl.issueBoards.BoardDelete,
|
'board-delete': gl.issueBoards.BoardDelete,
|
||||||
boardBlankState,
|
BoardBlankState,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
list: Object,
|
list: Object,
|
||||||
|
|
|
@ -1,42 +1,11 @@
|
||||||
|
<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 {
|
||||||
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({
|
||||||
|
|
||||||
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({
|
||||||
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({
|
||||||
|
|
||||||
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({
|
||||||
/>
|
/>
|
||||||
<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">
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
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';
|
||||||
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,
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
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';
|
||||||
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 @@
|
||||||
|
|
||||||
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: {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
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() {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
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({
|
||||||
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 {
|
||||||
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';
|
||||||
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
|
||||||
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 () => {
|
||||||
|
|
||||||
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,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
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 {
|
||||||
};
|
};
|
||||||
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 {
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
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 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
gl.issueBoards.ModalStore = new ModalStore();
|
export default new ModalStore();
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -1,25 +1,23 @@
|
||||||
<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: {
|
directives: {
|
||||||
tooltip,
|
tooltip,
|
||||||
},
|
},
|
||||||
mixins: [
|
mixins: [timeAgoMixin],
|
||||||
timeAgoMixin,
|
|
||||||
],
|
|
||||||
props: {
|
props: {
|
||||||
file: {
|
file: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -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 {
|
||||||
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
|
||||||
|
class="nav-links pull-left"
|
||||||
v-if="!shouldHideEditor">
|
v-if="!shouldHideEditor">
|
||||||
<ul class="nav-links pull-left">
|
|
||||||
<li :class="editTabCSS">
|
<li :class="editTabCSS">
|
||||||
<a
|
<a
|
||||||
href="javascript:void(0);"
|
href="javascript:void(0);"
|
||||||
|
@ -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 {
|
||||||
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 = () => ({
|
||||||
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';
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
/* 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 @@
|
||||||
* @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);
|
||||||
* @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();
|
||||||
* @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
|
||||||
* @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';
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
<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 }
|
||||||
|
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
: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>
|
||||||
|
|
142
app/assets/javascripts/monitoring/components/graph/axis.vue
Normal file
142
app/assets/javascripts/monitoring/components/graph/axis.vue
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
<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>
|
|
@ -1,11 +1,13 @@
|
||||||
<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 {
|
||||||
}
|
}
|
||||||
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 {
|
||||||
</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>
|
||||||
|
|
|
@ -1,204 +1,72 @@
|
||||||
<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"
|
class="legend-metric-title"
|
||||||
ref="legendTitleSvg"
|
v-if="timeSeries.length > 1">
|
||||||
x="38"
|
<track-info
|
||||||
:y="graphHeight - 30"
|
:track="series"
|
||||||
>
|
v-if="series.metricTag" />
|
||||||
{{ createSeriesString(index, series) }}
|
<track-info
|
||||||
</text>
|
|
||||||
<text
|
|
||||||
v-else
|
v-else
|
||||||
|
:track="series">
|
||||||
|
<strong>{{ legendTitle }}</strong> series {{ index + 1 }}
|
||||||
|
</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"
|
class="legend-metric-title"
|
||||||
ref="legendTitleSvg"
|
:track="track" />
|
||||||
x="38"
|
</td>
|
||||||
:y="graphHeight - 30"
|
|
||||||
>
|
|
||||||
{{ legendTitle }} {{ formatMetricUsage(series) }}
|
|
||||||
</text>
|
|
||||||
</g>
|
|
||||||
</template>
|
</template>
|
||||||
</g>
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
<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>
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
<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>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
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) {
|
||||||
|
|
|
@ -1,10 +1,21 @@
|
||||||
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'];
|
||||||
|
|
||||||
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
|
||||||
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');
|
||||||
|
|
||||||
const timeSeriesScaleX = d3.scaleTime()
|
if (trackName === 'Canary') {
|
||||||
.range([0, graphWidth - 70]);
|
renderCanary = true;
|
||||||
|
}
|
||||||
|
|
||||||
const timeSeriesScaleY = d3.scaleLinear()
|
const timeSeriesScaleX = d3.scaleTime().range([0, graphWidth - 70]);
|
||||||
.range([graphHeight - graphHeightOffset, 0]);
|
|
||||||
|
const timeSeriesScaleY = d3.scaleLinear().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
|
||||||
|
|
||||||
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
|
||||||
.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(
|
||||||
|
(allQueryResults, query) =>
|
||||||
|
allQueryResults.concat(
|
||||||
query.result.reduce((allResults, result) => allResults.concat(result.values), []),
|
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() {
|
||||||
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 {
|
||||||
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';
|
||||||
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 {
|
||||||
},
|
},
|
||||||
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 '';
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
10
app/assets/javascripts/pages/groups/settings/badges/index.js
Normal file
10
app/assets/javascripts/pages/groups/settings/badges/index.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
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);
|
||||||
|
});
|
|
@ -0,0 +1,10 @@
|
||||||
|
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);
|
||||||
|
});
|
|
@ -0,0 +1,3 @@
|
||||||
|
import initForm from '../form';
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', initForm);
|
|
@ -0,0 +1,19 @@
|
||||||
|
/* 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();
|
||||||
|
};
|
|
@ -1,17 +1,3 @@
|
||||||
/* 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
|
|
||||||
});
|
|
||||||
|
|
24
app/assets/javascripts/pages/shared/mount_badge_settings.js
Normal file
24
app/assets/javascripts/pages/shared/mount_badge_settings.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
|
@ -1,14 +1,15 @@
|
||||||
<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';
|
||||||
|
import eventHub from '../../event_hub';
|
||||||
|
/**
|
||||||
* Renders either a cancel, retry or play icon pointing to the given path.
|
* Renders either a cancel, retry or play icon pointing to the given path.
|
||||||
* TODO: Remove UJS from here and use an async request instead.
|
|
||||||
*/
|
*/
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
icon,
|
Icon,
|
||||||
},
|
},
|
||||||
|
|
||||||
directives: {
|
directives: {
|
||||||
|
@ -26,35 +27,46 @@
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
actionMethod: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
actionIcon: {
|
actionIcon: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
|
|
||||||
|
buttonDisabled: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
cssClass() {
|
cssClass() {
|
||||||
const actionIconDash = dasherize(this.actionIcon);
|
const actionIconDash = dasherize(this.actionIcon);
|
||||||
return `${actionIconDash} js-icon-${actionIconDash}`;
|
return `${actionIconDash} js-icon-${actionIconDash}`;
|
||||||
},
|
},
|
||||||
|
isDisabled() {
|
||||||
|
return this.buttonDisabled === this.link;
|
||||||
},
|
},
|
||||||
};
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onClickAction() {
|
||||||
|
$(this.$el).tooltip('hide');
|
||||||
|
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>
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
<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: {
|
||||||
|
@ -17,6 +17,11 @@
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
actionDisabled: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -48,7 +53,7 @@
|
||||||
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 @@
|
||||||
: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>
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<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`:
|
||||||
|
@ -17,6 +17,7 @@
|
||||||
* "text": "passed",
|
* "text": "passed",
|
||||||
* "label": "passed",
|
* "label": "passed",
|
||||||
* "group": "success",
|
* "group": "success",
|
||||||
|
* "tooltip": "passed",
|
||||||
* "details_path": "/root/ci-mock/builds/4256",
|
* "details_path": "/root/ci-mock/builds/4256",
|
||||||
* "action": {
|
* "action": {
|
||||||
* "icon": "retry",
|
* "icon": "retry",
|
||||||
|
@ -28,11 +29,11 @@
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
actionComponent,
|
ActionComponent,
|
||||||
dropdownActionComponent,
|
DropdownActionComponent,
|
||||||
jobNameComponent,
|
JobNameComponent,
|
||||||
},
|
},
|
||||||
|
|
||||||
directives: {
|
directives: {
|
||||||
|
@ -55,6 +56,12 @@
|
||||||
required: false,
|
required: false,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
actionDisabled: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -69,12 +76,12 @@
|
||||||
textBuilder.push(this.job.name);
|
textBuilder.push(this.job.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.job.name && this.status.label) {
|
if (this.job.name && this.status.tooltip) {
|
||||||
textBuilder.push('-');
|
textBuilder.push('-');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.status.label) {
|
if (this.status.tooltip) {
|
||||||
textBuilder.push(`${this.job.status.label}`);
|
textBuilder.push(`${this.job.status.tooltip}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return textBuilder.join(' ');
|
return textBuilder.join(' ');
|
||||||
|
@ -89,7 +96,7 @@
|
||||||
return this.job.status && this.job.status.action && this.job.status.action.path;
|
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 @@
|
||||||
: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 @@
|
||||||
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 @@
|
||||||
: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
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
<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: {
|
props: {
|
||||||
title: {
|
title: {
|
||||||
|
@ -29,6 +29,11 @@
|
||||||
required: false,
|
required: false,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
actionDisabled: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -44,7 +49,7 @@
|
||||||
return index === 0 && !this.isFirstColumn ? 'left-connector' : '';
|
return index === 0 && !this.isFirstColumn ? 'left-connector' : '';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<li
|
<li
|
||||||
|
@ -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 () => {
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
<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>
|
|
@ -1,10 +1,25 @@
|
||||||
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 {
|
||||||
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 @@
|
||||||
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 = {}) {
|
||||||
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);
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
<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: {
|
props: {
|
||||||
pipeline: {
|
pipeline: {
|
||||||
|
@ -35,22 +35,27 @@
|
||||||
return this.hasCi && !this.ciStatus;
|
return this.hasCi && !this.ciStatus;
|
||||||
},
|
},
|
||||||
status() {
|
status() {
|
||||||
return this.pipeline.details &&
|
return this.pipeline.details && this.pipeline.details.status
|
||||||
this.pipeline.details.status ? this.pipeline.details.status : {};
|
? this.pipeline.details.status
|
||||||
|
: {};
|
||||||
},
|
},
|
||||||
hasStages() {
|
hasStages() {
|
||||||
return this.pipeline.details &&
|
return (
|
||||||
this.pipeline.details.stages &&
|
this.pipeline.details && this.pipeline.details.stages && this.pipeline.details.stages.length
|
||||||
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 @@
|
||||||
#{{ pipeline.id }}
|
#{{ pipeline.id }}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{{ pipeline.details.status.label }} for
|
{{ pipeline.details.status.label }}
|
||||||
|
|
||||||
|
<template v-if="hasCommitInfo">
|
||||||
|
for
|
||||||
|
|
||||||
<a
|
<a
|
||||||
:href="pipeline.commit.commit_path"
|
:href="pipeline.commit.commit_path"
|
||||||
class="commit-sha js-commit-link"
|
class="commit-sha js-commit-link"
|
||||||
>
|
>
|
||||||
{{ pipeline.commit.short_id }}</a>.
|
{{ pipeline.commit.short_id }}</a>.
|
||||||
|
</template>
|
||||||
|
|
||||||
<span class="mr-widget-pipeline-graph">
|
<span class="mr-widget-pipeline-graph">
|
||||||
<span
|
<span
|
||||||
|
|
|
@ -1,17 +1,24 @@
|
||||||
<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 {
|
||||||
},
|
},
|
||||||
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 {
|
||||||
<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"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
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 = {
|
||||||
|
|
||||||
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',
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,68 @@
|
||||||
|
<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>
|
|
@ -1,12 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
const buttonVariants = [
|
const buttonVariants = ['danger', 'primary', 'success', 'warning'];
|
||||||
'danger',
|
|
||||||
'primary',
|
|
||||||
'success',
|
|
||||||
'warning',
|
|
||||||
];
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'GlModal',
|
name: 'GlModal',
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
|
@ -24,7 +19,7 @@
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'primary',
|
default: 'primary',
|
||||||
validator: value => buttonVariants.indexOf(value) !== -1,
|
validator: value => buttonVariants.includes(value),
|
||||||
},
|
},
|
||||||
footerPrimaryButtonText: {
|
footerPrimaryButtonText: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -41,7 +36,7 @@
|
||||||
this.$emit('submit', event);
|
this.$emit('submit', event);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -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 @@
|
||||||
<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 @@
|
||||||
</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 @@
|
||||||
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 @@
|
||||||
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 @@
|
||||||
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;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $list-warning-row-bg;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.smoke { background-color: $gray-light; }
|
&.smoke { background-color: $gray-light; }
|
||||||
|
|
||||||
&:not(.ui-sort-disabled):hover {
|
|
||||||
background: $row-hover;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
&: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;
|
||||||
Modals
|
Modals
|
||||||
*/
|
*/
|
||||||
$modal-body-height: 134px;
|
$modal-body-height: 134px;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Prometheus
|
||||||
|
*/
|
||||||
|
$prometheus-table-row-highlight-color: $theme-gray-100;
|
||||||
|
|
|
@ -391,7 +391,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $row-hover;
|
background-color: $dropdown-item-hover-bg;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-retry {
|
.icon-retry {
|
||||||
|
|
|
@ -107,7 +107,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.commits-compare-switch {
|
.commits-compare-switch {
|
||||||
float: left;
|
float: left;
|
||||||
margin-right: 9px;
|
margin-right: 9px;
|
||||||
|
@ -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 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commit-sha {
|
.label-monospace {
|
||||||
font-size: 14px;
|
@extend .monospace;
|
||||||
font-weight: $gl-font-weight-bold;
|
user-select: text;
|
||||||
|
color: $gl-text-color;
|
||||||
|
background-color: $gray-light;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ci-status-icon {
|
.btn svg {
|
||||||
position: relative;
|
top: auto;
|
||||||
top: 2px;
|
fill: $gl-text-color-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa-clipboard {
|
||||||
|
color: $gl-text-color-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
: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 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.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 @@
|
||||||
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 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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 @@
|
||||||
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 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.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 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.prometheus-table-row-highlight {
|
||||||
|
background-color: $prometheus-table-row-highlight-color;
|
||||||
|
}
|
||||||
|
|
60
app/assets/stylesheets/pages/pages.scss
Normal file
60
app/assets/stylesheets/pages/pages.scss
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
.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 @@
|
||||||
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 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.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 {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.project-badge {
|
||||||
|
opacity: 0.9;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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 @@
|
||||||
.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
|
||||||
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
|
||||||
# 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
|
||||||
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
|
||||||
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
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue