Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/javascripts/awards_handler.js15
-rw-r--r--app/assets/javascripts/badges/components/badge.vue121
-rw-r--r--app/assets/javascripts/badges/components/badge_form.vue219
-rw-r--r--app/assets/javascripts/badges/components/badge_list.vue57
-rw-r--r--app/assets/javascripts/badges/components/badge_list_row.vue89
-rw-r--r--app/assets/javascripts/badges/components/badge_settings.vue70
-rw-r--r--app/assets/javascripts/badges/constants.js2
-rw-r--r--app/assets/javascripts/badges/empty_badge.js7
-rw-r--r--app/assets/javascripts/badges/store/actions.js167
-rw-r--r--app/assets/javascripts/badges/store/index.js13
-rw-r--r--app/assets/javascripts/badges/store/mutation_types.js21
-rw-r--r--app/assets/javascripts/badges/store/mutations.js158
-rw-r--r--app/assets/javascripts/badges/store/state.js13
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js6
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.js13
-rw-r--r--app/assets/javascripts/boards/components/sidebar/remove_issue.js6
-rw-r--r--app/assets/javascripts/boards/filtered_search_boards.js1
-rw-r--r--app/assets/javascripts/boards/models/issue.js6
-rw-r--r--app/assets/javascripts/ide/components/ide_file_buttons.vue1
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue40
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue15
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js1
-rw-r--r--app/assets/javascripts/ide/stores/utils.js1
-rw-r--r--app/assets/javascripts/lib/utils/dom_utils.js7
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js20
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue24
-rw-r--r--app/assets/javascripts/monitoring/components/graph/axis.vue142
-rw-r--r--app/assets/javascripts/monitoring/components/graph/flag.vue32
-rw-r--r--app/assets/javascripts/monitoring/components/graph/legend.vue228
-rw-r--r--app/assets/javascripts/monitoring/components/graph/track_info.vue29
-rw-r--r--app/assets/javascripts/monitoring/components/graph/track_line.vue36
-rw-r--r--app/assets/javascripts/monitoring/stores/monitoring_store.js2
-rw-r--r--app/assets/javascripts/monitoring/utils/multiple_time_series.js84
-rw-r--r--app/assets/javascripts/mr_notes/index.js5
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue11
-rw-r--r--app/assets/javascripts/notes/constants.js6
-rw-r--r--app/assets/javascripts/notes/mixins/noteable.js11
-rw-r--r--app/assets/javascripts/pages/groups/settings/badges/index.js10
-rw-r--r--app/assets/javascripts/pages/projects/settings/badges/index/index.js10
-rw-r--r--app/assets/javascripts/pages/projects/settings/repository/create_deploy_token/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/settings/repository/form.js19
-rw-r--r--app/assets/javascripts/pages/projects/settings/repository/show/index.js18
-rw-r--r--app/assets/javascripts/pages/shared/mount_badge_settings.js24
-rw-r--r--app/assets/javascripts/pipelines/components/graph/action_component.vue96
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue84
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_component.vue180
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue78
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js23
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_mediator.js7
-rw-r--r--app/assets/javascripts/profile/account/components/update_username.vue121
-rw-r--r--app/assets/javascripts/profile/account/index.js15
-rw-r--r--app/assets/javascripts/search_autocomplete.js8
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue19
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js9
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue52
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue68
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_modal.vue77
-rw-r--r--app/assets/stylesheets/framework/images.scss2
-rw-r--r--app/assets/stylesheets/framework/responsive_tables.scss2
-rw-r--r--app/assets/stylesheets/framework/variables.scss5
-rw-r--r--app/assets/stylesheets/pages/commits.scss65
-rw-r--r--app/assets/stylesheets/pages/environments.scss46
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss14
-rw-r--r--app/assets/stylesheets/pages/projects.scss8
-rw-r--r--app/assets/stylesheets/pages/repo.scss39
-rw-r--r--app/assets/stylesheets/pages/settings.scss20
66 files changed, 2159 insertions, 642 deletions
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 0e1ca7fe883..976d32abe9b 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -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) {
diff --git a/app/assets/javascripts/badges/components/badge.vue b/app/assets/javascripts/badges/components/badge.vue
new file mode 100644
index 00000000000..6e6cb31e3ac
--- /dev/null
+++ b/app/assets/javascripts/badges/components/badge.vue
@@ -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>
diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue
new file mode 100644
index 00000000000..ae942b2c1a7
--- /dev/null
+++ b/app/assets/javascripts/badges/components/badge_form.vue
@@ -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>
diff --git a/app/assets/javascripts/badges/components/badge_list.vue b/app/assets/javascripts/badges/components/badge_list.vue
new file mode 100644
index 00000000000..ca7197e1e0f
--- /dev/null
+++ b/app/assets/javascripts/badges/components/badge_list.vue
@@ -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>
diff --git a/app/assets/javascripts/badges/components/badge_list_row.vue b/app/assets/javascripts/badges/components/badge_list_row.vue
new file mode 100644
index 00000000000..af062bdf8c6
--- /dev/null
+++ b/app/assets/javascripts/badges/components/badge_list_row.vue
@@ -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>
diff --git a/app/assets/javascripts/badges/components/badge_settings.vue b/app/assets/javascripts/badges/components/badge_settings.vue
new file mode 100644
index 00000000000..83f78394238
--- /dev/null
+++ b/app/assets/javascripts/badges/components/badge_settings.vue
@@ -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>
diff --git a/app/assets/javascripts/badges/constants.js b/app/assets/javascripts/badges/constants.js
new file mode 100644
index 00000000000..8fbe3db5ef1
--- /dev/null
+++ b/app/assets/javascripts/badges/constants.js
@@ -0,0 +1,2 @@
+export const GROUP_BADGE = 'group';
+export const PROJECT_BADGE = 'project';
diff --git a/app/assets/javascripts/badges/empty_badge.js b/app/assets/javascripts/badges/empty_badge.js
new file mode 100644
index 00000000000..49a9b5e1be8
--- /dev/null
+++ b/app/assets/javascripts/badges/empty_badge.js
@@ -0,0 +1,7 @@
+export default () => ({
+ imageUrl: '',
+ isDeleting: false,
+ linkUrl: '',
+ renderedImageUrl: '',
+ renderedLinkUrl: '',
+});
diff --git a/app/assets/javascripts/badges/store/actions.js b/app/assets/javascripts/badges/store/actions.js
new file mode 100644
index 00000000000..5542278b3e0
--- /dev/null
+++ b/app/assets/javascripts/badges/store/actions.js
@@ -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);
+ },
+};
diff --git a/app/assets/javascripts/badges/store/index.js b/app/assets/javascripts/badges/store/index.js
new file mode 100644
index 00000000000..7a5df403a0e
--- /dev/null
+++ b/app/assets/javascripts/badges/store/index.js
@@ -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,
+});
diff --git a/app/assets/javascripts/badges/store/mutation_types.js b/app/assets/javascripts/badges/store/mutation_types.js
new file mode 100644
index 00000000000..d73f91b6005
--- /dev/null
+++ b/app/assets/javascripts/badges/store/mutation_types.js
@@ -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',
+};
diff --git a/app/assets/javascripts/badges/store/mutations.js b/app/assets/javascripts/badges/store/mutations.js
new file mode 100644
index 00000000000..bd84e68c00f
--- /dev/null
+++ b/app/assets/javascripts/badges/store/mutations.js
@@ -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,
+ });
+ },
+};
diff --git a/app/assets/javascripts/badges/store/state.js b/app/assets/javascripts/badges/store/state.js
new file mode 100644
index 00000000000..43413aeb5bb
--- /dev/null
+++ b/app/assets/javascripts/badges/store/state.js
@@ -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,
+});
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index a44969272a1..c4ee4f6c855 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -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;
})
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js
index 8aee5b23c76..84fe9b1288a 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.js
+++ b/app/assets/javascripts/boards/components/issue_card_inner.js
@@ -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">
diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js b/app/assets/javascripts/boards/components/sidebar/remove_issue.js
index 09c683ff621..0a0820ec5fd 100644
--- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js
+++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js
@@ -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: {
diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js
index fb40b9f5565..70367c4f711 100644
--- a/app/assets/javascripts/boards/filtered_search_boards.js
+++ b/app/assets/javascripts/boards/filtered_search_boards.js
@@ -6,6 +6,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
constructor(store, updateUrl = false, cantEdit = []) {
super({
page: 'boards',
+ isGroupDecendent: true,
stateFiltersSelector: '.issues-state-filters',
});
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index 4c5079efc8b..b381d48d625 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -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);
}
}
diff --git a/app/assets/javascripts/ide/components/ide_file_buttons.vue b/app/assets/javascripts/ide/components/ide_file_buttons.vue
index 6d07329df71..a6c6f46a144 100644
--- a/app/assets/javascripts/ide/components/ide_file_buttons.vue
+++ b/app/assets/javascripts/ide/components/ide_file_buttons.vue
@@ -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"
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
index 9c386896448..152a5f632ad 100644
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -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">
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index 8a709b31ea0..711bafa17a9 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -22,7 +22,7 @@ export default {
...mapState(['rightPanelCollapsed', 'viewer', 'delayViewerUpdated', 'panelResizing']),
...mapGetters(['currentMergeRequest']),
shouldHideEditor() {
- return this.file && this.file.binary && !this.file.raw;
+ return this.file && this.file.binary && !this.file.content;
},
editTabCSS() {
return {
@@ -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.path"
+ :file-size="file.size"
:project-path="file.projectId"/>
</div>
</template>
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index 6a143e518f9..eeb14b5490c 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -43,6 +43,7 @@ export default {
raw: null,
baseRaw: null,
html: data.html,
+ size: data.size,
});
},
[types.SET_FILE_RAW_DATA](state, { file, raw }) {
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 4befcc501ef..05a019de54f 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -40,6 +40,7 @@ export const dataStructure = () => ({
eol: '',
viewMode: 'edit',
previewMode: null,
+ size: 0,
});
export const decorateData = entity => {
diff --git a/app/assets/javascripts/lib/utils/dom_utils.js b/app/assets/javascripts/lib/utils/dom_utils.js
index de65ea15a60..914de9de940 100644
--- a/app/assets/javascripts/lib/utils/dom_utils.js
+++ b/app/assets/javascripts/lib/utils/dom_utils.js
@@ -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();
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 94d03621bff..b54ecd2d543 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -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(' ');
+};
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
index 04d546fafa0..f93b1da4f58 100644
--- a/app/assets/javascripts/monitoring/components/graph.vue
+++ b/app/assets/javascripts/monitoring/components/graph.vue
@@ -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>
diff --git a/app/assets/javascripts/monitoring/components/graph/axis.vue b/app/assets/javascripts/monitoring/components/graph/axis.vue
new file mode 100644
index 00000000000..fc4b3689dfd
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/graph/axis.vue
@@ -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>
diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue
index 906c7c51f52..b8202e25685 100644
--- a/app/assets/javascripts/monitoring/components/graph/flag.vue
+++ b/app/assets/javascripts/monitoring/components/graph/flag.vue
@@ -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>
diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue
index a7a058a9203..da9280cf1f1 100644
--- a/app/assets/javascripts/monitoring/components/graph/legend.vue
+++ b/app/assets/javascripts/monitoring/components/graph/legend.vue
@@ -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"
- class="legend-metric-title"
- ref="legendTitleSvg"
- x="38"
- :y="graphHeight - 30"
- >
- {{ createSeriesString(index, series) }}
- </text>
- <text
- v-else
+ <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"
- >
- {{ 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>
diff --git a/app/assets/javascripts/monitoring/components/graph/track_info.vue b/app/assets/javascripts/monitoring/components/graph/track_info.vue
new file mode 100644
index 00000000000..ec1c2222af9
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/graph/track_info.vue
@@ -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>
+
diff --git a/app/assets/javascripts/monitoring/components/graph/track_line.vue b/app/assets/javascripts/monitoring/components/graph/track_line.vue
new file mode 100644
index 00000000000..79b322e2e42
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/graph/track_line.vue
@@ -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>
+
diff --git a/app/assets/javascripts/monitoring/stores/monitoring_store.js b/app/assets/javascripts/monitoring/stores/monitoring_store.js
index 854636e9a89..535c415cd6d 100644
--- a/app/assets/javascripts/monitoring/stores/monitoring_store.js
+++ b/app/assets/javascripts/monitoring/stores/monitoring_store.js
@@ -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) {
diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
index b5b8e3c255d..8a93c7e6bae 100644
--- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js
+++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
@@ -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';
-
-const d3 = { scaleLinear, scaleTime, line, area, curveLinear, extent, max, timeMinute };
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+
+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');
+
+ if (trackName === 'Canary') {
+ renderCanary = true;
+ }
- const timeSeriesScaleX = d3.scaleTime()
- .range([0, graphWidth - 70]);
+ const timeSeriesScaleX = d3.scaleTime().range([0, graphWidth - 70]);
- const timeSeriesScaleY = d3.scaleLinear()
- .range([graphHeight - graphHeightOffset, 0]);
+ 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))];
diff --git a/app/assets/javascripts/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js
index 096c4ef5f31..e3c5bf06b3d 100644
--- a/app/assets/javascripts/mr_notes/index.js
+++ b/app/assets/javascripts/mr_notes/index.js
@@ -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),
};
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 5bd81c7cad6..ebfc827ac57 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -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) {
diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js
index 68f8cb1cf1e..c4de4826eda 100644
--- a/app/assets/javascripts/notes/constants.js
+++ b/app/assets/javascripts/notes/constants.js
@@ -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,
+};
diff --git a/app/assets/javascripts/notes/mixins/noteable.js b/app/assets/javascripts/notes/mixins/noteable.js
index 5bf8216a1f3..b68543d71c8 100644
--- a/app/assets/javascripts/notes/mixins/noteable.js
+++ b/app/assets/javascripts/notes/mixins/noteable.js
@@ -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];
},
},
};
diff --git a/app/assets/javascripts/pages/groups/settings/badges/index.js b/app/assets/javascripts/pages/groups/settings/badges/index.js
new file mode 100644
index 00000000000..74e96ee4a8f
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/settings/badges/index.js
@@ -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);
+});
diff --git a/app/assets/javascripts/pages/projects/settings/badges/index/index.js b/app/assets/javascripts/pages/projects/settings/badges/index/index.js
new file mode 100644
index 00000000000..30469550866
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/settings/badges/index/index.js
@@ -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);
+});
diff --git a/app/assets/javascripts/pages/projects/settings/repository/create_deploy_token/index.js b/app/assets/javascripts/pages/projects/settings/repository/create_deploy_token/index.js
new file mode 100644
index 00000000000..ffc84dc106b
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/settings/repository/create_deploy_token/index.js
@@ -0,0 +1,3 @@
+import initForm from '../form';
+
+document.addEventListener('DOMContentLoaded', initForm);
diff --git a/app/assets/javascripts/pages/projects/settings/repository/form.js b/app/assets/javascripts/pages/projects/settings/repository/form.js
new file mode 100644
index 00000000000..a5c17ab322c
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/settings/repository/form.js
@@ -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();
+};
diff --git a/app/assets/javascripts/pages/projects/settings/repository/show/index.js b/app/assets/javascripts/pages/projects/settings/repository/show/index.js
index 788d86d1192..ffc84dc106b 100644
--- a/app/assets/javascripts/pages/projects/settings/repository/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/repository/show/index.js
@@ -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);
diff --git a/app/assets/javascripts/pages/shared/mount_badge_settings.js b/app/assets/javascripts/pages/shared/mount_badge_settings.js
new file mode 100644
index 00000000000..1397c0834ff
--- /dev/null
+++ b/app/assets/javascripts/pages/shared/mount_badge_settings.js
@@ -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);
+ },
+ });
+};
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue
index d7effb27bff..e99d949801f 100644
--- a/app/assets/javascripts/pipelines/components/graph/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue
@@ -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,
- },
+ directives: {
+ tooltip,
+ },
- props: {
- tooltipText: {
- type: String,
- required: true,
- },
+ props: {
+ tooltipText: {
+ type: String,
+ required: true,
+ },
- link: {
- type: String,
- required: true,
- },
+ link: {
+ type: String,
+ required: true,
+ },
- actionMethod: {
- type: String,
- required: true,
- },
+ actionIcon: {
+ type: String,
+ required: true,
+ },
- actionIcon: {
- type: String,
- required: true,
- },
+ 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;
},
+ },
- computed: {
- cssClass() {
- const actionIconDash = dasherize(this.actionIcon);
- return `${actionIconDash} js-icon-${actionIconDash}`;
- },
+ 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>
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index ab84711d4a2..ac9ce7e47d6 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -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,
- },
+ 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;
- },
+ computed: {
+ graph() {
+ return this.pipeline.details && this.pipeline.details.stages;
},
+ },
- methods: {
- capitalizeStageName(name) {
- return name.charAt(0).toUpperCase() + name.slice(1);
- },
+ methods: {
+ capitalizeStageName(name) {
+ return name.charAt(0).toUpperCase() + name.slice(1);
+ },
- isFirstColumn(index) {
- return index === 0;
- },
+ isFirstColumn(index) {
+ return index === 0;
+ },
- stageConnectorClass(index, stage) {
- let className;
+ 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';
- }
+ // 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;
- },
+ 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>
diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue
index d501c465a96..c6e5ae6df41 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue
@@ -1,96 +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';
-
- /**
- * 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,
+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",
+ * "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,
+ },
+
+ directives: {
+ tooltip,
+ },
+ props: {
+ job: {
+ type: Object,
+ required: true,
},
- directives: {
- tooltip,
+ cssClassJobName: {
+ type: String,
+ required: false,
+ default: '',
},
- props: {
- job: {
- type: Object,
- required: true,
- },
-
- cssClassJobName: {
- type: String,
- required: false,
- default: '',
- },
-
- isDropdown: {
- type: Boolean,
- required: false,
- default: false,
- },
+
+ 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(' ');
},
- 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;
- },
+ /**
+ * 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">
@@ -132,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
diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
index 7adcf4017b8..f6e6569e15b 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
@@ -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,
+export default {
+ components: {
+ JobComponent,
+ DropdownJobComponent,
+ },
+ props: {
+ title: {
+ type: String,
+ required: true,
},
- props: {
- title: {
- type: String,
- required: true,
- },
- jobs: {
- type: Array,
- required: true,
- },
+ jobs: {
+ type: Array,
+ required: true,
+ },
- isFirstColumn: {
- type: Boolean,
- required: false,
- default: false,
- },
+ isFirstColumn: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
- stageConnectorClass: {
- type: String,
- required: false,
- default: '',
- },
+ stageConnectorClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ actionDisabled: {
+ type: String,
+ required: false,
+ default: null,
},
+ },
- methods: {
- firstJob(list) {
- return list[0];
- },
+ methods: {
+ firstJob(list) {
+ return list[0];
+ },
- jobId(job) {
- return `ci-badge-${job.name}`;
- },
+ jobId(job) {
+ return `ci-badge-${job.name}`;
+ },
- buildConnnectorClass(index) {
- return index === 0 && !this.isFirstColumn ? 'left-connector' : '';
- },
+ 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
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 6b26708148c..900eb7855f4 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -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,
},
});
},
diff --git a/app/assets/javascripts/pipelines/pipeline_details_mediator.js b/app/assets/javascripts/pipelines/pipeline_details_mediator.js
index 10f238fe73b..621969cd622 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_mediator.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_mediator.js
@@ -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());
}
}
diff --git a/app/assets/javascripts/profile/account/components/update_username.vue b/app/assets/javascripts/profile/account/components/update_username.vue
new file mode 100644
index 00000000000..e5de3f69b01
--- /dev/null
+++ b/app/assets/javascripts/profile/account/components/update_username.vue
@@ -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>
diff --git a/app/assets/javascripts/profile/account/index.js b/app/assets/javascripts/profile/account/index.js
index 84049a1f0b7..59c13e1a042 100644
--- a/app/assets/javascripts/profile/account/index.js
+++ b/app/assets/javascripts/profile/account/index.js
@@ -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
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index 7dd3e9858c6..2da022fde63 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -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}`,
},
];
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue
index fb8ccea91c7..4155e1bab9c 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue
@@ -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"
/>
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js b/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js
index 4f2e1e47dd1..f01a51da0b3 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js
@@ -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',
};
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
new file mode 100644
index 00000000000..395a71acccf
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
@@ -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>
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
new file mode 100644
index 00000000000..a5999f909ca
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
@@ -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>
diff --git a/app/assets/javascripts/vue_shared/components/gl_modal.vue b/app/assets/javascripts/vue_shared/components/gl_modal.vue
index 67c9181c7b1..f28e5e2715d 100644
--- a/app/assets/javascripts/vue_shared/components/gl_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/gl_modal.vue
@@ -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,
},
+ 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);
- },
+ 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)"
diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss
index df1cafc9f8e..62a0fba3da3 100644
--- a/app/assets/stylesheets/framework/images.scss
+++ b/app/assets/stylesheets/framework/images.scss
@@ -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,
diff --git a/app/assets/stylesheets/framework/responsive_tables.scss b/app/assets/stylesheets/framework/responsive_tables.scss
index 7829d722560..34fccf6f0a4 100644
--- a/app/assets/stylesheets/framework/responsive_tables.scss
+++ b/app/assets/stylesheets/framework/responsive_tables.scss
@@ -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 + '%'};
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index a81904d5338..8ee1bb03d55 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -767,3 +767,8 @@ $border-color-settings: #e1e1e1;
Modals
*/
$modal-body-height: 134px;
+
+/*
+Prometheus
+*/
+$prometheus-table-row-highlight-color: $theme-gray-100;
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index b487f6278c2..86cdda0359e 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -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;
+ }
+
+ .label-monospace {
+ @extend .monospace;
+ user-select: text;
+ color: $gl-text-color;
+ background-color: $gray-light;
}
- .commit-sha {
- font-size: 14px;
- font-weight: $gl-font-weight-bold;
+ .btn svg {
+ top: auto;
+ fill: $gl-text-color-secondary;
}
- .ci-status-icon {
- position: relative;
- top: 2px;
+ .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;
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 58700661142..3a300086fa3 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -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;
+}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index ce2f1482456..8d5eb2e8c5a 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -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;
}
}
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 9a770d77685..790e91e4431 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -1143,3 +1143,11 @@ pre.light-well {
white-space: pre-wrap;
}
}
+
+.project-badge {
+ opacity: 0.9;
+
+ &:hover {
+ opacity: 1;
+ }
+}
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 8cc5c8fc877..a414deb8921 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -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;
}
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index a6ca8ed5016..c410049bc0b 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -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;
+ }
+}