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
|
@ -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
|
||||
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
|
||||
|
||||
.default-cache: &default-cache
|
||||
key: "ruby-2.3.6-with-yarn"
|
||||
key: "ruby-2.3.7-with-yarn"
|
||||
paths:
|
||||
- vendor/ruby
|
||||
- .yarn-cache/
|
||||
|
@ -571,7 +571,7 @@ static-analysis:
|
|||
script:
|
||||
- scripts/static-analysis
|
||||
cache:
|
||||
key: "ruby-2.3.6-with-yarn-and-rubocop"
|
||||
key: "ruby-2.3.7-with-yarn-and-rubocop"
|
||||
paths:
|
||||
- vendor/ruby
|
||||
- .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.
|
||||
DuplicateProperty:
|
||||
enabled: true
|
||||
ignore_consecutive:
|
||||
- cursor
|
||||
|
||||
# Separate rule, function, and mixin declarations with empty lines.
|
||||
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 'json-schema', '~> 2.8.0'
|
||||
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 'sham_rack', '~> 1.3.6'
|
||||
gem 'concurrent-ruby', '~> 1.0.5'
|
||||
|
@ -421,7 +422,7 @@ group :ed25519 do
|
|||
end
|
||||
|
||||
# Gitaly GRPC client
|
||||
gem 'gitaly-proto', '~> 0.91.0', require: 'gitaly'
|
||||
gem 'gitaly-proto', '~> 0.94.0', require: 'gitaly'
|
||||
gem 'grpc', '~> 1.10.0'
|
||||
|
||||
# Locked until https://github.com/google/protobuf/issues/4210 is closed
|
||||
|
@ -440,3 +441,5 @@ gem 'grape_logging', '~> 1.7'
|
|||
|
||||
# Asset synchronization
|
||||
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)
|
||||
rails (>= 3.2.0)
|
||||
gherkin-ruby (0.3.2)
|
||||
gitaly-proto (0.91.0)
|
||||
gitaly-proto (0.94.0)
|
||||
google-protobuf (~> 3.1)
|
||||
grpc (~> 1.0)
|
||||
github-linguist (5.3.3)
|
||||
|
@ -320,6 +320,9 @@ GEM
|
|||
rubyntlm (~> 0.5)
|
||||
globalid (0.4.1)
|
||||
activesupport (>= 4.2.0)
|
||||
goldiloader (2.0.1)
|
||||
activerecord (>= 4.2, < 5.2)
|
||||
activesupport (>= 4.2, < 5.2)
|
||||
gollum-grit_adapter (1.0.1)
|
||||
gitlab-grit (~> 2.7, >= 2.7.1)
|
||||
gollum-lib (4.2.7)
|
||||
|
@ -587,7 +590,7 @@ GEM
|
|||
orm_adapter (0.5.0)
|
||||
os (0.9.6)
|
||||
parallel (1.12.1)
|
||||
parser (2.5.0.3)
|
||||
parser (2.5.0.5)
|
||||
ast (~> 2.4.0)
|
||||
parslet (1.5.0)
|
||||
blankslate (~> 2.0)
|
||||
|
@ -1061,12 +1064,13 @@ DEPENDENCIES
|
|||
gettext (~> 3.2.2)
|
||||
gettext_i18n_rails (~> 1.8.0)
|
||||
gettext_i18n_rails_js (~> 1.3)
|
||||
gitaly-proto (~> 0.91.0)
|
||||
gitaly-proto (~> 0.94.0)
|
||||
github-linguist (~> 5.3.3)
|
||||
gitlab-flowdock-git-hook (~> 1.0.1)
|
||||
gitlab-markup (~> 1.6.2)
|
||||
gitlab-styles (~> 2.3)
|
||||
gitlab_omniauth-ldap (~> 2.0.4)
|
||||
goldiloader (~> 2.0)
|
||||
gollum-lib (~> 4.2)
|
||||
gollum-rugged_adapter (~> 0.4.4)
|
||||
gon (~> 6.1.0)
|
||||
|
|
|
@ -291,7 +291,7 @@ GEM
|
|||
po_to_json (>= 1.0.0)
|
||||
rails (>= 3.2.0)
|
||||
gherkin-ruby (0.3.2)
|
||||
gitaly-proto (0.91.0)
|
||||
gitaly-proto (0.94.0)
|
||||
google-protobuf (~> 3.1)
|
||||
grpc (~> 1.0)
|
||||
github-linguist (5.3.3)
|
||||
|
@ -587,7 +587,7 @@ GEM
|
|||
orm_adapter (0.5.0)
|
||||
os (0.9.6)
|
||||
parallel (1.12.1)
|
||||
parser (2.5.0.4)
|
||||
parser (2.5.0.5)
|
||||
ast (~> 2.4.0)
|
||||
parslet (1.5.0)
|
||||
blankslate (~> 2.0)
|
||||
|
@ -678,6 +678,10 @@ GEM
|
|||
bundler (>= 1.3.0)
|
||||
railties (= 5.0.6)
|
||||
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)
|
||||
activesupport (>= 4.2.0.alpha)
|
||||
rails-dom-testing (2.0.3)
|
||||
|
@ -1062,7 +1066,7 @@ DEPENDENCIES
|
|||
gettext (~> 3.2.2)
|
||||
gettext_i18n_rails (~> 1.8.0)
|
||||
gettext_i18n_rails_js (~> 1.3)
|
||||
gitaly-proto (~> 0.91.0)
|
||||
gitaly-proto (~> 0.94.0)
|
||||
github-linguist (~> 5.3.3)
|
||||
gitlab-flowdock-git-hook (~> 1.0.1)
|
||||
gitlab-markup (~> 1.6.2)
|
||||
|
@ -1145,6 +1149,7 @@ DEPENDENCIES
|
|||
rack-oauth2 (~> 1.2.1)
|
||||
rack-proxy (~> 0.6.0)
|
||||
rails (= 5.0.6)
|
||||
rails-controller-testing
|
||||
rails-deprecated_sanitizer (~> 1.0.3)
|
||||
rails-i18n (~> 5.1)
|
||||
rainbow (~> 2.2)
|
||||
|
|
|
@ -4,7 +4,8 @@ import $ from 'jquery';
|
|||
import _ from 'underscore';
|
||||
import Cookies from 'js-cookie';
|
||||
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 axios from './lib/utils/axios_utils';
|
||||
|
||||
|
@ -243,7 +244,7 @@ class AwardsHandler {
|
|||
addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) {
|
||||
const isMainAwardsBlock = votesBlock.closest('.js-noteable-awards').length;
|
||||
|
||||
if (this.isInVueNoteablePage() && !isMainAwardsBlock) {
|
||||
if (isInVueNoteablePage() && !isMainAwardsBlock) {
|
||||
const id = votesBlock.attr('id').replace('note_', '');
|
||||
|
||||
this.hideMenuElement($('.emoji-menu'));
|
||||
|
@ -295,16 +296,8 @@ class AwardsHandler {
|
|||
}
|
||||
}
|
||||
|
||||
isVueMRDiscussions() {
|
||||
return isInMRPage() && hasVueMRDiscussionsCookie() && !$('#diffs').is(':visible');
|
||||
}
|
||||
|
||||
isInVueNoteablePage() {
|
||||
return isInIssuePage() || isInEpicPage() || this.isVueMRDiscussions();
|
||||
}
|
||||
|
||||
getVotesBlock() {
|
||||
if (this.isInVueNoteablePage()) {
|
||||
if (isInVueNoteablePage()) {
|
||||
const $el = $('.js-add-award.is-active').closest('.note.timeline-entry');
|
||||
|
||||
if ($el.length) {
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,2 @@
|
|||
export const GROUP_BADGE = 'group';
|
||||
export const PROJECT_BADGE = 'project';
|
|
@ -0,0 +1,7 @@
|
|||
export default () => ({
|
||||
imageUrl: '',
|
||||
isDeleting: false,
|
||||
linkUrl: '',
|
||||
renderedImageUrl: '',
|
||||
renderedLinkUrl: '',
|
||||
});
|
|
@ -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);
|
||||
},
|
||||
};
|
|
@ -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,
|
||||
});
|
|
@ -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',
|
||||
};
|
|
@ -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,
|
||||
});
|
||||
},
|
||||
};
|
|
@ -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];
|
||||
if (hash === 'preview') {
|
||||
this.hideTemplateSelectorMenu();
|
||||
} else if (hash === 'editor') {
|
||||
} else if (hash === 'editor' && !this.typeSelector.isHidden()) {
|
||||
this.showTemplateSelectorMenu();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -32,6 +32,10 @@ export default class FileTemplateSelector {
|
|||
}
|
||||
}
|
||||
|
||||
isHidden() {
|
||||
return this.$wrapper.hasClass('hidden');
|
||||
}
|
||||
|
||||
getToggleText() {
|
||||
return this.$dropdownToggleText.text();
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import Sortable from 'vendor/Sortable';
|
|||
import Vue from 'vue';
|
||||
import AccessorUtilities from '../../lib/utils/accessor';
|
||||
import boardList from './board_list.vue';
|
||||
import boardBlankState from './board_blank_state';
|
||||
import BoardBlankState from './board_blank_state.vue';
|
||||
import './board_delete';
|
||||
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
|
@ -18,7 +18,7 @@ gl.issueBoards.Board = Vue.extend({
|
|||
components: {
|
||||
boardList,
|
||||
'board-delete': gl.issueBoards.BoardDelete,
|
||||
boardBlankState,
|
||||
BoardBlankState,
|
||||
},
|
||||
props: {
|
||||
list: Object,
|
||||
|
|
|
@ -1,42 +1,11 @@
|
|||
<script>
|
||||
/* global ListLabel */
|
||||
|
||||
import _ from 'underscore';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
|
||||
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() {
|
||||
return {
|
||||
predefinedLabels: [
|
||||
|
@ -89,3 +58,41 @@ export default {
|
|||
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.list = this.detail.list;
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.endpoint = this.$refs.assigneeDropdown.dataset.issueUpdate;
|
||||
});
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
|
@ -91,7 +87,7 @@ gl.issueBoards.BoardSidebar = Vue.extend({
|
|||
saveAssignees () {
|
||||
this.loadingAssignees = true;
|
||||
|
||||
gl.issueBoards.BoardsStore.detail.issue.update(this.endpoint)
|
||||
gl.issueBoards.BoardsStore.detail.issue.update()
|
||||
.then(() => {
|
||||
this.loadingAssignees = false;
|
||||
})
|
||||
|
|
|
@ -68,15 +68,6 @@ gl.issueBoards.IssueCardInner = Vue.extend({
|
|||
|
||||
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() {
|
||||
if (this.issue.iid) {
|
||||
return `#${this.issue.iid}`;
|
||||
|
@ -153,13 +144,13 @@ gl.issueBoards.IssueCardInner = Vue.extend({
|
|||
/>
|
||||
<a
|
||||
class="js-no-trigger"
|
||||
:href="cardUrl"
|
||||
:href="issue.path"
|
||||
:title="issue.title">{{ issue.title }}</a>
|
||||
<span
|
||||
class="card-number"
|
||||
v-if="issueId"
|
||||
>
|
||||
<template v-if="groupId && issue.project">{{issue.project.path}}</template>{{ issueId }}
|
||||
{{ issue.referencePath }}
|
||||
</span>
|
||||
</h4>
|
||||
<div class="card-assignee">
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
import ModalStore from '../../stores/modal_store';
|
||||
import modalMixin from '../../mixins/modal_mixins';
|
||||
|
||||
gl.issueBoards.ModalEmptyState = Vue.extend({
|
||||
mixins: [gl.issueBoards.ModalMixins],
|
||||
mixins: [modalMixin],
|
||||
data() {
|
||||
return ModalStore.store;
|
||||
},
|
||||
|
|
|
@ -3,11 +3,11 @@ import Flash from '../../../flash';
|
|||
import { __ } from '../../../locale';
|
||||
import './lists_dropdown';
|
||||
import { pluralize } from '../../../lib/utils/text_utility';
|
||||
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
import ModalStore from '../../stores/modal_store';
|
||||
import modalMixin from '../../mixins/modal_mixins';
|
||||
|
||||
gl.issueBoards.ModalFooter = Vue.extend({
|
||||
mixins: [gl.issueBoards.ModalMixins],
|
||||
mixins: [modalMixin],
|
||||
data() {
|
||||
return {
|
||||
modal: ModalStore.store,
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import Vue from 'vue';
|
||||
import modalFilters from './filters';
|
||||
import './tabs';
|
||||
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
import ModalStore from '../../stores/modal_store';
|
||||
import modalMixin from '../../mixins/modal_mixins';
|
||||
|
||||
gl.issueBoards.ModalHeader = Vue.extend({
|
||||
mixins: [gl.issueBoards.ModalMixins],
|
||||
mixins: [modalMixin],
|
||||
props: {
|
||||
projectId: {
|
||||
type: Number,
|
||||
|
|
|
@ -7,8 +7,7 @@ import './header';
|
|||
import './list';
|
||||
import './footer';
|
||||
import './empty_state';
|
||||
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
import ModalStore from '../../stores/modal_store';
|
||||
|
||||
gl.issueBoards.IssuesModal = Vue.extend({
|
||||
props: {
|
||||
|
|
|
@ -2,8 +2,7 @@
|
|||
|
||||
import Vue from 'vue';
|
||||
import bp from '../../../breakpoints';
|
||||
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
import ModalStore from '../../stores/modal_store';
|
||||
|
||||
gl.issueBoards.ModalList = Vue.extend({
|
||||
props: {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
import ModalStore from '../../stores/modal_store';
|
||||
|
||||
gl.issueBoards.ModalFooterListsDropdown = Vue.extend({
|
||||
data() {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
import ModalStore from '../../stores/modal_store';
|
||||
import modalMixin from '../../mixins/modal_mixins';
|
||||
|
||||
gl.issueBoards.ModalTabs = Vue.extend({
|
||||
mixins: [gl.issueBoards.ModalMixins],
|
||||
mixins: [modalMixin],
|
||||
data() {
|
||||
return ModalStore.store;
|
||||
},
|
||||
|
|
|
@ -17,14 +17,10 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
issueUpdate: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
updateUrl() {
|
||||
return this.issueUpdate.replace(':project_path', this.issue.project.path);
|
||||
return this.issue.path;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -6,6 +6,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
|
|||
constructor(store, updateUrl = false, cantEdit = []) {
|
||||
super({
|
||||
page: 'boards',
|
||||
isGroupDecendent: true,
|
||||
stateFiltersSelector: '.issues-state-filters',
|
||||
});
|
||||
|
||||
|
|
|
@ -17,9 +17,9 @@ import './models/milestone';
|
|||
import './models/project';
|
||||
import './models/assignee';
|
||||
import './stores/boards_store';
|
||||
import './stores/modal_store';
|
||||
import ModalStore from './stores/modal_store';
|
||||
import BoardService from './services/board_service';
|
||||
import './mixins/modal_mixins';
|
||||
import modalMixin from './mixins/modal_mixins';
|
||||
import './mixins/sortable_default_options';
|
||||
import './filters/due_date_filters';
|
||||
import './components/board';
|
||||
|
@ -31,7 +31,6 @@ import '~/vue_shared/vue_resource_interceptor'; // eslint-disable-line import/fi
|
|||
export default () => {
|
||||
const $boardApp = document.getElementById('board-app');
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
|
||||
window.gl = window.gl || {};
|
||||
|
||||
|
@ -176,7 +175,7 @@ export default () => {
|
|||
|
||||
gl.IssueBoardsModalAddBtn = new Vue({
|
||||
el: document.getElementById('js-add-issues-btn'),
|
||||
mixins: [gl.issueBoards.ModalMixins],
|
||||
mixins: [modalMixin],
|
||||
data() {
|
||||
return {
|
||||
modal: ModalStore.store,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const ModalStore = gl.issueBoards.ModalStore;
|
||||
import ModalStore from '../stores/modal_store';
|
||||
|
||||
gl.issueBoards.ModalMixins = {
|
||||
export default {
|
||||
methods: {
|
||||
toggleModal(toggle) {
|
||||
ModalStore.store.showAddIssuesModal = toggle;
|
||||
|
|
|
@ -23,6 +23,8 @@ class ListIssue {
|
|||
};
|
||||
this.isLoading = {};
|
||||
this.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
|
||||
this.referencePath = obj.reference_path;
|
||||
this.path = obj.real_path;
|
||||
this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
|
||||
this.milestone_id = obj.milestone_id;
|
||||
this.project_id = obj.project_id;
|
||||
|
@ -98,7 +100,7 @@ class ListIssue {
|
|||
this.isLoading[key] = value;
|
||||
}
|
||||
|
||||
update (url) {
|
||||
update () {
|
||||
const data = {
|
||||
issue: {
|
||||
milestone_id: this.milestone ? this.milestone.id : null,
|
||||
|
@ -113,7 +115,7 @@ class ListIssue {
|
|||
}
|
||||
|
||||
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 {
|
||||
constructor() {
|
||||
this.store = {
|
||||
|
@ -95,4 +92,4 @@ class ModalStore {
|
|||
}
|
||||
}
|
||||
|
||||
gl.issueBoards.ModalStore = new ModalStore();
|
||||
export default new ModalStore();
|
||||
|
|
|
@ -36,6 +36,7 @@ export default {
|
|||
>
|
||||
<a
|
||||
v-tooltip
|
||||
v-if="!file.binary"
|
||||
:href="file.blamePath"
|
||||
:title="__('Blame')"
|
||||
class="btn btn-xs btn-transparent blame"
|
||||
|
|
|
@ -1,25 +1,23 @@
|
|||
<script>
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import timeAgoMixin from '~/vue_shared/mixins/timeago';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import timeAgoMixin from '~/vue_shared/mixins/timeago';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
mixins: [timeAgoMixin],
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
mixins: [
|
||||
timeAgoMixin,
|
||||
],
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -50,7 +48,9 @@
|
|||
<div class="text-right">
|
||||
{{ file.eol }}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div
|
||||
class="text-right"
|
||||
v-if="!file.binary">
|
||||
{{ file.editorRow }}:{{ file.editorColumn }}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
|
|
|
@ -171,10 +171,10 @@ export default {
|
|||
id="ide"
|
||||
class="blob-viewer-container blob-editor-container"
|
||||
>
|
||||
<div
|
||||
class="ide-mode-tabs clearfix"
|
||||
v-if="!shouldHideEditor">
|
||||
<ul class="nav-links pull-left">
|
||||
<div class="ide-mode-tabs clearfix">
|
||||
<ul
|
||||
class="nav-links pull-left"
|
||||
v-if="!shouldHideEditor">
|
||||
<li :class="editTabCSS">
|
||||
<a
|
||||
href="javascript:void(0);"
|
||||
|
@ -210,9 +210,10 @@ export default {
|
|||
>
|
||||
</div>
|
||||
<content-viewer
|
||||
v-if="!shouldHideEditor && file.viewMode === 'preview'"
|
||||
v-if="shouldHideEditor || file.viewMode === 'preview'"
|
||||
:content="file.content || file.raw"
|
||||
:path="file.path"
|
||||
:path="file.rawPath"
|
||||
:file-size="file.size"
|
||||
:project-path="file.projectId"/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -43,6 +43,7 @@ export default {
|
|||
raw: null,
|
||||
baseRaw: null,
|
||||
html: data.html,
|
||||
size: data.size,
|
||||
});
|
||||
},
|
||||
[types.SET_FILE_RAW_DATA](state, { file, raw }) {
|
||||
|
|
|
@ -40,6 +40,7 @@ export const dataStructure = () => ({
|
|||
eol: '',
|
||||
viewMode: 'edit',
|
||||
previewMode: null,
|
||||
size: 0,
|
||||
});
|
||||
|
||||
export const decorateData = entity => {
|
||||
|
|
|
@ -10,6 +10,7 @@ import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
|
|||
import DropdownUtils from './filtered_search/dropdown_utils';
|
||||
import CreateLabelDropdown from './create_label';
|
||||
import flash from './flash';
|
||||
import ModalStore from './boards/stores/modal_store';
|
||||
|
||||
export default class LabelsSelect {
|
||||
constructor(els, options = {}) {
|
||||
|
@ -350,7 +351,7 @@ export default class LabelsSelect {
|
|||
}
|
||||
|
||||
if ($dropdown.closest('.add-issues-modal').length) {
|
||||
boardsModel = gl.issueBoards.ModalStore.store.filter;
|
||||
boardsModel = ModalStore.store.filter;
|
||||
}
|
||||
|
||||
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) => {
|
||||
if (element) {
|
||||
element.classList.add(className);
|
||||
}
|
||||
};
|
||||
|
||||
export const isInVueNoteablePage = () => isInIssuePage() || isInEpicPage() || isVueMRDiscussions();
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
* @param {String} text
|
||||
* @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.
|
||||
|
@ -22,7 +23,8 @@ export const highCountTrim = count => (count > 99 ? '99+' : count);
|
|||
* @param {String} 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
|
||||
|
@ -53,7 +55,7 @@ export const slugify = str => str.trim().toLowerCase();
|
|||
* @param {Number} maxLength
|
||||
* @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
|
||||
|
@ -80,3 +82,15 @@ export const stripHtml = (string, replace = '') => string.replace(/<[^>]*>/g, re
|
|||
* @param {*} string
|
||||
*/
|
||||
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 axios from './lib/utils/axios_utils';
|
||||
import { timeFor } from './lib/utils/datetime_utility';
|
||||
import ModalStore from './boards/stores/modal_store';
|
||||
|
||||
export default class MilestoneSelect {
|
||||
constructor(currentProject, els, options = {}) {
|
||||
|
@ -164,7 +165,7 @@ export default class MilestoneSelect {
|
|||
}
|
||||
|
||||
if ($dropdown.closest('.add-issues-modal').length) {
|
||||
boardsStore = gl.issueBoards.ModalStore.store.filter;
|
||||
boardsStore = ModalStore.store.filter;
|
||||
}
|
||||
|
||||
if (boardsStore) {
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
<script>
|
||||
import { scaleLinear, scaleTime } from 'd3-scale';
|
||||
import { axisLeft, axisBottom } from 'd3-axis';
|
||||
import _ from 'underscore';
|
||||
import { max, extent } from 'd3-array';
|
||||
import { select } from 'd3-selection';
|
||||
import GraphAxis from './graph/axis.vue';
|
||||
import GraphLegend from './graph/legend.vue';
|
||||
import GraphFlag from './graph/flag.vue';
|
||||
import GraphDeployment from './graph/deployment.vue';
|
||||
|
@ -18,10 +20,11 @@ const d3 = { scaleLinear, scaleTime, axisLeft, axisBottom, max, extent, select }
|
|||
|
||||
export default {
|
||||
components: {
|
||||
GraphLegend,
|
||||
GraphAxis,
|
||||
GraphFlag,
|
||||
GraphDeployment,
|
||||
GraphPath,
|
||||
GraphLegend,
|
||||
},
|
||||
mixins: [MonitoringMixin],
|
||||
props: {
|
||||
|
@ -138,7 +141,7 @@ export default {
|
|||
this.legendTitle = query.label || 'Average';
|
||||
this.graphWidth = this.$refs.baseSvg.clientWidth - this.margin.left - this.margin.right;
|
||||
this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
|
||||
this.baseGraphHeight = this.graphHeight;
|
||||
this.baseGraphHeight = this.graphHeight - 50;
|
||||
this.baseGraphWidth = this.graphWidth;
|
||||
|
||||
// pixel offsets inside the svg and outside are not 1:1
|
||||
|
@ -177,10 +180,8 @@ export default {
|
|||
this.graphHeightOffset,
|
||||
);
|
||||
|
||||
if (!this.showLegend) {
|
||||
this.baseGraphHeight -= 50;
|
||||
} else if (this.timeSeries.length > 3) {
|
||||
this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20;
|
||||
if (_.findWhere(this.timeSeries, { renderCanary: true })) {
|
||||
this.timeSeries = this.timeSeries.map(series => ({ ...series, renderCanary: true }));
|
||||
}
|
||||
|
||||
const axisXScale = d3.scaleTime().range([0, this.graphWidth - 70]);
|
||||
|
@ -251,17 +252,13 @@ export default {
|
|||
class="y-axis"
|
||||
transform="translate(70, 20)"
|
||||
/>
|
||||
<graph-legend
|
||||
<graph-axis
|
||||
:graph-width="graphWidth"
|
||||
:graph-height="graphHeight"
|
||||
:margin="margin"
|
||||
:measurements="measurements"
|
||||
:legend-title="legendTitle"
|
||||
:y-axis-label="yAxisLabel"
|
||||
:time-series="timeSeries"
|
||||
:unit-of-display="unitOfDisplay"
|
||||
:current-data-index="currentDataIndex"
|
||||
:show-legend-group="showLegend"
|
||||
/>
|
||||
<svg
|
||||
class="graph-data"
|
||||
|
@ -306,5 +303,10 @@ export default {
|
|||
:deployment-flag-data="deploymentFlagData"
|
||||
/>
|
||||
</div>
|
||||
<graph-legend
|
||||
v-if="showLegend"
|
||||
:legend-title="legendTitle"
|
||||
:time-series="timeSeries"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -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>
|
||||
import { dateFormat, timeFormat } from '../../utils/date_time_formatters';
|
||||
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 {
|
||||
components: {
|
||||
icon,
|
||||
Icon,
|
||||
TrackLine,
|
||||
},
|
||||
props: {
|
||||
currentXCoordinate: {
|
||||
|
@ -107,11 +109,6 @@ export default {
|
|||
}
|
||||
return `series ${index + 1}`;
|
||||
},
|
||||
strokeDashArray(type) {
|
||||
if (type === 'dashed') return '6, 3';
|
||||
if (type === 'dotted') return '3, 3';
|
||||
return null;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -160,28 +157,13 @@ export default {
|
|||
</div>
|
||||
</div>
|
||||
<div class="popover-content">
|
||||
<table>
|
||||
<table class="prometheus-table">
|
||||
<tr
|
||||
v-for="(series, index) in timeSeries"
|
||||
:key="index"
|
||||
>
|
||||
<td>
|
||||
<svg
|
||||
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>
|
||||
<track-line :track="series"/>
|
||||
<td>{{ series.track }} {{ seriesMetricLabel(index, series) }}</td>
|
||||
<td>
|
||||
<strong>{{ seriesMetricValue(series) }}</strong>
|
||||
</td>
|
||||
|
|
|
@ -1,204 +1,72 @@
|
|||
<script>
|
||||
import { formatRelevantDigits } from '../../../lib/utils/number_utils';
|
||||
import TrackLine from './track_line.vue';
|
||||
import TrackInfo from './track_info.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TrackLine,
|
||||
TrackInfo,
|
||||
},
|
||||
props: {
|
||||
graphWidth: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
graphHeight: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
margin: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
measurements: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
legendTitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
yAxisLabel: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
timeSeries: {
|
||||
type: Array,
|
||||
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: {
|
||||
translateLegendGroup(index) {
|
||||
return `translate(0, ${12 * index})`;
|
||||
},
|
||||
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;
|
||||
isStable(track) {
|
||||
return {
|
||||
'prometheus-table-row-highlight': track.trackName !== 'Canary' && track.renderCanary,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</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"
|
||||
>
|
||||
{{ 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"
|
||||
<div class="prometheus-graph-legends prepend-left-10">
|
||||
<table class="prometheus-table">
|
||||
<tr
|
||||
v-for="(series, index) in timeSeries"
|
||||
:key="index"
|
||||
:transform="translateLegendGroup(index)"
|
||||
v-if="series.shouldRenderLegend"
|
||||
:class="isStable(series)"
|
||||
>
|
||||
<line
|
||||
:stroke="series.lineColor"
|
||||
:stroke-width="measurements.legends.height"
|
||||
:stroke-dasharray="strokeDashArray(series.lineStyle)"
|
||||
:x1="measurements.legends.offsetX"
|
||||
:x2="measurements.legends.offsetX + measurements.legends.width"
|
||||
:y1="graphHeight - measurements.legends.offsetY"
|
||||
:y2="graphHeight - measurements.legends.offsetY"
|
||||
/>
|
||||
<text
|
||||
v-if="timeSeries.length > 1"
|
||||
<td>
|
||||
<strong v-if="series.renderCanary">{{ series.trackName }}</strong>
|
||||
</td>
|
||||
<track-line :track="series" />
|
||||
<td
|
||||
class="legend-metric-title"
|
||||
ref="legendTitleSvg"
|
||||
x="38"
|
||||
:y="graphHeight - 30"
|
||||
>
|
||||
{{ createSeriesString(index, series) }}
|
||||
</text>
|
||||
<text
|
||||
v-else
|
||||
class="legend-metric-title"
|
||||
ref="legendTitleSvg"
|
||||
x="38"
|
||||
:y="graphHeight - 30"
|
||||
>
|
||||
{{ legendTitle }} {{ formatMetricUsage(series) }}
|
||||
</text>
|
||||
</g>
|
||||
</template>
|
||||
</g>
|
||||
v-if="timeSeries.length > 1">
|
||||
<track-info
|
||||
:track="series"
|
||||
v-if="series.metricTag" />
|
||||
<track-info
|
||||
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"
|
||||
:track="track" />
|
||||
</td>
|
||||
</template>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</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';
|
||||
|
||||
function sortMetrics(metrics) {
|
||||
return _.chain(metrics).sortBy('weight').sortBy('title').value();
|
||||
return _.chain(metrics).sortBy('title').sortBy('weight').value();
|
||||
}
|
||||
|
||||
function normalizeMetrics(metrics) {
|
||||
|
|
|
@ -1,10 +1,21 @@
|
|||
import _ from 'underscore';
|
||||
import { scaleLinear, scaleTime } from 'd3-scale';
|
||||
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 { 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 = {
|
||||
blue: ['#1f78d1', '#8fbce8'],
|
||||
|
@ -20,6 +31,8 @@ const defaultStyleOrder = ['solid', 'dashed', 'dotted'];
|
|||
|
||||
function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom, yDom, lineStyle) {
|
||||
let usedColors = [];
|
||||
let renderCanary = false;
|
||||
const timeSeriesParsed = [];
|
||||
|
||||
function pickColor(name) {
|
||||
let pick;
|
||||
|
@ -38,16 +51,23 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
|
|||
return defaultColorPalette[pick];
|
||||
}
|
||||
|
||||
return query.result.map((timeSeries, timeSeriesNumber) => {
|
||||
query.result.forEach((timeSeries, timeSeriesNumber) => {
|
||||
let metricTag = '';
|
||||
let lineColor = '';
|
||||
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()
|
||||
.range([0, graphWidth - 70]);
|
||||
if (trackName === 'Canary') {
|
||||
renderCanary = true;
|
||||
}
|
||||
|
||||
const timeSeriesScaleY = d3.scaleLinear()
|
||||
.range([graphHeight - graphHeightOffset, 0]);
|
||||
const timeSeriesScaleX = d3.scaleTime().range([0, graphWidth - 70]);
|
||||
|
||||
const timeSeriesScaleY = d3.scaleLinear().range([graphHeight - graphHeightOffset, 0]);
|
||||
|
||||
timeSeriesScaleX.domain(xDom);
|
||||
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 lineFunction = d3.line()
|
||||
const lineFunction = d3
|
||||
.line()
|
||||
.defined(defined)
|
||||
.curve(d3.curveLinear) // d3 v4 uses curbe instead of interpolate
|
||||
.x(d => timeSeriesScaleX(d.time))
|
||||
.y(d => timeSeriesScaleY(d.value));
|
||||
|
||||
const areaFunction = d3.area()
|
||||
const areaFunction = d3
|
||||
.area()
|
||||
.defined(defined)
|
||||
.curve(d3.curveLinear)
|
||||
.x(d => timeSeriesScaleX(d.time))
|
||||
|
@ -69,38 +91,62 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
|
|||
.y1(d => timeSeriesScaleY(d.value));
|
||||
|
||||
const timeSeriesMetricLabel = timeSeries.metric[Object.keys(timeSeries.metric)[0]];
|
||||
const seriesCustomizationData = query.series != null &&
|
||||
_.findWhere(query.series[0].when, { value: timeSeriesMetricLabel });
|
||||
const seriesCustomizationData =
|
||||
query.series != null && _.findWhere(query.series[0].when, { value: timeSeriesMetricLabel });
|
||||
|
||||
if (seriesCustomizationData) {
|
||||
metricTag = seriesCustomizationData.value || timeSeriesMetricLabel;
|
||||
[lineColor, areaColor] = pickColor(seriesCustomizationData.color);
|
||||
shouldRenderLegend = false;
|
||||
} else {
|
||||
metricTag = timeSeriesMetricLabel || query.label || `series ${timeSeriesNumber + 1}`;
|
||||
[lineColor, areaColor] = pickColor();
|
||||
if (timeSeriesParsed.length > 1) {
|
||||
shouldRenderLegend = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (query.track) {
|
||||
metricTag += ` - ${query.track}`;
|
||||
if (!shouldRenderLegend) {
|
||||
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),
|
||||
areaPath: areaFunction(timeSeries.values),
|
||||
timeSeriesScaleX,
|
||||
values: timeSeries.values,
|
||||
max: maximumValue,
|
||||
average: accum / timeSeries.values.length,
|
||||
lineStyle,
|
||||
lineColor,
|
||||
areaColor,
|
||||
metricTag,
|
||||
};
|
||||
trackName,
|
||||
shouldRenderLegend,
|
||||
renderCanary,
|
||||
});
|
||||
});
|
||||
|
||||
return timeSeriesParsed;
|
||||
}
|
||||
|
||||
export default function createTimeSeries(queries, graphWidth, graphHeight, graphHeightOffset) {
|
||||
const allValues = queries.reduce((allQueryResults, query) => allQueryResults.concat(
|
||||
query.result.reduce((allResults, result) => allResults.concat(result.values), []),
|
||||
), []);
|
||||
const allValues = queries.reduce(
|
||||
(allQueryResults, query) =>
|
||||
allQueryResults.concat(
|
||||
query.result.reduce((allResults, result) => allResults.concat(result.values), []),
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const xDom = d3.extent(allValues, d => d.time);
|
||||
const yDom = [0, d3.max(allValues.map(d => d.value))];
|
||||
|
|
|
@ -13,8 +13,11 @@ export default function initMrNotes() {
|
|||
data() {
|
||||
const notesDataset = document.getElementById('js-vue-mr-discussions')
|
||||
.dataset;
|
||||
const noteableData = JSON.parse(notesDataset.noteableData);
|
||||
noteableData.noteableType = notesDataset.noteableType;
|
||||
|
||||
return {
|
||||
noteableData: JSON.parse(notesDataset.noteableData),
|
||||
noteableData,
|
||||
currentUserData: JSON.parse(notesDataset.currentUserData),
|
||||
notesData: JSON.parse(notesDataset.notesData),
|
||||
};
|
||||
|
|
|
@ -49,16 +49,7 @@ export default {
|
|||
computed: {
|
||||
...mapGetters(['notes', 'getNotesDataByProp', 'discussionCount']),
|
||||
noteableType() {
|
||||
// FIXME -- @fatihacet Get this from JSON data.
|
||||
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;
|
||||
return this.noteableData.noteableType;
|
||||
},
|
||||
allNotes() {
|
||||
if (this.isLoading) {
|
||||
|
|
|
@ -14,3 +14,9 @@ export const EPIC_NOTEABLE_TYPE = 'epic';
|
|||
export const MERGE_REQUEST_NOTEABLE_TYPE = 'merge_request';
|
||||
export const UNRESOLVE_NOTE_METHOD_NAME = 'delete';
|
||||
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: {
|
||||
noteableType() {
|
||||
switch (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 '';
|
||||
}
|
||||
return constants.NOTEABLE_TYPE_MAPPING[this.note.noteable_type];
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
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
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', initForm);
|
||||
|
|
|
@ -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,60 +1,72 @@
|
|||
<script>
|
||||
import tooltip from '../../../vue_shared/directives/tooltip';
|
||||
import icon from '../../../vue_shared/components/icon.vue';
|
||||
import { dasherize } from '../../../lib/utils/text_utility';
|
||||
/**
|
||||
* Renders either a cancel, retry or play icon pointing to the given path.
|
||||
* TODO: Remove UJS from here and use an async request instead.
|
||||
*/
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
import $ from 'jquery';
|
||||
import tooltip from '../../../vue_shared/directives/tooltip';
|
||||
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.
|
||||
*/
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
},
|
||||
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
|
||||
props: {
|
||||
tooltipText: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
directives: {
|
||||
tooltip,
|
||||
link: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
props: {
|
||||
tooltipText: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
link: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
actionMethod: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
actionIcon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
actionIcon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
computed: {
|
||||
cssClass() {
|
||||
const actionIconDash = dasherize(this.actionIcon);
|
||||
return `${actionIconDash} js-icon-${actionIconDash}`;
|
||||
},
|
||||
buttonDisabled: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
cssClass() {
|
||||
const actionIconDash = dasherize(this.actionIcon);
|
||||
return `${actionIconDash} js-icon-${actionIconDash}`;
|
||||
},
|
||||
isDisabled() {
|
||||
return this.buttonDisabled === this.link;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClickAction() {
|
||||
$(this.$el).tooltip('hide');
|
||||
eventHub.$emit('graphAction', this.link);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<a
|
||||
<button
|
||||
type="button"
|
||||
@click="onClickAction"
|
||||
v-tooltip
|
||||
:data-method="actionMethod"
|
||||
:title="tooltipText"
|
||||
:href="link"
|
||||
class="ci-action-icon-container ci-action-icon-wrapper"
|
||||
class="btn btn-blank btn-transparent ci-action-icon-container ci-action-icon-wrapper"
|
||||
:class="cssClass"
|
||||
data-container="body"
|
||||
:disabled="isDisabled"
|
||||
>
|
||||
<icon :name="actionIcon" />
|
||||
</a>
|
||||
</button>
|
||||
</template>
|
||||
|
|
|
@ -1,54 +1,59 @@
|
|||
<script>
|
||||
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
|
||||
import stageColumnComponent from './stage_column_component.vue';
|
||||
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
|
||||
import StageColumnComponent from './stage_column_component.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
stageColumnComponent,
|
||||
loadingIcon,
|
||||
export default {
|
||||
components: {
|
||||
StageColumnComponent,
|
||||
LoadingIcon,
|
||||
},
|
||||
|
||||
props: {
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
pipeline: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
actionDisabled: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
graph() {
|
||||
return this.pipeline.details && this.pipeline.details.stages;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
capitalizeStageName(name) {
|
||||
return name.charAt(0).toUpperCase() + name.slice(1);
|
||||
},
|
||||
|
||||
props: {
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
pipeline: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isFirstColumn(index) {
|
||||
return index === 0;
|
||||
},
|
||||
|
||||
computed: {
|
||||
graph() {
|
||||
return this.pipeline.details && this.pipeline.details.stages;
|
||||
},
|
||||
stageConnectorClass(index, stage) {
|
||||
let className;
|
||||
|
||||
// If it's the first stage column and only has one job
|
||||
if (index === 0 && stage.groups.length === 1) {
|
||||
className = 'no-margin';
|
||||
} else if (index > 0) {
|
||||
// If it is not the first column
|
||||
className = 'left-margin';
|
||||
}
|
||||
|
||||
return className;
|
||||
},
|
||||
|
||||
methods: {
|
||||
capitalizeStageName(name) {
|
||||
return name.charAt(0).toUpperCase() + name.slice(1);
|
||||
},
|
||||
|
||||
isFirstColumn(index) {
|
||||
return index === 0;
|
||||
},
|
||||
|
||||
stageConnectorClass(index, stage) {
|
||||
let className;
|
||||
|
||||
// If it's the first stage column and only has one job
|
||||
if (index === 0 && stage.groups.length === 1) {
|
||||
className = 'no-margin';
|
||||
} else if (index > 0) {
|
||||
// If it is not the first column
|
||||
className = 'left-margin';
|
||||
}
|
||||
|
||||
return className;
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="build-content middle-block js-pipeline-graph">
|
||||
|
@ -70,6 +75,7 @@
|
|||
:key="stage.name"
|
||||
:stage-connector-class="stageConnectorClass(index, stage)"
|
||||
:is-first-column="isFirstColumn(index)"
|
||||
:action-disabled="actionDisabled"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -1,95 +1,102 @@
|
|||
<script>
|
||||
import actionComponent from './action_component.vue';
|
||||
import dropdownActionComponent from './dropdown_action_component.vue';
|
||||
import jobNameComponent from './job_name_component.vue';
|
||||
import tooltip from '../../../vue_shared/directives/tooltip';
|
||||
import ActionComponent from './action_component.vue';
|
||||
import DropdownActionComponent from './dropdown_action_component.vue';
|
||||
import JobNameComponent from './job_name_component.vue';
|
||||
import tooltip from '../../../vue_shared/directives/tooltip';
|
||||
|
||||
/**
|
||||
* Renders the badge for the pipeline graph and the job's dropdown.
|
||||
*
|
||||
* The following object should be provided as `job`:
|
||||
*
|
||||
* {
|
||||
* "id": 4256,
|
||||
* "name": "test",
|
||||
* "status": {
|
||||
* "icon": "icon_status_success",
|
||||
* "text": "passed",
|
||||
* "label": "passed",
|
||||
* "group": "success",
|
||||
* "details_path": "/root/ci-mock/builds/4256",
|
||||
* "action": {
|
||||
* "icon": "retry",
|
||||
* "title": "Retry",
|
||||
* "path": "/root/ci-mock/builds/4256/retry",
|
||||
* "method": "post"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
/**
|
||||
* Renders the badge for the pipeline graph and the job's dropdown.
|
||||
*
|
||||
* The following object should be provided as `job`:
|
||||
*
|
||||
* {
|
||||
* "id": 4256,
|
||||
* "name": "test",
|
||||
* "status": {
|
||||
* "icon": "icon_status_success",
|
||||
* "text": "passed",
|
||||
* "label": "passed",
|
||||
* "group": "success",
|
||||
* "tooltip": "passed",
|
||||
* "details_path": "/root/ci-mock/builds/4256",
|
||||
* "action": {
|
||||
* "icon": "retry",
|
||||
* "title": "Retry",
|
||||
* "path": "/root/ci-mock/builds/4256/retry",
|
||||
* "method": "post"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
export default {
|
||||
components: {
|
||||
actionComponent,
|
||||
dropdownActionComponent,
|
||||
jobNameComponent,
|
||||
export default {
|
||||
components: {
|
||||
ActionComponent,
|
||||
DropdownActionComponent,
|
||||
JobNameComponent,
|
||||
},
|
||||
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
props: {
|
||||
job: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
props: {
|
||||
job: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
cssClassJobName: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
|
||||
isDropdown: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
cssClassJobName: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
|
||||
computed: {
|
||||
status() {
|
||||
return this.job && this.job.status ? this.job.status : {};
|
||||
},
|
||||
|
||||
tooltipText() {
|
||||
const textBuilder = [];
|
||||
|
||||
if (this.job.name) {
|
||||
textBuilder.push(this.job.name);
|
||||
}
|
||||
|
||||
if (this.job.name && this.status.label) {
|
||||
textBuilder.push('-');
|
||||
}
|
||||
|
||||
if (this.status.label) {
|
||||
textBuilder.push(`${this.job.status.label}`);
|
||||
}
|
||||
|
||||
return textBuilder.join(' ');
|
||||
},
|
||||
|
||||
/**
|
||||
* Verifies if the provided job has an action path
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
hasAction() {
|
||||
return this.job.status && this.job.status.action && this.job.status.action.path;
|
||||
},
|
||||
isDropdown: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
};
|
||||
|
||||
actionDisabled: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
status() {
|
||||
return this.job && this.job.status ? this.job.status : {};
|
||||
},
|
||||
|
||||
tooltipText() {
|
||||
const textBuilder = [];
|
||||
|
||||
if (this.job.name) {
|
||||
textBuilder.push(this.job.name);
|
||||
}
|
||||
|
||||
if (this.job.name && this.status.tooltip) {
|
||||
textBuilder.push('-');
|
||||
}
|
||||
|
||||
if (this.status.tooltip) {
|
||||
textBuilder.push(`${this.job.status.tooltip}`);
|
||||
}
|
||||
|
||||
return textBuilder.join(' ');
|
||||
},
|
||||
|
||||
/**
|
||||
* Verifies if the provided job has an action path
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
hasAction() {
|
||||
return this.job.status && this.job.status.action && this.job.status.action.path;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="ci-job-component">
|
||||
|
@ -100,6 +107,7 @@
|
|||
:title="tooltipText"
|
||||
:class="cssClassJobName"
|
||||
data-container="body"
|
||||
data-html="true"
|
||||
class="js-pipeline-graph-job-link"
|
||||
>
|
||||
|
||||
|
@ -115,6 +123,7 @@
|
|||
class="js-job-component-tooltip"
|
||||
:title="tooltipText"
|
||||
:class="cssClassJobName"
|
||||
data-html="true"
|
||||
data-container="body"
|
||||
>
|
||||
|
||||
|
@ -129,7 +138,7 @@
|
|||
:tooltip-text="status.action.title"
|
||||
:link="status.action.path"
|
||||
:action-icon="status.action.icon"
|
||||
:action-method="status.action.method"
|
||||
:button-disabled="actionDisabled"
|
||||
/>
|
||||
|
||||
<dropdown-action-component
|
||||
|
|
|
@ -1,50 +1,55 @@
|
|||
<script>
|
||||
import jobComponent from './job_component.vue';
|
||||
import dropdownJobComponent from './dropdown_job_component.vue';
|
||||
import JobComponent from './job_component.vue';
|
||||
import DropdownJobComponent from './dropdown_job_component.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
jobComponent,
|
||||
dropdownJobComponent,
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
jobs: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
|
||||
isFirstColumn: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
|
||||
stageConnectorClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
export default {
|
||||
components: {
|
||||
JobComponent,
|
||||
DropdownJobComponent,
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
methods: {
|
||||
firstJob(list) {
|
||||
return list[0];
|
||||
},
|
||||
|
||||
jobId(job) {
|
||||
return `ci-badge-${job.name}`;
|
||||
},
|
||||
|
||||
buildConnnectorClass(index) {
|
||||
return index === 0 && !this.isFirstColumn ? 'left-connector' : '';
|
||||
},
|
||||
jobs: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
};
|
||||
|
||||
isFirstColumn: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
|
||||
stageConnectorClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
actionDisabled: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
firstJob(list) {
|
||||
return list[0];
|
||||
},
|
||||
|
||||
jobId(job) {
|
||||
return `ci-badge-${job.name}`;
|
||||
},
|
||||
|
||||
buildConnnectorClass(index) {
|
||||
return index === 0 && !this.isFirstColumn ? 'left-connector' : '';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<li
|
||||
|
@ -69,6 +74,7 @@
|
|||
v-if="job.size === 1"
|
||||
:job="job"
|
||||
css-class-job-name="build-content"
|
||||
:action-disabled="actionDisabled"
|
||||
/>
|
||||
|
||||
<dropdown-job-component
|
||||
|
|
|
@ -25,13 +25,36 @@ export default () => {
|
|||
data() {
|
||||
return {
|
||||
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) {
|
||||
return createElement('pipeline-graph', {
|
||||
props: {
|
||||
isLoading: this.mediator.state.isLoading,
|
||||
pipeline: this.mediator.store.state.pipeline,
|
||||
actionDisabled: this.actionDisabled,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
@ -52,8 +52,11 @@ export default class pipelinesMediator {
|
|||
}
|
||||
|
||||
refreshPipeline() {
|
||||
this.service.getPipeline()
|
||||
this.poll.stop();
|
||||
|
||||
return this.service.getPipeline()
|
||||
.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 Translate from '~/vue_shared/translate';
|
||||
import UpdateUsername from './components/update_username.vue';
|
||||
import deleteAccountModal from './components/delete_account_modal.vue';
|
||||
|
||||
export default () => {
|
||||
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 deleteAccountModalEl = document.getElementById('delete-account-modal');
|
||||
// eslint-disable-next-line no-new
|
||||
|
|
|
@ -233,21 +233,21 @@ export default class SearchAutocomplete {
|
|||
const issueItems = [
|
||||
{
|
||||
text: 'Issues assigned to me',
|
||||
url: `${issuesPath}/?assignee_username=${userName}`,
|
||||
url: `${issuesPath}/?assignee_id=${userId}`,
|
||||
},
|
||||
{
|
||||
text: "Issues I've created",
|
||||
url: `${issuesPath}/?author_username=${userName}`,
|
||||
url: `${issuesPath}/?author_id=${userId}`,
|
||||
},
|
||||
];
|
||||
const mergeRequestItems = [
|
||||
{
|
||||
text: 'Merge requests assigned to me',
|
||||
url: `${mrPath}/?assignee_username=${userName}`,
|
||||
url: `${mrPath}/?assignee_id=${userId}`,
|
||||
},
|
||||
{
|
||||
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 'underscore';
|
||||
import axios from './lib/utils/axios_utils';
|
||||
import ModalStore from './boards/stores/modal_store';
|
||||
|
||||
// TODO: remove eventHub hack after code splitting refactor
|
||||
window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
|
||||
|
@ -441,7 +442,7 @@ function UsersSelect(currentUser, els, options = {}) {
|
|||
return;
|
||||
}
|
||||
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) {
|
||||
e.preventDefault();
|
||||
handleClick(user, isMarking);
|
||||
|
|
|
@ -1,56 +1,61 @@
|
|||
<script>
|
||||
/* eslint-disable vue/require-default-prop */
|
||||
import pipelineStage from '~/pipelines/components/stage.vue';
|
||||
import ciIcon from '~/vue_shared/components/ci_icon.vue';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
/* eslint-disable vue/require-default-prop */
|
||||
import PipelineStage from '~/pipelines/components/stage.vue';
|
||||
import CiIcon from '~/vue_shared/components/ci_icon.vue';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
|
||||
export default {
|
||||
name: 'MRWidgetPipeline',
|
||||
components: {
|
||||
pipelineStage,
|
||||
ciIcon,
|
||||
icon,
|
||||
export default {
|
||||
name: 'MRWidgetPipeline',
|
||||
components: {
|
||||
PipelineStage,
|
||||
CiIcon,
|
||||
Icon,
|
||||
},
|
||||
props: {
|
||||
pipeline: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
props: {
|
||||
pipeline: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
// This prop needs to be camelCase, html attributes are case insensive
|
||||
// https://vuejs.org/v2/guide/components.html#camelCase-vs-kebab-case
|
||||
hasCi: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
ciStatus: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
// This prop needs to be camelCase, html attributes are case insensive
|
||||
// https://vuejs.org/v2/guide/components.html#camelCase-vs-kebab-case
|
||||
hasCi: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
computed: {
|
||||
hasPipeline() {
|
||||
return this.pipeline && Object.keys(this.pipeline).length > 0;
|
||||
},
|
||||
hasCIError() {
|
||||
return this.hasCi && !this.ciStatus;
|
||||
},
|
||||
status() {
|
||||
return this.pipeline.details &&
|
||||
this.pipeline.details.status ? this.pipeline.details.status : {};
|
||||
},
|
||||
hasStages() {
|
||||
return this.pipeline.details &&
|
||||
this.pipeline.details.stages &&
|
||||
this.pipeline.details.stages.length;
|
||||
},
|
||||
ciStatus: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasPipeline() {
|
||||
return this.pipeline && Object.keys(this.pipeline).length > 0;
|
||||
},
|
||||
hasCIError() {
|
||||
return this.hasCi && !this.ciStatus;
|
||||
},
|
||||
status() {
|
||||
return this.pipeline.details && this.pipeline.details.status
|
||||
? this.pipeline.details.status
|
||||
: {};
|
||||
},
|
||||
hasStages() {
|
||||
return (
|
||||
this.pipeline.details && this.pipeline.details.stages && this.pipeline.details.stages.length
|
||||
);
|
||||
},
|
||||
hasCommitInfo() {
|
||||
return this.pipeline.commit && Object.keys(this.pipeline.commit).length > 0;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="hasPipeline || hasCIError"
|
||||
class="mr-widget-heading">
|
||||
class="mr-widget-heading"
|
||||
>
|
||||
<div class="ci-widget media">
|
||||
<template v-if="hasCIError">
|
||||
<div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10">
|
||||
|
@ -77,13 +82,17 @@
|
|||
#{{ pipeline.id }}
|
||||
</a>
|
||||
|
||||
{{ pipeline.details.status.label }} for
|
||||
{{ pipeline.details.status.label }}
|
||||
|
||||
<a
|
||||
:href="pipeline.commit.commit_path"
|
||||
class="commit-sha js-commit-link"
|
||||
>
|
||||
{{ pipeline.commit.short_id }}</a>.
|
||||
<template v-if="hasCommitInfo">
|
||||
for
|
||||
|
||||
<a
|
||||
:href="pipeline.commit.commit_path"
|
||||
class="commit-sha js-commit-link"
|
||||
>
|
||||
{{ pipeline.commit.short_id }}</a>.
|
||||
</template>
|
||||
|
||||
<span class="mr-widget-pipeline-graph">
|
||||
<span
|
||||
|
|
|
@ -1,17 +1,24 @@
|
|||
<script>
|
||||
import { viewerInformationForPath } from './lib/viewer_utils';
|
||||
import MarkdownViewer from './viewers/markdown_viewer.vue';
|
||||
import ImageViewer from './viewers/image_viewer.vue';
|
||||
import DownloadViewer from './viewers/download_viewer.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
content: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
path: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
fileSize: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0,
|
||||
},
|
||||
projectPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
|
@ -20,12 +27,18 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
viewer() {
|
||||
if (!this.path) return null;
|
||||
|
||||
const previewInfo = viewerInformationForPath(this.path);
|
||||
if (!previewInfo) return DownloadViewer;
|
||||
|
||||
switch (previewInfo.id) {
|
||||
case 'markdown':
|
||||
return MarkdownViewer;
|
||||
case 'image':
|
||||
return ImageViewer;
|
||||
default:
|
||||
return null;
|
||||
return DownloadViewer;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -36,6 +49,8 @@ export default {
|
|||
<div class="preview-container">
|
||||
<component
|
||||
:is="viewer"
|
||||
:path="path"
|
||||
:file-size="fileSize"
|
||||
:project-path="projectPath"
|
||||
:content="content"
|
||||
/>
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
const viewers = {
|
||||
image: {
|
||||
id: 'image',
|
||||
},
|
||||
markdown: {
|
||||
id: 'markdown',
|
||||
previewTitle: 'Preview Markdown',
|
||||
|
@ -7,6 +10,12 @@ const viewers = {
|
|||
|
||||
const fileNameViewers = {};
|
||||
const fileExtensionViewers = {
|
||||
jpg: 'image',
|
||||
jpeg: 'image',
|
||||
gif: 'image',
|
||||
png: 'image',
|
||||
bmp: 'image',
|
||||
ico: 'image',
|
||||
md: '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,47 +1,42 @@
|
|||
<script>
|
||||
const buttonVariants = [
|
||||
'danger',
|
||||
'primary',
|
||||
'success',
|
||||
'warning',
|
||||
];
|
||||
const buttonVariants = ['danger', 'primary', 'success', 'warning'];
|
||||
|
||||
export default {
|
||||
name: 'GlModal',
|
||||
export default {
|
||||
name: 'GlModal',
|
||||
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
headerTitleText: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
footerPrimaryButtonVariant: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'primary',
|
||||
validator: value => buttonVariants.indexOf(value) !== -1,
|
||||
},
|
||||
footerPrimaryButtonText: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
|
||||
methods: {
|
||||
emitCancel(event) {
|
||||
this.$emit('cancel', event);
|
||||
},
|
||||
emitSubmit(event) {
|
||||
this.$emit('submit', event);
|
||||
},
|
||||
headerTitleText: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
};
|
||||
footerPrimaryButtonVariant: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'primary',
|
||||
validator: value => buttonVariants.includes(value),
|
||||
},
|
||||
footerPrimaryButtonText: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
emitCancel(event) {
|
||||
this.$emit('cancel', event);
|
||||
},
|
||||
emitSubmit(event) {
|
||||
this.$emit('submit', event);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -60,7 +55,7 @@
|
|||
<slot name="header">
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
class="close js-modal-close-action"
|
||||
data-dismiss="modal"
|
||||
:aria-label="s__('Modal|Close')"
|
||||
@click="emitCancel($event)"
|
||||
|
@ -83,7 +78,7 @@
|
|||
<slot name="footer">
|
||||
<button
|
||||
type="button"
|
||||
class="btn"
|
||||
class="btn js-modal-cancel-action"
|
||||
data-dismiss="modal"
|
||||
@click="emitCancel($event)"
|
||||
>
|
||||
|
@ -91,7 +86,7 @@
|
|||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn"
|
||||
class="btn js-modal-primary-action"
|
||||
:class="`btn-${footerPrimaryButtonVariant}`"
|
||||
data-dismiss="modal"
|
||||
@click="emitSubmit($event)"
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
$image-widths: 80 250 306 394 430;
|
||||
$image-widths: 80 130 250 306 394 430;
|
||||
@each $width in $image-widths {
|
||||
&.svg-#{$width} {
|
||||
img,
|
||||
|
|
|
@ -24,6 +24,10 @@
|
|||
color: $list-text-disabled-color;
|
||||
}
|
||||
|
||||
&:not(.ui-sort-disabled):hover {
|
||||
background: $row-hover;
|
||||
}
|
||||
|
||||
&.unstyled {
|
||||
&:hover {
|
||||
background: none;
|
||||
|
@ -34,14 +38,15 @@
|
|||
background-color: $list-warning-row-bg;
|
||||
border-color: $list-warning-row-border;
|
||||
color: $list-warning-row-color;
|
||||
|
||||
&:hover {
|
||||
background: $list-warning-row-bg;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&.smoke { background-color: $gray-light; }
|
||||
|
||||
&:not(.ui-sort-disabled):hover {
|
||||
background: $row-hover;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
.table-section {
|
||||
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 {
|
||||
&.section-#{$width} {
|
||||
flex: 0 0 #{$width + '%'};
|
||||
|
|
|
@ -289,6 +289,11 @@ body {
|
|||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&.with-button {
|
||||
line-height: 34px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.page-title-empty {
|
||||
|
|
|
@ -767,3 +767,8 @@ $border-color-settings: #e1e1e1;
|
|||
Modals
|
||||
*/
|
||||
$modal-body-height: 134px;
|
||||
|
||||
/*
|
||||
Prometheus
|
||||
*/
|
||||
$prometheus-table-row-highlight-color: $theme-gray-100;
|
||||
|
|
|
@ -391,7 +391,7 @@
|
|||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $row-hover;
|
||||
background-color: $dropdown-item-hover-bg;
|
||||
}
|
||||
|
||||
.icon-retry {
|
||||
|
|
|
@ -107,7 +107,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
.commits-compare-switch {
|
||||
float: left;
|
||||
margin-right: 9px;
|
||||
|
@ -179,7 +178,7 @@
|
|||
.commit-detail {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
|
||||
.merge-request-branches & {
|
||||
|
@ -200,37 +199,63 @@
|
|||
}
|
||||
|
||||
.ci-status-link {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.btn-clipboard,
|
||||
.btn-transparent {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
> .ci-status-link,
|
||||
> .btn,
|
||||
> .commit-sha-group {
|
||||
margin-left: $gl-padding-8;
|
||||
}
|
||||
}
|
||||
|
||||
.commit-sha-group {
|
||||
display: inline-flex;
|
||||
|
||||
.label,
|
||||
.btn {
|
||||
&:not(:first-child) {
|
||||
margin-left: $gl-padding;
|
||||
}
|
||||
padding: $gl-vert-padding $gl-btn-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 {
|
||||
font-size: 14px;
|
||||
font-weight: $gl-font-weight-bold;
|
||||
.label-monospace {
|
||||
@extend .monospace;
|
||||
user-select: text;
|
||||
color: $gl-text-color;
|
||||
background-color: $gray-light;
|
||||
}
|
||||
|
||||
.ci-status-icon {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
.btn svg {
|
||||
top: auto;
|
||||
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,
|
||||
.generic_commit_status {
|
||||
|
||||
a,
|
||||
button {
|
||||
color: $gl-text-color;
|
||||
|
@ -303,10 +328,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
.gpg-status-box {
|
||||
padding: 2px 10px;
|
||||
margin-right: $gl-padding;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
|
|
|
@ -273,21 +273,6 @@
|
|||
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 {
|
||||
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 {
|
||||
position: relative;
|
||||
height: 0;
|
||||
|
@ -330,8 +335,7 @@
|
|||
padding: 0;
|
||||
padding-bottom: 100%;
|
||||
|
||||
.text-metric-usage,
|
||||
.legend-metric-title {
|
||||
.text-metric-usage {
|
||||
fill: $black;
|
||||
font-weight: $gl-font-weight-normal;
|
||||
font-size: 12px;
|
||||
|
@ -374,10 +378,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.text-metric-title {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.y-label-text,
|
||||
.x-label-text {
|
||||
fill: $gray-darkest;
|
||||
|
@ -414,3 +414,7 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.prometheus-table-row-highlight {
|
||||
background-color: $prometheus-table-row-highlight-color;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
fill: $gl-text-color-secondary;
|
||||
position: relative;
|
||||
left: 5px;
|
||||
top: 2px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
left: 1px;
|
||||
top: -1px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
&.play {
|
||||
svg {
|
||||
width: #{$ci-action-icon-size - 8};
|
||||
height: #{$ci-action-icon-size - 8};
|
||||
left: 8px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
left: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -210,13 +210,8 @@
|
|||
}
|
||||
|
||||
.created-personal-access-token-container {
|
||||
#created-personal-access-token {
|
||||
width: 90%;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.btn-clipboard {
|
||||
margin-left: 5px;
|
||||
border: 1px solid $border-color;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1143,3 +1143,11 @@ pre.light-well {
|
|||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.project-badge {
|
||||
opacity: 0.9;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -312,6 +312,45 @@
|
|||
height: 100%;
|
||||
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 {
|
||||
padding: $gl-padding;
|
||||
}
|
||||
|
|
|
@ -284,3 +284,23 @@
|
|||
.deprecated-service {
|
||||
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(
|
||||
only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position],
|
||||
labels: true,
|
||||
sidebar_endpoints: true,
|
||||
issue_endpoints: true,
|
||||
include_full_project_path: board.group_board?,
|
||||
include: {
|
||||
project: { only: [:id, :path] },
|
||||
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`
|
||||
# manually, not skipping this action would cause a "You are already signed
|
||||
# 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
|
||||
|
||||
# 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 MergeRequestsAction
|
||||
|
||||
FILTER_PARAMS = [
|
||||
:author_id,
|
||||
:assignee_id,
|
||||
:milestone_title,
|
||||
:label_name
|
||||
].freeze
|
||||
|
||||
before_action :event_filter, only: :activity
|
||||
before_action :projects, 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
|
||||
|
||||
|
@ -39,4 +47,15 @@ class DashboardController < Dashboard::ApplicationController
|
|||
def set_show_full_reference
|
||||
@show_full_reference = true
|
||||
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
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue