diff options
author | Grzegorz Bizon <grzesiek.bizon@gmail.com> | 2017-05-02 10:40:10 +0200 |
---|---|---|
committer | Grzegorz Bizon <grzesiek.bizon@gmail.com> | 2017-05-02 10:40:10 +0200 |
commit | d276ea93728e7972ed5b8275460929128d98fccd (patch) | |
tree | d6c8188cea2d25685f02c7043f7521f8b1f3509e /app/assets | |
parent | c9def85844531ffdd2984707a1bc8cbca18f6742 (diff) | |
parent | 6068b863c66f785bea0a56881d60e8c23da08a0b (diff) | |
download | gitlab-ce-d276ea93728e7972ed5b8275460929128d98fccd.tar.gz |
Merge branch 'master' into feature/gb/manual-actions-protected-branches-permissions
* master: (314 commits)
Better Explore Groups view
Update Carrierwave and fog-core
Add specs for Gitlab::RequestProfiler
Add scripts/static-analysis to run all the static analysers in one go
Shorten and improve some job names
Group static-analysis jobs into a single job
Don't blow up when email has no References header
Update CHANGELOG.md for 9.1.2
Add changelog
Add changelog
Show Raw button as Download for binary files
Use blob viewers for snippets
Fix typo
Fixed transient failure related to dropdown animations
Revert "Merge branch 'tc-no-todo-service-select' into 'master'"
fix link to MR 10416
Another change from .click -> .trigger('click') to make spec pass
Change from .click -> .trigger('click') to make spec pass
Disable AddColumnWithDefaultToLargeTable cop for pre-existing migrations
Add AddColumnWithDefaultToLargeTable cop
...
Conflicts:
spec/requests/api/jobs_spec.rb
Diffstat (limited to 'app/assets')
61 files changed, 1435 insertions, 432 deletions
diff --git a/app/assets/javascripts/blob/blob_fork_suggestion.js b/app/assets/javascripts/blob/blob_fork_suggestion.js index 3baf81905fe..47c431fb809 100644 --- a/app/assets/javascripts/blob/blob_fork_suggestion.js +++ b/app/assets/javascripts/blob/blob_fork_suggestion.js @@ -16,47 +16,44 @@ const defaults = { class BlobForkSuggestion { constructor(options) { this.elementMap = Object.assign({}, defaults, options); - this.onClickWrapper = this.onClick.bind(this); - - document.addEventListener('click', this.onClickWrapper); + this.onOpenButtonClick = this.onOpenButtonClick.bind(this); + this.onCancelButtonClick = this.onCancelButtonClick.bind(this); } - showSuggestionSection(forkPath, action = 'edit') { - [].forEach.call(this.elementMap.suggestionSections, (suggestionSection) => { - suggestionSection.classList.remove('hidden'); - }); + init() { + this.bindEvents(); - [].forEach.call(this.elementMap.forkButtons, (forkButton) => { - forkButton.setAttribute('href', forkPath); - }); + return this; + } - [].forEach.call(this.elementMap.actionTextPieces, (actionTextPiece) => { - // eslint-disable-next-line no-param-reassign - actionTextPiece.textContent = action; - }); + bindEvents() { + $(this.elementMap.openButtons).on('click', this.onOpenButtonClick); + $(this.elementMap.cancelButtons).on('click', this.onCancelButtonClick); } - hideSuggestionSection() { - [].forEach.call(this.elementMap.suggestionSections, (suggestionSection) => { - suggestionSection.classList.add('hidden'); - }); + showSuggestionSection(forkPath, action = 'edit') { + $(this.elementMap.suggestionSections).removeClass('hidden'); + $(this.elementMap.forkButtons).attr('href', forkPath); + $(this.elementMap.actionTextPieces).text(action); } - onClick(e) { - const el = e.target; + hideSuggestionSection() { + $(this.elementMap.suggestionSections).addClass('hidden'); + } - if ([].includes.call(this.elementMap.openButtons, el)) { - const { forkPath, action } = el.dataset; - this.showSuggestionSection(forkPath, action); - } + onOpenButtonClick(e) { + const forkPath = $(e.currentTarget).attr('data-fork-path'); + const action = $(e.currentTarget).attr('data-action'); + this.showSuggestionSection(forkPath, action); + } - if ([].includes.call(this.elementMap.cancelButtons, el)) { - this.hideSuggestionSection(); - } + onCancelButtonClick() { + this.hideSuggestionSection(); } destroy() { - document.removeEventListener('click', this.onClickWrapper); + $(this.elementMap.openButtons).off('click', this.onOpenButtonClick); + $(this.elementMap.cancelButtons).off('click', this.onCancelButtonClick); } } diff --git a/app/assets/javascripts/blob/notebook/index.js b/app/assets/javascripts/blob/notebook/index.js index 9b8bfbfc8c0..36fe8a7184f 100644 --- a/app/assets/javascripts/blob/notebook/index.js +++ b/app/assets/javascripts/blob/notebook/index.js @@ -1,10 +1,9 @@ /* eslint-disable no-new */ import Vue from 'vue'; import VueResource from 'vue-resource'; -import NotebookLab from 'vendor/notebooklab'; +import notebookLab from '../../notebook/index.vue'; Vue.use(VueResource); -Vue.use(NotebookLab); export default () => { const el = document.getElementById('js-notebook-viewer'); @@ -19,6 +18,9 @@ export default () => { json: {}, }; }, + components: { + notebookLab, + }, template: ` <div class="container-fluid md prepend-top-default append-bottom-default"> <div diff --git a/app/assets/javascripts/blob/pdf/index.js b/app/assets/javascripts/blob/pdf/index.js index a74c2db9a61..0ed915c1ac9 100644 --- a/app/assets/javascripts/blob/pdf/index.js +++ b/app/assets/javascripts/blob/pdf/index.js @@ -1,11 +1,6 @@ /* eslint-disable no-new */ import Vue from 'vue'; -import PDFLab from 'vendor/pdflab'; -import workerSrc from 'vendor/pdf.worker'; - -Vue.use(PDFLab, { - workerSrc, -}); +import pdfLab from '../../pdf/index.vue'; export default () => { const el = document.getElementById('js-pdf-viewer'); @@ -20,6 +15,9 @@ export default () => { pdf: el.dataset.endpoint, }; }, + components: { + pdfLab, + }, methods: { onLoad() { this.loading = false; @@ -31,7 +29,7 @@ export default () => { }, }, template: ` - <div class="container-fluid md prepend-top-default append-bottom-default"> + <div class="js-pdf-viewer container-fluid md prepend-top-default append-bottom-default"> <div class="text-center loading" v-if="loading && !error"> diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js new file mode 100644 index 00000000000..07d67d49aa5 --- /dev/null +++ b/app/assets/javascripts/blob/viewer/index.js @@ -0,0 +1,120 @@ +/* global Flash */ +export default class BlobViewer { + constructor() { + this.switcher = document.querySelector('.js-blob-viewer-switcher'); + this.switcherBtns = document.querySelectorAll('.js-blob-viewer-switch-btn'); + this.copySourceBtn = document.querySelector('.js-copy-blob-source-btn'); + this.simpleViewer = document.querySelector('.blob-viewer[data-type="simple"]'); + this.richViewer = document.querySelector('.blob-viewer[data-type="rich"]'); + this.$fileHolder = $('.file-holder'); + + let initialViewerName = document.querySelector('.blob-viewer:not(.hidden)').getAttribute('data-type'); + + this.initBindings(); + + if (this.switcher && location.hash.indexOf('#L') === 0) { + initialViewerName = 'simple'; + } + + this.switchToViewer(initialViewerName); + } + + initBindings() { + if (this.switcherBtns.length) { + Array.from(this.switcherBtns) + .forEach((el) => { + el.addEventListener('click', this.switchViewHandler.bind(this)); + }); + } + + if (this.copySourceBtn) { + this.copySourceBtn.addEventListener('click', () => { + if (this.copySourceBtn.classList.contains('disabled')) return; + + this.switchToViewer('simple'); + }); + } + } + + switchViewHandler(e) { + const target = e.currentTarget; + + e.preventDefault(); + + this.switchToViewer(target.getAttribute('data-viewer')); + } + + toggleCopyButtonState() { + if (!this.copySourceBtn) return; + + if (this.simpleViewer.getAttribute('data-loaded')) { + this.copySourceBtn.setAttribute('title', 'Copy source to clipboard'); + this.copySourceBtn.classList.remove('disabled'); + } else if (this.activeViewer === this.simpleViewer) { + this.copySourceBtn.setAttribute('title', 'Wait for the source to load to copy it to the clipboard'); + this.copySourceBtn.classList.add('disabled'); + } else { + this.copySourceBtn.setAttribute('title', 'Switch to the source to copy it to the clipboard'); + this.copySourceBtn.classList.add('disabled'); + } + + $(this.copySourceBtn).tooltip('fixTitle'); + } + + loadViewer(viewerParam) { + const viewer = viewerParam; + const url = viewer.getAttribute('data-url'); + + if (!url || viewer.getAttribute('data-loaded') || viewer.getAttribute('data-loading')) { + return; + } + + viewer.setAttribute('data-loading', 'true'); + + $.ajax({ + url, + dataType: 'JSON', + }) + .fail(() => new Flash('Error loading source view')) + .done((data) => { + viewer.innerHTML = data.html; + $(viewer).syntaxHighlight(); + + viewer.setAttribute('data-loaded', 'true'); + + this.$fileHolder.trigger('highlight:line'); + + this.toggleCopyButtonState(); + }); + } + + switchToViewer(name) { + const newViewer = document.querySelector(`.blob-viewer[data-type='${name}']`); + if (this.activeViewer === newViewer) return; + + const oldButton = document.querySelector('.js-blob-viewer-switch-btn.active'); + const newButton = document.querySelector(`.js-blob-viewer-switch-btn[data-viewer='${name}']`); + const oldViewer = document.querySelector(`.blob-viewer:not([data-type='${name}'])`); + + if (oldButton) { + oldButton.classList.remove('active'); + } + + if (newButton) { + newButton.classList.add('active'); + newButton.blur(); + } + + if (oldViewer) { + oldViewer.classList.add('hidden'); + } + + newViewer.classList.remove('hidden'); + + this.activeViewer = newViewer; + + this.toggleCopyButtonState(); + + this.loadViewer(newViewer); + } +} diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js index 68a1c1de1df..e704be8b53e 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js @@ -106,15 +106,6 @@ export default Vue.component('pipelines-table', { eventHub.$on('refreshPipelines', this.fetchPipelines); }, - beforeUpdate() { - if (this.state.pipelines.length && - this.$children && - !this.isMakingRequest && - !this.isLoading) { - this.store.startTimeAgoLoops.call(this, Vue); - } - }, - beforeDestroyed() { eventHub.$off('refreshPipelines'); }, diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 20db2698ba8..0bdce52cc89 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -44,10 +44,12 @@ import GroupsList from './groups_list'; import ProjectsList from './projects_list'; import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater'; +import Landing from './landing'; import BlobForkSuggestion from './blob/blob_fork_suggestion'; import UserCallout from './user_callout'; import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags'; import ShortcutsWiki from './shortcuts_wiki'; +import BlobViewer from './blob/viewer/index'; const ShortcutsBlob = require('./shortcuts_blob'); @@ -97,7 +99,8 @@ const ShortcutsBlob = require('./shortcuts_blob'); cancelButtons: document.querySelectorAll('.js-cancel-fork-suggestion-button'), suggestionSections: document.querySelectorAll('.js-file-fork-suggestion-section'), actionTextPieces: document.querySelectorAll('.js-file-fork-suggestion-section-action'), - }); + }) + .init(); } switch (page) { @@ -146,8 +149,19 @@ const ShortcutsBlob = require('./shortcuts_blob'); new ProjectsList(); break; case 'dashboard:groups:index': + new GroupsList(); + break; case 'explore:groups:index': new GroupsList(); + + const landingElement = document.querySelector('.js-explore-groups-landing'); + if (!landingElement) break; + const exploreGroupsLanding = new Landing( + landingElement, + landingElement.querySelector('.dismiss-button'), + 'explore_groups_landing_dismissed', + ); + exploreGroupsLanding.toggle(); break; case 'projects:milestones:new': case 'projects:milestones:edit': @@ -298,6 +312,7 @@ const ShortcutsBlob = require('./shortcuts_blob'); gl.TargetBranchDropDown.bootstrap(); break; case 'projects:blob:show': + new BlobViewer(); gl.TargetBranchDropDown.bootstrap(); initBlob(); break; @@ -353,6 +368,10 @@ const ShortcutsBlob = require('./shortcuts_blob'); case 'users:show': new UserCallout(); break; + case 'snippets:show': + new LineHighlighter(); + new BlobViewer(); + break; } switch (path.first()) { case 'sessions': @@ -431,6 +450,8 @@ const ShortcutsBlob = require('./shortcuts_blob'); shortcut_handler = new ShortcutsNavigation(); if (path[2] === 'show') { new ZenMode(); + new LineHighlighter(); + new BlobViewer(); } break; case 'labels': diff --git a/app/assets/javascripts/environments/components/environment.js b/app/assets/javascripts/environments/components/environment.vue index f7175e412da..f319d6ca0c8 100644 --- a/app/assets/javascripts/environments/components/environment.js +++ b/app/assets/javascripts/environments/components/environment.vue @@ -1,6 +1,7 @@ +<script> + /* eslint-disable no-new */ /* global Flash */ -import Vue from 'vue'; import EnvironmentsService from '../services/environments_service'; import EnvironmentTable from './environments_table.vue'; import EnvironmentsStore from '../stores/environments_store'; @@ -8,7 +9,7 @@ import TablePaginationComponent from '../../vue_shared/components/table_paginati import '../../lib/utils/common_utils'; import eventHub from '../event_hub'; -export default Vue.component('environment-component', { +export default { components: { 'environment-table': EnvironmentTable, @@ -140,76 +141,90 @@ export default Vue.component('environment-component', { }); }, }, - - template: ` - <div :class="cssContainerClass"> - <div class="top-area"> - <ul v-if="!isLoading" class="nav-links"> - <li v-bind:class="{ 'active': scope === null || scope === 'available' }"> - <a :href="projectEnvironmentsPath"> - Available - <span class="badge js-available-environments-count"> - {{state.availableCounter}} - </span> - </a> - </li> - <li v-bind:class="{ 'active' : scope === 'stopped' }"> - <a :href="projectStoppedEnvironmentsPath"> - Stopped - <span class="badge js-stopped-environments-count"> - {{state.stoppedCounter}} - </span> - </a> - </li> - </ul> - <div v-if="canCreateEnvironmentParsed && !isLoading" class="nav-controls"> - <a :href="newEnvironmentPath" class="btn btn-create"> - New environment +}; +</script> +<template> + <div :class="cssContainerClass"> + <div class="top-area"> + <ul + v-if="!isLoading" + class="nav-links"> + <li :class="{ active: scope === null || scope === 'available' }"> + <a :href="projectEnvironmentsPath"> + Available + <span class="badge js-available-environments-count"> + {{state.availableCounter}} + </span> + </a> + </li> + <li :class="{ active : scope === 'stopped' }"> + <a :href="projectStoppedEnvironmentsPath"> + Stopped + <span class="badge js-stopped-environments-count"> + {{state.stoppedCounter}} + </span> </a> - </div> + </li> + </ul> + <div + v-if="canCreateEnvironmentParsed && !isLoading" + class="nav-controls"> + <a + :href="newEnvironmentPath" + class="btn btn-create"> + New environment + </a> </div> + </div> + + <div class="content-list environments-container"> + <div + class="environments-list-loading text-center" + v-if="isLoading"> - <div class="content-list environments-container"> - <div class="environments-list-loading text-center" v-if="isLoading"> - <i class="fa fa-spinner fa-spin" aria-hidden="true"></i> - </div> - - <div class="blank-state blank-state-no-icon" - v-if="!isLoading && state.environments.length === 0"> - <h2 class="blank-state-title js-blank-state-title"> - You don't have any environments right now. - </h2> - <p class="blank-state-text"> - Environments are places where code gets deployed, such as staging or production. - <br /> - <a :href="helpPagePath"> - Read more about environments - </a> - </p> - - <a v-if="canCreateEnvironmentParsed" - :href="newEnvironmentPath" - class="btn btn-create js-new-environment-button"> - New Environment + <i + class="fa fa-spinner fa-spin" + aria-hidden="true" /> + </div> + + <div + class="blank-state blank-state-no-icon" + v-if="!isLoading && state.environments.length === 0"> + <h2 class="blank-state-title js-blank-state-title"> + You don't have any environments right now. + </h2> + <p class="blank-state-text"> + Environments are places where code gets deployed, such as staging or production. + <br /> + <a :href="helpPagePath"> + Read more about environments </a> - </div> - - <div class="table-holder" - v-if="!isLoading && state.environments.length > 0"> - - <environment-table - :environments="state.environments" - :can-create-deployment="canCreateDeploymentParsed" - :can-read-environment="canReadEnvironmentParsed" - :service="service" - :is-loading-folder-content="isLoadingFolderContent" /> - </div> - - <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1" - :change="changePage" - :pageInfo="state.paginationInformation"> - </table-pagination> + </p> + + <a + v-if="canCreateEnvironmentParsed" + :href="newEnvironmentPath" + class="btn btn-create js-new-environment-button"> + New Environment + </a> </div> + + <div + class="table-holder" + v-if="!isLoading && state.environments.length > 0"> + + <environment-table + :environments="state.environments" + :can-create-deployment="canCreateDeploymentParsed" + :can-read-environment="canReadEnvironmentParsed" + :service="service" + :is-loading-folder-content="isLoadingFolderContent" /> + </div> + + <table-pagination + v-if="state.paginationInformation && state.paginationInformation.totalPages > 1" + :change="changePage" + :pageInfo="state.paginationInformation" /> </div> - `, -}); + </div> +</template> diff --git a/app/assets/javascripts/environments/environments_bundle.js b/app/assets/javascripts/environments/environments_bundle.js index 8d963b335cf..c0662125f28 100644 --- a/app/assets/javascripts/environments/environments_bundle.js +++ b/app/assets/javascripts/environments/environments_bundle.js @@ -1,13 +1,10 @@ -import EnvironmentsComponent from './components/environment'; +import Vue from 'vue'; +import EnvironmentsComponent from './components/environment.vue'; -$(() => { - window.gl = window.gl || {}; - - if (gl.EnvironmentsListApp) { - gl.EnvironmentsListApp.$destroy(true); - } - - gl.EnvironmentsListApp = new EnvironmentsComponent({ - el: document.querySelector('#environments-list-view'), - }); -}); +document.addEventListener('DOMContentLoaded', () => new Vue({ + el: '#environments-list-view', + components: { + 'environments-table-app': EnvironmentsComponent, + }, + render: createElement => createElement('environments-table-app'), +})); diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js index f939eccf246..9add8c3d721 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js +++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js @@ -1,13 +1,10 @@ -import EnvironmentsFolderComponent from './environments_folder_view'; +import Vue from 'vue'; +import EnvironmentsFolderComponent from './environments_folder_view.vue'; -$(() => { - window.gl = window.gl || {}; - - if (gl.EnvironmentsListFolderApp) { - gl.EnvironmentsListFolderApp.$destroy(true); - } - - gl.EnvironmentsListFolderApp = new EnvironmentsFolderComponent({ - el: document.querySelector('#environments-folder-list-view'), - }); -}); +document.addEventListener('DOMContentLoaded', () => new Vue({ + el: '#environments-folder-list-view', + components: { + 'environments-folder-app': EnvironmentsFolderComponent, + }, + render: createElement => createElement('environments-folder-app'), +})); diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.js b/app/assets/javascripts/environments/folder/environments_folder_view.vue index 05d44f77d1d..d27b2acfcdf 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_view.js +++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue @@ -1,6 +1,6 @@ +<script> /* eslint-disable no-new */ /* global Flash */ -import Vue from 'vue'; import EnvironmentsService from '../services/environments_service'; import EnvironmentTable from '../components/environments_table.vue'; import EnvironmentsStore from '../stores/environments_store'; @@ -8,7 +8,7 @@ import TablePaginationComponent from '../../vue_shared/components/table_paginati import '../../lib/utils/common_utils'; import '../../vue_shared/vue_resource_interceptor'; -export default Vue.component('environment-folder-view', { +export default { components: { 'environment-table': EnvironmentTable, 'table-pagination': TablePaginationComponent, @@ -116,54 +116,66 @@ export default Vue.component('environment-folder-view', { return param; }, }, +}; +</script> +<template> + <div :class="cssContainerClass"> + <div + class="top-area" + v-if="!isLoading"> + + <h4 class="js-folder-name environments-folder-name"> + Environments / <b>{{folderName}}</b> + </h4> + + <ul class="nav-links"> + <li :class="{ active: scope === null || scope === 'available' }"> + <a + :href="availablePath" + class="js-available-environments-folder-tab"> + Available + <span class="badge js-available-environments-count"> + {{state.availableCounter}} + </span> + </a> + </li> + <li :class="{ active : scope === 'stopped' }"> + <a + :href="stoppedPath" + class="js-stopped-environments-folder-tab"> + Stopped + <span class="badge js-stopped-environments-count"> + {{state.stoppedCounter}} + </span> + </a> + </li> + </ul> + </div> - template: ` - <div :class="cssContainerClass"> - <div class="top-area" v-if="!isLoading"> - - <h4 class="js-folder-name environments-folder-name"> - Environments / <b>{{folderName}}</b> - </h4> - - <ul class="nav-links"> - <li v-bind:class="{ 'active': scope === null || scope === 'available' }"> - <a :href="availablePath" class="js-available-environments-folder-tab"> - Available - <span class="badge js-available-environments-count"> - {{state.availableCounter}} - </span> - </a> - </li> - <li v-bind:class="{ 'active' : scope === 'stopped' }"> - <a :href="stoppedPath" class="js-stopped-environments-folder-tab"> - Stopped - <span class="badge js-stopped-environments-count"> - {{state.stoppedCounter}} - </span> - </a> - </li> - </ul> + <div class="environments-container"> + <div + class="environments-list-loading text-center" + v-if="isLoading"> + <i + class="fa fa-spinner fa-spin" + aria-hidden="true"/> </div> - <div class="environments-container"> - <div class="environments-list-loading text-center" v-if="isLoading"> - <i class="fa fa-spinner fa-spin"></i> - </div> - - <div class="table-holder" - v-if="!isLoading && state.environments.length > 0"> + <div + class="table-holder" + v-if="!isLoading && state.environments.length > 0"> - <environment-table - :environments="state.environments" - :can-create-deployment="canCreateDeploymentParsed" - :can-read-environment="canReadEnvironmentParsed" - :service="service"/> + <environment-table + :environments="state.environments" + :can-create-deployment="canCreateDeploymentParsed" + :can-read-environment="canReadEnvironmentParsed" + :service="service"/> - <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1" - :change="changePage" - :pageInfo="state.paginationInformation"/> - </div> + <table-pagination + v-if="state.paginationInformation && state.paginationInformation.totalPages > 1" + :change="changePage" + :pageInfo="state.paginationInformation"/> </div> </div> - `, -}); + </div> +</template> diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 68a832102a0..36af0674ac6 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -77,13 +77,14 @@ class FilteredSearchManager { this.checkForEnterWrapper = this.checkForEnter.bind(this); this.onClearSearchWrapper = this.onClearSearch.bind(this); this.checkForBackspaceWrapper = this.checkForBackspace.bind(this); - this.removeSelectedTokenWrapper = this.removeSelectedToken.bind(this); + this.removeSelectedTokenKeydownWrapper = this.removeSelectedTokenKeydown.bind(this); this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this); this.editTokenWrapper = this.editToken.bind(this); this.tokenChange = this.tokenChange.bind(this); this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this); this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this); this.onrecentSearchesItemSelectedWrapper = this.onrecentSearchesItemSelected.bind(this); + this.removeTokenWrapper = this.removeToken.bind(this); this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit); this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper); @@ -96,12 +97,13 @@ class FilteredSearchManager { this.filteredSearchInput.addEventListener('keyup', this.tokenChange); this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper); this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken); + this.tokensContainer.addEventListener('click', this.removeTokenWrapper); this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper); this.clearSearchButton.addEventListener('click', this.onClearSearchWrapper); document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens); document.addEventListener('click', this.unselectEditTokensWrapper); document.addEventListener('click', this.removeInputContainerFocusWrapper); - document.addEventListener('keydown', this.removeSelectedTokenWrapper); + document.addEventListener('keydown', this.removeSelectedTokenKeydownWrapper); eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper); } @@ -117,12 +119,13 @@ class FilteredSearchManager { this.filteredSearchInput.removeEventListener('keyup', this.tokenChange); this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper); this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken); + this.tokensContainer.removeEventListener('click', this.removeTokenWrapper); this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper); this.clearSearchButton.removeEventListener('click', this.onClearSearchWrapper); document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens); document.removeEventListener('click', this.unselectEditTokensWrapper); document.removeEventListener('click', this.removeInputContainerFocusWrapper); - document.removeEventListener('keydown', this.removeSelectedTokenWrapper); + document.removeEventListener('keydown', this.removeSelectedTokenKeydownWrapper); eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper); } @@ -195,14 +198,28 @@ class FilteredSearchManager { static selectToken(e) { const button = e.target.closest('.selectable'); + const removeButtonSelected = e.target.closest('.remove-token'); - if (button) { + if (!removeButtonSelected && button) { e.preventDefault(); e.stopPropagation(); gl.FilteredSearchVisualTokens.selectToken(button); } } + removeToken(e) { + const removeButtonSelected = e.target.closest('.remove-token'); + + if (removeButtonSelected) { + e.preventDefault(); + e.stopPropagation(); + + const button = e.target.closest('.selectable'); + gl.FilteredSearchVisualTokens.selectToken(button, true); + this.removeSelectedToken(); + } + } + unselectEditTokens(e) { const inputContainer = this.container.querySelector('.filtered-search-box'); const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target); @@ -248,16 +265,21 @@ class FilteredSearchManager { } } - removeSelectedToken(e) { + removeSelectedTokenKeydown(e) { // 8 = Backspace Key // 46 = Delete Key if (e.keyCode === 8 || e.keyCode === 46) { - gl.FilteredSearchVisualTokens.removeSelectedToken(); - this.handleInputPlaceholder(); - this.toggleClearSearchButton(); + this.removeSelectedToken(); } } + removeSelectedToken() { + gl.FilteredSearchVisualTokens.removeSelectedToken(); + this.handleInputPlaceholder(); + this.toggleClearSearchButton(); + this.dropdownManager.updateCurrentDropdownOffset(); + } + onClearSearch(e) { e.preventDefault(); this.clearSearch(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js index a5657fc8720..453ecccc6fc 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -16,11 +16,11 @@ class FilteredSearchVisualTokens { [].forEach.call(otherTokens, t => t.classList.remove('selected')); } - static selectToken(tokenButton) { + static selectToken(tokenButton, forceSelection = false) { const selected = tokenButton.classList.contains('selected'); FilteredSearchVisualTokens.unselectTokens(); - if (!selected) { + if (!selected || forceSelection) { tokenButton.classList.add('selected'); } } @@ -38,7 +38,12 @@ class FilteredSearchVisualTokens { return ` <div class="selectable" role="button"> <div class="name"></div> - <div class="value"></div> + <div class="value-container"> + <div class="value"></div> + <div class="remove-token" role="button"> + <i class="fa fa-close"></i> + </div> + </div> </div> `; } @@ -122,7 +127,8 @@ class FilteredSearchVisualTokens { if (value) { const button = lastVisualToken.querySelector('.selectable'); - button.removeChild(value); + const valueContainer = lastVisualToken.querySelector('.value-container'); + button.removeChild(valueContainer); lastVisualToken.innerHTML = button.innerHTML; } else { lastVisualToken.closest('.tokens-container').removeChild(lastVisualToken); diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index b62b2cec4d8..687a462a0d4 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -3,6 +3,7 @@ import emojiMap from 'emojis/digests.json'; import emojiAliases from 'emojis/aliases.json'; import { glEmojiTag } from '~/behaviors/gl_emoji'; +import glRegexp from '~/lib/utils/regexp'; // Creates the variables for setting up GFM auto-completion window.gl = window.gl || {}; @@ -127,7 +128,15 @@ window.gl.GfmAutoComplete = { callbacks: { sorter: this.DefaultOptions.sorter, beforeInsert: this.DefaultOptions.beforeInsert, - filter: this.DefaultOptions.filter + filter: this.DefaultOptions.filter, + + matcher: (flag, subtext) => { + const relevantText = subtext.trim().split(/\s/).pop(); + const regexp = new RegExp(`(?:[^${glRegexp.unicodeLetters}0-9:]|\n|^):([^:]*)$`, 'gi'); + const match = regexp.exec(relevantText); + + return match && match.length ? match[1] : null; + } } }); // Team Members diff --git a/app/assets/javascripts/landing.js b/app/assets/javascripts/landing.js new file mode 100644 index 00000000000..8c0950ad5d5 --- /dev/null +++ b/app/assets/javascripts/landing.js @@ -0,0 +1,37 @@ +import Cookies from 'js-cookie'; + +class Landing { + constructor(landingElement, dismissButton, cookieName) { + this.landingElement = landingElement; + this.cookieName = cookieName; + this.dismissButton = dismissButton; + this.eventWrapper = {}; + } + + toggle() { + const isDismissed = this.isDismissed(); + + this.landingElement.classList.toggle('hidden', isDismissed); + if (!isDismissed) this.addEvents(); + } + + addEvents() { + this.eventWrapper.dismissLanding = this.dismissLanding.bind(this); + this.dismissButton.addEventListener('click', this.eventWrapper.dismissLanding); + } + + removeEvents() { + this.dismissButton.removeEventListener('click', this.eventWrapper.dismissLanding); + } + + dismissLanding() { + this.landingElement.classList.add('hidden'); + Cookies.set(this.cookieName, 'true', { expires: 365 }); + } + + isDismissed() { + return Cookies.get(this.cookieName) === 'true'; + } +} + +export default Landing; diff --git a/app/assets/javascripts/lib/utils/regexp.js b/app/assets/javascripts/lib/utils/regexp.js new file mode 100644 index 00000000000..baa0b51d59b --- /dev/null +++ b/app/assets/javascripts/lib/utils/regexp.js @@ -0,0 +1,10 @@ +/** + * Regexp utility for the convenience of working with regular expressions. + * + */ + +// Inspired by https://github.com/mishoo/UglifyJS/blob/2bc1d02363db3798d5df41fb5059a19edca9b7eb/lib/parse-js.js#L203 +// Unicode 6.1 +const unicodeLetters = '\\u0041-\\u005A\\u0061-\\u007A\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376\\u0377\\u037A-\\u037D\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u0527\\u0531-\\u0556\\u0559\\u0561-\\u0587\\u05D0-\\u05EA\\u05F0-\\u05F2\\u0620-\\u064A\\u066E\\u066F\\u0671-\\u06D3\\u06D5\\u06E5\\u06E6\\u06EE\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824\\u0828\\u0840-\\u0858\\u08A0\\u08A2-\\u08AC\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971-\\u0977\\u0979-\\u097F\\u0985-\\u098C\\u098F\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC\\u09DD\\u09DF-\\u09E1\\u09F0\\u09F1\\u0A05-\\u0A0A\\u0A0F\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32\\u0A33\\u0A35\\u0A36\\u0A38\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0\\u0AE1\\u0B05-\\u0B0C\\u0B0F\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99\\u0B9A\\u0B9C\\u0B9E\\u0B9F\\u0BA3\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C33\\u0C35-\\u0C39\\u0C3D\\u0C58\\u0C59\\u0C60\\u0C61\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDE\\u0CE0\\u0CE1\\u0CF1\\u0CF2\\u0D05-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D60\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32\\u0E33\\u0E40-\\u0E46\\u0E81\\u0E82\\u0E84\\u0E87\\u0E88\\u0E8A\\u0E8D\\u0E94-\\u0E97\\u0E99-\\u0E9F\\u0EA1-\\u0EA3\\u0EA5\\u0EA7\\u0EAA\\u0EAB\\u0EAD-\\u0EB0\\u0EB2\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F4\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16EE-\\u16F0\\u1700-\\u170C\\u170E-\\u1711\\u1720-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1877\\u1880-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191C\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19C1-\\u19C7\\u1A00-\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4B\\u1B83-\\u1BA0\\u1BAE\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1CE9-\\u1CEC\\u1CEE-\\u1CF1\\u1CF5\\u1CF6\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2160-\\u2188\\u2C00-\\u2C2E\\u2C30-\\u2C5E\\u2C60-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005-\\u3007\\u3021-\\u3029\\u3031-\\u3035\\u3038-\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312D\\u3131-\\u318E\\u31A0-\\u31BA\\u31F0-\\u31FF\\u3400-\\u4DB5\\u4E00-\\u9FCC\\uA000-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A\\uA62B\\uA640-\\uA66E\\uA67F-\\uA697\\uA6A0-\\uA6EF\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA78E\\uA790-\\uA793\\uA7A0-\\uA7AA\\uA7F8-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA90A-\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA80-\\uAAAF\\uAAB1\\uAAB5\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uABC0-\\uABE2\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40\\uFB41\\uFB43\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC'; + +export default { unicodeLetters }; diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js index 1821ca18053..3ac6dedf131 100644 --- a/app/assets/javascripts/line_highlighter.js +++ b/app/assets/javascripts/line_highlighter.js @@ -41,7 +41,6 @@ require('vendor/jquery.scrollTo'); LineHighlighter.prototype._hash = ''; function LineHighlighter(hash) { - var range; if (hash == null) { // Initialize a LineHighlighter object // @@ -51,10 +50,22 @@ require('vendor/jquery.scrollTo'); this.setHash = bind(this.setHash, this); this.highlightLine = bind(this.highlightLine, this); this.clickHandler = bind(this.clickHandler, this); + this.highlightHash = this.highlightHash.bind(this); this._hash = hash; this.bindEvents(); - if (hash !== '') { - range = this.hashToRange(hash); + this.highlightHash(); + } + + LineHighlighter.prototype.bindEvents = function() { + const $fileHolder = $('.file-holder'); + $fileHolder.on('click', 'a[data-line-number]', this.clickHandler); + $fileHolder.on('highlight:line', this.highlightHash); + }; + + LineHighlighter.prototype.highlightHash = function() { + var range; + if (this._hash !== '') { + range = this.hashToRange(this._hash); if (range[0]) { this.highlightRange(range); $.scrollTo("#L" + range[0], { @@ -64,10 +75,6 @@ require('vendor/jquery.scrollTo'); }); } } - } - - LineHighlighter.prototype.bindEvents = function() { - $('#blob-content-holder').on('click', 'a[data-line-number]', this.clickHandler); }; LineHighlighter.prototype.clickHandler = function(event) { diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index f7f6a773036..93c30c54a8e 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -5,6 +5,7 @@ import Cookies from 'js-cookie'; import './breakpoints'; import './flash'; +import BlobForkSuggestion from './blob/blob_fork_suggestion'; /* eslint-disable max-len */ // MergeRequestTabs @@ -266,6 +267,17 @@ import './flash'; new gl.Diff(); this.scrollToElement('#diffs'); + + $('.diff-file').each((i, el) => { + new BlobForkSuggestion({ + openButtons: $(el).find('.js-edit-blob-link-fork-toggler'), + forkButtons: $(el).find('.js-fork-suggestion-button'), + cancelButtons: $(el).find('.js-cancel-fork-suggestion-button'), + suggestionSections: $(el).find('.js-file-fork-suggestion-section'), + actionTextPieces: $(el).find('.js-file-fork-suggestion-section-action'), + }) + .init(); + }); }, }); } diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js b/app/assets/javascripts/mini_pipeline_graph_dropdown.js index 9c58c465001..64c1447f427 100644 --- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js +++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js @@ -28,7 +28,9 @@ export default class MiniPipelineGraph { * All dropdown events are fired at the .dropdown-menu's parent element. */ bindEvents() { - $(document).off('shown.bs.dropdown', this.container).on('shown.bs.dropdown', this.container, this.getBuildsList); + $(document) + .off('shown.bs.dropdown', this.container) + .on('shown.bs.dropdown', this.container, this.getBuildsList); } /** @@ -91,6 +93,9 @@ export default class MiniPipelineGraph { }, error: () => { this.toggleLoading(button); + if ($(button).parent().hasClass('open')) { + $(button).dropdown('toggle'); + } new Flash('An error occurred while fetching the builds.', 'alert'); }, }); diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js index aff507abb91..78bb0e6fb47 100644 --- a/app/assets/javascripts/monitoring/prometheus_graph.js +++ b/app/assets/javascripts/monitoring/prometheus_graph.js @@ -22,6 +22,7 @@ class PrometheusGraph { const hasMetrics = $prometheusContainer.data('has-metrics'); this.docLink = $prometheusContainer.data('doc-link'); this.integrationLink = $prometheusContainer.data('prometheus-integration'); + this.state = ''; $(document).ajaxError(() => {}); @@ -38,8 +39,9 @@ class PrometheusGraph { this.configureGraph(); this.init(); } else { + const prevState = this.state; this.state = '.js-getting-started'; - this.updateState(); + this.updateState(prevState); } } @@ -53,26 +55,26 @@ class PrometheusGraph { } init() { - this.getData().then((metricsResponse) => { + return this.getData().then((metricsResponse) => { let enoughData = true; - Object.keys(metricsResponse.metrics).forEach((key) => { - let currentKey; - if (key === 'cpu_values' || key === 'memory_values') { - currentKey = metricsResponse.metrics[key]; - if (Object.keys(currentKey).length === 0) { - enoughData = false; - } - } - }); - if (!enoughData) { - this.state = '.js-loading'; - this.updateState(); + if (typeof metricsResponse === 'undefined') { + enoughData = false; } else { + Object.keys(metricsResponse.metrics).forEach((key) => { + if (key === 'cpu_values' || key === 'memory_values') { + const currentData = (metricsResponse.metrics[key])[0]; + if (currentData.values.length <= 2) { + enoughData = false; + } + } + }); + } + if (enoughData) { + $(prometheusStatesContainer).hide(); + $(prometheusParentGraphContainer).show(); this.transformData(metricsResponse); this.createGraph(); } - }).catch(() => { - new Flash('An error occurred when trying to load metrics. Please try again.'); }); } @@ -342,6 +344,8 @@ class PrometheusGraph { getData() { const maxNumberOfRequests = 3; + this.state = '.js-loading'; + this.updateState(); return gl.utils.backOff((next, stop) => { $.ajax({ url: metricsEndpoint, @@ -352,12 +356,11 @@ class PrometheusGraph { this.backOffRequestCounter = this.backOffRequestCounter += 1; if (this.backOffRequestCounter < maxNumberOfRequests) { next(); - } else { - stop({ - status: resp.status, - metrics: data, - }); + } else if (this.backOffRequestCounter >= maxNumberOfRequests) { + stop(new Error('loading')); } + } else if (!data.success) { + stop(new Error('loading')); } else { stop({ status: resp.status, @@ -373,8 +376,9 @@ class PrometheusGraph { return resp.metrics; }) .catch(() => { + const prevState = this.state; this.state = '.js-unable-to-connect'; - this.updateState(); + this.updateState(prevState); }); } @@ -382,19 +386,20 @@ class PrometheusGraph { Object.keys(metricsResponse.metrics).forEach((key) => { if (key === 'cpu_values' || key === 'memory_values') { const metricValues = (metricsResponse.metrics[key])[0]; - if (metricValues !== undefined) { - this.graphSpecificProperties[key].data = metricValues.values.map(metric => ({ - time: new Date(metric[0] * 1000), - value: metric[1], - })); - } + this.graphSpecificProperties[key].data = metricValues.values.map(metric => ({ + time: new Date(metric[0] * 1000), + value: metric[1], + })); } }); } - updateState() { + updateState(prevState) { const $statesContainer = $(prometheusStatesContainer); $(prometheusParentGraphContainer).hide(); + if (prevState) { + $(`${prevState}`, $statesContainer).addClass('hidden'); + } $(`${this.state}`, $statesContainer).removeClass('hidden'); $(prometheusStatesContainer).show(); } diff --git a/app/assets/javascripts/notebook/cells/code.vue b/app/assets/javascripts/notebook/cells/code.vue new file mode 100644 index 00000000000..b8a16356576 --- /dev/null +++ b/app/assets/javascripts/notebook/cells/code.vue @@ -0,0 +1,58 @@ +<template> + <div class="cell"> + <code-cell + type="input" + :raw-code="rawInputCode" + :count="cell.execution_count" + :code-css-class="codeCssClass" /> + <output-cell + v-if="hasOutput" + :count="cell.execution_count" + :output="output" + :code-css-class="codeCssClass" /> + </div> +</template> + +<script> +import CodeCell from './code/index.vue'; +import OutputCell from './output/index.vue'; + +export default { + components: { + 'code-cell': CodeCell, + 'output-cell': OutputCell, + }, + props: { + cell: { + type: Object, + required: true, + }, + codeCssClass: { + type: String, + required: false, + default: '', + }, + }, + computed: { + rawInputCode() { + if (this.cell.source) { + return this.cell.source.join(''); + } + + return ''; + }, + hasOutput() { + return this.cell.outputs.length; + }, + output() { + return this.cell.outputs[0]; + }, + }, +}; +</script> + +<style scoped> +.cell { + flex-direction: column; +} +</style> diff --git a/app/assets/javascripts/notebook/cells/code/index.vue b/app/assets/javascripts/notebook/cells/code/index.vue new file mode 100644 index 00000000000..31b30f601e2 --- /dev/null +++ b/app/assets/javascripts/notebook/cells/code/index.vue @@ -0,0 +1,57 @@ +<template> + <div :class="type"> + <prompt + :type="promptType" + :count="count" /> + <pre + class="language-python" + :class="codeCssClass" + ref="code" + v-text="code"> + </pre> + </div> +</template> + +<script> + import Prism from '../../lib/highlight'; + import Prompt from '../prompt.vue'; + + export default { + components: { + prompt: Prompt, + }, + props: { + count: { + type: Number, + required: false, + default: 0, + }, + codeCssClass: { + type: String, + required: false, + default: '', + }, + type: { + type: String, + required: true, + }, + rawCode: { + type: String, + required: true, + }, + }, + computed: { + code() { + return this.rawCode; + }, + promptType() { + const type = this.type.split('put')[0]; + + return type.charAt(0).toUpperCase() + type.slice(1); + }, + }, + mounted() { + Prism.highlightElement(this.$refs.code); + }, + }; +</script> diff --git a/app/assets/javascripts/notebook/cells/index.js b/app/assets/javascripts/notebook/cells/index.js new file mode 100644 index 00000000000..e4c255609fe --- /dev/null +++ b/app/assets/javascripts/notebook/cells/index.js @@ -0,0 +1,2 @@ +export { default as MarkdownCell } from './markdown.vue'; +export { default as CodeCell } from './code.vue'; diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue new file mode 100644 index 00000000000..3e8240d10ec --- /dev/null +++ b/app/assets/javascripts/notebook/cells/markdown.vue @@ -0,0 +1,98 @@ +<template> + <div class="cell text-cell"> + <prompt /> + <div class="markdown" v-html="markdown"></div> + </div> +</template> + +<script> + /* global katex */ + import marked from 'marked'; + import Prompt from './prompt.vue'; + + const renderer = new marked.Renderer(); + + /* + Regex to match KaTex blocks. + + Supports the following: + + \begin{equation}<math>\end{equation} + $$<math>$$ + inline $<math>$ + + The matched text then goes through the KaTex renderer & then outputs the HTML + */ + const katexRegexString = `( + ^\\\\begin{[a-zA-Z]+}\\s + | + ^\\$\\$ + | + \\s\\$(?!\\$) + ) + (.+?) + ( + \\s\\\\end{[a-zA-Z]+}$ + | + \\$\\$$ + | + \\$ + ) + `.replace(/\s/g, '').trim(); + + renderer.paragraph = (t) => { + let text = t; + let inline = false; + + if (typeof katex !== 'undefined') { + const katexString = text.replace(/\\/g, '\\'); + const matches = new RegExp(katexRegexString, 'gi').exec(katexString); + + if (matches && matches.length > 0) { + if (matches[1].trim() === '$' && matches[3].trim() === '$') { + inline = true; + + text = `${katexString.replace(matches[0], '')} ${katex.renderToString(matches[2])}`; + } else { + text = katex.renderToString(matches[2]); + } + } + } + + return `<p class="${inline ? 'inline-katex' : ''}">${text}</p>`; + }; + + marked.setOptions({ + sanitize: true, + renderer, + }); + + export default { + components: { + prompt: Prompt, + }, + props: { + cell: { + type: Object, + required: true, + }, + }, + computed: { + markdown() { + return marked(this.cell.source.join('')); + }, + }, + }; +</script> + +<style> +.markdown .katex { + display: block; + text-align: center; +} + +.markdown .inline-katex .katex { + display: inline; + text-align: initial; +} +</style> diff --git a/app/assets/javascripts/notebook/cells/output/html.vue b/app/assets/javascripts/notebook/cells/output/html.vue new file mode 100644 index 00000000000..0f39cd138df --- /dev/null +++ b/app/assets/javascripts/notebook/cells/output/html.vue @@ -0,0 +1,22 @@ +<template> + <div class="output"> + <prompt /> + <div v-html="rawCode"></div> + </div> +</template> + +<script> +import Prompt from '../prompt.vue'; + +export default { + props: { + rawCode: { + type: String, + required: true, + }, + }, + components: { + prompt: Prompt, + }, +}; +</script> diff --git a/app/assets/javascripts/notebook/cells/output/image.vue b/app/assets/javascripts/notebook/cells/output/image.vue new file mode 100644 index 00000000000..f3b873bbc0f --- /dev/null +++ b/app/assets/javascripts/notebook/cells/output/image.vue @@ -0,0 +1,27 @@ +<template> + <div class="output"> + <prompt /> + <img + :src="'data:' + outputType + ';base64,' + rawCode" /> + </div> +</template> + +<script> +import Prompt from '../prompt.vue'; + +export default { + props: { + outputType: { + type: String, + required: true, + }, + rawCode: { + type: String, + required: true, + }, + }, + components: { + prompt: Prompt, + }, +}; +</script> diff --git a/app/assets/javascripts/notebook/cells/output/index.vue b/app/assets/javascripts/notebook/cells/output/index.vue new file mode 100644 index 00000000000..23c9ea78939 --- /dev/null +++ b/app/assets/javascripts/notebook/cells/output/index.vue @@ -0,0 +1,83 @@ +<template> + <component :is="componentName" + type="output" + :outputType="outputType" + :count="count" + :raw-code="rawCode" + :code-css-class="codeCssClass" /> +</template> + +<script> +import CodeCell from '../code/index.vue'; +import Html from './html.vue'; +import Image from './image.vue'; + +export default { + props: { + codeCssClass: { + type: String, + required: false, + default: '', + }, + count: { + type: Number, + required: false, + default: 0, + }, + output: { + type: Object, + requred: true, + }, + }, + components: { + 'code-cell': CodeCell, + 'html-output': Html, + 'image-output': Image, + }, + data() { + return { + outputType: '', + }; + }, + computed: { + componentName() { + if (this.output.text) { + return 'code-cell'; + } else if (this.output.data['image/png']) { + this.outputType = 'image/png'; + + return 'image-output'; + } else if (this.output.data['text/html']) { + this.outputType = 'text/html'; + + return 'html-output'; + } else if (this.output.data['image/svg+xml']) { + this.outputType = 'image/svg+xml'; + + return 'html-output'; + } + + this.outputType = 'text/plain'; + return 'code-cell'; + }, + rawCode() { + if (this.output.text) { + return this.output.text.join(''); + } + + return this.dataForType(this.outputType); + }, + }, + methods: { + dataForType(type) { + let data = this.output.data[type]; + + if (typeof data === 'object') { + data = data.join(''); + } + + return data; + }, + }, +}; +</script> diff --git a/app/assets/javascripts/notebook/cells/prompt.vue b/app/assets/javascripts/notebook/cells/prompt.vue new file mode 100644 index 00000000000..4540e4248d8 --- /dev/null +++ b/app/assets/javascripts/notebook/cells/prompt.vue @@ -0,0 +1,30 @@ +<template> + <div class="prompt"> + <span v-if="type && count"> + {{ type }} [{{ count }}]: + </span> + </div> +</template> + +<script> + export default { + props: { + type: { + type: String, + required: false, + }, + count: { + type: Number, + required: false, + }, + }, + }; +</script> + +<style scoped> +.prompt { + padding: 0 10px; + min-width: 7em; + font-family: monospace; +} +</style> diff --git a/app/assets/javascripts/notebook/index.vue b/app/assets/javascripts/notebook/index.vue new file mode 100644 index 00000000000..fd62c1231ef --- /dev/null +++ b/app/assets/javascripts/notebook/index.vue @@ -0,0 +1,75 @@ +<template> + <div v-if="hasNotebook"> + <component + v-for="(cell, index) in cells" + :is="cellType(cell.cell_type)" + :cell="cell" + :key="index" + :code-css-class="codeCssClass" /> + </div> +</template> + +<script> + import { + MarkdownCell, + CodeCell, + } from './cells'; + + export default { + components: { + 'code-cell': CodeCell, + 'markdown-cell': MarkdownCell, + }, + props: { + notebook: { + type: Object, + required: true, + }, + codeCssClass: { + type: String, + required: false, + default: '', + }, + }, + methods: { + cellType(type) { + return `${type}-cell`; + }, + }, + computed: { + cells() { + if (this.notebook.worksheets) { + const data = { + cells: [], + }; + + return this.notebook.worksheets.reduce((cellData, sheet) => { + const cellDataCopy = cellData; + cellDataCopy.cells = cellDataCopy.cells.concat(sheet.cells); + return cellDataCopy; + }, data).cells; + } + + return this.notebook.cells; + }, + hasNotebook() { + return Object.keys(this.notebook).length; + }, + }, + }; +</script> + +<style> +.cell, +.input, +.output { + display: flex; + width: 100%; + margin-bottom: 10px; +} + +.cell pre { + margin: 0; + width: 100%; +} +</style> diff --git a/app/assets/javascripts/notebook/lib/highlight.js b/app/assets/javascripts/notebook/lib/highlight.js new file mode 100644 index 00000000000..74ade6d2edf --- /dev/null +++ b/app/assets/javascripts/notebook/lib/highlight.js @@ -0,0 +1,22 @@ +import Prism from 'prismjs'; +import 'prismjs/components/prism-python'; +import 'prismjs/plugins/custom-class/prism-custom-class'; + +Prism.plugins.customClass.map({ + comment: 'c', + error: 'err', + operator: 'o', + constant: 'kc', + namespace: 'kn', + keyword: 'k', + string: 's', + number: 'm', + 'attr-name': 'na', + builtin: 'nb', + entity: 'ni', + function: 'nf', + tag: 'nt', + variable: 'nv', +}); + +export default Prism; diff --git a/app/assets/javascripts/pdf/assets/img/bg.gif b/app/assets/javascripts/pdf/assets/img/bg.gif Binary files differnew file mode 100644 index 00000000000..c7e98e044f5 --- /dev/null +++ b/app/assets/javascripts/pdf/assets/img/bg.gif diff --git a/app/assets/javascripts/pdf/index.vue b/app/assets/javascripts/pdf/index.vue new file mode 100644 index 00000000000..4603859d7b0 --- /dev/null +++ b/app/assets/javascripts/pdf/index.vue @@ -0,0 +1,73 @@ +<template> + <div class="pdf-viewer" v-if="hasPDF"> + <page v-for="(page, index) in pages" + :key="index" + :v-if="!loading" + :page="page" + :number="index + 1" /> + </div> +</template> + +<script> + import pdfjsLib from 'pdfjs-dist'; + import workerSrc from 'vendor/pdf.worker'; + + import page from './page/index.vue'; + + export default { + props: { + pdf: { + type: [String, Uint8Array], + required: true, + }, + }, + data() { + return { + loading: false, + pages: [], + }; + }, + components: { page }, + watch: { pdf: 'load' }, + computed: { + document() { + return typeof this.pdf === 'string' ? this.pdf : { data: this.pdf }; + }, + hasPDF() { + return this.pdf && this.pdf.length > 0; + }, + }, + methods: { + load() { + this.pages = []; + return pdfjsLib.getDocument(this.document) + .then(this.renderPages) + .then(() => this.$emit('pdflabload')) + .catch(error => this.$emit('pdflaberror', error)) + .then(() => { this.loading = false; }); + }, + renderPages(pdf) { + const pagePromises = []; + this.loading = true; + for (let num = 1; num <= pdf.numPages; num += 1) { + pagePromises.push( + pdf.getPage(num).then(p => this.pages.push(p)), + ); + } + return Promise.all(pagePromises); + }, + }, + mounted() { + pdfjsLib.PDFJS.workerSrc = workerSrc; + if (this.hasPDF) this.load(); + }, + }; +</script> + +<style> + .pdf-viewer { + background: url('./assets/img/bg.gif'); + display: flex; + flex-flow: column nowrap; + } +</style> diff --git a/app/assets/javascripts/pdf/page/index.vue b/app/assets/javascripts/pdf/page/index.vue new file mode 100644 index 00000000000..7b74ee4eb2e --- /dev/null +++ b/app/assets/javascripts/pdf/page/index.vue @@ -0,0 +1,68 @@ +<template> + <canvas + class="pdf-page" + ref="canvas" + :data-page="number" /> +</template> + +<script> + export default { + props: { + page: { + type: Object, + required: true, + }, + number: { + type: Number, + required: true, + }, + }, + data() { + return { + scale: 4, + rendering: false, + }; + }, + computed: { + viewport() { + return this.page.getViewport(this.scale); + }, + context() { + return this.$refs.canvas.getContext('2d'); + }, + renderContext() { + return { + canvasContext: this.context, + viewport: this.viewport, + }; + }, + }, + mounted() { + this.$refs.canvas.height = this.viewport.height; + this.$refs.canvas.width = this.viewport.width; + this.rendering = true; + this.page.render(this.renderContext) + .then(() => { this.rendering = false; }) + .catch(error => this.$emit('pdflaberror', error)); + }, + }; +</script> + +<style> +.pdf-page { + margin: 8px auto 0 auto; + border-top: 1px #ddd solid; + border-bottom: 1px #ddd solid; + width: 100%; +} + +.pdf-page:first-child { + margin-top: 0px; + border-top: 0px; +} + +.pdf-page:last-child { + margin-bottom: 0px; + border-bottom: 0px; +} +</style> diff --git a/app/assets/javascripts/pipelines/components/stage.js b/app/assets/javascripts/pipelines/components/stage.js index b8cc3630611..203485f2990 100644 --- a/app/assets/javascripts/pipelines/components/stage.js +++ b/app/assets/javascripts/pipelines/components/stage.js @@ -2,13 +2,6 @@ import StatusIconEntityMap from '../../ci_status_icons'; export default { - data() { - return { - builds: '', - spinner: '<span class="fa fa-spinner fa-spin"></span>', - }; - }, - props: { stage: { type: Object, @@ -16,6 +9,13 @@ export default { }, }, + data() { + return { + builds: '', + spinner: '<span class="fa fa-spinner fa-spin"></span>', + }; + }, + updated() { if (this.builds) { this.stopDropdownClickPropagation(); @@ -31,7 +31,13 @@ export default { return this.$http.get(this.stage.dropdown_path) .then((response) => { this.builds = JSON.parse(response.body).html; - }, () => { + }) + .catch(() => { + // If dropdown is opened we'll close it. + if (this.$el.classList.contains('open')) { + $(this.$refs.dropdown).dropdown('toggle'); + } + const flash = new Flash('Something went wrong on our end.'); return flash; }); @@ -46,9 +52,10 @@ export default { * target the click event of this component. */ stopDropdownClickPropagation() { - $(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')).on('click', (e) => { - e.stopPropagation(); - }); + $(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')) + .on('click', (e) => { + e.stopPropagation(); + }); }, }, computed: { @@ -81,12 +88,22 @@ export default { data-placement="top" data-toggle="dropdown" type="button" - :aria-label="stage.title"> - <span v-html="svgHTML" aria-hidden="true"></span> - <i class="fa fa-caret-down" aria-hidden="true"></i> + :aria-label="stage.title" + ref="dropdown"> + <span + v-html="svgHTML" + aria-hidden="true"> + </span> + <i + class="fa fa-caret-down" + aria-hidden="true" /> </button> - <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"> - <div class="arrow-up" aria-hidden="true"></div> + <ul + ref="dropdown-content" + class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"> + <div + class="arrow-up" + aria-hidden="true"></div> <div :class="dropdownClass" class="js-builds-dropdown-list scrollable-menu" diff --git a/app/assets/javascripts/pipelines/components/time_ago.js b/app/assets/javascripts/pipelines/components/time_ago.js index 498d0715f54..188f74cc705 100644 --- a/app/assets/javascripts/pipelines/components/time_ago.js +++ b/app/assets/javascripts/pipelines/components/time_ago.js @@ -2,68 +2,95 @@ import iconTimerSvg from 'icons/_icon_timer.svg'; import '../../lib/utils/datetime_utility'; export default { + props: { + finishedTime: { + type: String, + required: true, + }, + + duration: { + type: Number, + required: true, + }, + }, + data() { return { - currentTime: new Date(), iconTimerSvg, }; }, - props: ['pipeline'], + + updated() { + $(this.$refs.tooltip).tooltip('fixTitle'); + }, + computed: { - timeAgo() { - return gl.utils.getTimeago(); + hasDuration() { + return this.duration > 0; }, - localTimeFinished() { - return gl.utils.formatDate(this.pipeline.details.finished_at); + + hasFinishedTime() { + return this.finishedTime !== ''; }, - timeStopped() { - const changeTime = this.currentTime; - const options = { - weekday: 'long', - year: 'numeric', - month: 'short', - day: 'numeric', - }; - options.timeZoneName = 'short'; - const finished = this.pipeline.details.finished_at; - if (!finished && changeTime) return false; - return ({ words: this.timeAgo.format(finished) }); + + localTimeFinished() { + return gl.utils.formatDate(this.finishedTime); }, - duration() { - const { duration } = this.pipeline.details; - const date = new Date(duration * 1000); + + durationFormated() { + const date = new Date(this.duration * 1000); let hh = date.getUTCHours(); let mm = date.getUTCMinutes(); let ss = date.getSeconds(); - if (hh < 10) hh = `0${hh}`; - if (mm < 10) mm = `0${mm}`; - if (ss < 10) ss = `0${ss}`; + // left pad + if (hh < 10) { + hh = `0${hh}`; + } + if (mm < 10) { + mm = `0${mm}`; + } + if (ss < 10) { + ss = `0${ss}`; + } - if (duration !== null) return `${hh}:${mm}:${ss}`; - return false; + return `${hh}:${mm}:${ss}`; }, - }, - methods: { - changeTime() { - this.currentTime = new Date(); + + finishedTimeFormated() { + const timeAgo = gl.utils.getTimeago(); + + return timeAgo.format(this.finishedTime); }, }, + template: ` <td class="pipelines-time-ago"> - <p class="duration" v-if='duration'> - <span v-html="iconTimerSvg"></span> - {{duration}} + <p + class="duration" + v-if="hasDuration"> + <span + v-html="iconTimerSvg"> + </span> + {{durationFormated}} </p> - <p class="finished-at" v-if='timeStopped'> - <i class="fa fa-calendar"></i> + + <p + class="finished-at" + v-if="hasFinishedTime"> + + <i + class="fa fa-calendar" + aria-hidden="true" /> + <time + ref="tooltip" data-toggle="tooltip" data-placement="top" data-container="body" - :data-original-title='localTimeFinished'> - {{timeStopped.words}} + :title="localTimeFinished"> + {{finishedTimeFormated}} </time> </p> </td> diff --git a/app/assets/javascripts/pipelines/pipelines.js b/app/assets/javascripts/pipelines/pipelines.js index 6eea4812f33..93d4818231f 100644 --- a/app/assets/javascripts/pipelines/pipelines.js +++ b/app/assets/javascripts/pipelines/pipelines.js @@ -1,4 +1,3 @@ -import Vue from 'vue'; import Visibility from 'visibilityjs'; import PipelinesService from './services/pipelines_service'; import eventHub from './event_hub'; @@ -161,15 +160,6 @@ export default { eventHub.$on('refreshPipelines', this.fetchPipelines); }, - beforeUpdate() { - if (this.state.pipelines.length && - this.$children && - !this.isMakingRequest && - !this.isLoading) { - this.store.startTimeAgoLoops.call(this, Vue); - } - }, - beforeDestroyed() { eventHub.$off('refreshPipelines'); }, diff --git a/app/assets/javascripts/pipelines/stores/pipelines_store.js b/app/assets/javascripts/pipelines/stores/pipelines_store.js index 377ec8ba2cc..ffefe0192f2 100644 --- a/app/assets/javascripts/pipelines/stores/pipelines_store.js +++ b/app/assets/javascripts/pipelines/stores/pipelines_store.js @@ -1,6 +1,3 @@ -/* eslint-disable no-underscore-dangle*/ -import VueRealtimeListener from '../../vue_realtime_listener'; - export default class PipelinesStore { constructor() { this.state = {}; @@ -30,32 +27,4 @@ export default class PipelinesStore { this.state.pageInfo = paginationInfo; } - - /** - * FIXME: Move this inside the component. - * - * Once the data is received we will start the time ago loops. - * - * Everytime a request is made like retry or cancel a pipeline, every 10 seconds we - * update the time to show how long as passed. - * - */ - startTimeAgoLoops() { - const startTimeLoops = () => { - this.timeLoopInterval = setInterval(() => { - this.$children[0].$children.reduce((acc, component) => { - const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0]; - acc.push(timeAgoComponent); - return acc; - }, []).forEach(e => e.changeTime()); - }, 10000); - }; - - startTimeLoops(); - - const removeIntervals = () => clearInterval(this.timeLoopInterval); - const startIntervals = () => startTimeLoops(); - - VueRealtimeListener(removeIntervals, startIntervals); - } } diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 30902767705..0344ce9ffb4 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -33,6 +33,7 @@ var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser, showMenuAbove; $dropdown = $(dropdown); options.projectId = $dropdown.data('project-id'); + options.groupId = $dropdown.data('group-id'); options.showCurrentUser = $dropdown.data('current-user'); options.todoFilter = $dropdown.data('todo-filter'); options.todoStateFilter = $dropdown.data('todo-state-filter'); diff --git a/app/assets/javascripts/vue_realtime_listener/index.js b/app/assets/javascripts/vue_realtime_listener/index.js deleted file mode 100644 index 4ddb2f975b0..00000000000 --- a/app/assets/javascripts/vue_realtime_listener/index.js +++ /dev/null @@ -1,9 +0,0 @@ -export default (removeIntervals, startIntervals) => { - window.removeEventListener('focus', startIntervals); - window.removeEventListener('blur', removeIntervals); - window.removeEventListener('onbeforeload', removeIntervals); - - window.addEventListener('focus', startIntervals); - window.addEventListener('blur', removeIntervals); - window.addEventListener('onbeforeload', removeIntervals); -}; diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js index 62b7131de51..79806bc7204 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js +++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js @@ -1,5 +1,4 @@ /* eslint-disable no-param-reassign */ - import AsyncButtonComponent from '../../pipelines/components/async_button.vue'; import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions'; import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts'; @@ -166,6 +165,32 @@ export default { } return undefined; }, + + /** + * Timeago components expects a number + * + * @return {type} description + */ + pipelineDuration() { + if (this.pipeline.details && this.pipeline.details.duration) { + return this.pipeline.details.duration; + } + + return 0; + }, + + /** + * Timeago component expects a String. + * + * @return {String} + */ + pipelineFinishedAt() { + if (this.pipeline.details && this.pipeline.details.finished_at) { + return this.pipeline.details.finished_at; + } + + return ''; + }, }, template: ` @@ -192,7 +217,9 @@ export default { </div> </td> - <time-ago :pipeline="pipeline"/> + <time-ago + :duration="pipelineDuration" + :finished-time="pipelineFinishedAt" /> <td class="pipeline-actions"> <div class="pull-right btn-group"> diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss index f614f262316..9159927ed8b 100644 --- a/app/assets/stylesheets/framework/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -108,8 +108,7 @@ } .award-control { - margin: 3px 5px 3px 0; - padding: .35em .4em; + margin-right: 5px; outline: 0; &.disabled { @@ -228,8 +227,8 @@ .award-control-icon-positive, .award-control-icon-super-positive { position: absolute; - left: 7px; - bottom: 9px; + left: 11px; + bottom: 7px; opacity: 0; @include transition(opacity, transform); } diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 52425262925..ac1fc0eb8ae 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -230,7 +230,6 @@ float: right; margin-top: 8px; padding-bottom: 8px; - border-bottom: 1px solid $border-color; } } @@ -255,6 +254,63 @@ padding: 10px 0; } +.landing { + margin-bottom: $gl-padding; + overflow: hidden; + display: flex; + position: relative; + border: 1px solid $blue-300; + border-radius: $border-radius-default; + background-color: $blue-25; + justify-content: center; + + .dismiss-button { + position: absolute; + right: 6px; + top: 6px; + cursor: pointer; + color: $blue-300; + z-index: 1; + border: none; + background-color: transparent; + + &:hover, + &:focus { + border: none; + color: $blue-400; + } + } + + .svg-container { + align-self: center; + } + + .inner-content { + text-align: left; + white-space: nowrap; + + h4 { + color: $gl-text-color; + font-size: 17px; + } + + p { + color: $gl-text-color; + margin-bottom: $gl-padding; + } + } + + @media (max-width: $screen-sm-min) { + flex-direction: column; + + .inner-content { + white-space: normal; + padding: 0 28px; + text-align: center; + } + } +} + .empty-state { margin: 100px 0 0; diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 0fd7203e72b..1a6f36d032d 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -70,7 +70,7 @@ pre { } hr { - margin: $gl-padding 0; + margin: 24px 0; border-top: 1px solid darken($gray-normal, 8%); } @@ -424,6 +424,11 @@ table { } } +.bordered-box { + border: 1px solid $border-color; + border-radius: $border-radius-default; +} + .str-truncated { &-60 { @include str-truncated(60%); diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 30d785464ac..1313ea25c2a 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -195,7 +195,6 @@ border: 1px solid $dropdown-border-color; border-radius: $border-radius-base; box-shadow: 0 2px 4px $dropdown-shadow-color; - overflow: hidden; @include set-invisible; @media (max-width: $screen-sm-min) { diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index a5a8522739e..c197bf6b9f5 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -61,11 +61,13 @@ .file-content { background: $white-light; - &.image_file { + &.image_file, + &.video { background: $file-image-bg; text-align: center; - img { + img, + video { padding: 20px; max-width: 80%; } @@ -73,14 +75,6 @@ &.wiki { padding: 30px $gl-padding; - - .highlight { - margin-bottom: 9px; - - > pre { - margin: 0; - } - } } &.blob-no-preview { diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 11d44df4867..0692f65043b 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -104,6 +104,24 @@ padding: 2px 7px; } + .value { + padding-right: 0; + } + + .remove-token { + display: inline-block; + padding-left: 4px; + padding-right: 8px; + + .fa-close { + color: $gl-text-color-disabled; + } + + &:hover .fa-close { + color: $gl-text-color-secondary; + } + } + .name { background-color: $filter-name-resting-color; color: $filter-name-text-color; @@ -112,7 +130,7 @@ text-transform: capitalize; } - .value { + .value-container { background-color: $white-normal; color: $filter-value-text-color; border-radius: 0 2px 2px 0; @@ -124,7 +142,7 @@ background-color: $filter-name-selected-color; } - .value { + .value-container { background-color: $filter-value-selected-color; } } diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index a668a6c4c39..80691a234f8 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -120,6 +120,10 @@ // Ensure that image does not exceed viewport max-height: calc(100vh - 100px); } + + table { + @include markdown-table; + } } .toolbar-group { diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index b3340d41333..3a98332e46c 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -13,6 +13,13 @@ } /* + * Mixin for markdown tables + */ +@mixin markdown-table { + width: auto; +} + +/* * Base mixin for lists in GitLab */ @mixin basic-list { diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index e6d808717f3..b6cf5101d60 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -110,7 +110,7 @@ .top-area { @include clearfix; - border-bottom: 1px solid $white-normal; + border-bottom: 1px solid $border-color; .nav-text { padding-top: 16px; diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index cd23deb6d75..d2164a1d333 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -4,7 +4,7 @@ padding: 0; .timeline-entry { - padding: $gl-padding $gl-btn-padding 14px; + padding: $gl-padding $gl-btn-padding 0; border-color: $white-normal; color: $gl-text-color; border-bottom: 1px solid $border-white-light; diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 1839cadcc10..96d8a812723 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -8,6 +8,13 @@ img { max-width: 100%; + margin: 0 0 8px; + } + + p a:not(.no-attachment-icon) img { + // Remove bottom padding because + // <p> already has $gl-padding bottom + margin-bottom: 0; } *:first-child:not(.katex-display) { @@ -47,44 +54,50 @@ h1 { font-size: 1.75em; font-weight: 600; - margin: 16px 0 10px; - padding: 0 0 0.3em; + margin: 24px 0 16px; + padding-bottom: 0.3em; border-bottom: 1px solid $white-dark; color: $gl-text-color; + + &:first-child { + margin-top: 0; + } } h2 { font-size: 1.5em; font-weight: 600; - margin: 16px 0 10px; + margin: 24px 0 16px; + padding-bottom: 0.3em; + border-bottom: 1px solid $white-dark; color: $gl-text-color; } h3 { - margin: 16px 0 10px; + margin: 24px 0 16px; font-size: 1.3em; } h4 { - margin: 16px 0 10px; + margin: 24px 0 16px; font-size: 1.2em; } h5 { - margin: 16px 0 10px; + margin: 24px 0 16px; font-size: 1em; } h6 { - margin: 16px 0 10px; + margin: 24px 0 16px; font-size: 0.95em; } blockquote { color: $gl-grayish-blue; font-size: inherit; - padding: 8px 21px; - margin: 12px 0; + padding: 8px 24px; + margin: 16px 0; border-left: 3px solid $white-dark; } @@ -95,19 +108,20 @@ blockquote p { color: $gl-grayish-blue !important; + margin: 0; font-size: inherit; line-height: 1.5; } p { color: $gl-text-color; - margin: 6px 0 0; + margin: 0 0 16px; } table { @extend .table; @extend .table-bordered; - margin: 12px 0; + margin: 16px 0; color: $gl-text-color; th { @@ -120,7 +134,7 @@ } pre { - margin: 12px 0; + margin-bottom: 16px; font-size: 13px; line-height: 1.6em; overflow-x: auto; @@ -134,7 +148,7 @@ ul, ol { padding: 0; - margin: 3px 0 !important; + margin: 0 0 16px !important; } ul:dir(rtl), diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 411f1c4442b..724b4080ee0 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -200,6 +200,7 @@ .header-content { flex: 1; + line-height: 1.8; a { color: $gl-text-color; diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index ad3dbc7ac48..403724cd68a 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -93,11 +93,6 @@ top: $gl-padding-top; } - .bordered-box { - border: 1px solid $border-color; - border-radius: $border-radius-default; - } - .content-list { li { padding: 18px $gl-padding $gl-padding; @@ -139,42 +134,9 @@ } } - .landing { - margin-bottom: $gl-padding; - overflow: hidden; - - .dismiss-icon { - position: absolute; - right: $cycle-analytics-box-padding; - cursor: pointer; - color: $cycle-analytics-dismiss-icon-color; - } - - .svg-container { - text-align: center; - - svg { - width: 136px; - height: 136px; - } - } - - .inner-content { - @media (max-width: $screen-xs-max) { - padding: 0 28px; - text-align: center; - } - - h4 { - color: $gl-text-color; - font-size: 17px; - } - - p { - color: $cycle-analytics-box-text-color; - margin-bottom: $gl-padding; - } - } + .landing svg { + width: 136px; + height: 136px; } .fa-spinner { diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index 46fd19c93f9..f3de05aa5f6 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -29,11 +29,5 @@ .description { margin-top: 6px; - - p { - &:last-child { - margin-bottom: 0; - } - } } } diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 73a5889867a..72d73b89a2a 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -88,3 +88,26 @@ color: $gl-text-color-secondary; margin-top: 10px; } + +.explore-groups.landing { + margin-top: 10px; + + .inner-content { + padding: 0; + + p { + margin: 7px 0 0; + max-width: 480px; + padding: 0 $gl-padding; + + @media (max-width: $screen-sm-min) { + margin: 0 auto; + } + } + } + + svg { + width: 62px; + height: 50px; + } +} diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 8d3d1a72b9b..97fab513b01 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -52,7 +52,7 @@ .title { padding: 0; - margin: 0; + margin-bottom: 16px; border-bottom: none; } @@ -357,6 +357,8 @@ } .detail-page-description { + padding: 16px 0 0; + small { color: $gray-darkest; } @@ -364,6 +366,8 @@ .edited-text { color: $gray-darkest; + display: block; + margin: 0 0 16px; .author_link { color: $gray-darkest; diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index b2f45625a2a..2aa52986e0a 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -101,11 +101,16 @@ ul.related-merge-requests > li { } } -.merge-request-ci-status { +.merge-request-ci-status, +.related-merge-requests { + .ci-status-link { + display: block; + margin-top: 3px; + margin-right: 5px; + } + svg { - margin-right: 4px; - position: relative; - top: 1px; + display: block; } } diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index b637994adf8..62f654ed343 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -28,7 +28,7 @@ .note-edit-form { .note-form-actions { position: relative; - margin-top: $gl-padding; + margin: $gl-padding 0; } .note-preview-holder { @@ -387,6 +387,7 @@ @media (max-width: $screen-xs-max) { display: flex; width: 100%; + margin-bottom: 10px; .comment-btn { flex-grow: 1; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 2ea2ff8362b..7cf74502a3a 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -97,18 +97,21 @@ ul.notes { padding-left: 1.3em; } } + + table { + @include markdown-table; + } } } .note-awards { .js-awards-block { - padding: 2px; - margin-top: 10px; + margin-bottom: 16px; } } .note-header { - padding-bottom: 3px; + padding-bottom: 8px; padding-right: 20px; @media (min-width: $screen-sm-min) { @@ -151,6 +154,10 @@ ul.notes { margin-left: 65px; } + .note-header { + padding-bottom: 0; + } + &.timeline-entry::after { clear: none; } @@ -386,6 +393,10 @@ ul.notes { .note-headline-meta { display: inline-block; white-space: nowrap; + + .system-note-message { + white-space: normal; + } } /** diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 28a8f9cb335..c119f0c9b22 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -614,6 +614,7 @@ pre.light-well { .controls { margin-left: auto; + text-align: right; } .ci-status-link { diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index f3916622b6f..03c75ce61f5 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -160,7 +160,6 @@ .tree-controls { float: right; - margin-top: 11px; position: relative; z-index: 2; diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index 9bc47bbe173..04ff2d52b91 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -159,3 +159,9 @@ ul.wiki-pages-list.content-list { padding: 5px 0; } } + +.wiki { + table { + @include markdown-table; + } +} |