diff options
Diffstat (limited to 'app/assets')
110 files changed, 1815 insertions, 429 deletions
diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js index bfb073fdcdc..789a057caf8 100644 --- a/app/assets/javascripts/behaviors/markdown/render_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js @@ -2,6 +2,7 @@ import $ from 'jquery'; import syntaxHighlight from '~/syntax_highlight'; import renderMath from './render_math'; import renderMermaid from './render_mermaid'; +import renderMetrics from './render_metrics'; import highlightCurrentUser from './highlight_current_user'; import initUserPopovers from '../../user_popovers'; import initMRPopovers from '../../mr_popover'; @@ -17,6 +18,9 @@ $.fn.renderGFM = function renderGFM() { highlightCurrentUser(this.find('.gfm-project_member').get()); initUserPopovers(this.find('.gfm-project_member').get()); initMRPopovers(this.find('.gfm-merge_request').get()); + if (gon.features && gon.features.gfmEmbeddedMetrics) { + renderMetrics(this.find('.js-render-metrics').get()); + } return this; }; diff --git a/app/assets/javascripts/behaviors/markdown/render_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_mermaid.js index b23de36f860..27708504791 100644 --- a/app/assets/javascripts/behaviors/markdown/render_mermaid.js +++ b/app/assets/javascripts/behaviors/markdown/render_mermaid.js @@ -33,10 +33,12 @@ export default function renderMermaid($els) { flowchart: { htmlLabels: false, }, + securityLevel: 'strict', }); $els.each((i, el) => { - const source = el.textContent; + // Mermaid doesn't like `<br />` tags, so collapse all like tags into `<br>`, which is parsed correctly. + const source = el.textContent.replace(/<br\s*\/>/g, '<br>'); /** * Restrict the rendering to a certain amount of character to diff --git a/app/assets/javascripts/behaviors/markdown/render_metrics.js b/app/assets/javascripts/behaviors/markdown/render_metrics.js new file mode 100644 index 00000000000..252b98610b6 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/render_metrics.js @@ -0,0 +1,24 @@ +import Vue from 'vue'; +import Metrics from '~/monitoring/components/embed.vue'; +import { createStore } from '~/monitoring/stores'; + +// TODO: Handle copy-pasting - https://gitlab.com/gitlab-org/gitlab-ce/issues/64369. +export default function renderMetrics(elements) { + if (!elements.length) { + return; + } + + elements.forEach(element => { + const { dashboardUrl } = element.dataset; + const MetricsComponent = Vue.extend(Metrics); + + // eslint-disable-next-line no-new + new MetricsComponent({ + el: element, + store: createStore(), + propsData: { + dashboardUrl, + }, + }); + }); +} diff --git a/app/assets/javascripts/boards/components/board_blank_state.vue b/app/assets/javascripts/boards/components/board_blank_state.vue index d8b0b60c183..9f26337d153 100644 --- a/app/assets/javascripts/boards/components/board_blank_state.vue +++ b/app/assets/javascripts/boards/components/board_blank_state.vue @@ -1,6 +1,6 @@ <script> import { __ } from '~/locale'; -/* global ListLabel */ +import ListLabel from '~/boards/models/label'; import Cookies from 'js-cookie'; import boardsStore from '../stores/boards_store'; @@ -30,13 +30,17 @@ export default { }); // Save the labels - gl.boardService + boardsStore .generateDefaultLists() .then(res => res.data) .then(data => { data.forEach(listObj => { const list = boardsStore.findList('title', listObj.title); + if (!list) { + return; + } + list.id = listObj.id; list.label.id = listObj.label.id; list.getIssues().catch(() => { @@ -69,8 +73,7 @@ export default { <span :style="{ backgroundColor: label.color }" class="label-color position-relative d-inline-block rounded" - > - </span> + ></span> {{ label.title }} </li> </ul> diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue new file mode 100644 index 00000000000..6754abf4019 --- /dev/null +++ b/app/assets/javascripts/boards/components/board_form.vue @@ -0,0 +1,216 @@ +<script> +import Flash from '~/flash'; +import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; +import { visitUrl } from '~/lib/utils/url_utility'; +import boardsStore from '~/boards/stores/boards_store'; + +const boardDefaults = { + id: false, + name: '', + labels: [], + milestone_id: undefined, + assignee: {}, + assignee_id: undefined, + weight: null, +}; + +export default { + components: { + BoardScope: () => import('ee_component/boards/components/board_scope.vue'), + DeprecatedModal, + }, + props: { + canAdminBoard: { + type: Boolean, + required: true, + }, + milestonePath: { + type: String, + required: true, + }, + labelsPath: { + type: String, + required: true, + }, + scopedIssueBoardFeatureEnabled: { + type: Boolean, + required: false, + default: false, + }, + projectId: { + type: Number, + required: false, + default: 0, + }, + groupId: { + type: Number, + required: false, + default: 0, + }, + weights: { + type: Array, + required: false, + default: () => [], + }, + enableScopedLabels: { + type: Boolean, + required: false, + default: false, + }, + scopedLabelsDocumentationLink: { + type: String, + required: false, + default: '#', + }, + }, + data() { + return { + board: { ...boardDefaults, ...this.currentBoard }, + currentBoard: boardsStore.state.currentBoard, + currentPage: boardsStore.state.currentPage, + isLoading: false, + }; + }, + computed: { + isNewForm() { + return this.currentPage === 'new'; + }, + isDeleteForm() { + return this.currentPage === 'delete'; + }, + isEditForm() { + return this.currentPage === 'edit'; + }, + isVisible() { + return this.currentPage !== ''; + }, + buttonText() { + if (this.isNewForm) { + return 'Create board'; + } + if (this.isDeleteForm) { + return 'Delete'; + } + return 'Save changes'; + }, + buttonKind() { + if (this.isNewForm) { + return 'success'; + } + if (this.isDeleteForm) { + return 'danger'; + } + return 'info'; + }, + title() { + if (this.isNewForm) { + return 'Create new board'; + } + if (this.isDeleteForm) { + return 'Delete board'; + } + if (this.readonly) { + return 'Board scope'; + } + return 'Edit board'; + }, + readonly() { + return !this.canAdminBoard; + }, + submitDisabled() { + return this.isLoading || this.board.name.length === 0; + }, + }, + mounted() { + this.resetFormState(); + if (this.$refs.name) { + this.$refs.name.focus(); + } + }, + methods: { + submit() { + if (this.board.name.length === 0) return; + this.isLoading = true; + if (this.isDeleteForm) { + gl.boardService + .deleteBoard(this.currentBoard) + .then(() => { + visitUrl(boardsStore.rootPath); + }) + .catch(() => { + Flash('Failed to delete board. Please try again.'); + this.isLoading = false; + }); + } else { + gl.boardService + .createBoard(this.board) + .then(resp => resp.data) + .then(data => { + visitUrl(data.board_path); + }) + .catch(() => { + Flash('Unable to save your changes. Please try again.'); + this.isLoading = false; + }); + } + }, + cancel() { + boardsStore.showPage(''); + }, + resetFormState() { + if (this.isNewForm) { + // Clear the form when we open the "New board" modal + this.board = { ...boardDefaults }; + } else if (this.currentBoard && Object.keys(this.currentBoard).length) { + this.board = { ...boardDefaults, ...this.currentBoard }; + } + }, + }, +}; +</script> + +<template> + <deprecated-modal + v-show="isVisible" + :hide-footer="readonly" + :title="title" + :primary-button-label="buttonText" + :kind="buttonKind" + :submit-disabled="submitDisabled" + modal-dialog-class="board-config-modal" + @cancel="cancel" + @submit="submit" + > + <template slot="body"> + <p v-if="isDeleteForm">Are you sure you want to delete this board?</p> + <form v-else class="js-board-config-modal" @submit.prevent> + <div v-if="!readonly" class="append-bottom-20"> + <label class="form-section-title label-bold" for="board-new-name"> Board name </label> + <input + id="board-new-name" + ref="name" + v-model="board.name" + class="form-control" + type="text" + placeholder="Enter board name" + @keyup.enter="submit" + /> + </div> + + <board-scope + v-if="scopedIssueBoardFeatureEnabled" + :collapse-scope="isNewForm" + :board="board" + :can-admin-board="canAdminBoard" + :milestone-path="milestonePath" + :labels-path="labelsPath" + :scoped-labels-documentation-link="scopedLabelsDocumentationLink" + :enable-scoped-labels="enableScopedLabels" + :project-id="projectId" + :group-id="groupId" + :weights="weights" + /> + </form> + </template> + </deprecated-modal> +</template> diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue new file mode 100644 index 00000000000..b05de4538f2 --- /dev/null +++ b/app/assets/javascripts/boards/components/boards_selector.vue @@ -0,0 +1,334 @@ +<script> +import { throttle } from 'underscore'; +import { + GlLoadingIcon, + GlSearchBoxByType, + GlDropdown, + GlDropdownDivider, + GlDropdownHeader, + GlDropdownItem, +} from '@gitlab/ui'; + +import Icon from '~/vue_shared/components/icon.vue'; +import httpStatusCodes from '~/lib/utils/http_status'; +import boardsStore from '../stores/boards_store'; +import BoardForm from './board_form.vue'; + +const MIN_BOARDS_TO_VIEW_RECENT = 10; + +export default { + name: 'BoardsSelector', + components: { + Icon, + BoardForm, + GlLoadingIcon, + GlSearchBoxByType, + GlDropdown, + GlDropdownDivider, + GlDropdownHeader, + GlDropdownItem, + }, + props: { + currentBoard: { + type: Object, + required: true, + }, + milestonePath: { + type: String, + required: true, + }, + throttleDuration: { + type: Number, + default: 200, + }, + boardBaseUrl: { + type: String, + required: true, + }, + hasMissingBoards: { + type: Boolean, + required: true, + }, + canAdminBoard: { + type: Boolean, + required: true, + }, + multipleIssueBoardsAvailable: { + type: Boolean, + required: true, + }, + labelsPath: { + type: String, + required: true, + }, + projectId: { + type: Number, + required: true, + }, + groupId: { + type: Number, + required: true, + }, + scopedIssueBoardFeatureEnabled: { + type: Boolean, + required: true, + }, + weights: { + type: Array, + required: true, + }, + enabledScopedLabels: { + type: Boolean, + required: false, + default: false, + }, + scopedLabelsDocumentationLink: { + type: String, + required: false, + default: '#', + }, + }, + data() { + return { + loading: true, + hasScrollFade: false, + scrollFadeInitialized: false, + boards: [], + recentBoards: [], + state: boardsStore.state, + throttledSetScrollFade: throttle(this.setScrollFade, this.throttleDuration), + contentClientHeight: 0, + maxPosition: 0, + store: boardsStore, + filterTerm: '', + }; + }, + computed: { + currentPage() { + return this.state.currentPage; + }, + filteredBoards() { + return this.boards.filter(board => + board.name.toLowerCase().includes(this.filterTerm.toLowerCase()), + ); + }, + reload: { + get() { + return this.state.reload; + }, + set(newValue) { + this.state.reload = newValue; + }, + }, + board() { + return this.state.currentBoard; + }, + showDelete() { + return this.boards.length > 1; + }, + scrollFadeClass() { + return { + 'fade-out': !this.hasScrollFade, + }; + }, + showRecentSection() { + return ( + this.recentBoards.length && + this.boards.length > MIN_BOARDS_TO_VIEW_RECENT && + !this.filterTerm.length + ); + }, + }, + watch: { + filteredBoards() { + this.scrollFadeInitialized = false; + this.$nextTick(this.setScrollFade); + }, + reload() { + if (this.reload) { + this.boards = []; + this.recentBoards = []; + this.loading = true; + this.reload = false; + + this.loadBoards(false); + } + }, + }, + created() { + boardsStore.setCurrentBoard(this.currentBoard); + }, + methods: { + showPage(page) { + boardsStore.showPage(page); + }, + loadBoards(toggleDropdown = true) { + if (toggleDropdown && this.boards.length > 0) { + return; + } + + const recentBoardsPromise = new Promise((resolve, reject) => + gl.boardService + .recentBoards() + .then(resolve) + .catch(err => { + /** + * If user is unauthorized we'd still want to resolve the + * request to display all boards. + */ + if (err.response.status === httpStatusCodes.UNAUTHORIZED) { + resolve({ data: [] }); // recent boards are empty + return; + } + reject(err); + }), + ); + + Promise.all([gl.boardService.allBoards(), recentBoardsPromise]) + .then(([allBoards, recentBoards]) => [allBoards.data, recentBoards.data]) + .then(([allBoardsJson, recentBoardsJson]) => { + this.loading = false; + this.boards = allBoardsJson; + this.recentBoards = recentBoardsJson; + }) + .then(() => this.$nextTick()) // Wait for boards list in DOM + .then(() => { + this.setScrollFade(); + }) + .catch(() => { + this.loading = false; + }); + }, + isScrolledUp() { + const { content } = this.$refs; + const currentPosition = this.contentClientHeight + content.scrollTop; + + return content && currentPosition < this.maxPosition; + }, + initScrollFade() { + this.scrollFadeInitialized = true; + + const { content } = this.$refs; + + this.contentClientHeight = content.clientHeight; + this.maxPosition = content.scrollHeight; + }, + setScrollFade() { + if (!this.scrollFadeInitialized) this.initScrollFade(); + + this.hasScrollFade = this.isScrolledUp(); + }, + }, +}; +</script> + +<template> + <div class="boards-switcher js-boards-selector append-right-10"> + <span class="boards-selector-wrapper js-boards-selector-wrapper"> + <gl-dropdown + toggle-class="dropdown-menu-toggle js-dropdown-toggle" + menu-class="flex-column dropdown-extended-height" + :text="board.name" + @show="loadBoards" + > + <div> + <div class="dropdown-title mb-0" @mousedown.prevent> + {{ s__('IssueBoards|Switch board') }} + </div> + </div> + + <gl-dropdown-header class="mt-0"> + <gl-search-box-by-type ref="searchBox" v-model="filterTerm" /> + </gl-dropdown-header> + + <div + v-if="!loading" + ref="content" + class="dropdown-content flex-fill" + @scroll.passive="throttledSetScrollFade" + > + <gl-dropdown-item + v-show="filteredBoards.length === 0" + class="no-pointer-events text-secondary" + > + {{ s__('IssueBoards|No matching boards found') }} + </gl-dropdown-item> + + <h6 v-if="showRecentSection" class="dropdown-bold-header my-0"> + {{ __('Recent') }} + </h6> + + <template v-if="showRecentSection"> + <gl-dropdown-item + v-for="recentBoard in recentBoards" + :key="`recent-${recentBoard.id}`" + class="js-dropdown-item" + :href="`${boardBaseUrl}/${recentBoard.id}`" + > + {{ recentBoard.name }} + </gl-dropdown-item> + </template> + + <hr v-if="showRecentSection" class="my-1" /> + + <h6 v-if="showRecentSection" class="dropdown-bold-header my-0"> + {{ __('All') }} + </h6> + + <gl-dropdown-item + v-for="otherBoard in filteredBoards" + :key="otherBoard.id" + class="js-dropdown-item" + :href="`${boardBaseUrl}/${otherBoard.id}`" + > + {{ otherBoard.name }} + </gl-dropdown-item> + <gl-dropdown-item v-if="hasMissingBoards" class="small unclickable"> + {{ + s__( + 'IssueBoards|Some of your boards are hidden, activate a license to see them again.', + ) + }} + </gl-dropdown-item> + </div> + + <div + v-show="filteredBoards.length > 0" + class="dropdown-content-faded-mask" + :class="scrollFadeClass" + ></div> + + <gl-loading-icon v-if="loading" /> + + <div v-if="canAdminBoard"> + <gl-dropdown-divider /> + + <gl-dropdown-item v-if="multipleIssueBoardsAvailable" @click.prevent="showPage('new')"> + {{ s__('IssueBoards|Create new board') }} + </gl-dropdown-item> + + <gl-dropdown-item + v-if="showDelete" + class="text-danger" + @click.prevent="showPage('delete')" + > + {{ s__('IssueBoards|Delete board') }} + </gl-dropdown-item> + </div> + </gl-dropdown> + + <board-form + v-if="currentPage" + :milestone-path="milestonePath" + :labels-path="labelsPath" + :project-id="projectId" + :group-id="groupId" + :can-admin-board="canAdminBoard" + :scoped-issue-board-feature-enabled="scopedIssueBoardFeatureEnabled" + :weights="weights" + :enable-scoped-labels="enabledScopedLabels" + :scoped-labels-documentation-link="scopedLabelsDocumentationLink" + /> + </span> + </div> +</template> diff --git a/app/assets/javascripts/boards/components/modal/footer.vue b/app/assets/javascripts/boards/components/modal/footer.vue index a1d634c8f19..5f100c617a0 100644 --- a/app/assets/javascripts/boards/components/modal/footer.vue +++ b/app/assets/javascripts/boards/components/modal/footer.vue @@ -1,4 +1,5 @@ <script> +import footerEEMixin from 'ee_else_ce/boards/mixins/modal_footer'; import Flash from '../../../flash'; import { __, n__ } from '../../../locale'; import ListsDropdown from './lists_dropdown.vue'; @@ -10,7 +11,7 @@ export default { components: { ListsDropdown, }, - mixins: [modalMixin], + mixins: [modalMixin, footerEEMixin], data() { return { modal: ModalStore.store, @@ -41,7 +42,7 @@ export default { const req = this.buildUpdateRequest(list); // Post the data to the backend - gl.boardService.bulkUpdate(issueIds, req).catch(() => { + boardsStore.bulkUpdate(issueIds, req).catch(() => { Flash(__('Failed to update issues, please try again.')); selectedIssues.forEach(issue => { diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue index a1cf1866faf..e8d25e84be1 100644 --- a/app/assets/javascripts/boards/components/project_select.vue +++ b/app/assets/javascripts/boards/components/project_select.vue @@ -68,13 +68,15 @@ export default { <li> <a href='#' class='dropdown-menu-link' data-project-id="${ project.id - }" data-project-name="${project.name}"> - ${_.escape(project.name)} + }" data-project-name="${project.name}" data-project-name-with-namespace="${ + project.name_with_namespace + }"> + ${_.escape(project.name_with_namespace)} </a> </li> `; }, - text: project => project.name, + text: project => project.name_with_namespace, }); }, }; diff --git a/app/assets/javascripts/boards/ee_functions.js b/app/assets/javascripts/boards/ee_functions.js new file mode 100644 index 00000000000..583270fcae5 --- /dev/null +++ b/app/assets/javascripts/boards/ee_functions.js @@ -0,0 +1,7 @@ +export const setPromotionState = () => {}; + +export const setWeigthFetchingState = () => {}; +export const setEpicFetchingState = () => {}; + +export const getMilestoneTitle = () => ({}); +export const getBoardsModalData = () => ({}); diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index d6a5372b22d..3bded4a3258 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -1,7 +1,6 @@ import $ from 'jquery'; import Vue from 'vue'; -import mountMultipleBoardsSwitcher from 'ee_else_ce/boards/mount_multiple_boards_switcher'; import Flash from '~/flash'; import { __ } from '~/locale'; import './models/label'; @@ -31,6 +30,14 @@ import { } from '~/lib/utils/common_utils'; import boardConfigToggle from 'ee_else_ce/boards/config_toggle'; import toggleFocusMode from 'ee_else_ce/boards/toggle_focus'; +import { + setPromotionState, + setWeigthFetchingState, + setEpicFetchingState, + getMilestoneTitle, + getBoardsModalData, +} from 'ee_else_ce/boards/ee_functions'; +import mountMultipleBoardsSwitcher from './mount_multiple_boards_switcher'; let issueBoardsApp; @@ -129,6 +136,7 @@ export default () => { }); boardsStore.addBlankState(); + setPromotionState(boardsStore); this.loading = false; }) .catch(() => { @@ -143,6 +151,8 @@ export default () => { const { sidebarInfoEndpoint } = newIssue; if (sidebarInfoEndpoint && newIssue.subscribed === undefined) { newIssue.setFetchingState('subscriptions', true); + setWeigthFetchingState(newIssue, true); + setEpicFetchingState(newIssue, true); BoardService.getIssueInfo(sidebarInfoEndpoint) .then(res => res.data) .then(data => { @@ -157,6 +167,8 @@ export default () => { } = convertObjectPropsToCamelCase(data); newIssue.setFetchingState('subscriptions', false); + setWeigthFetchingState(newIssue, false); + setEpicFetchingState(newIssue, false); newIssue.updateData({ humanTimeSpent: humanTotalTimeSpent, timeSpent: totalTimeSpent, @@ -169,6 +181,7 @@ export default () => { }) .catch(() => { newIssue.setFetchingState('subscriptions', false); + setWeigthFetchingState(newIssue, false); Flash(__('An error occurred while fetching sidebar data')); }); } @@ -203,6 +216,7 @@ export default () => { el: document.getElementById('js-add-list'), data: { filters: boardsStore.state.filters, + ...getMilestoneTitle($boardApp), }, mounted() { initNewListDropdown(); @@ -222,6 +236,7 @@ export default () => { return { modal: ModalStore.store, store: boardsStore.state, + ...getBoardsModalData($boardApp), canAdminList: this.$options.el.hasAttribute('data-can-admin-list'), }; }, @@ -285,6 +300,6 @@ export default () => { }); } - toggleFocusMode(ModalStore, boardsStore); + toggleFocusMode(ModalStore, boardsStore, $boardApp); mountMultipleBoardsSwitcher(); }; diff --git a/app/assets/javascripts/boards/mixins/modal_footer.js b/app/assets/javascripts/boards/mixins/modal_footer.js new file mode 100644 index 00000000000..ff8b4c56321 --- /dev/null +++ b/app/assets/javascripts/boards/mixins/modal_footer.js @@ -0,0 +1 @@ +export default {}; diff --git a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js index bdb14a7f2f2..8d22f009784 100644 --- a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js +++ b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js @@ -1,2 +1,35 @@ -// this will be moved from EE to CE as part of https://gitlab.com/gitlab-org/gitlab-ce/issues/53811 -export default () => {}; +import Vue from 'vue'; +import { parseBoolean } from '~/lib/utils/common_utils'; +import BoardsSelector from '~/boards/components/boards_selector.vue'; + +export default () => { + const boardsSwitcherElement = document.getElementById('js-multiple-boards-switcher'); + return new Vue({ + el: boardsSwitcherElement, + components: { + BoardsSelector, + }, + data() { + const { dataset } = boardsSwitcherElement; + + const boardsSelectorProps = { + ...dataset, + currentBoard: JSON.parse(dataset.currentBoard), + hasMissingBoards: parseBoolean(dataset.hasMissingBoards), + canAdminBoard: parseBoolean(dataset.canAdminBoard), + multipleIssueBoardsAvailable: parseBoolean(dataset.multipleIssueBoardsAvailable), + projectId: Number(dataset.projectId), + groupId: Number(dataset.groupId), + scopedIssueBoardFeatureEnabled: parseBoolean(dataset.scopedIssueBoardFeatureEnabled), + weights: JSON.parse(dataset.weights), + }; + + return { boardsSelectorProps }; + }, + render(createElement) { + return createElement(BoardsSelector, { + props: this.boardsSelectorProps, + }); + }, + }); +}; diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js index 580d04a3649..5202620057c 100644 --- a/app/assets/javascripts/boards/services/board_service.js +++ b/app/assets/javascripts/boards/services/board_service.js @@ -62,6 +62,22 @@ export default class BoardService { static toggleIssueSubscription(endpoint) { return boardsStore.toggleIssueSubscription(endpoint); } + + allBoards() { + return boardsStore.allBoards(); + } + + recentBoards() { + return boardsStore.recentBoards(); + } + + createBoard(board) { + return boardsStore.createBoard(board); + } + + deleteBoard({ id }) { + return boardsStore.deleteBoard({ id }); + } } window.BoardService = BoardService; diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index b9cd4a143ef..f57c684691c 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -340,6 +340,44 @@ const boardsStore = { toggleIssueSubscription(endpoint) { return axios.post(endpoint); }, + + allBoards() { + return axios.get(this.generateBoardsPath()); + }, + + recentBoards() { + return axios.get(this.state.endpoints.recentBoardsEndpoint); + }, + + createBoard(board) { + const boardPayload = { ...board }; + boardPayload.label_ids = (board.labels || []).map(b => b.id); + + if (boardPayload.label_ids.length === 0) { + boardPayload.label_ids = ['']; + } + + if (boardPayload.assignee) { + boardPayload.assignee_id = boardPayload.assignee.id; + } + + if (boardPayload.milestone) { + boardPayload.milestone_id = boardPayload.milestone.id; + } + + if (boardPayload.id) { + return axios.put(this.generateBoardsPath(boardPayload.id), { board: boardPayload }); + } + return axios.post(this.generateBoardsPath(), { board: boardPayload }); + }, + + deleteBoard({ id }) { + return axios.delete(this.generateBoardsPath(id)); + }, + + setCurrentBoard(board) { + this.state.currentBoard = board; + }, }; BoardsStoreEE.initEESpecific(boardsStore); diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js index 54e2589c707..7dd75d03ab9 100644 --- a/app/assets/javascripts/commits.js +++ b/app/assets/javascripts/commits.js @@ -1,5 +1,5 @@ import $ from 'jquery'; -import { pluralize } from './lib/utils/text_utility'; +import { n__ } from '~/locale'; import { localTimeAgo } from './lib/utils/datetime_utility'; import Pager from './pager'; import axios from './lib/utils/axios_utils'; @@ -90,9 +90,10 @@ export default class CommitsList { .first() .find('li.commit').length, ); + $commitsHeadersLast .find('span.commits-count') - .text(`${commitsCount} ${pluralize('commit', commitsCount)}`); + .text(n__('%d commit', '%d commits', commitsCount)); } localTimeAgo($processedData.find('.js-timeago')); diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js index a4394ab7e92..7a6ad3dc771 100644 --- a/app/assets/javascripts/commons/polyfills.js +++ b/app/assets/javascripts/commons/polyfills.js @@ -13,6 +13,7 @@ import 'core-js/es/string/code-point-at'; import 'core-js/es/string/from-code-point'; import 'core-js/es/string/includes'; import 'core-js/es/string/starts-with'; +import 'core-js/es/string/ends-with'; import 'core-js/es/symbol'; import 'core-js/es/map'; import 'core-js/es/weak-map'; diff --git a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue index 99d77a75c23..197a0706062 100644 --- a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue +++ b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue @@ -105,7 +105,7 @@ export default { </script> <template> - <div class="form-group"> + <div class="confidential-merge-request-fork-group form-group"> <label>{{ __('Project') }}</label> <div> <dropdown @@ -126,6 +126,14 @@ export default { {{ __('No forks available to you.') }}<br /> <span v-html="noForkText"></span> </template> + <gl-link + :href="helpPagePath" + class="w-auto p-0 d-inline-block text-primary bg-transparent" + target="_blank" + > + <span class="sr-only">{{ __('Read more') }}</span> + <i class="fa fa-question-circle" aria-hidden="true"></i> + </gl-link> </p> </div> </div> diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js index 052168bb21c..dce9c1a5410 100644 --- a/app/assets/javascripts/create_merge_request_dropdown.js +++ b/app/assets/javascripts/create_merge_request_dropdown.js @@ -182,7 +182,7 @@ export default class CreateMergeRequestDropdown { } enable() { - if (!canCreateConfidentialMergeRequest()) return; + if (isConfidentialIssue() && !canCreateConfidentialMergeRequest()) return; this.createMergeRequestButton.classList.remove('disabled'); this.createMergeRequestButton.removeAttribute('disabled'); diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index b56e08175cc..d4b994d4922 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -17,6 +17,7 @@ Vue.use(Translate); export default () => { const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed'; + const cycleAnalyticsEl = document.querySelector('#cycle-analytics'); // eslint-disable-next-line no-new new Vue({ @@ -33,7 +34,6 @@ export default () => { 'stage-production-component': stageComponent, }, data() { - const cycleAnalyticsEl = document.querySelector('#cycle-analytics'); const cycleAnalyticsService = new CycleAnalyticsService({ requestPath: cycleAnalyticsEl.dataset.requestPath, }); @@ -56,7 +56,13 @@ export default () => { }, }, created() { - this.fetchCycleAnalyticsData(); + // Conditional check placed here to prevent this method from being called on the + // new Cycle Analytics page (i.e. the new page will be initialized blank and only + // after a group is selected the cycle analyitcs data will be fetched). Once the + // old (current) page has been removed this entire created method as well as the + // variable itself can be completely removed. + // Follow up issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/64490 + if (cycleAnalyticsEl.dataset.requestPath) this.fetchCycleAnalyticsData(); }, methods: { handleError() { diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js index a0426301a0a..babbfe93082 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js @@ -8,22 +8,26 @@ export default class CycleAnalyticsService { } fetchCycleAnalyticsData(options = { startDate: 30 }) { + const { startDate, projectIds } = options; + return this.axios .get('', { params: { - 'cycle_analytics[start_date]': options.startDate, + 'cycle_analytics[start_date]': startDate, + 'cycle_analytics[project_ids]': projectIds, }, }) .then(x => x.data); } fetchStageData(options) { - const { stage, startDate } = options; + const { stage, startDate, projectIds } = options; return this.axios .get(`events/${stage.name}.json`, { params: { 'cycle_analytics[start_date]': startDate, + 'cycle_analytics[project_ids]': projectIds, }, }) .then(x => x.data); diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index 63350fafefa..2514274224d 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -67,6 +67,18 @@ export default { errorMessage() { return this.file.viewer.error_message; }, + forkMessage() { + return sprintf( + __( + "You're not allowed to %{tag_start}edit%{tag_end} files in this project directly. Please fork this project, make your changes there, and submit a merge request.", + ), + { + tag_start: '<span class="js-file-fork-suggestion-section-action">', + tag_end: '</span>', + }, + false, + ); + }, }, watch: { isCollapsed: function fileCollapsedWatch(newVal, oldVal) { @@ -150,12 +162,7 @@ export default { /> <div v-if="forkMessageVisible" class="js-file-fork-suggestion-section file-fork-suggestion"> - <span class="file-fork-suggestion-note"> - {{ sprintf(__("You're not allowed to %{tag_start}edit%{tag_end} files in this project - directly. Please fork this project, make your changes there, and submit a merge request."), - { tag_start: '<span class="js-file-fork-suggestion-section-action">', tag_end: '</span>' }) - }} - </span> + <span class="file-fork-suggestion-note" v-html="forkMessage"></span> <a :href="file.fork_path" class="js-fork-suggestion-button btn btn-grouped btn-inverted btn-success" diff --git a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue index af5550aec3b..7ede7a4f430 100644 --- a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue +++ b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue @@ -1,6 +1,7 @@ <script> +import { n__ } from '~/locale'; import Icon from '~/vue_shared/components/icon.vue'; -import { pluralize, truncate } from '~/lib/utils/text_utility'; +import { truncate } from '~/lib/utils/text_utility'; import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; import { GlTooltipDirective } from '@gitlab/ui'; import { COUNT_OF_AVATARS_IN_GUTTER, LENGTH_OF_AVATAR_TOOLTIP } from '../constants'; @@ -42,7 +43,7 @@ export default { return ''; } - return pluralize(`${this.moreCount} more comment`, this.moreCount); + return n__('%d more comment', '%d more comments', this.moreCount); }, }, methods: { diff --git a/app/assets/javascripts/filterable_list.js b/app/assets/javascripts/filterable_list.js index 64b09c8b62c..77080691dcb 100644 --- a/app/assets/javascripts/filterable_list.js +++ b/app/assets/javascripts/filterable_list.js @@ -17,11 +17,13 @@ export default class FilterableList { } getFilterEndpoint() { - return `${this.filterForm.getAttribute('action')}?${$(this.filterForm).serialize()}`; + return this.getPagePath(); } getPagePath() { - return this.getFilterEndpoint(); + const action = this.filterForm.getAttribute('action'); + const params = $(this.filterForm).serialize(); + return `${action}${action.indexOf('?') > 0 ? '&' : '?'}${params}`; } initSearch() { diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index 9909f437fc8..830385941d8 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -129,7 +129,7 @@ export default { <item-stats-value :icon-name="visibilityIcon" :title="visibilityTooltip" - css-class="item-visibility d-inline-flex align-items-center prepend-top-8 append-right-4" + css-class="item-visibility d-inline-flex align-items-center prepend-top-8 append-right-4 text-secondary" /> <span v-if="group.permission" class="user-access-role prepend-top-8"> {{ group.permission }} diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index 03756a634d5..802b7f1fa6f 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -4,7 +4,12 @@ import { viewerInformationForPath } from '~/vue_shared/components/content_viewer import flash from '~/flash'; import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue'; import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue'; -import { activityBarViews, viewerTypes } from '../constants'; +import { + activityBarViews, + viewerTypes, + FILE_VIEW_MODE_EDITOR, + FILE_VIEW_MODE_PREVIEW, +} from '../constants'; import Editor from '../lib/editor'; import ExternalLink from './external_link.vue'; import FileTemplatesBar from './file_templates/bar.vue'; @@ -49,10 +54,10 @@ export default { return this.shouldHideEditor && this.file.mrChange && this.viewer === viewerTypes.mr; }, isEditorViewMode() { - return this.file.viewMode === 'editor'; + return this.file.viewMode === FILE_VIEW_MODE_EDITOR; }, isPreviewViewMode() { - return this.file.viewMode === 'preview'; + return this.file.viewMode === FILE_VIEW_MODE_PREVIEW; }, editTabCSS() { return { @@ -85,7 +90,7 @@ export default { if (this.currentActivityView !== activityBarViews.edit) { this.setFileViewMode({ file: this.file, - viewMode: 'editor', + viewMode: FILE_VIEW_MODE_EDITOR, }); } } @@ -94,7 +99,7 @@ export default { if (this.currentActivityView !== activityBarViews.edit) { this.setFileViewMode({ file: this.file, - viewMode: 'editor', + viewMode: FILE_VIEW_MODE_EDITOR, }); } }, @@ -244,6 +249,8 @@ export default { }, }, viewerTypes, + FILE_VIEW_MODE_EDITOR, + FILE_VIEW_MODE_PREVIEW, }; </script> @@ -255,7 +262,7 @@ export default { <a href="javascript:void(0);" role="button" - @click.prevent="setFileViewMode({ file, viewMode: 'editor' })" + @click.prevent="setFileViewMode({ file, viewMode: $options.FILE_VIEW_MODE_EDITOR })" > <template v-if="viewer === $options.viewerTypes.edit">{{ __('Edit') }}</template> <template v-else>{{ __('Review') }}</template> @@ -265,7 +272,7 @@ export default { <a href="javascript:void(0);" role="button" - @click.prevent="setFileViewMode({ file, viewMode: 'preview' })" + @click.prevent="setFileViewMode({ file, viewMode: $options.FILE_VIEW_MODE_PREVIEW })" >{{ file.previewMode.previewTitle }}</a > </li> diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js index e30670e119f..673ac1bfa9a 100644 --- a/app/assets/javascripts/ide/constants.js +++ b/app/assets/javascripts/ide/constants.js @@ -4,6 +4,10 @@ export const MAX_WINDOW_HEIGHT_COMPACT = 750; export const MAX_TITLE_LENGTH = 50; export const MAX_BODY_LENGTH = 72; +// File view modes +export const FILE_VIEW_MODE_EDITOR = 'editor'; +export const FILE_VIEW_MODE_PREVIEW = 'preview'; + export const activityBarViews = { edit: 'ide-tree', commit: 'commit-section', diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js index 840761f68db..ba33b6826d6 100644 --- a/app/assets/javascripts/ide/services/index.js +++ b/app/assets/javascripts/ide/services/index.js @@ -56,13 +56,7 @@ export default { return Api.branchSingle(projectId, currentBranchId); }, commit(projectId, payload) { - // Currently the `commit` endpoint does not support `start_sha` so we - // have to make the request in the FE. This is not ideal and will be - // resolved soon. https://gitlab.com/gitlab-org/gitlab-ce/issues/59023 - const { branch, start_sha: ref } = payload; - const branchPromise = ref ? Api.createBranch(projectId, { ref, branch }) : Promise.resolve(); - - return branchPromise.then(() => Api.commitMultiple(projectId, payload)); + return Api.commitMultiple(projectId, payload); }, getFiles(projectUrl, branchId) { const url = `${projectUrl}/files/${branchId}`; diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js index a52f1e235ed..1442ea7dbfa 100644 --- a/app/assets/javascripts/ide/stores/mutations/file.js +++ b/app/assets/javascripts/ide/stores/mutations/file.js @@ -43,10 +43,14 @@ export default { [stateEntry, stagedFile, openFile, changedFile].forEach(f => { if (f) { - Object.assign(f, convertObjectPropsToCamelCase(data, { dropKeys: ['raw', 'baseRaw'] }), { - raw: (stateEntry && stateEntry.raw) || null, - baseRaw: null, - }); + Object.assign( + f, + convertObjectPropsToCamelCase(data, { dropKeys: ['path', 'name', 'raw', 'baseRaw'] }), + { + raw: (stateEntry && stateEntry.raw) || null, + baseRaw: null, + }, + ); } }); }, diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js index 01f78a29cf6..04e86afb268 100644 --- a/app/assets/javascripts/ide/stores/utils.js +++ b/app/assets/javascripts/ide/stores/utils.js @@ -1,4 +1,4 @@ -import { commitActionTypes } from '../constants'; +import { commitActionTypes, FILE_VIEW_MODE_EDITOR } from '../constants'; export const dataStructure = () => ({ id: '', @@ -43,7 +43,7 @@ export const dataStructure = () => ({ editorColumn: 1, fileLanguage: '', eol: '', - viewMode: 'editor', + viewMode: FILE_VIEW_MODE_EDITOR, previewMode: null, size: 0, parentPath: null, @@ -155,11 +155,11 @@ export const createCommitPayload = ({ last_commit_id: newBranch || f.deleted || f.prevPath || f.replaces ? undefined : f.lastCommitSha, })), - start_sha: newBranch ? rootGetters.lastCommit.short_id : undefined, + start_sha: newBranch ? rootGetters.lastCommit.id : undefined, }); export const createNewMergeRequestUrl = (projectUrl, source, target) => - `${projectUrl}/merge_requests/new?merge_request[source_branch]=${source}&merge_request[target_branch]=${target}`; + `${projectUrl}/merge_requests/new?merge_request[source_branch]=${source}&merge_request[target_branch]=${target}&nav_source=webide`; const sortTreesByTypeAndName = (a, b) => { if (a.type === 'tree' && b.type === 'blob') { diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index de2a9664cde..9ca38d6bbfa 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -55,6 +55,11 @@ export default { required: false, default: true, }, + zoomMeetingUrl: { + type: String, + required: false, + default: null, + }, issuableRef: { type: String, required: true, @@ -342,7 +347,7 @@ export default { :title-text="state.titleText" :show-inline-edit-button="showInlineEditButton" /> - <pinned-links :description-html="state.descriptionHtml" /> + <pinned-links :zoom-meeting-url="zoomMeetingUrl" /> <description-component v-if="state.descriptionHtml" :can-update="canUpdate" diff --git a/app/assets/javascripts/issue_show/components/locked_warning.vue b/app/assets/javascripts/issue_show/components/locked_warning.vue index 2f3e611e089..19c7a11d87b 100644 --- a/app/assets/javascripts/issue_show/components/locked_warning.vue +++ b/app/assets/javascripts/issue_show/components/locked_warning.vue @@ -1,18 +1,27 @@ <script> +import { __, sprintf } from '~/locale'; + export default { computed: { currentPath() { return window.location.pathname; }, + alertMessage() { + return sprintf( + __( + 'Someone edited the issue at the same time you did. Please check out %{linkStart}the issue%{linkEnd} and make sure your changes will not unintentionally remove theirs.', + ), + { + linkStart: `<a href="${this.currentPath}" target="_blank" rel="nofollow">`, + linkEnd: `</a>`, + }, + false, + ); + }, }, }; </script> <template> - <div class="alert alert-danger"> - {{ sprintf(__("Someone edited the issue at the same time you did. Please check out - %{linkStart}%the issue%{linkEnd} and make sure your changes will not unintentionally remove - theirs."), { linkStart: `<a href="${currentPath}" target="_blank" rel="nofollow">` linkEnd: '</a - >', }) }} - </div> + <div class="alert alert-danger" v-html="alertMessage"></div> </template> diff --git a/app/assets/javascripts/issue_show/components/pinned_links.vue b/app/assets/javascripts/issue_show/components/pinned_links.vue index 7a54b26bc2b..965e8a3d751 100644 --- a/app/assets/javascripts/issue_show/components/pinned_links.vue +++ b/app/assets/javascripts/issue_show/components/pinned_links.vue @@ -8,40 +8,19 @@ export default { GlLink, }, props: { - descriptionHtml: { + zoomMeetingUrl: { type: String, - required: true, - }, - }, - computed: { - linksInDescription() { - const el = document.createElement('div'); - el.innerHTML = this.descriptionHtml; - return [...el.querySelectorAll('a')].map(a => a.href); - }, - // Detect links matching the following formats: - // Zoom Start links: https://zoom.us/s/<meeting-id> - // Zoom Join links: https://zoom.us/j/<meeting-id> - // Personal Zoom links: https://zoom.us/my/<meeting-id> - // Vanity Zoom links: https://gitlab.zoom.us/j/<meeting-id> (also /s and /my) - zoomHref() { - const zoomRegex = /^https:\/\/([\w\d-]+\.)?zoom\.us\/(s|j|my)\/.+/; - return this.linksInDescription.reduce((acc, currentLink) => { - let lastLink = acc; - if (zoomRegex.test(currentLink)) { - lastLink = currentLink; - } - return lastLink; - }, ''); + required: false, + default: null, }, }, }; </script> <template> - <div v-if="zoomHref" class="border-bottom mb-3 mt-n2"> + <div v-if="zoomMeetingUrl" class="border-bottom mb-3 mt-n2"> <gl-link - :href="zoomHref" + :href="zoomMeetingUrl" target="_blank" class="btn btn-inverted btn-secondary btn-sm text-dark mb-3" > diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index bea43430edc..f50a6e3b19d 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -311,7 +311,8 @@ export default class LabelsSelect { // We need to identify which items are actually labels if (label.id) { - selectedClass.push('label-item'); + const selectedLayoutClasses = ['d-flex', 'flex-row', 'text-break-word']; + selectedClass.push('label-item', ...selectedLayoutClasses); linkEl.dataset.labelId = label.id; } diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 062d21ed247..a4715789337 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -2,8 +2,7 @@ import $ from 'jquery'; import _ from 'underscore'; import timeago from 'timeago.js'; import dateFormat from 'dateformat'; -import { pluralize } from './text_utility'; -import { languageCode, s__, __ } from '../../locale'; +import { languageCode, s__, __, n__ } from '../../locale'; window.timeago = timeago; @@ -231,14 +230,10 @@ export const timeIntervalInWords = intervalInSeconds => { const secondsInteger = parseInt(intervalInSeconds, 10); const minutes = Math.floor(secondsInteger / 60); const seconds = secondsInteger - minutes * 60; - let text = ''; - - if (minutes >= 1) { - text = `${minutes} ${pluralize('minute', minutes)} ${seconds} ${pluralize('second', seconds)}`; - } else { - text = `${seconds} ${pluralize('second', seconds)}`; - } - return text; + const secondsText = n__('%d second', '%d seconds', seconds); + return minutes >= 1 + ? [n__('%d minute', '%d minutes', minutes), secondsText].join(' ') + : secondsText; }; export const dateInWords = (date, abbreviated = false, hideYear = false) => { diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index d38f59b5861..d13fbeb5fc7 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -29,14 +29,6 @@ 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 - * @param {String} str - * @param {Number} count - * @returns {String} - */ -export const pluralize = (str, count) => str + (count > 1 || count === 0 ? 's' : ''); - -/** * Replaces underscores with dashes * @param {*} str * @returns {String} diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue index 81773bd140e..edf9423c74c 100644 --- a/app/assets/javascripts/monitoring/components/charts/area.vue +++ b/app/assets/javascripts/monitoring/components/charts/area.vue @@ -8,6 +8,7 @@ import { getSvgIconPathContent } from '~/lib/utils/icon_utils'; import Icon from '~/vue_shared/components/icon.vue'; import { chartHeight, graphTypes, lineTypes } from '../../constants'; import { makeDataSeries } from '~/helpers/monitor_helper'; +import { graphDataValidatorForValues } from '../../utils'; let debouncedResize; @@ -23,19 +24,7 @@ export default { graphData: { type: Object, required: true, - validator(data) { - return ( - Array.isArray(data.queries) && - data.queries.filter(query => { - if (Array.isArray(query.result)) { - return ( - query.result.filter(res => Array.isArray(res.values)).length === query.result.length - ); - } - return false; - }).length === data.queries.length - ); - }, + validator: graphDataValidatorForValues.bind(null, false), }, containerWidth: { type: Number, @@ -48,7 +37,13 @@ export default { }, projectPath: { type: String, - required: true, + required: false, + default: () => '', + }, + showBorder: { + type: Boolean, + required: false, + default: () => false, }, thresholds: { type: Array, @@ -245,52 +240,54 @@ export default { </script> <template> - <div class="prometheus-graph col-12 col-lg-6"> - <div class="prometheus-graph-header"> - <h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5> - <div ref="graphWidgets" class="prometheus-graph-widgets"><slot></slot></div> - </div> - <gl-area-chart - ref="areaChart" - v-bind="$attrs" - :data="chartData" - :option="chartOptions" - :format-tooltip-text="formatTooltipText" - :thresholds="thresholds" - :width="width" - :height="height" - @updated="onChartUpdated" - > - <template v-if="tooltip.isDeployment"> - <template slot="tooltipTitle"> - {{ __('Deployed') }} - </template> - <div slot="tooltipContent" class="d-flex align-items-center"> - <icon name="commit" class="mr-2" /> - <gl-link :href="tooltip.commitUrl">{{ tooltip.sha }}</gl-link> - </div> - </template> - <template v-else> - <template slot="tooltipTitle"> - <div class="text-nowrap"> - {{ tooltip.title }} + <div class="col-12 col-lg-6" :class="[showBorder ? 'p-2' : 'p-0']"> + <div class="prometheus-graph" :class="{ 'prometheus-graph-embed w-100 p-3': showBorder }"> + <div class="prometheus-graph-header"> + <h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5> + <div ref="graphWidgets" class="prometheus-graph-widgets"><slot></slot></div> + </div> + <gl-area-chart + ref="areaChart" + v-bind="$attrs" + :data="chartData" + :option="chartOptions" + :format-tooltip-text="formatTooltipText" + :thresholds="thresholds" + :width="width" + :height="height" + @updated="onChartUpdated" + > + <template v-if="tooltip.isDeployment"> + <template slot="tooltipTitle"> + {{ __('Deployed') }} + </template> + <div slot="tooltipContent" class="d-flex align-items-center"> + <icon name="commit" class="mr-2" /> + <gl-link :href="tooltip.commitUrl">{{ tooltip.sha }}</gl-link> </div> </template> - <template slot="tooltipContent"> - <div - v-for="(content, key) in tooltip.content" - :key="key" - class="d-flex justify-content-between" - > - <gl-chart-series-label :color="isMultiSeries ? content.color : ''"> - {{ content.name }} - </gl-chart-series-label> - <div class="prepend-left-32"> - {{ content.value }} + <template v-else> + <template slot="tooltipTitle"> + <div class="text-nowrap"> + {{ tooltip.title }} </div> - </div> + </template> + <template slot="tooltipContent"> + <div + v-for="(content, key) in tooltip.content" + :key="key" + class="d-flex justify-content-between" + > + <gl-chart-series-label :color="isMultiSeries ? content.color : ''"> + {{ content.name }} + </gl-chart-series-label> + <div class="prepend-left-32"> + {{ content.value }} + </div> + </div> + </template> </template> - </template> - </gl-area-chart> + </gl-area-chart> + </div> </div> </template> diff --git a/app/assets/javascripts/monitoring/components/charts/column.vue b/app/assets/javascripts/monitoring/components/charts/column.vue index 05a2036f4c3..83136d43479 100644 --- a/app/assets/javascripts/monitoring/components/charts/column.vue +++ b/app/assets/javascripts/monitoring/components/charts/column.vue @@ -4,6 +4,7 @@ import { debounceByAnimationFrame } from '~/lib/utils/common_utils'; import { getSvgIconPathContent } from '~/lib/utils/icon_utils'; import { chartHeight } from '../../constants'; import { makeDataSeries } from '~/helpers/monitor_helper'; +import { graphDataValidatorForValues } from '../../utils'; export default { components: { @@ -14,23 +15,11 @@ export default { graphData: { type: Object, required: true, - validator(data) { - return ( - Array.isArray(data.queries) && - data.queries.filter(query => { - if (Array.isArray(query.result)) { - return ( - query.result.filter(res => Array.isArray(res.values)).length === query.result.length - ); - } - return false; - }).length === data.queries.length - ); - }, - containerWidth: { - type: Number, - required: true, - }, + validator: graphDataValidatorForValues.bind(null, false), + }, + containerWidth: { + type: Number, + required: true, }, }, data() { diff --git a/app/assets/javascripts/monitoring/components/charts/empty_chart.vue b/app/assets/javascripts/monitoring/components/charts/empty_chart.vue new file mode 100644 index 00000000000..73682adc4ee --- /dev/null +++ b/app/assets/javascripts/monitoring/components/charts/empty_chart.vue @@ -0,0 +1,41 @@ +<script> +import chartEmptyStateIllustration from '@gitlab/svgs/dist/illustrations/chart-empty-state.svg'; +import { chartHeight } from '../../constants'; + +export default { + props: { + graphTitle: { + type: String, + required: true, + }, + }, + data() { + return { + height: chartHeight, + }; + }, + computed: { + svgContainerStyle() { + return { + height: `${this.height}px`, + }; + }, + }, + created() { + this.chartEmptyStateIllustration = chartEmptyStateIllustration; + }, +}; +</script> +<template> + <div class="prometheus-graph col-12 col-lg-6 d-flex flex-column justify-content-center"> + <div class="prometheus-graph-header"> + <h5 ref="graphTitle" class="prometheus-graph-title">{{ graphTitle }}</h5> + </div> + <div + class="prepend-top-8 svg-w-100 d-flex align-items-center" + :style="svgContainerStyle" + v-html="chartEmptyStateIllustration" + ></div> + <h5 class="text-center prepend-top-8">{{ __('No data to display') }}</h5> + </div> +</template> diff --git a/app/assets/javascripts/monitoring/components/charts/single_stat.vue b/app/assets/javascripts/monitoring/components/charts/single_stat.vue index b03a6ca1806..7428b27a9c3 100644 --- a/app/assets/javascripts/monitoring/components/charts/single_stat.vue +++ b/app/assets/javascripts/monitoring/components/charts/single_stat.vue @@ -1,5 +1,7 @@ <script> import { GlSingleStat } from '@gitlab/ui/dist/charts'; +import { roundOffFloat } from '~/lib/utils/common_utils'; +import { graphDataValidatorForValues } from '../../utils'; export default { components: { @@ -7,22 +9,21 @@ export default { }, inheritAttrs: false, props: { - title: { - type: String, - required: true, - }, - value: { - type: Number, - required: true, - }, - unit: { - type: String, + graphData: { + type: Object, required: true, + validator: graphDataValidatorForValues.bind(null, true), }, }, computed: { - valueWithUnit() { - return `${this.value}${this.unit}`; + queryInfo() { + return this.graphData.queries[0]; + }, + engineeringNotation() { + return `${roundOffFloat(this.queryInfo.result[0].value[1], 1)}${this.queryInfo.unit}`; + }, + graphTitle() { + return this.queryInfo.label; }, }, }; @@ -30,8 +31,8 @@ export default { <template> <div class="prometheus-graph col-12 col-lg-6"> <div class="prometheus-graph-header"> - <h5 ref="graphTitle" class="prometheus-graph-title">{{ title }}</h5> + <h5 ref="graphTitle" class="prometheus-graph-title">{{ graphTitle }}</h5> </div> - <gl-single-stat :value="valueWithUnit" :title="title" variant="success" /> + <gl-single-stat :value="engineeringNotation" :title="graphTitle" variant="success" /> </div> </template> diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index ba79a697df2..745488255ab 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -7,17 +7,20 @@ import Icon from '~/vue_shared/components/icon.vue'; import { getParameterValues } from '~/lib/utils/url_utility'; import invalidUrl from '~/lib/utils/invalid_url'; import MonitorAreaChart from './charts/area.vue'; +import MonitorSingleStatChart from './charts/single_stat.vue'; +import PanelType from './panel_type.vue'; import GraphGroup from './graph_group.vue'; import EmptyState from './empty_state.vue'; -import { timeWindows, timeWindowsKeyNames } from '../constants'; +import { sidebarAnimationDuration, timeWindows, timeWindowsKeyNames } from '../constants'; import { getTimeDiff } from '../utils'; -const sidebarAnimationDuration = 150; let sidebarMutationObserver; export default { components: { MonitorAreaChart, + MonitorSingleStatChart, + PanelType, GraphGroup, EmptyState, Icon, @@ -152,10 +155,8 @@ export default { 'useDashboardEndpoint', 'allDashboards', 'multipleDashboardsEnabled', + 'additionalPanelTypesEnabled', ]), - groupsWithData() { - return this.groups.filter(group => this.chartsWithData(group.metrics).length > 0); - }, selectedDashboardText() { return this.currentDashboard || (this.allDashboards[0] && this.allDashboards[0].display_name); }, @@ -173,6 +174,7 @@ export default { deploymentsEndpoint: this.deploymentsEndpoint, dashboardEndpoint: this.dashboardEndpoint, currentDashboard: this.currentDashboard, + projectPath: this.projectPath, }); this.timeWindows = timeWindows; @@ -220,6 +222,8 @@ export default { chart.metrics.some(metric => this.metricsWithData.includes(metric.metric_id)), ); }, + // TODO: BEGIN, Duplicated code with panel_type until feature flag is removed + // Issue number: https://gitlab.com/gitlab-org/gitlab-ce/issues/63845 getGraphAlerts(queries) { if (!this.allAlerts) return {}; const metricIdsForChart = queries.map(q => q.metricId); @@ -228,6 +232,7 @@ export default { getGraphAlertValues(queries) { return Object.values(this.getGraphAlerts(queries)); }, + // TODO: END hideAddMetricModal() { this.$refs.addMetricModal.hide(); }, @@ -248,6 +253,9 @@ export default { setTimeWindowParameter(key) { return `?time_window=${key}`; }, + groupHasData(group) { + return this.chartsWithData(group.metrics).length > 0; + }, }, addMetric: { title: s__('Metrics|Add metric'), @@ -361,29 +369,40 @@ export default { </div> <div v-if="!showEmptyState"> <graph-group - v-for="(groupData, index) in groupsWithData" - :key="index" + v-for="groupData in groups" + :key="`${groupData.group}.${groupData.priority}`" :name="groupData.group" :show-panels="showPanels" + :collapse-group="groupHasData(groupData)" > - <monitor-area-chart - v-for="(graphData, graphIndex) in chartsWithData(groupData.metrics)" - :key="graphIndex" - :project-path="projectPath" - :graph-data="graphData" - :deployment-data="deploymentData" - :thresholds="getGraphAlertValues(graphData.queries)" - :container-width="elWidth" - group-id="monitor-area-chart" - > - <alert-widget - v-if="alertWidgetAvailable && graphData" - :alerts-endpoint="alertsEndpoint" - :relevant-queries="graphData.queries" - :alerts-to-manage="getGraphAlerts(graphData.queries)" - @setAlerts="setAlerts" + <template v-if="additionalPanelTypesEnabled"> + <panel-type + v-for="(graphData, graphIndex) in groupData.metrics" + :key="`panel-type-${graphIndex}`" + :graph-data="graphData" + :dashboard-width="elWidth" /> - </monitor-area-chart> + </template> + <template v-else> + <monitor-area-chart + v-for="(graphData, graphIndex) in chartsWithData(groupData.metrics)" + :key="graphIndex" + :graph-data="graphData" + :deployment-data="deploymentData" + :thresholds="getGraphAlertValues(graphData.queries)" + :container-width="elWidth" + :project-path="projectPath" + group-id="monitor-area-chart" + > + <alert-widget + v-if="alertWidgetAvailable && graphData" + :alerts-endpoint="alertsEndpoint" + :relevant-queries="graphData.queries" + :alerts-to-manage="getGraphAlerts(graphData.queries)" + @setAlerts="setAlerts" + /> + </monitor-area-chart> + </template> </graph-group> </div> <empty-state diff --git a/app/assets/javascripts/monitoring/components/embed.vue b/app/assets/javascripts/monitoring/components/embed.vue new file mode 100644 index 00000000000..e17f03de0fd --- /dev/null +++ b/app/assets/javascripts/monitoring/components/embed.vue @@ -0,0 +1,97 @@ +<script> +import { mapActions, mapState } from 'vuex'; +import GraphGroup from './graph_group.vue'; +import MonitorAreaChart from './charts/area.vue'; +import { sidebarAnimationDuration, timeWindowsKeyNames, timeWindows } from '../constants'; +import { getTimeDiff } from '../utils'; + +let sidebarMutationObserver; + +export default { + components: { + GraphGroup, + MonitorAreaChart, + }, + props: { + dashboardUrl: { + type: String, + required: true, + }, + }, + data() { + return { + params: { + ...getTimeDiff(timeWindows[timeWindowsKeyNames.eightHours]), + }, + elWidth: 0, + }; + }, + computed: { + ...mapState('monitoringDashboard', ['groups', 'metricsWithData']), + groupData() { + const groupsWithData = this.groups.filter(group => this.chartsWithData(group.metrics).length); + if (groupsWithData.length) { + return groupsWithData[0]; + } + return null; + }, + }, + mounted() { + this.setInitialState(); + this.fetchMetricsData(this.params); + sidebarMutationObserver = new MutationObserver(this.onSidebarMutation); + sidebarMutationObserver.observe(document.querySelector('.layout-page'), { + attributes: true, + childList: false, + subtree: false, + }); + }, + beforeDestroy() { + if (sidebarMutationObserver) { + sidebarMutationObserver.disconnect(); + } + }, + methods: { + ...mapActions('monitoringDashboard', [ + 'fetchMetricsData', + 'setEndpoints', + 'setFeatureFlags', + 'setShowErrorBanner', + ]), + chartsWithData(charts) { + return charts.filter(chart => + chart.metrics.some(metric => this.metricsWithData.includes(metric.metric_id)), + ); + }, + onSidebarMutation() { + setTimeout(() => { + this.elWidth = this.$el.clientWidth; + }, sidebarAnimationDuration); + }, + setInitialState() { + this.setFeatureFlags({ + prometheusEndpointEnabled: true, + }); + this.setEndpoints({ + dashboardEndpoint: this.dashboardUrl, + }); + this.setShowErrorBanner(false); + }, + }, +}; +</script> +<template> + <div class="metrics-embed"> + <div v-if="groupData" class="row w-100 m-n2 pb-4"> + <monitor-area-chart + v-for="graphData in chartsWithData(groupData.metrics)" + :key="graphData.title" + :graph-data="graphData" + :container-width="elWidth" + group-id="monitor-area-chart" + :project-path="null" + :show-border="true" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/monitoring/components/graph_group.vue b/app/assets/javascripts/monitoring/components/graph_group.vue index b20ad1802f3..0f5c5b3d60f 100644 --- a/app/assets/javascripts/monitoring/components/graph_group.vue +++ b/app/assets/javascripts/monitoring/components/graph_group.vue @@ -10,6 +10,10 @@ export default { required: false, default: true, }, + collapseGroup: { + type: Boolean, + required: true, + }, }, }; </script> @@ -19,7 +23,7 @@ export default { <div class="card-header"> <h4>{{ name }}</h4> </div> - <div class="card-body prometheus-graph-group"><slot></slot></div> + <div v-if="collapseGroup" class="card-body prometheus-graph-group"><slot></slot></div> </div> <div v-else class="prometheus-graph-group"><slot></slot></div> </template> diff --git a/app/assets/javascripts/monitoring/components/panel_type.vue b/app/assets/javascripts/monitoring/components/panel_type.vue new file mode 100644 index 00000000000..d7cd2c57871 --- /dev/null +++ b/app/assets/javascripts/monitoring/components/panel_type.vue @@ -0,0 +1,71 @@ +<script> +import { mapState } from 'vuex'; +import _ from 'underscore'; +import MonitorAreaChart from './charts/area.vue'; +import MonitorSingleStatChart from './charts/single_stat.vue'; +import MonitorEmptyChart from './charts/empty_chart.vue'; + +export default { + components: { + MonitorAreaChart, + MonitorSingleStatChart, + MonitorEmptyChart, + }, + props: { + graphData: { + type: Object, + required: true, + }, + dashboardWidth: { + type: Number, + required: true, + }, + }, + computed: { + ...mapState('monitoringDashboard', ['deploymentData', 'projectPath']), + alertWidgetAvailable() { + return IS_EE && this.prometheusAlertsAvailable && this.alertsEndpoint && this.graphData; + }, + graphDataHasMetrics() { + return this.graphData.queries[0].result.length > 0; + }, + }, + methods: { + getGraphAlerts(queries) { + if (!this.allAlerts) return {}; + const metricIdsForChart = queries.map(q => q.metricId); + return _.pick(this.allAlerts, alert => metricIdsForChart.includes(alert.metricId)); + }, + getGraphAlertValues(queries) { + return Object.values(this.getGraphAlerts(queries)); + }, + isPanelType(type) { + return this.graphData.type && this.graphData.type === type; + }, + }, +}; +</script> +<template> + <monitor-single-stat-chart + v-if="isPanelType('single-stat') && graphDataHasMetrics" + :graph-data="graphData" + /> + <monitor-area-chart + v-else-if="graphDataHasMetrics" + :graph-data="graphData" + :deployment-data="deploymentData" + :project-path="projectPath" + :thresholds="getGraphAlertValues(graphData.queries)" + :container-width="dashboardWidth" + group-id="monitor-area-chart" + > + <alert-widget + v-if="alertWidgetAvailable" + :alerts-endpoint="alertsEndpoint" + :relevant-queries="graphData.queries" + :alerts-to-manage="getGraphAlerts(graphData.queries)" + @setAlerts="setAlerts" + /> + </monitor-area-chart> + <monitor-empty-chart v-else :graph-title="graphData.title" /> +</template> diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js index 26f1bf3f68d..605c95e6da5 100644 --- a/app/assets/javascripts/monitoring/constants.js +++ b/app/assets/javascripts/monitoring/constants.js @@ -1,5 +1,7 @@ import { __ } from '~/locale'; +export const sidebarAnimationDuration = 300; // milliseconds. + export const chartHeight = 300; export const graphTypes = { diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js index 97d149e9ad5..c0fee1ebb99 100644 --- a/app/assets/javascripts/monitoring/monitoring_bundle.js +++ b/app/assets/javascripts/monitoring/monitoring_bundle.js @@ -12,6 +12,7 @@ export default (props = {}) => { store.dispatch('monitoringDashboard/setFeatureFlags', { prometheusEndpointEnabled: gon.features.environmentMetricsUsePrometheusEndpoint, multipleDashboardsEnabled: gon.features.environmentMetricsShowMultipleDashboards, + additionalPanelTypesEnabled: gon.features.environmentMetricsAdditionalPanelTypes, }); } diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js index 0fa2a5d6370..245cc2eaca3 100644 --- a/app/assets/javascripts/monitoring/stores/actions.js +++ b/app/assets/javascripts/monitoring/stores/actions.js @@ -37,10 +37,15 @@ export const setEndpoints = ({ commit }, endpoints) => { export const setFeatureFlags = ( { commit }, - { prometheusEndpointEnabled, multipleDashboardsEnabled }, + { prometheusEndpointEnabled, multipleDashboardsEnabled, additionalPanelTypesEnabled }, ) => { commit(types.SET_DASHBOARD_ENABLED, prometheusEndpointEnabled); commit(types.SET_MULTIPLE_DASHBOARDS_ENABLED, multipleDashboardsEnabled); + commit(types.SET_ADDITIONAL_PANEL_TYPES_ENABLED, additionalPanelTypesEnabled); +}; + +export const setShowErrorBanner = ({ commit }, enabled) => { + commit(types.SET_SHOW_ERROR_BANNER, enabled); }; export const requestMetricsDashboard = ({ commit }) => { @@ -98,7 +103,9 @@ export const fetchMetricsData = ({ state, dispatch }, params) => { }) .catch(error => { dispatch('receiveMetricsDataFailure', error); - createFlash(s__('Metrics|There was an error while retrieving metrics')); + if (state.setShowErrorBanner) { + createFlash(s__('Metrics|There was an error while retrieving metrics')); + } }); }; @@ -118,7 +125,9 @@ export const fetchDashboard = ({ state, dispatch }, params) => { }) .catch(error => { dispatch('receiveMetricsDashboardFailure', error); - createFlash(s__('Metrics|There was an error while retrieving metrics')); + if (state.setShowErrorBanner) { + createFlash(s__('Metrics|There was an error while retrieving metrics')); + } }); }; diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js index 2c78a0b9315..4b1aadbcf05 100644 --- a/app/assets/javascripts/monitoring/stores/mutation_types.js +++ b/app/assets/javascripts/monitoring/stores/mutation_types.js @@ -11,7 +11,9 @@ export const SET_QUERY_RESULT = 'SET_QUERY_RESULT'; export const SET_TIME_WINDOW = 'SET_TIME_WINDOW'; export const SET_DASHBOARD_ENABLED = 'SET_DASHBOARD_ENABLED'; export const SET_MULTIPLE_DASHBOARDS_ENABLED = 'SET_MULTIPLE_DASHBOARDS_ENABLED'; +export const SET_ADDITIONAL_PANEL_TYPES_ENABLED = 'SET_ADDITIONAL_PANEL_TYPES_ENABLED'; export const SET_ALL_DASHBOARDS = 'SET_ALL_DASHBOARDS'; export const SET_ENDPOINTS = 'SET_ENDPOINTS'; export const SET_GETTING_STARTED_EMPTY_STATE = 'SET_GETTING_STARTED_EMPTY_STATE'; export const SET_NO_DATA_EMPTY_STATE = 'SET_NO_DATA_EMPTY_STATE'; +export const SET_SHOW_ERROR_BANNER = 'SET_SHOW_ERROR_BANNER'; diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js index a85a7723c1f..b19520d6638 100644 --- a/app/assets/javascripts/monitoring/stores/mutations.js +++ b/app/assets/javascripts/monitoring/stores/mutations.js @@ -75,6 +75,7 @@ export default { state.deploymentsEndpoint = endpoints.deploymentsEndpoint; state.dashboardEndpoint = endpoints.dashboardEndpoint; state.currentDashboard = endpoints.currentDashboard; + state.projectPath = endpoints.projectPath; }, [types.SET_DASHBOARD_ENABLED](state, enabled) { state.useDashboardEndpoint = enabled; @@ -92,4 +93,10 @@ export default { [types.SET_ALL_DASHBOARDS](state, dashboards) { state.allDashboards = dashboards; }, + [types.SET_ADDITIONAL_PANEL_TYPES_ENABLED](state, enabled) { + state.additionalPanelTypesEnabled = enabled; + }, + [types.SET_SHOW_ERROR_BANNER](state, enabled) { + state.showErrorBanner = enabled; + }, }; diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js index de711d6ccae..440bdc951e0 100644 --- a/app/assets/javascripts/monitoring/stores/state.js +++ b/app/assets/javascripts/monitoring/stores/state.js @@ -9,12 +9,15 @@ export default () => ({ dashboardEndpoint: invalidUrl, useDashboardEndpoint: false, multipleDashboardsEnabled: false, + additionalPanelTypesEnabled: false, emptyState: 'gettingStarted', showEmptyState: true, + showErrorBanner: true, groups: [], deploymentData: [], environments: [], metricsWithData: [], allDashboards: [], currentDashboard: null, + projectPath: null, }); diff --git a/app/assets/javascripts/monitoring/stores/utils.js b/app/assets/javascripts/monitoring/stores/utils.js index 721942f9d3b..938ee2f0a9a 100644 --- a/app/assets/javascripts/monitoring/stores/utils.js +++ b/app/assets/javascripts/monitoring/stores/utils.js @@ -69,13 +69,26 @@ export const sortMetrics = metrics => .sortBy('weight') .value(); -export const normalizeQueryResult = timeSeries => ({ - ...timeSeries, - values: timeSeries.values.map(([timestamp, value]) => [ - new Date(timestamp * 1000).toISOString(), - Number(value), - ]), -}); +export const normalizeQueryResult = timeSeries => { + let normalizedResult = {}; + + if (timeSeries.values) { + normalizedResult = { + ...timeSeries, + values: timeSeries.values.map(([timestamp, value]) => [ + new Date(timestamp * 1000).toISOString(), + Number(value), + ]), + }; + } else if (timeSeries.value) { + normalizedResult = { + ...timeSeries, + value: [new Date(timeSeries.value[0] * 1000).toISOString(), Number(timeSeries.value[1])], + }; + } + + return normalizedResult; +}; export const normalizeMetrics = metrics => { const groupedMetrics = groupQueriesByChartInfo(metrics); diff --git a/app/assets/javascripts/monitoring/utils.js b/app/assets/javascripts/monitoring/utils.js index ef309c8a398..478e2b3d06c 100644 --- a/app/assets/javascripts/monitoring/utils.js +++ b/app/assets/javascripts/monitoring/utils.js @@ -30,4 +30,28 @@ export const getTimeDiff = selectedTimeWindow => { return { start, end }; }; +/** + * This method is used to validate if the graph data format for a chart component + * that needs a time series as a response from a prometheus query (query_range) is + * of a valid format or not. + * @param {Object} graphData the graph data response from a prometheus request + * @returns {boolean} whether the graphData format is correct + */ +export const graphDataValidatorForValues = (isValues, graphData) => { + const responseValueKeyName = isValues ? 'value' : 'values'; + + return ( + Array.isArray(graphData.queries) && + graphData.queries.filter(query => { + if (Array.isArray(query.result)) { + return ( + query.result.filter(res => Array.isArray(res[responseValueKeyName])).length === + query.result.length + ); + } + return false; + }).length === graphData.queries.length + ); +}; + export default {}; diff --git a/app/assets/javascripts/notes/components/discussion_actions.vue b/app/assets/javascripts/notes/components/discussion_actions.vue index f4570c1292c..7aa8580d794 100644 --- a/app/assets/javascripts/notes/components/discussion_actions.vue +++ b/app/assets/javascripts/notes/components/discussion_actions.vue @@ -39,30 +39,27 @@ export default { </script> <template> - <div class="discussion-with-resolve-btn"> + <div class="discussion-with-resolve-btn clearfix"> <reply-placeholder :button-text="s__('MergeRequests|Reply...')" class="qa-discussion-reply" @onClick="$emit('showReplyForm')" /> - <resolve-discussion-button - v-if="discussion.resolvable" - :is-resolving="isResolving" - :button-title="resolveButtonTitle" - @onClick="$emit('resolve')" - /> - <div v-if="discussion.resolvable" class="btn-group discussion-actions ml-sm-2" role="group"> - <resolve-with-issue-button v-if="resolveWithIssuePath" :url="resolveWithIssuePath" /> - <jump-to-next-discussion-button - v-if="shouldShowJumpToNextDiscussion" - @onClick="$emit('jumpToNextDiscussion')" - /> + + <div class="btn-group discussion-actions" role="group"> + <div class="btn-group"> + <resolve-discussion-button + v-if="discussion.resolvable" + :is-resolving="isResolving" + :button-title="resolveButtonTitle" + @onClick="$emit('resolve')" + /> + </div> <resolve-with-issue-button v-if="discussion.resolvable && resolveWithIssuePath" :url="resolveWithIssuePath" /> </div> - <div v-if="discussion.resolvable && shouldShowJumpToNextDiscussion" class="btn-group discussion-actions ml-sm-2" diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index 844d0c3e376..6cc873359da 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -165,7 +165,7 @@ export default { v-gl-tooltip type="button" title="Edit comment" - class="note-action-button js-note-edit btn btn-transparent" + class="note-action-button js-note-edit btn btn-transparent qa-note-edit-button" @click="onEdit" > <icon name="pencil" css-classes="link-highlight" /> diff --git a/app/assets/javascripts/notes/components/note_actions/reply_button.vue b/app/assets/javascripts/notes/components/note_actions/reply_button.vue index be8e42af9ea..1aeb07d6608 100644 --- a/app/assets/javascripts/notes/components/note_actions/reply_button.vue +++ b/app/assets/javascripts/notes/components/note_actions/reply_button.vue @@ -19,7 +19,7 @@ export default { <gl-button ref="button" v-gl-tooltip - class="note-action-button" + class="note-action-button js-note-action-reply" variant="transparent" :title="__('Reply to comment')" @click="$emit('startReplying')" diff --git a/app/assets/javascripts/notes/services/notes_service.js b/app/assets/javascripts/notes/services/notes_service.js index bc0f5c19b9d..9e0392110b6 100644 --- a/app/assets/javascripts/notes/services/notes_service.js +++ b/app/assets/javascripts/notes/services/notes_service.js @@ -9,9 +9,6 @@ export default { const config = filter !== undefined ? { params: { notes_filter: filter } } : null; return Vue.http.get(endpoint, config); }, - deleteNote(endpoint) { - return Vue.http.delete(endpoint); - }, replyToDiscussion(endpoint, data) { return Vue.http.post(endpoint, data, { emulateJSON: true }); }, diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 2eefef8bd6e..762a87ce0ff 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -62,7 +62,7 @@ export const updateDiscussion = ({ commit, state }, discussion) => { }; export const deleteNote = ({ commit, dispatch, state }, note) => - service.deleteNote(note.path).then(() => { + axios.delete(note.path).then(() => { const discussion = state.discussions.find(({ id }) => id === note.discussion_id); commit(types.DELETE_NOTE, note); @@ -357,11 +357,11 @@ export const poll = ({ commit, state, getters, dispatch }) => { }; export const stopPolling = () => { - eTagPoll.stop(); + if (eTagPoll) eTagPoll.stop(); }; export const restartPolling = () => { - eTagPoll.restart(); + if (eTagPoll) eTagPoll.restart(); }; export const fetchData = ({ commit, state, getters }) => { diff --git a/app/assets/javascripts/pages/groups/merge_requests/index.js b/app/assets/javascripts/pages/groups/merge_requests/index.js index 12a26fd88fa..7520cfb6da0 100644 --- a/app/assets/javascripts/pages/groups/merge_requests/index.js +++ b/app/assets/javascripts/pages/groups/merge_requests/index.js @@ -1,11 +1,15 @@ import projectSelect from '~/project_select'; import initFilteredSearch from '~/pages/search/init_filtered_search'; +import issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar'; import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests'; import { FILTERED_SEARCH } from '~/pages/constants'; +const ISSUABLE_BULK_UPDATE_PREFIX = 'merge_request_'; + document.addEventListener('DOMContentLoaded', () => { addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys); + issuableInitBulkUpdateSidebar.init(ISSUABLE_BULK_UPDATE_PREFIX); initFilteredSearch({ page: FILTERED_SEARCH.MERGE_REQUESTS, diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue index 015c1527500..f05db8376a4 100644 --- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue +++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue @@ -42,6 +42,12 @@ export default { keys: ['feature', 'request'], }, { + metric: 'rugged', + header: 'Rugged calls', + details: 'details', + keys: ['feature', 'args'], + }, + { metric: 'redis', header: 'Redis calls', details: 'details', diff --git a/app/assets/javascripts/projects/projects_filterable_list.js b/app/assets/javascripts/projects/projects_filterable_list.js new file mode 100644 index 00000000000..433c894e668 --- /dev/null +++ b/app/assets/javascripts/projects/projects_filterable_list.js @@ -0,0 +1,7 @@ +import FilterableList from '~/filterable_list'; + +export default class ProjectsFilterableList extends FilterableList { + getFilterEndpoint() { + return this.getPagePath().replace('/projects?', '/projects.json?'); + } +} diff --git a/app/assets/javascripts/projects_list.js b/app/assets/javascripts/projects_list.js index c67d59d2be5..913b62ba26d 100644 --- a/app/assets/javascripts/projects_list.js +++ b/app/assets/javascripts/projects_list.js @@ -1,4 +1,4 @@ -import FilterableList from './filterable_list'; +import ProjectsFilterableList from './projects/projects_filterable_list'; /** * Makes search request for projects when user types a value in the search input. @@ -11,7 +11,7 @@ export default class ProjectsList { const holder = document.querySelector('.js-projects-list-holder'); if (form && filter && holder) { - const list = new FilterableList(form, filter, holder); + const list = new ProjectsFilterableList(form, filter, holder); list.initSearch(); } } diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue index 7752723baac..38519c220c5 100644 --- a/app/assets/javascripts/registry/components/app.vue +++ b/app/assets/javascripts/registry/components/app.vue @@ -2,6 +2,7 @@ import { mapGetters, mapActions } from 'vuex'; import { GlLoadingIcon } from '@gitlab/ui'; import store from '../stores'; +import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; import CollapsibleContainer from './collapsible_container.vue'; import SvgMessage from './svg_message.vue'; import { s__, sprintf } from '../../locale'; @@ -9,6 +10,7 @@ import { s__, sprintf } from '../../locale'; export default { name: 'RegistryListApp', components: { + clipboardButton, CollapsibleContainer, GlLoadingIcon, SvgMessage, @@ -46,10 +48,10 @@ export default { dockerConnectionErrorText() { return sprintf( s__(`ContainerRegistry|We are having trouble connecting to Docker, which could be due to an - issue with your project name or path. For more information, please review the - %{docLinkStart}Container Registry documentation%{docLinkEnd}.`), + issue with your project name or path. + %{docLinkStart}More Information%{docLinkEnd}`), { - docLinkStart: `<a href="${this.helpPagePath}#docker-connection-error">`, + docLinkStart: `<a href="${this.helpPagePath}#docker-connection-error" target="_blank">`, docLinkEnd: '</a>', }, false, @@ -58,10 +60,10 @@ export default { introText() { return sprintf( s__(`ContainerRegistry|With the Docker Container Registry integrated into GitLab, every - project can have its own space to store its Docker images. Learn more about the - %{docLinkStart}Container Registry%{docLinkEnd}.`), + project can have its own space to store its Docker images. + %{docLinkStart}More Information%{docLinkEnd}`), { - docLinkStart: `<a href="${this.helpPagePath}">`, + docLinkStart: `<a href="${this.helpPagePath}" target="_blank">`, docLinkEnd: '</a>', }, false, @@ -70,14 +72,20 @@ export default { noContainerImagesText() { return sprintf( s__(`ContainerRegistry|With the Container Registry, every project can have its own space to - store its Docker images. Learn more about the %{docLinkStart}Container Registry%{docLinkEnd}.`), + store its Docker images. %{docLinkStart}More Information%{docLinkEnd}`), { - docLinkStart: `<a href="${this.helpPagePath}">`, + docLinkStart: `<a href="${this.helpPagePath}" target="_blank">`, docLinkEnd: '</a>', }, false, ); }, + dockerBuildCommand() { + return `docker build -t ${this.repositoryUrl} .`; + }, + dockerPushCommand() { + return `docker push ${this.repositoryUrl}`; + }, }, created() { this.setMainEndpoint(this.endpoint); @@ -99,7 +107,7 @@ export default { <p v-html="dockerConnectionErrorText"></p> </svg-message> - <gl-loading-icon v-else-if="isLoading" size="md" class="prepend-top-16" /> + <gl-loading-icon v-else-if="isLoading && !characterError" size="md" class="prepend-top-16" /> <div v-else-if="!isLoading && !characterError && repos.length"> <h4>{{ s__('ContainerRegistry|Container Registry') }}</h4> @@ -126,10 +134,27 @@ export default { }} </p> - <pre> - docker build -t {{ repositoryUrl }} . - docker push {{ repositoryUrl }} - </pre> + <div class="input-group append-bottom-10"> + <input :value="dockerBuildCommand" type="text" class="form-control monospace" readonly /> + <span class="input-group-append"> + <clipboard-button + :text="dockerBuildCommand" + :title="s__('ContainerRegistry|Copy build command to clipboard')" + class="input-group-text" + /> + </span> + </div> + + <div class="input-group"> + <input :value="dockerPushCommand" type="text" class="form-control monospace" readonly /> + <span class="input-group-append"> + <clipboard-button + :text="dockerPushCommand" + :title="s__('ContainerRegistry|Copy push command to clipboard')" + class="input-group-text" + /> + </span> + </div> </svg-message> </div> </template> diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue index 1e266dd4ced..e157036871b 100644 --- a/app/assets/javascripts/registry/components/collapsible_container.vue +++ b/app/assets/javascripts/registry/components/collapsible_container.vue @@ -31,6 +31,7 @@ export default { data() { return { isOpen: false, + modalId: `confirm-repo-deletion-modal-${this.repo.id}`, }; }, computed: { @@ -80,7 +81,7 @@ export default { <gl-button v-if="repo.canDelete" v-gl-tooltip - v-gl-modal="'confirm-repo-deletion-modal'" + v-gl-modal="modalId" :title="s__('ContainerRegistry|Remove repository')" :aria-label="s__('ContainerRegistry|Remove repository')" class="js-remove-repo" @@ -100,12 +101,7 @@ export default { {{ s__('ContainerRegistry|No tags in Container Registry for this container image.') }} </div> </div> - - <gl-modal - modal-id="confirm-repo-deletion-modal" - ok-variant="danger" - @ok="handleDeleteRepository" - > + <gl-modal :modal-id="modalId" ok-variant="danger" @ok="handleDeleteRepository"> <template v-slot:modal-title>{{ s__('ContainerRegistry|Remove repository') }}</template> <p v-html=" diff --git a/app/assets/javascripts/registry/components/svg_message.vue b/app/assets/javascripts/registry/components/svg_message.vue index d0d44bf2d14..617093e054e 100644 --- a/app/assets/javascripts/registry/components/svg_message.vue +++ b/app/assets/javascripts/registry/components/svg_message.vue @@ -15,10 +15,12 @@ export default { </script> <template> - <div :id="id" class="empty-state container-message mw-70p"> + <div :id="id" class="empty-state container-message"> <div class="svg-content"> <img :src="svgPath" class="flex-align-self-center" /> </div> - <slot></slot> + <div class="text-content"> + <slot></slot> + </div> </div> </template> diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue index 0ec5e2c7a87..a498a553908 100644 --- a/app/assets/javascripts/registry/components/table_registry.vue +++ b/app/assets/javascripts/registry/components/table_registry.vue @@ -32,6 +32,7 @@ export default { data() { return { itemToBeDeleted: null, + modalId: `confirm-image-deletion-modal-${this.repo.id}`, }; }, computed: { @@ -114,7 +115,7 @@ export default { <gl-button v-if="item.canDelete" v-gl-tooltip - v-gl-modal="'confirm-image-deletion-modal'" + v-gl-modal="modalId" :title="s__('ContainerRegistry|Remove image')" :aria-label="s__('ContainerRegistry|Remove image')" variant="danger" @@ -134,11 +135,7 @@ export default { :page-info="repo.pagination" /> - <gl-modal - modal-id="confirm-image-deletion-modal" - ok-variant="danger" - @ok="handleDeleteRegistry" - > + <gl-modal :modal-id="modalId" ok-variant="danger" @ok="handleDeleteRegistry"> <template v-slot:modal-title>{{ s__('ContainerRegistry|Remove image') }}</template> <template v-slot:modal-ok>{{ s__('ContainerRegistry|Remove image and tags') }}</template> <p diff --git a/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue b/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue index 6d908524da9..f0112a5a623 100644 --- a/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue +++ b/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue @@ -65,7 +65,7 @@ export default { <template> <div v-if="isFetchingMergeRequests || (!isFetchingMergeRequests && totalCount)"> - <div id="merge-requests" class="card-slim mt-3"> + <div id="merge-requests" class="card card-slim mt-3"> <div class="card-header"> <div class="card-title mt-0 mb-0 h5 merge-requests-title"> <span class="mr-1"> diff --git a/app/assets/javascripts/reports/components/issue_status_icon.vue b/app/assets/javascripts/reports/components/issue_status_icon.vue index 04fba43b2f3..386653b9444 100644 --- a/app/assets/javascripts/reports/components/issue_status_icon.vue +++ b/app/assets/javascripts/reports/components/issue_status_icon.vue @@ -16,7 +16,7 @@ export default { statusIconSize: { type: Number, required: false, - default: 32, + default: 24, }, }, computed: { diff --git a/app/assets/javascripts/reports/components/report_item.vue b/app/assets/javascripts/reports/components/report_item.vue index 2be9c37b00a..f3f7d2648a8 100644 --- a/app/assets/javascripts/reports/components/report_item.vue +++ b/app/assets/javascripts/reports/components/report_item.vue @@ -27,7 +27,7 @@ export default { statusIconSize: { type: Number, required: false, - default: 32, + default: 24, }, isNew: { type: Boolean, @@ -43,12 +43,15 @@ export default { }; </script> <template> - <li :class="{ 'is-dismissed': issue.isDismissed }" class="report-block-list-issue"> + <li + :class="{ 'is-dismissed': issue.isDismissed }" + class="report-block-list-issue align-items-center" + > <issue-status-icon v-if="showReportSectionStatusIcon" :status="status" :status-icon-size="statusIconSize" - class="append-right-5" + class="append-right-default" /> <component :is="component" v-if="component" :issue="issue" :status="status" :is-new="isNew" /> diff --git a/app/assets/javascripts/reports/components/report_section.vue b/app/assets/javascripts/reports/components/report_section.vue index 3d576caaf8f..9bc3e6388e3 100644 --- a/app/assets/javascripts/reports/components/report_section.vue +++ b/app/assets/javascripts/reports/components/report_section.vue @@ -165,8 +165,8 @@ export default { <template> <section class="media-section"> <div class="media"> - <status-icon :status="statusIconName" /> - <div class="media-body d-flex flex-align-self-center"> + <status-icon :status="statusIconName" :size="24" /> + <div class="media-body d-flex flex-align-self-center prepend-left-default"> <span class="js-code-text code-text"> {{ headerText }} <slot :name="slotName"></slot> diff --git a/app/assets/javascripts/reports/components/summary_row.vue b/app/assets/javascripts/reports/components/summary_row.vue index 97a68531d29..aba798e63d0 100644 --- a/app/assets/javascripts/reports/components/summary_row.vue +++ b/app/assets/javascripts/reports/components/summary_row.vue @@ -44,10 +44,14 @@ export default { }; </script> <template> - <div class="report-block-list-issue report-block-list-issue-parent"> - <div class="report-block-list-icon append-right-10 prepend-left-5"> - <gl-loading-icon v-if="statusIcon === 'loading'" css-class="report-block-list-loading-icon" /> - <ci-icon v-else :status="iconStatus" /> + <div class="report-block-list-issue report-block-list-issue-parent align-items-center"> + <div class="report-block-list-icon append-right-default"> + <gl-loading-icon + v-if="statusIcon === 'loading'" + css-class="report-block-list-loading-icon" + size="md" + /> + <ci-icon v-else :status="iconStatus" :size="24" /> </div> <div class="report-block-list-issue-description"> diff --git a/app/assets/javascripts/repository/components/breadcrumbs.vue b/app/assets/javascripts/repository/components/breadcrumbs.vue index 67963dc1923..afb58a60155 100644 --- a/app/assets/javascripts/repository/components/breadcrumbs.vue +++ b/app/assets/javascripts/repository/components/breadcrumbs.vue @@ -1,12 +1,41 @@ <script> +import { GlDropdown, GlDropdownDivider, GlDropdownHeader, GlDropdownItem } from '@gitlab/ui'; +import { __ } from '../../locale'; +import Icon from '../../vue_shared/components/icon.vue'; import getRefMixin from '../mixins/get_ref'; import getProjectShortPath from '../queries/getProjectShortPath.query.graphql'; +import getProjectPath from '../queries/getProjectPath.query.graphql'; +import getPermissions from '../queries/getPermissions.query.graphql'; + +const ROW_TYPES = { + header: 'header', + divider: 'divider', +}; export default { + components: { + GlDropdown, + GlDropdownDivider, + GlDropdownHeader, + GlDropdownItem, + Icon, + }, apollo: { projectShortPath: { query: getProjectShortPath, }, + projectPath: { + query: getProjectPath, + }, + userPermissions: { + query: getPermissions, + variables() { + return { + projectPath: this.projectPath, + }; + }, + update: data => data.project.userPermissions, + }, }, mixins: [getRefMixin], props: { @@ -15,10 +44,52 @@ export default { required: false, default: '/', }, + canCollaborate: { + type: Boolean, + required: false, + default: false, + }, + canEditTree: { + type: Boolean, + required: false, + default: false, + }, + newBranchPath: { + type: String, + required: false, + default: null, + }, + newTagPath: { + type: String, + required: false, + default: null, + }, + newBlobPath: { + type: String, + required: false, + default: null, + }, + forkNewBlobPath: { + type: String, + required: false, + default: null, + }, + forkNewDirectoryPath: { + type: String, + required: false, + default: null, + }, + forkUploadBlobPath: { + type: String, + required: false, + default: null, + }, }, data() { return { projectShortPath: '', + projectPath: '', + userPermissions: {}, }; }, computed: { @@ -39,11 +110,112 @@ export default { [{ name: this.projectShortPath, path: '/', to: `/tree/${this.ref}/` }], ); }, + canCreateMrFromFork() { + return this.userPermissions.forkProject && this.userPermissions.createMergeRequestIn; + }, + dropdownItems() { + const items = []; + + if (this.canEditTree) { + items.push( + { + type: ROW_TYPES.header, + text: __('This directory'), + }, + { + attrs: { + href: this.newBlobPath, + class: 'qa-new-file-option', + }, + text: __('New file'), + }, + { + attrs: { + href: '#modal-upload-blob', + 'data-target': '#modal-upload-blob', + 'data-toggle': 'modal', + }, + text: __('Upload file'), + }, + { + attrs: { + href: '#modal-create-new-dir', + 'data-target': '#modal-create-new-dir', + 'data-toggle': 'modal', + }, + text: __('New directory'), + }, + ); + } else if (this.canCreateMrFromFork) { + items.push( + { + attrs: { + href: this.forkNewBlobPath, + 'data-method': 'post', + }, + text: __('New file'), + }, + { + attrs: { + href: this.forkUploadBlobPath, + 'data-method': 'post', + }, + text: __('Upload file'), + }, + { + attrs: { + href: this.forkNewDirectoryPath, + 'data-method': 'post', + }, + text: __('New directory'), + }, + ); + } + + if (this.userPermissions.pushCode) { + items.push( + { + type: ROW_TYPES.divider, + }, + { + type: ROW_TYPES.header, + text: __('This repository'), + }, + { + attrs: { + href: this.newBranchPath, + }, + text: __('New branch'), + }, + { + attrs: { + href: this.newTagPath, + }, + text: __('New tag'), + }, + ); + } + + return items; + }, + renderAddToTreeDropdown() { + return this.canCollaborate || this.canCreateMrFromFork; + }, }, methods: { isLast(i) { return i === this.pathLinks.length - 1; }, + getComponent(type) { + switch (type) { + case ROW_TYPES.divider: + return 'gl-dropdown-divider'; + case ROW_TYPES.header: + return 'gl-dropdown-header'; + default: + return 'gl-dropdown-item'; + } + }, }, }; </script> @@ -56,6 +228,20 @@ export default { {{ link.name }} </router-link> </li> + <li v-if="renderAddToTreeDropdown" class="breadcrumb-item"> + <gl-dropdown toggle-class="add-to-tree qa-add-to-tree ml-1"> + <template slot="button-content"> + <span class="sr-only">{{ __('Add to tree') }}</span> + <icon name="plus" :size="16" class="float-left" /> + <icon name="arrow-down" :size="16" class="float-left" /> + </template> + <template v-for="(item, i) in dropdownItems"> + <component :is="getComponent(item.type)" :key="i" v-bind="item.attrs"> + {{ item.text }} + </component> + </template> + </gl-dropdown> + </li> </ol> </nav> </template> diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue index 0d9e992e596..610c7e8d99e 100644 --- a/app/assets/javascripts/repository/components/table/index.vue +++ b/app/assets/javascripts/repository/components/table/index.vue @@ -137,6 +137,7 @@ export default { :path="entry.flatPath" :type="entry.type" :url="entry.webUrl" + :submodule-tree-url="entry.treeUrl" :lfs-oid="entry.lfsOid" /> </template> diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue index 3e060e9ecb6..6029460d975 100644 --- a/app/assets/javascripts/repository/components/table/row.vue +++ b/app/assets/javascripts/repository/components/table/row.vue @@ -62,6 +62,11 @@ export default { required: false, default: null, }, + submoduleTreeUrl: { + type: String, + required: false, + default: null, + }, }, data() { return { @@ -112,7 +117,7 @@ export default { </component> <gl-badge v-if="lfsOid" variant="default" class="label-lfs ml-1">LFS</gl-badge> <template v-if="isSubmodule"> - @ <gl-link href="#" class="commit-sha">{{ shortSha }}</gl-link> + @ <gl-link :href="submoduleTreeUrl" class="commit-sha">{{ shortSha }}</gl-link> </template> </td> <td class="d-none d-sm-table-cell tree-commit"> diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js index ea051eaa414..f9727960040 100644 --- a/app/assets/javascripts/repository/index.js +++ b/app/assets/javascripts/repository/index.js @@ -5,6 +5,7 @@ import Breadcrumbs from './components/breadcrumbs.vue'; import LastCommit from './components/last_commit.vue'; import apolloProvider from './graphql'; import { setTitle } from './utils/title'; +import { parseBoolean } from '../lib/utils/common_utils'; export default function setupVueRepositoryList() { const el = document.getElementById('js-tree-list'); @@ -36,19 +37,42 @@ export default function setupVueRepositoryList() { .forEach(elem => elem.classList.toggle('hidden', !isRoot)); }); - // eslint-disable-next-line no-new - new Vue({ - el: document.getElementById('js-repo-breadcrumb'), - router, - apolloProvider, - render(h) { - return h(Breadcrumbs, { - props: { - currentPath: this.$route.params.pathMatch, - }, - }); - }, - }); + const breadcrumbEl = document.getElementById('js-repo-breadcrumb'); + + if (breadcrumbEl) { + const { + canCollaborate, + canEditTree, + newBranchPath, + newTagPath, + newBlobPath, + forkNewBlobPath, + forkNewDirectoryPath, + forkUploadBlobPath, + } = breadcrumbEl.dataset; + + // eslint-disable-next-line no-new + new Vue({ + el: breadcrumbEl, + router, + apolloProvider, + render(h) { + return h(Breadcrumbs, { + props: { + currentPath: this.$route.params.pathMatch, + canCollaborate: parseBoolean(canCollaborate), + canEditTree: parseBoolean(canEditTree), + newBranchPath, + newTagPath, + newBlobPath, + forkNewBlobPath, + forkNewDirectoryPath, + forkUploadBlobPath, + }, + }); + }, + }); + } // eslint-disable-next-line no-new new Vue({ diff --git a/app/assets/javascripts/repository/queries/getFiles.query.graphql b/app/assets/javascripts/repository/queries/getFiles.query.graphql index 4c24fc4087f..b3cc0878cad 100644 --- a/app/assets/javascripts/repository/queries/getFiles.query.graphql +++ b/app/assets/javascripts/repository/queries/getFiles.query.graphql @@ -35,6 +35,8 @@ query getFiles( edges { node { ...TreeEntry + webUrl + treeUrl } } pageInfo { diff --git a/app/assets/javascripts/repository/queries/getPermissions.query.graphql b/app/assets/javascripts/repository/queries/getPermissions.query.graphql new file mode 100644 index 00000000000..092fa44e2d0 --- /dev/null +++ b/app/assets/javascripts/repository/queries/getPermissions.query.graphql @@ -0,0 +1,9 @@ +query getPermissions($projectPath: ID!) { + project(fullPath: $projectPath) { + userPermissions { + pushCode + forkProject + createMergeRequestIn + } + } +} diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue index a75daca156c..0d1faceef11 100644 --- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue +++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue @@ -73,22 +73,22 @@ export default { <template> <div> - <div class="sidebar-collapsed-icon" @click="onClickCollapsedIcon"> - <span - v-tooltip - :title="notificationTooltip" - data-container="body" - data-placement="left" - data-boundary="viewport" - > - <icon - :name="notificationIcon" - :size="16" - aria-hidden="true" - class="sidebar-item-icon is-active" - /> - </span> - </div> + <span + v-tooltip + class="sidebar-collapsed-icon" + :title="notificationTooltip" + data-container="body" + data-placement="left" + data-boundary="viewport" + @click="onClickCollapsedIcon" + > + <icon + :name="notificationIcon" + :size="16" + aria-hidden="true" + class="sidebar-item-icon is-active" + /> + </span> <span class="issuable-header-text hide-collapsed float-left"> {{ __('Notifications') }} </span> <toggle-button ref="toggleButton" diff --git a/app/assets/javascripts/visual_review_toolbar/styles/toolbar.css b/app/assets/javascripts/visual_review_toolbar/styles/toolbar.css index 00a55c0027a..6a7b2f52549 100644 --- a/app/assets/javascripts/visual_review_toolbar/styles/toolbar.css +++ b/app/assets/javascripts/visual_review_toolbar/styles/toolbar.css @@ -48,6 +48,7 @@ font-size: .8rem; font-weight: 400; color: #2e2e2e; + z-index: 9999; /* toolbar should always be on top */ } .gitlab-wrapper-open { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue index 4b57693e8f1..57d4d8b7ae6 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue @@ -14,6 +14,6 @@ export default { <template> <div class="circle-icon-container append-right-default align-self-start align-self-lg-center"> - <icon :name="name" /> + <icon :name="name" :size="24" /> </div> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue index f5fa68308bc..40c095aa954 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue @@ -96,16 +96,14 @@ export default { <template> <div class="ci-widget media js-ci-widget"> <template v-if="!hasPipeline || hasCIError"> - <div - class="add-border ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-default" - > - <icon :size="32" name="status_failed_borderless" /> + <div class="add-border ci-status-icon ci-status-icon-failed ci-error js-ci-error"> + <icon :size="24" name="status_failed_borderless" /> </div> - <div class="media-body" v-html="errorText"></div> + <div class="media-body prepend-left-default" v-html="errorText"></div> </template> <template v-else-if="hasPipeline"> <a :href="status.details_path" class="align-self-start append-right-default"> - <ci-icon :status="status" :size="32" :borderless="true" class="add-border" /> + <ci-icon :status="status" :size="24" :borderless="true" class="add-border" /> </a> <div class="ci-widget-container d-flex"> <div class="ci-widget-content"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue index 392eb6fb425..8dbd9e52cfe 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue @@ -32,8 +32,8 @@ export default { }; </script> <template> - <div class="space-children d-flex append-right-10 widget-status-icon"> - <div v-if="isLoading" class="mr-widget-icon"><gl-loading-icon size="md" /></div> + <div class="d-flex widget-status-icon"> + <div v-if="isLoading" class="mr-widget-icon"><gl-loading-icon size="sm" /></div> <ci-icon v-else :status="statusObj" :size="24" /> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue index 0312b147b62..01524f4b650 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue @@ -83,7 +83,7 @@ export default { <gl-button :aria-label="ariaLabel" variant="blank" - class="commit-edit-toggle square s24 mr-2" + class="commit-edit-toggle square s24 append-right-default" @click.stop="toggle()" > <icon :name="collapseIcon" :size="16" /> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue index 7312b31c01c..4d7d49398eb 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue @@ -18,7 +18,9 @@ export default { <template> <div class="mr-widget-body mr-widget-empty-state"> <div class="row"> - <div class="artwork col-md-5 order-md-last col-12 text-center"> + <div + class="artwork col-md-5 order-md-last col-12 text-center d-flex justify-content-center align-items-center" + > <span v-html="emptyStateSVG"></span> </div> <div class="text col-md-7 order-md-first col-12"> diff --git a/app/assets/stylesheets/_ee/application_ee.scss b/app/assets/stylesheets/_ee/application_ee.scss new file mode 100644 index 00000000000..0fb2c9b68a9 --- /dev/null +++ b/app/assets/stylesheets/_ee/application_ee.scss @@ -0,0 +1,5 @@ +/* + This is a noop-file. In EE: + ee/app/assets/stylesheets/_ee/application_ee.scss + will take precedence over it and import more styles + */ diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index d5ef66af31a..fbf16aa324a 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -34,4 +34,8 @@ // Styles for JS behaviors. @import "behaviors"; +// EE-only stylesheets +@import "application_ee"; + +// CSS util classes @import "utilities"; diff --git a/app/assets/stylesheets/components/avatar.scss b/app/assets/stylesheets/components/avatar.scss index 8e9650cdf34..312123aeef9 100644 --- a/app/assets/stylesheets/components/avatar.scss +++ b/app/assets/stylesheets/components/avatar.scss @@ -50,6 +50,11 @@ $avatar-sizes: ( line-height: 88px, border-radius: $border-radius-large ), + 96: ( + font-size: 36px, + line-height: 94px, + border-radius: $border-radius-large + ), 100: ( font-size: 36px, line-height: 98px, diff --git a/app/assets/stylesheets/components/popover.scss b/app/assets/stylesheets/components/popover.scss index 8c40c4adb5c..6654553aaa2 100644 --- a/app/assets/stylesheets/components/popover.scss +++ b/app/assets/stylesheets/components/popover.scss @@ -102,6 +102,7 @@ .onboarding-popover { box-shadow: 0 2px 4px $dropdown-shadow-color; + max-width: 280px; .popover-body { font-size: $gl-font-size; diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 9b1d9d51f9c..82b4ec750ff 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -9,7 +9,6 @@ @import 'framework/animations'; @import 'framework/vue_transitions'; -@import 'framework/asciidoctor'; @import 'framework/banner'; @import 'framework/blocks'; @import 'framework/buttons'; diff --git a/app/assets/stylesheets/framework/asciidoctor.scss b/app/assets/stylesheets/framework/asciidoctor.scss deleted file mode 100644 index 1586265d40e..00000000000 --- a/app/assets/stylesheets/framework/asciidoctor.scss +++ /dev/null @@ -1,27 +0,0 @@ -.admonitionblock td.icon { - width: 1%; - - [class^='fa icon-'] { - @extend .fa-2x; - } - - .icon-note { - @extend .fa-thumb-tack; - } - - .icon-tip { - @extend .fa-lightbulb-o; - } - - .icon-warning { - @extend .fa-exclamation-triangle; - } - - .icon-caution { - @extend .fa-fire; - } - - .icon-important { - @extend .fa-exclamation-circle; - } -} diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 1bd5043ed10..61ab0476c42 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -434,6 +434,7 @@ img.emoji { /** COMMON SIZING CLASSES **/ .w-0 { width: 0; } +.w-8em { width: 8em; } .h-12em { height: 12em; } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index e75c1379dfb..29f63e9578d 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -63,7 +63,8 @@ margin-top: 11px; } -.dropdown-toggle { +.dropdown-toggle, +.confidential-merge-request-fork-group .dropdown-toggle { padding: 6px 8px 6px 10px; background-color: $white-light; color: $gl-text-color; @@ -288,7 +289,7 @@ padding: 0 1px; a, - button:not(.dropdown-toggle,.ci-action-icon-container), + button, .menu-item { @include dropdown-link; } diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 26cbb7f5c13..5984efd1cf8 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -9,14 +9,14 @@ float: right; margin-right: 0; - @include media-breakpoint-down(xs) { + @include media-breakpoint-down(sm) { float: none; } } } .filters-section { - @include media-breakpoint-down(xs) { + @include media-breakpoint-down(sm) { display: inline-block; } } @@ -37,7 +37,7 @@ } } -@include media-breakpoint-down(xs) { +@include media-breakpoint-down(sm) { .filter-item { display: block; margin: 0 0 10px; @@ -50,12 +50,6 @@ } .filtered-search-wrapper { - display: flex; - - @include media-breakpoint-down(xs) { - flex-direction: column; - } - .tokens-container { display: flex; flex: 1; @@ -186,7 +180,7 @@ border: 1px solid $border-color; background-color: $white-light; - @include media-breakpoint-down(xs) { + @include media-breakpoint-down(sm) { flex: 1 1 auto; margin-bottom: 10px; } @@ -259,7 +253,7 @@ max-width: 280px; overflow: auto; - @include media-breakpoint-down(xs) { + @include media-breakpoint-down(sm) { width: auto; left: 0; right: 0; @@ -311,7 +305,7 @@ .filtered-search-history-dropdown { width: 40%; - @include media-breakpoint-down(xs) { + @include media-breakpoint-down(sm) { left: 0; right: 0; max-width: none; @@ -341,35 +335,46 @@ } .filter-dropdown-container { - display: flex; - .dropdown-toggle { line-height: 22px; } } -@include media-breakpoint-down(xs) { +@include media-breakpoint-down(sm) { .issues-details-filters { - padding: 0 0 10px; + padding-top: 0; + padding-bottom: 0; background-color: $white-light; border-top: 0; } - .filter-dropdown-container { + .boards-switcher { + margin: 0 0 10px; + + .boards-selector-wrapper, .dropdown { - margin-left: 0; + display: block; } } -} -@include media-breakpoint-down(sm) { - .filter-dropdown-container { - .dropdown-toggle, - .dropdown, - .dropdown-menu { + .filter-dropdown-container > div { + margin: 0; + + > .btn { + margin: 0 0 10px; width: 100%; } } + + .boards-add-list > .btn { + text-align: left; + + > svg { + position: absolute; + top: 11px; + right: 6px; + } + } } .droplab-dropdown .dropdown-menu .filter-dropdown-item { diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss index 1be5ef276fd..7332c4981d2 100644 --- a/app/assets/stylesheets/framework/icons.scss +++ b/app/assets/stylesheets/framework/icons.scss @@ -88,8 +88,5 @@ display: flex; align-items: center; justify-content: center; - border: $border-size solid $gray-400; - border-radius: 50%; - padding: $gl-padding-8 - $border-size; color: $gray-700; } diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 954551fef97..460d9ea9526 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -265,7 +265,6 @@ ul.controls { } .issuable-pipeline-broken a, - .issuable-pipeline-status a, .author-link { display: flex; } diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index 1d00372d04d..fd9a75bc5b6 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -19,16 +19,23 @@ } } - // leave enough space for the close icon .modal-title { + line-height: $gl-line-height-24; + + // leave enough space for the close icon &.mw-100, &.w-100 { - // after upgrading to Bootstrap 4.2 we can use $modal-header-padding-x here - // https://github.com/twbs/bootstrap/pull/26976 - margin-right: -28px; - padding-right: 28px; + margin-right: -$modal-header-padding-x; + padding-right: $modal-header-padding-x; } } + + .close { + font-weight: $gl-font-weight-normal; + line-height: $gl-line-height; + color: $gray-900; + opacity: 1; + } } .modal-body { @@ -63,6 +70,10 @@ margin-left: $grid-size; } + .btn-group .btn + .btn { + margin-left: -1px; + } + @include media-breakpoint-down(xs) { flex-direction: column; @@ -72,6 +83,11 @@ margin-left: 0; margin-top: $grid-size; } + + .btn-group .btn + .btn { + margin-left: -1px; + margin-top: 0; + } } } @@ -90,6 +106,20 @@ body.modal-open { .modal { background-color: $black-transparent; + .modal-content { + border-radius: $modal-border-radius; + + > :first-child { + border-top-left-radius: $modal-border-radius; + border-top-right-radius: $modal-border-radius; + } + + > :last-child { + border-bottom-left-radius: $modal-border-radius; + border-bottom-right-radius: $modal-border-radius; + } + } + @include media-breakpoint-up(sm) { .modal-dialog { margin: 64px auto; diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss index cd3d6f8297e..d9c93fed1c4 100644 --- a/app/assets/stylesheets/framework/panels.scss +++ b/app/assets/stylesheets/framework/panels.scss @@ -3,7 +3,6 @@ } .card-slim { - @extend .card; margin-bottom: $gl-vert-padding; } diff --git a/app/assets/stylesheets/framework/tooltips.scss b/app/assets/stylesheets/framework/tooltips.scss index 98f28987a82..edc2fb532c8 100644 --- a/app/assets/stylesheets/framework/tooltips.scss +++ b/app/assets/stylesheets/framework/tooltips.scss @@ -1,7 +1,6 @@ .tooltip-inner { - font-size: $tooltip-font-size; + font-size: $gl-font-size-small; border-radius: $border-radius-default; - line-height: 16px; + line-height: $gl-line-height; font-weight: $gl-font-weight-normal; - padding: 8px; } diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 7baab478034..c201605e83d 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -1,5 +1,5 @@ /** - * Apply Markdown typography + * Apply Markup (Markdown/AsciiDoc) typography * */ .md:not(.use-csslab) { @@ -245,6 +245,21 @@ } } + ul.checklist, + ul.none, + ol.none, + ul.no-bullet, + ol.no-bullet, + ol.unnumbered, + ul.unstyled, + ol.unstyled { + list-style-type: none; + + li { + margin-left: 0; + } + } + li { line-height: 1.6em; margin-left: 25px; @@ -321,6 +336,54 @@ visibility: visible; } } + + .big { + font-size: larger; + } + + .small { + font-size: smaller; + } + + .underline { + text-decoration: underline; + } + + .overline { + text-decoration: overline; + } + + .line-through { + text-decoration: line-through; + } + + .admonitionblock td.icon { + width: 1%; + + [class^='fa icon-'] { + @extend .fa-2x; + } + + .icon-note { + @extend .fa-thumb-tack; + } + + .icon-tip { + @extend .fa-lightbulb-o; + } + + .icon-warning { + @extend .fa-exclamation-triangle; + } + + .icon-caution { + @extend .fa-fire; + } + + .icon-important { + @extend .fa-exclamation-circle; + } + } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 4521643ce08..c108f45622f 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -806,6 +806,7 @@ Modals */ $modal-body-height: 80px; $modal-border-color: #e9ecef; +$modal-border-radius: 0.25rem; $priority-label-empty-state-width: 114px; diff --git a/app/assets/stylesheets/framework/variables_overrides.scss b/app/assets/stylesheets/framework/variables_overrides.scss index ea96381a098..604b48e11ab 100644 --- a/app/assets/stylesheets/framework/variables_overrides.scss +++ b/app/assets/stylesheets/framework/variables_overrides.scss @@ -48,3 +48,7 @@ $spacers: ( 9: ($spacer * 8) ); $pagination-color: $gl-text-color; +$tooltip-padding-y: 0.5rem; +$tooltip-padding-x: 0.75rem; +$tooltip-arrow-height: 0.5rem; +$tooltip-arrow-width: 1rem; diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index e12ea6fcb99..0b0a4e50146 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -167,7 +167,7 @@ min-width: 0; .project-namespace { - color: $gl-text-color-secondary; + color: $gl-text-color-tertiary; } } diff --git a/app/assets/stylesheets/pages/container_registry.scss b/app/assets/stylesheets/pages/container_registry.scss index cca5214a508..a21fa29f34a 100644 --- a/app/assets/stylesheets/pages/container_registry.scss +++ b/app/assets/stylesheets/pages/container_registry.scss @@ -6,6 +6,10 @@ pre { white-space: pre-line; } + + span .btn { + margin: 0; + } } .container-image { diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 3ffe8ae304d..95ea49ad465 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -14,7 +14,7 @@ position: -webkit-sticky; position: sticky; top: $mr-file-header-top; - z-index: 220; + z-index: 120; &::before { content: ''; diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index cff2e274390..1502cf18440 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -412,10 +412,6 @@ table.pipeline-project-metrics tr td { font-size: $gl-font-size-large; } - .item-visibility { - color: $gl-text-color-secondary; - } - @include media-breakpoint-down(md) { .title { font-size: $gl-font-size; diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss index 7610c5cf6f3..ef872e693e0 100644 --- a/app/assets/stylesheets/pages/help.scss +++ b/app/assets/stylesheets/pages/help.scss @@ -38,3 +38,9 @@ .documentation { padding: 7px; } + +.card.links-card { + a { + color: $blue-600; + } +} diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 6a0127eb51c..66b4f3bad2b 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -390,7 +390,7 @@ .block { width: $gutter-collapsed-width - 2px; - padding: 15px 0 0; + padding: 0; border-bottom: 0; overflow: hidden; @@ -427,10 +427,13 @@ } .sidebar-collapsed-icon { - display: block; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; width: 100%; + height: $sidebar-toggle-height; text-align: center; - margin-bottom: 10px; color: $gl-text-color-secondary; svg { @@ -470,6 +473,16 @@ } .btn-clipboard { + /* + This change should be temporary, because the DOM currently gets + generated from a ruby definition in `app/helpers/button_helper.rb`. + As soon as the `copy to clipboard` button will be transfered to + Vue this should be adjusted as well. + */ + flex: 1; + align-self: stretch; + padding: 0; + border: 0; background: transparent; color: $gl-text-color-secondary; @@ -493,7 +506,6 @@ .sidebar-collapsed-user { padding-bottom: 0; - margin-bottom: 10px; .author-link { padding-left: 0; diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 11e8a32389f..7d5e185834b 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -30,6 +30,10 @@ .dropdown-content { max-height: 135px; } + + .dropdown-label-box { + flex: 0 0 auto; + } } .dropdown-new-label { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 3917937f4af..2780afa11fa 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -10,8 +10,8 @@ float: left; } - > *:not(:last-child) { - margin-right: 10px; + > *:not(:first-child) { + margin-left: 10px; } } @@ -69,7 +69,7 @@ content: ''; border-left: 1px solid $gray-200; position: absolute; - left: 32px; + left: 28px; top: -17px; height: 16px; } @@ -114,7 +114,7 @@ padding: $gl-padding; @include media-breakpoint-up(md) { - padding-left: $gl-padding-50; + padding-left: $gl-padding-8 * 7; } } } @@ -264,6 +264,10 @@ .widget-status-icon { align-self: flex-start; + + button { + margin-left: $gl-padding; + } } .mr-widget-body { @@ -271,8 +275,8 @@ @include clearfix; - &.media > *:first-child { - margin-right: 10px; + button { + margin-left: $gl-padding; } .approve-btn { @@ -312,6 +316,7 @@ .bold { font-weight: $gl-font-weight-bold; color: $gl-gray-light; + margin-left: 10px; } .state-label { @@ -377,9 +382,13 @@ &.mr-widget-empty-state { line-height: 20px; + padding: $gl-padding; .artwork { - margin-bottom: $gl-padding; + + @include media-breakpoint-down(md) { + margin-bottom: $gl-padding; + } } .text { @@ -395,7 +404,7 @@ } .mr-widget-help { - padding: 10px 16px 10px $gl-padding-50; + padding: 10px 16px 10px ($gl-padding-8 * 7); font-style: italic; } @@ -913,7 +922,7 @@ .media-body { min-width: 0; font-size: 12px; - margin-left: 48px; + margin-left: 40px; } &:not(:last-child) { diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index aa6bbc8e473..5f4db37c317 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -588,8 +588,8 @@ } .ci-status-icon svg { - height: 20px; - width: 20px; + height: 24px; + width: 24px; } .dropdown-menu-toggle { @@ -695,6 +695,10 @@ top: -1px; } + .spinner { + top: 2px; + } + &.play { svg { left: 2px; @@ -861,6 +865,7 @@ button.mini-pipeline-graph-dropdown-toggle { } } + .spinner, svg { width: $ci-action-dropdown-svg-size; height: $ci-action-dropdown-svg-size; diff --git a/app/assets/stylesheets/pages/prometheus.scss b/app/assets/stylesheets/pages/prometheus.scss index 2d600e3aef6..05a4cc168a8 100644 --- a/app/assets/stylesheets/pages/prometheus.scss +++ b/app/assets/stylesheets/pages/prometheus.scss @@ -29,6 +29,11 @@ padding: $gl-padding / 2; } +.prometheus-graph-embed { + border: 1px solid $border-color; + border-radius: $border-radius-default; +} + .prometheus-graph-header { display: flex; align-items: center; diff --git a/app/assets/stylesheets/pages/reports.scss b/app/assets/stylesheets/pages/reports.scss index 94da72622af..85e9f303dde 100644 --- a/app/assets/stylesheets/pages/reports.scss +++ b/app/assets/stylesheets/pages/reports.scss @@ -57,7 +57,7 @@ .report-block-container { border-top: 1px solid $border-color; - padding: $gl-padding-top; + padding: $gl-padding - 2; background-color: $gray-light; // Clean MR widget CSS @@ -96,17 +96,14 @@ .ci-status-icon { svg { - width: 16px; - height: 16px; - left: -2px; + width: 24px; + height: 24px; } } } .report-block-list-issue { display: flex; - align-items: flex-start; - align-content: flex-start; } .is-dismissed .report-block-list-issue-description, |