diff options
Diffstat (limited to 'app/assets')
105 files changed, 1612 insertions, 703 deletions
diff --git a/app/assets/images/favicon-yellow.png b/app/assets/images/favicon-yellow.png Binary files differindex 2d5289818b4..a80827808fc 100644 --- a/app/assets/images/favicon-yellow.png +++ b/app/assets/images/favicon-yellow.png diff --git a/app/assets/javascripts/batch_comments/mixins/resolved_status.js b/app/assets/javascripts/batch_comments/mixins/resolved_status.js index 20c31d9f8a4..3bbbaa86b51 100644 --- a/app/assets/javascripts/batch_comments/mixins/resolved_status.js +++ b/app/assets/javascripts/batch_comments/mixins/resolved_status.js @@ -1,10 +1,12 @@ +import { sprintf, __ } from '~/locale'; + export default { computed: { resolveButtonTitle() { - let title = 'Mark as resolved'; + let title = __('Mark comment as resolved'); if (this.resolvedBy) { - title = `Resolved by ${this.resolvedBy.name}`; + title = sprintf(__('Resolved by %{name}'), { name: this.resolvedBy.name }); } return title; diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js index da82b52330a..51565c597e6 100644 --- a/app/assets/javascripts/boards/stores/actions.js +++ b/app/assets/javascripts/boards/stores/actions.js @@ -1,5 +1,7 @@ +import { __ } from '~/locale'; + const notImplemented = () => { - throw new Error('Not implemented!'); + throw new Error(__('Not implemented!')); }; export default { diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js index 77ba68be07e..8e61b93e824 100644 --- a/app/assets/javascripts/boards/stores/mutations.js +++ b/app/assets/javascripts/boards/stores/mutations.js @@ -1,7 +1,8 @@ import * as mutationTypes from './mutation_types'; +import { __ } from '~/locale'; const notImplemented = () => { - throw new Error('Not implemented!'); + throw new Error(__('Not implemented!')); }; export default { diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index 561b6bdd9f1..70af333a0dd 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -1,5 +1,6 @@ import Visibility from 'visibilityjs'; import Vue from 'vue'; +import AccessorUtilities from '~/lib/utils/accessor'; import { GlToast } from '@gitlab/ui'; import PersistentUserCallout from '../persistent_user_callout'; import { s__, sprintf } from '../locale'; @@ -43,8 +44,10 @@ export default class Clusters { helpPath, ingressHelpPath, ingressDnsHelpPath, + clusterId, } = document.querySelector('.js-edit-cluster-form').dataset; + this.clusterId = clusterId; this.store = new ClustersStore(); this.store.setHelpPaths(helpPath, ingressHelpPath, ingressDnsHelpPath); this.store.setManagePrometheusPath(managePrometheusPath); @@ -69,6 +72,10 @@ export default class Clusters { this.errorContainer = document.querySelector('.js-cluster-error'); this.successContainer = document.querySelector('.js-cluster-success'); this.creatingContainer = document.querySelector('.js-cluster-creating'); + this.unreachableContainer = document.querySelector('.js-cluster-api-unreachable'); + this.authenticationFailureContainer = document.querySelector( + '.js-cluster-authentication-failure', + ); this.errorReasonContainer = this.errorContainer.querySelector('.js-error-reason'); this.successApplicationContainer = document.querySelector('.js-cluster-application-notice'); this.showTokenButton = document.querySelector('.js-show-cluster-token'); @@ -125,6 +132,13 @@ export default class Clusters { PersistentUserCallout.factory(callout); } + addBannerCloseHandler(el, status) { + el.querySelector('.js-close-banner').addEventListener('click', () => { + el.classList.add('hidden'); + this.setBannerDismissedState(status, true); + }); + } + addListeners() { if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken); eventHub.$on('installApplication', this.installApplication); @@ -133,6 +147,9 @@ export default class Clusters { eventHub.$on('saveKnativeDomain', data => this.saveKnativeDomain(data)); eventHub.$on('setKnativeHostname', data => this.setKnativeHostname(data)); eventHub.$on('uninstallApplication', data => this.uninstallApplication(data)); + // Add event listener to all the banner close buttons + this.addBannerCloseHandler(this.unreachableContainer, 'unreachable'); + this.addBannerCloseHandler(this.authenticationFailureContainer, 'authentication_failure'); } removeListeners() { @@ -205,6 +222,8 @@ export default class Clusters { this.errorContainer.classList.add('hidden'); this.successContainer.classList.add('hidden'); this.creatingContainer.classList.add('hidden'); + this.unreachableContainer.classList.add('hidden'); + this.authenticationFailureContainer.classList.add('hidden'); } checkForNewInstalls(prevApplicationMap, newApplicationMap) { @@ -228,9 +247,32 @@ export default class Clusters { } } + setBannerDismissedState(status, isDismissed) { + if (AccessorUtilities.isLocalStorageAccessSafe()) { + window.localStorage.setItem( + `cluster_${this.clusterId}_banner_dismissed`, + `${status}_${isDismissed}`, + ); + } + } + + isBannerDismissed(status) { + let bannerState; + if (AccessorUtilities.isLocalStorageAccessSafe()) { + bannerState = window.localStorage.getItem(`cluster_${this.clusterId}_banner_dismissed`); + } + + return bannerState === `${status}_true`; + } + updateContainer(prevStatus, status, error) { this.hideAll(); + if (this.isBannerDismissed(status)) { + return; + } + this.setBannerDismissedState(status, false); + // We poll all the time but only want the `created` banner to show when newly created if (this.store.state.status !== 'created' || prevStatus !== this.store.state.status) { switch (status) { @@ -241,6 +283,12 @@ export default class Clusters { this.errorContainer.classList.remove('hidden'); this.errorReasonContainer.textContent = error; break; + case 'unreachable': + this.unreachableContainer.classList.remove('hidden'); + break; + case 'authentication_failure': + this.authenticationFailureContainer.classList.remove('hidden'); + break; case 'scheduled': case 'creating': this.creatingContainer.classList.remove('hidden'); diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js index a0ca44caa07..9216d4ab372 100644 --- a/app/assets/javascripts/commons/polyfills.js +++ b/app/assets/javascripts/commons/polyfills.js @@ -6,6 +6,7 @@ import 'core-js/fn/array/from'; import 'core-js/fn/array/includes'; import 'core-js/fn/object/assign'; import 'core-js/fn/object/values'; +import 'core-js/fn/object/entries'; import 'core-js/fn/promise'; import 'core-js/fn/promise/finally'; import 'core-js/fn/string/code-point-at'; diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js index 02aa507ba03..8f5cece0788 100644 --- a/app/assets/javascripts/create_merge_request_dropdown.js +++ b/app/assets/javascripts/create_merge_request_dropdown.js @@ -118,7 +118,7 @@ export default class CreateMergeRequestDropdown { this.branchCreated = true; window.location.href = data.url; }) - .catch(() => Flash('Failed to create a branch for this issue. Please try again.')); + .catch(() => Flash(__('Failed to create a branch for this issue. Please try again.'))); } createMergeRequest() { @@ -130,7 +130,7 @@ export default class CreateMergeRequestDropdown { this.mergeRequestCreated = true; window.location.href = data.url; }) - .catch(() => Flash('Failed to create Merge Request. Please try again.')); + .catch(() => Flash(__('Failed to create Merge Request. Please try again.'))); } disable() { @@ -227,7 +227,7 @@ export default class CreateMergeRequestDropdown { .catch(() => { this.unavailable(); this.disable(); - new Flash('Failed to get ref.'); + new Flash(__('Failed to get ref.')); this.isGettingRef = false; diff --git a/app/assets/javascripts/dirty_submit/dirty_submit_form.js b/app/assets/javascripts/dirty_submit/dirty_submit_form.js index 765969daa32..0fcaec9531c 100644 --- a/app/assets/javascripts/dirty_submit/dirty_submit_form.js +++ b/app/assets/javascripts/dirty_submit/dirty_submit_form.js @@ -21,10 +21,15 @@ class DirtySubmitForm { } registerListeners() { - const throttledUpdateDirtyInput = _.throttle( - event => this.updateDirtyInput(event), - DirtySubmitForm.THROTTLE_DURATION, + const getThrottledHandlerForInput = _.memoize(() => + _.throttle(event => this.updateDirtyInput(event), DirtySubmitForm.THROTTLE_DURATION), ); + + const throttledUpdateDirtyInput = event => { + const throttledHandler = getThrottledHandlerForInput(event.target.name); + throttledHandler(event); + }; + this.form.addEventListener('input', throttledUpdateDirtyInput); this.form.addEventListener('change', throttledUpdateDirtyInput); $(this.form).on('change.select2', throttledUpdateDirtyInput); diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index 9987fbcb6a7..0ff26445a6a 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -4,6 +4,7 @@ import _ from 'underscore'; import './behaviors/preview_markdown'; import csrf from './lib/utils/csrf'; import axios from './lib/utils/axios_utils'; +import { n__, __ } from '~/locale'; Dropzone.autoDiscover = false; @@ -90,7 +91,7 @@ export default function dropzoneInput(form) { if (!processingFileCount) $attachButton.removeClass('hide'); addFileToForm(response.link.url); }, - error: (file, errorMessage = 'Attaching the file failed.', xhr) => { + error: (file, errorMessage = __('Attaching the file failed.'), xhr) => { // If 'error' event is fired by dropzone, the second parameter is error message. // If the 'errorMessage' parameter is empty, the default error message is set. // If the 'error' event is fired by backend (xhr) error response, the third parameter is @@ -273,19 +274,11 @@ export default function dropzoneInput(form) { }; updateAttachingMessage = (files, messageContainer) => { - let attachingMessage; const filesCount = files.filter(file => file.status === 'uploading' || file.status === 'queued') .length; + const attachingMessage = n__('Attaching a file', 'Attaching %d files', filesCount); - // Dinamycally change uploading files text depending on files number in - // dropzone files queue. - if (filesCount > 1) { - attachingMessage = `Attaching ${filesCount} files -`; - } else { - attachingMessage = 'Attaching a file -'; - } - - messageContainer.text(attachingMessage); + messageContainer.text(`${attachingMessage} -`); }; form.find('.markdown-selector').click(function onMarkdownClick(e) { diff --git a/app/assets/javascripts/error_tracking_settings/index.js b/app/assets/javascripts/error_tracking_settings/index.js index ce315963723..e39452353f5 100644 --- a/app/assets/javascripts/error_tracking_settings/index.js +++ b/app/assets/javascripts/error_tracking_settings/index.js @@ -1,8 +1,10 @@ import Vue from 'vue'; import ErrorTrackingSettings from './components/app.vue'; import createStore from './store'; +import initSettingsPanels from '~/settings_panels'; export default () => { + initSettingsPanels(); const formContainerEl = document.querySelector('.js-error-tracking-form'); const { dataset: { apiHost, enabled, project, token, listProjectsEndpoint, operationsSettingsEndpoint }, diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js index f1e7be6bde1..a65c0012b4d 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js +++ b/app/assets/javascripts/filtered_search/dropdown_user.js @@ -18,6 +18,7 @@ export default class DropdownUser extends DropdownAjaxFilter { group_id: this.getGroupId(), project_id: this.getProjectId(), current_user: true, + ...this.projectOrGroupId(), }, onLoadingFinished: () => { this.hideCurrentUser(); @@ -36,4 +37,17 @@ export default class DropdownUser extends DropdownAjaxFilter { getProjectId() { return this.input.getAttribute('data-project-id'); } + + projectOrGroupId() { + const projectId = this.getProjectId(); + const groupId = this.getGroupId(); + if (groupId) { + return { + group_id: groupId, + }; + } + return { + project_id: projectId, + }; + } } diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 1c6b18c0e03..a143d79097b 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -335,6 +335,10 @@ GitLabDropdown = (function() { _this.fullData = data; _this.parseData(_this.fullData); _this.focusTextInput(); + + // Update dropdown position since remote data may have changed dropdown size + _this.dropdown.find('.dropdown-menu-toggle').dropdown('update'); + if ( _this.options.filterable && _this.filter && @@ -702,6 +706,10 @@ GitLabDropdown = (function() { } html = document.createElement('li'); + if (rowHidden) { + html.style.display = 'none'; + } + if (data === 'divider' || data === 'separator') { html.className = data; return html; diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index 9894ebb0624..e41b1530226 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -1,6 +1,7 @@ <script> import Vue from 'vue'; import { mapActions, mapState, mapGetters } from 'vuex'; +import { GlButton, GlLoadingIcon } from '@gitlab/ui'; import { __ } from '~/locale'; import FindFile from '~/vue_shared/components/file_finder/index.vue'; import NewModal from './new_dropdown/modal.vue'; @@ -22,6 +23,8 @@ export default { FindFile, ErrorMessage, CommitEditorHeader, + GlButton, + GlLoadingIcon, }, props: { rightPaneComponent: { @@ -47,13 +50,15 @@ export default { 'someUncommittedChanges', 'isCommitModeActive', 'allBlobs', + 'emptyRepo', + 'currentTree', ]), }, mounted() { window.onbeforeunload = e => this.onBeforeUnload(e); }, methods: { - ...mapActions(['toggleFileFinder']), + ...mapActions(['toggleFileFinder', 'openNewEntryModal']), onBeforeUnload(e = {}) { const returnValue = __('Are you sure you want to lose unsaved changes?'); @@ -98,17 +103,40 @@ export default { <repo-editor :file="activeFile" class="multi-file-edit-pane-content" /> </template> <template v-else> - <div v-once class="ide-empty-state"> + <div class="ide-empty-state"> <div class="row js-empty-state"> <div class="col-12"> <div class="svg-content svg-250"><img :src="emptyStateSvgPath" /></div> </div> <div class="col-12"> <div class="text-content text-center"> - <h4>Welcome to the GitLab IDE</h4> - <p> - Select a file from the left sidebar to begin editing. Afterwards, you'll be able - to commit your changes. + <h4> + {{ __('Make and review changes in the browser with the Web IDE') }} + </h4> + <template v-if="emptyRepo"> + <p> + {{ + __( + "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes.", + ) + }} + </p> + <gl-button + variant="success" + :title="__('New file')" + :aria-label="__('New file')" + @click="openNewEntryModal({ type: 'blob' })" + > + {{ __('New file') }} + </gl-button> + </template> + <gl-loading-icon v-else-if="!currentTree || currentTree.loading" size="md" /> + <p v-else> + {{ + __( + "Select a file from the left sidebar to begin editing. Afterwards, you'll be able to commit your changes.", + ) + }} </p> </div> </div> diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue index 81374f26645..95782b2c88a 100644 --- a/app/assets/javascripts/ide/components/ide_tree_list.vue +++ b/app/assets/javascripts/ide/components/ide_tree_list.vue @@ -54,14 +54,17 @@ export default { <slot name="header"></slot> </header> <div class="ide-tree-body h-100"> - <file-row - v-for="file in currentTree.tree" - :key="file.key" - :file="file" - :level="0" - :extra-component="$options.FileRowExtra" - @toggleTreeOpen="toggleTreeOpen" - /> + <template v-if="currentTree.tree.length"> + <file-row + v-for="file in currentTree.tree" + :key="file.key" + :file="file" + :level="0" + :extra-component="$options.FileRowExtra" + @toggleTreeOpen="toggleTreeOpen" + /> + </template> + <div v-else class="file-row">{{ __('No files') }}</div> </div> </template> </div> diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index fd678e6e10c..dc8ca732879 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -1,12 +1,15 @@ import $ from 'jquery'; import Vue from 'vue'; +import { __, sprintf } from '~/locale'; import { visitUrl } from '~/lib/utils/url_utility'; import flash from '~/flash'; +import _ from 'underscore'; import * as types from './mutation_types'; import { decorateFiles } from '../lib/files'; import { stageKeys } from '../constants'; +import service from '../services'; -export const redirectToUrl = (_, url) => visitUrl(url); +export const redirectToUrl = (self, url) => visitUrl(url); export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data); @@ -239,6 +242,53 @@ export const renameEntry = ( } }; +export const getBranchData = ({ commit, state }, { projectId, branchId, force = false } = {}) => + new Promise((resolve, reject) => { + const currentProject = state.projects[projectId]; + if (!currentProject || !currentProject.branches[branchId] || force) { + service + .getBranchData(projectId, branchId) + .then(({ data }) => { + const { id } = data.commit; + commit(types.SET_BRANCH, { + projectPath: projectId, + branchName: branchId, + branch: data, + }); + commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id }); + resolve(data); + }) + .catch(e => { + if (e.response.status === 404) { + reject(e); + } else { + flash( + __('Error loading branch data. Please try again.'), + 'alert', + document, + null, + false, + true, + ); + + reject( + new Error( + sprintf( + __('Branch not loaded - %{branchId}'), + { + branchId: `<strong>${_.escape(projectId)}/${_.escape(branchId)}</strong>`, + }, + false, + ), + ), + ); + } + }); + } else { + resolve(currentProject.branches[branchId]); + } + }); + export * from './actions/tree'; export * from './actions/file'; export * from './actions/project'; diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js index 4b10d148ebf..dd8f17e4f3a 100644 --- a/app/assets/javascripts/ide/stores/actions/project.js +++ b/app/assets/javascripts/ide/stores/actions/project.js @@ -35,48 +35,6 @@ export const getProjectData = ({ commit, state }, { namespace, projectId, force } }); -export const getBranchData = ( - { commit, dispatch, state }, - { projectId, branchId, force = false } = {}, -) => - new Promise((resolve, reject) => { - if ( - typeof state.projects[`${projectId}`] === 'undefined' || - !state.projects[`${projectId}`].branches[branchId] || - force - ) { - service - .getBranchData(`${projectId}`, branchId) - .then(({ data }) => { - const { id } = data.commit; - commit(types.SET_BRANCH, { - projectPath: `${projectId}`, - branchName: branchId, - branch: data, - }); - commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id }); - resolve(data); - }) - .catch(e => { - if (e.response.status === 404) { - dispatch('showBranchNotFoundError', branchId); - } else { - flash( - __('Error loading branch data. Please try again.'), - 'alert', - document, - null, - false, - true, - ); - } - reject(new Error(`Branch not loaded - ${projectId}/${branchId}`)); - }); - } else { - resolve(state.projects[`${projectId}`].branches[branchId]); - } - }); - export const refreshLastCommitData = ({ commit }, { projectId, branchId } = {}) => service .getBranchData(projectId, branchId) @@ -125,40 +83,66 @@ export const showBranchNotFoundError = ({ dispatch }, branchId) => { }); }; -export const openBranch = ({ dispatch, state }, { projectId, branchId, basePath }) => { - dispatch('setCurrentBranchId', branchId); - - dispatch('getBranchData', { - projectId, - branchId, +export const showEmptyState = ({ commit, state }, { projectId, branchId }) => { + const treePath = `${projectId}/${branchId}`; + commit(types.CREATE_TREE, { treePath }); + commit(types.TOGGLE_LOADING, { + entry: state.trees[treePath], + forceValue: false, }); +}; - return dispatch('getFiles', { +export const openBranch = ({ dispatch, state, getters }, { projectId, branchId, basePath }) => { + dispatch('setCurrentBranchId', branchId); + + if (getters.emptyRepo) { + return dispatch('showEmptyState', { projectId, branchId }); + } + return dispatch('getBranchData', { projectId, branchId, }) .then(() => { - if (basePath) { - const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath; - const treeEntryKey = Object.keys(state.entries).find( - key => key === path && !state.entries[key].pending, - ); - const treeEntry = state.entries[treeEntryKey]; - - if (treeEntry) { - dispatch('handleTreeEntryAction', treeEntry); - } else { - dispatch('createTempEntry', { - name: path, - type: 'blob', - }); - } - } - }) - .then(() => { dispatch('getMergeRequestsForBranch', { projectId, branchId, }); + dispatch('getFiles', { + projectId, + branchId, + }) + .then(() => { + if (basePath) { + const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath; + const treeEntryKey = Object.keys(state.entries).find( + key => key === path && !state.entries[key].pending, + ); + const treeEntry = state.entries[treeEntryKey]; + + if (treeEntry) { + dispatch('handleTreeEntryAction', treeEntry); + } else { + dispatch('createTempEntry', { + name: path, + type: 'blob', + }); + } + } + }) + .catch( + () => + new Error( + sprintf( + __('An error occurred whilst getting files for - %{branchId}'), + { + branchId: `<strong>${_.escape(projectId)}/${_.escape(branchId)}</strong>`, + }, + false, + ), + ), + ); + }) + .catch(() => { + dispatch('showBranchNotFoundError', branchId); }); }; diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js index 3d83e4a9ba5..75511574d3e 100644 --- a/app/assets/javascripts/ide/stores/actions/tree.js +++ b/app/assets/javascripts/ide/stores/actions/tree.js @@ -74,17 +74,13 @@ export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = resolve(); }) .catch(e => { - if (e.response.status === 404) { - dispatch('showBranchNotFoundError', branchId); - } else { - dispatch('setErrorMessage', { - text: __('An error occurred whilst loading all the files.'), - action: payload => - dispatch('getFiles', payload).then(() => dispatch('setErrorMessage', null)), - actionText: __('Please try again'), - actionPayload: { projectId, branchId }, - }); - } + dispatch('setErrorMessage', { + text: __('An error occurred whilst loading all the files.'), + action: payload => + dispatch('getFiles', payload).then(() => dispatch('setErrorMessage', null)), + actionText: __('Please try again'), + actionPayload: { projectId, branchId }, + }); reject(e); }); } else { diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js index 490658a4543..7a88ac5b116 100644 --- a/app/assets/javascripts/ide/stores/getters.js +++ b/app/assets/javascripts/ide/stores/getters.js @@ -36,6 +36,9 @@ export const currentMergeRequest = state => { export const currentProject = state => state.projects[state.currentProjectId]; +export const emptyRepo = state => + state.projects[state.currentProjectId] && state.projects[state.currentProjectId].empty_repo; + export const currentTree = state => state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index c2760eb1554..f81bdb8a30e 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -135,6 +135,17 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo return null; } + if (!data.parent_ids.length) { + commit( + rootTypes.TOGGLE_EMPTY_STATE, + { + projectPath: rootState.currentProjectId, + value: false, + }, + { root: true }, + ); + } + dispatch('setLastCommitMessage', data); dispatch('updateCommitMessage', ''); return dispatch('updateFilesAfterCommit', { diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index a5f8098dc17..86ab76136df 100644 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -12,6 +12,7 @@ export const SET_LINKS = 'SET_LINKS'; export const SET_PROJECT = 'SET_PROJECT'; export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT'; export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN'; +export const TOGGLE_EMPTY_STATE = 'TOGGLE_EMPTY_STATE'; // Merge Request Mutation Types export const SET_MERGE_REQUEST = 'SET_MERGE_REQUEST'; diff --git a/app/assets/javascripts/ide/stores/mutations/branch.js b/app/assets/javascripts/ide/stores/mutations/branch.js index e09f88878f4..6afd8de2aa4 100644 --- a/app/assets/javascripts/ide/stores/mutations/branch.js +++ b/app/assets/javascripts/ide/stores/mutations/branch.js @@ -19,6 +19,12 @@ export default { }); }, [types.SET_BRANCH_WORKING_REFERENCE](state, { projectId, branchId, reference }) { + if (!state.projects[projectId].branches[branchId]) { + Object.assign(state.projects[projectId].branches, { + [branchId]: {}, + }); + } + Object.assign(state.projects[projectId].branches[branchId], { workingReference: reference, }); diff --git a/app/assets/javascripts/ide/stores/mutations/project.js b/app/assets/javascripts/ide/stores/mutations/project.js index 284b39a2c72..9230f3839c1 100644 --- a/app/assets/javascripts/ide/stores/mutations/project.js +++ b/app/assets/javascripts/ide/stores/mutations/project.js @@ -21,4 +21,9 @@ export default { }), }); }, + [types.TOGGLE_EMPTY_STATE](state, { projectPath, value }) { + Object.assign(state.projects[projectPath], { + empty_repo: value, + }); + }, }; diff --git a/app/assets/javascripts/import_projects/store/getters.js b/app/assets/javascripts/import_projects/store/getters.js index f03474a8404..727b80765bd 100644 --- a/app/assets/javascripts/import_projects/store/getters.js +++ b/app/assets/javascripts/import_projects/store/getters.js @@ -1,3 +1,5 @@ +import { __ } from '~/locale'; + export const namespaceSelectOptions = state => { const serializedNamespaces = state.namespaces.map(({ fullPath }) => ({ id: fullPath, @@ -5,9 +7,9 @@ export const namespaceSelectOptions = state => { })); return [ - { text: 'Groups', children: serializedNamespaces }, + { text: __('Groups'), children: serializedNamespaces }, { - text: 'Users', + text: __('Users'), children: [{ id: state.defaultTargetNamespace, text: state.defaultTargetNamespace }], }, ]; diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue index 7594edfac27..79fb67d38cd 100644 --- a/app/assets/javascripts/jobs/components/job_app.vue +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -86,6 +86,7 @@ export default { 'isScrollTopDisabled', 'isScrolledToBottomBeforeReceivingTrace', 'hasError', + 'selectedStage', ]), ...mapGetters([ 'headerTime', @@ -121,7 +122,13 @@ export default { // fetch the stages for the dropdown on the sidebar job(newVal, oldVal) { if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) { - this.fetchStages(); + const stages = this.job.pipeline.details.stages || []; + + const defaultStage = stages.find(stage => stage && stage.name === this.selectedStage); + + if (defaultStage) { + this.fetchJobsForStage(defaultStage); + } } if (newVal.archived) { @@ -160,7 +167,7 @@ export default { 'setJobEndpoint', 'setTraceOptions', 'fetchJob', - 'fetchStages', + 'fetchJobsForStage', 'hideSidebar', 'showSidebar', 'toggleSidebar', @@ -269,7 +276,6 @@ export default { :class="{ 'sticky-top border-bottom-0': hasTrace }" > <icon name="lock" class="align-text-bottom" /> - {{ __('This job is archived. Only the complete pipeline can be retried.') }} </div> <!-- job log --> diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue index 1691ac62100..24276c06486 100644 --- a/app/assets/javascripts/jobs/components/sidebar.vue +++ b/app/assets/javascripts/jobs/components/sidebar.vue @@ -34,7 +34,7 @@ export default { }, }, computed: { - ...mapState(['job', 'stages', 'jobs', 'selectedStage', 'isLoadingStages']), + ...mapState(['job', 'stages', 'jobs', 'selectedStage']), coverage() { return `${this.job.coverage}%`; }, @@ -208,7 +208,6 @@ export default { /> <stages-dropdown - v-if="!isLoadingStages" :stages="stages" :pipeline="job.pipeline" :selected-stage="selectedStage" diff --git a/app/assets/javascripts/jobs/store/actions.js b/app/assets/javascripts/jobs/store/actions.js index 8045f6dc3ff..12d67a43599 100644 --- a/app/assets/javascripts/jobs/store/actions.js +++ b/app/assets/javascripts/jobs/store/actions.js @@ -179,37 +179,13 @@ export const receiveTraceError = ({ commit }) => { }; /** - * Stages dropdown on sidebar - */ -export const requestStages = ({ commit }) => commit(types.REQUEST_STAGES); -export const fetchStages = ({ state, dispatch }) => { - dispatch('requestStages'); - - axios - .get(`${state.job.pipeline.path}.json`) - .then(({ data }) => { - // Set selected stage - dispatch('receiveStagesSuccess', data.details.stages); - const selectedStage = data.details.stages.find(stage => stage.name === state.selectedStage); - dispatch('fetchJobsForStage', selectedStage); - }) - .catch(() => dispatch('receiveStagesError')); -}; -export const receiveStagesSuccess = ({ commit }, data) => - commit(types.RECEIVE_STAGES_SUCCESS, data); -export const receiveStagesError = ({ commit }) => { - commit(types.RECEIVE_STAGES_ERROR); - flash(__('An error occurred while fetching stages.')); -}; - -/** * Jobs list on sidebar - depend on stages dropdown */ export const requestJobsForStage = ({ commit }, stage) => commit(types.REQUEST_JOBS_FOR_STAGE, stage); // On stage click, set selected stage + fetch job -export const fetchJobsForStage = ({ dispatch }, stage) => { +export const fetchJobsForStage = ({ dispatch }, stage = {}) => { dispatch('requestJobsForStage', stage); axios diff --git a/app/assets/javascripts/jobs/store/mutation_types.js b/app/assets/javascripts/jobs/store/mutation_types.js index fd098f13e90..39146b2eefd 100644 --- a/app/assets/javascripts/jobs/store/mutation_types.js +++ b/app/assets/javascripts/jobs/store/mutation_types.js @@ -24,10 +24,6 @@ export const STOP_POLLING_TRACE = 'STOP_POLLING_TRACE'; export const RECEIVE_TRACE_SUCCESS = 'RECEIVE_TRACE_SUCCESS'; export const RECEIVE_TRACE_ERROR = 'RECEIVE_TRACE_ERROR'; -export const REQUEST_STAGES = 'REQUEST_STAGES'; -export const RECEIVE_STAGES_SUCCESS = 'RECEIVE_STAGES_SUCCESS'; -export const RECEIVE_STAGES_ERROR = 'RECEIVE_STAGES_ERROR'; - export const SET_SELECTED_STAGE = 'SET_SELECTED_STAGE'; export const REQUEST_JOBS_FOR_STAGE = 'REQUEST_JOBS_FOR_STAGE'; export const RECEIVE_JOBS_FOR_STAGE_SUCCESS = 'RECEIVE_JOBS_FOR_STAGE_SUCCESS'; diff --git a/app/assets/javascripts/jobs/store/mutations.js b/app/assets/javascripts/jobs/store/mutations.js index cd440d21c1f..ad08f27b147 100644 --- a/app/assets/javascripts/jobs/store/mutations.js +++ b/app/assets/javascripts/jobs/store/mutations.js @@ -65,6 +65,11 @@ export default { state.isLoading = false; state.job = job; + state.stages = + job.pipeline && job.pipeline.details && job.pipeline.details.stages + ? job.pipeline.details.stages + : []; + /** * We only update it on the first request * The dropdown can be changed by the user @@ -101,19 +106,7 @@ export default { state.isScrolledToBottomBeforeReceivingTrace = toggle; }, - [types.REQUEST_STAGES](state) { - state.isLoadingStages = true; - }, - [types.RECEIVE_STAGES_SUCCESS](state, stages) { - state.isLoadingStages = false; - state.stages = stages; - }, - [types.RECEIVE_STAGES_ERROR](state) { - state.isLoadingStages = false; - state.stages = []; - }, - - [types.REQUEST_JOBS_FOR_STAGE](state, stage) { + [types.REQUEST_JOBS_FOR_STAGE](state, stage = {}) { state.isLoadingJobs = true; state.selectedStage = stage.name; }, diff --git a/app/assets/javascripts/jobs/store/state.js b/app/assets/javascripts/jobs/store/state.js index 04825187c99..6019214e62c 100644 --- a/app/assets/javascripts/jobs/store/state.js +++ b/app/assets/javascripts/jobs/store/state.js @@ -25,7 +25,6 @@ export default () => ({ traceState: null, // sidebar dropdown & list of jobs - isLoadingStages: false, isLoadingJobs: false, selectedStage: '', stages: [], diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js index 4d304c5fe69..c6dd21cd2d4 100644 --- a/app/assets/javascripts/label_manager.js +++ b/app/assets/javascripts/label_manager.js @@ -5,13 +5,14 @@ import Sortable from 'sortablejs'; import flash from './flash'; import axios from './lib/utils/axios_utils'; +import { __ } from './locale'; export default class LabelManager { constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) { this.togglePriorityButton = togglePriorityButton || $('.js-toggle-priority'); this.prioritizedLabels = prioritizedLabels || $('.js-prioritized-labels'); this.otherLabels = otherLabels || $('.js-other-labels'); - this.errorMessage = 'Unable to update label prioritization at this time'; + this.errorMessage = __('Unable to update label prioritization at this time'); this.emptyState = document.querySelector('#js-priority-labels-empty-state'); this.$badgeItemTemplate = $('#js-badge-item-template'); diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 2c30b4ea587..3f954b43ee3 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -4,7 +4,7 @@ import $ from 'jquery'; import _ from 'underscore'; -import { sprintf, __ } from './locale'; +import { sprintf, s__, __ } from './locale'; import axios from './lib/utils/axios_utils'; import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; import CreateLabelDropdown from './create_label'; @@ -178,7 +178,7 @@ export default class LabelsSelect { }); } } else { - template = '<span class="no-value">None</span>'; + template = `<span class="no-value">${__('None')}</span>`; } $value.removeAttr('style').html(template); $sidebarCollapsedValue.text(labelCount); @@ -190,7 +190,9 @@ export default class LabelsSelect { if (labelTitles.length > 5) { labelTitles = labelTitles.slice(0, 5); - labelTitles.push('and ' + (data.labels.length - 5) + ' more'); + labelTitles.push( + sprintf(s__('Labels|and %{count} more'), { count: data.labels.length - 5 }), + ); } labelTooltipTitle = labelTitles.join(', '); @@ -219,13 +221,13 @@ export default class LabelsSelect { if (showNo) { extraData.unshift({ id: 0, - title: 'No Label', + title: __('No Label'), }); } if (showAny) { extraData.unshift({ isAny: true, - title: 'Any Label', + title: __('Any Label'), }); } if (extraData.length) { @@ -341,7 +343,7 @@ export default class LabelsSelect { if (selected && selected.id === 0) { this.selected = []; - return 'No Label'; + return __('No Label'); } else if (isSelected) { this.selected.push(title); } else if (!isSelected && title) { @@ -579,7 +581,7 @@ export default class LabelsSelect { if ($('.selected-issuable:checked').length) { return; } - return $('.issues-bulk-update .labels-filter .dropdown-toggle-text').text('Label'); + return $('.issues-bulk-update .labels-filter .dropdown-toggle-text').text(__('Label')); } // eslint-disable-next-line class-methods-use-this enableBulkLabelDropdown() { diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js index 498c2348ca2..47e91dedd5a 100644 --- a/app/assets/javascripts/lib/graphql.js +++ b/app/assets/javascripts/lib/graphql.js @@ -3,12 +3,12 @@ import { InMemoryCache } from 'apollo-cache-inmemory'; import { createUploadLink } from 'apollo-upload-client'; import csrf from '~/lib/utils/csrf'; -export default (resolvers = {}, baseUrl = '') => { +export default (resolvers = {}, config = {}) => { let uri = `${gon.relative_url_root}/api/graphql`; - if (baseUrl) { + if (config.baseUrl) { // Prepend baseUrl and ensure that `///` are replaced with `/` - uri = `${baseUrl}${uri}`.replace(/\/{3,}/g, '/'); + uri = `${config.baseUrl}${uri}`.replace(/\/{3,}/g, '/'); } return new ApolloClient({ @@ -18,7 +18,7 @@ export default (resolvers = {}, baseUrl = '') => { [csrf.headerKey]: csrf.token, }, }), - cache: new InMemoryCache(), + cache: new InMemoryCache(config.cacheConfig), resolvers, }); }; diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 4d6327840db..32cafb74d91 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -3,7 +3,7 @@ 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__, __ } from '../../locale'; window.timeago = timeago; @@ -63,7 +63,15 @@ export const pad = (val, len = 2) => `0${val}`.slice(-len); * @returns {String} */ export const getDayName = date => - ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][date.getDay()]; + [ + __('Sunday'), + __('Monday'), + __('Tuesday'), + __('Wednesday'), + __('Thursday'), + __('Friday'), + __('Saturday'), + ][date.getDay()]; /** * @example @@ -71,7 +79,12 @@ export const getDayName = date => * @param {date} datetime * @returns {String} */ -export const formatDate = datetime => dateFormat(datetime, 'mmm d, yyyy h:MMtt Z'); +export const formatDate = datetime => { + if (_.isString(datetime) && datetime.match(/\d+-\d+\d+ /)) { + throw new Error('Invalid date'); + } + return dateFormat(datetime, 'mmm d, yyyy h:MMtt Z'); +}; /** * Timeago uses underscores instead of dashes to separate language from country code. @@ -320,13 +333,13 @@ export const getSundays = date => { } const daysToSunday = [ - 'Saturday', - 'Friday', - 'Thursday', - 'Wednesday', - 'Tuesday', - 'Monday', - 'Sunday', + __('Saturday'), + __('Friday'), + __('Thursday'), + __('Wednesday'), + __('Tuesday'), + __('Monday'), + __('Sunday'), ]; const month = date.getMonth(); @@ -336,7 +349,7 @@ export const getSundays = date => { while (dateOfMonth.getMonth() === month) { const dayName = getDayName(dateOfMonth); - if (dayName === 'Sunday') { + if (dayName === __('Sunday')) { sundays.push(new Date(dateOfMonth.getTime())); } diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js index 19c4de6083d..9ddfb4bca11 100644 --- a/app/assets/javascripts/lib/utils/number_utils.js +++ b/app/assets/javascripts/lib/utils/number_utils.js @@ -1,4 +1,5 @@ import { BYTES_IN_KIB } from './constants'; +import { sprintf, __ } from '~/locale'; /** * Function that allows a number with an X amount of decimals @@ -72,13 +73,13 @@ export function bytesToGiB(number) { */ export function numberToHumanSize(size) { if (size < BYTES_IN_KIB) { - return `${size} bytes`; + return sprintf(__('%{size} bytes'), { size }); } else if (size < BYTES_IN_KIB * BYTES_IN_KIB) { - return `${bytesToKiB(size).toFixed(2)} KiB`; + return sprintf(__('%{size} KiB'), { size: bytesToKiB(size).toFixed(2) }); } else if (size < BYTES_IN_KIB * BYTES_IN_KIB * BYTES_IN_KIB) { - return `${bytesToMiB(size).toFixed(2)} MiB`; + return sprintf(__('%{size} MiB'), { size: bytesToMiB(size).toFixed(2) }); } - return `${bytesToGiB(size).toFixed(2)} GiB`; + return sprintf(__('%{size} GiB'), { size: bytesToGiB(size).toFixed(2) }); } /** diff --git a/app/assets/javascripts/locale/index.js b/app/assets/javascripts/locale/index.js index 1ae3362c4bc..41aa0f4ddb9 100644 --- a/app/assets/javascripts/locale/index.js +++ b/app/assets/javascripts/locale/index.js @@ -11,7 +11,7 @@ delete window.translations; @param text The text to be translated @returns {String} The translated text */ -const gettext = text => locale.gettext.bind(locale)(ensureSingleLine(text)); +const gettext = text => locale.gettext(ensureSingleLine(text)); /** Translate the text with a number diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue index afe8d87a8d6..c43791f2426 100644 --- a/app/assets/javascripts/monitoring/components/charts/area.vue +++ b/app/assets/javascripts/monitoring/components/charts/area.vue @@ -125,17 +125,17 @@ export default { }, earliestDatapoint() { return this.chartData.reduce((acc, series) => { - if (!series.data.length) { + const { data } = series; + const { length } = data; + if (!length) { return acc; } - const [[timestamp]] = series.data.sort(([a], [b]) => { - if (a < b) { - return -1; - } - return a > b ? 1 : 0; - }); - return timestamp < acc || acc === null ? timestamp : acc; + const [first] = data[0]; + const [last] = data[length - 1]; + const seriesEarliest = first < last ? first : last; + + return seriesEarliest < acc || acc === null ? seriesEarliest : acc; }, null); }, isMultiSeries() { diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index ff1e1805948..a55a47c277d 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -8,16 +8,14 @@ import { GlLink, } from '@gitlab/ui'; import _ from 'underscore'; +import { mapActions, mapState } from 'vuex'; import { s__ } from '~/locale'; import Icon from '~/vue_shared/components/icon.vue'; import '~/vue_shared/mixins/is_ee'; import { getParameterValues } from '~/lib/utils/url_utility'; -import Flash from '../../flash'; -import MonitoringService from '../services/monitoring_service'; import MonitorAreaChart from './charts/area.vue'; import GraphGroup from './graph_group.vue'; import EmptyState from './empty_state.vue'; -import MonitoringStore from '../stores/monitoring_store'; import { timeWindows, timeWindowsKeyNames } from '../constants'; import { getTimeDiff } from '../utils'; @@ -128,9 +126,7 @@ export default { }, data() { return { - store: new MonitoringStore(), state: 'gettingStarted', - showEmptyState: true, elWidth: 0, selectedTimeWindow: '', selectedTimeWindowKey: '', @@ -141,13 +137,21 @@ export default { canAddMetrics() { return this.customMetricsAvailable && this.customMetricsPath.length; }, + ...mapState('monitoringDashboard', [ + 'groups', + 'emptyState', + 'showEmptyState', + 'environments', + 'deploymentData', + ]), }, created() { - this.service = new MonitoringService({ + this.setEndpoints({ metricsEndpoint: this.metricsEndpoint, - deploymentEndpoint: this.deploymentEndpoint, environmentsEndpoint: this.environmentsEndpoint, + deploymentsEndpoint: this.deploymentEndpoint, }); + this.timeWindows = timeWindows; this.selectedTimeWindowKey = _.escape(getParameterValues('time_window')[0]) || timeWindowsKeyNames.eightHours; @@ -165,31 +169,11 @@ export default { } }, mounted() { - const startEndWindow = getTimeDiff(this.timeWindows[this.selectedTimeWindowKey]); - this.servicePromises = [ - this.service - .getGraphsData(startEndWindow) - .then(data => this.store.storeMetrics(data)) - .catch(() => Flash(s__('Metrics|There was an error while retrieving metrics'))), - this.service - .getDeploymentData() - .then(data => this.store.storeDeploymentData(data)) - .catch(() => Flash(s__('Metrics|There was an error getting deployment information.'))), - ]; if (!this.hasMetrics) { - this.state = 'gettingStarted'; + this.setGettingStartedEmptyState(); } else { - if (this.environmentsEndpoint) { - this.servicePromises.push( - this.service - .getEnvironmentsData() - .then(data => this.store.storeEnvironmentsData(data)) - .catch(() => - Flash(s__('Metrics|There was an error getting environments information.')), - ), - ); - } - this.getGraphsData(); + this.fetchData(getTimeDiff(this.timeWindows.eightHours)); + sidebarMutationObserver = new MutationObserver(this.onSidebarMutation); sidebarMutationObserver.observe(document.querySelector('.layout-page'), { attributes: true, @@ -199,6 +183,11 @@ export default { } }, methods: { + ...mapActions('monitoringDashboard', [ + 'fetchData', + 'setGettingStartedEmptyState', + 'setEndpoints', + ]), getGraphAlerts(queries) { if (!this.allAlerts) return {}; const metricIdsForChart = queries.map(q => q.metricId); @@ -207,21 +196,6 @@ export default { getGraphAlertValues(queries) { return Object.values(this.getGraphAlerts(queries)); }, - getGraphsData() { - this.state = 'loading'; - Promise.all(this.servicePromises) - .then(() => { - if (this.store.groups.length < 1) { - this.state = 'noData'; - return; - } - - this.showEmptyState = false; - }) - .catch(() => { - this.state = 'unableToConnect'; - }); - }, hideAddMetricModal() { this.$refs.addMetricModal.hide(); }, @@ -263,10 +237,10 @@ export default { class="prepend-left-10 js-environments-dropdown" toggle-class="dropdown-menu-toggle" :text="currentEnvironmentName" - :disabled="store.environmentsData.length === 0" + :disabled="environments.length === 0" > <gl-dropdown-item - v-for="environment in store.environmentsData" + v-for="environment in environments" :key="environment.id" :active="environment.name === currentEnvironmentName" active-class="is-active" @@ -336,7 +310,7 @@ export default { </div> </div> <graph-group - v-for="(groupData, index) in store.groups" + v-for="(groupData, index) in groups" :key="index" :name="groupData.group" :show-panels="showPanels" @@ -345,7 +319,7 @@ export default { v-for="(graphData, graphIndex) in groupData.metrics" :key="graphIndex" :graph-data="graphData" - :deployment-data="store.deploymentData" + :deployment-data="deploymentData" :thresholds="getGraphAlertValues(graphData.queries)" :container-width="elWidth" group-id="monitor-area-chart" @@ -362,7 +336,7 @@ export default { </div> <empty-state v-else - :selected-state="state" + :selected-state="emptyState" :documentation-path="documentationPath" :settings-path="settingsPath" :clusters-path="clustersPath" diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js index 08dc57d545c..57771ccf4d9 100644 --- a/app/assets/javascripts/monitoring/monitoring_bundle.js +++ b/app/assets/javascripts/monitoring/monitoring_bundle.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import { parseBoolean } from '~/lib/utils/common_utils'; import Dashboard from 'ee_else_ce/monitoring/components/dashboard.vue'; +import store from './stores'; export default (props = {}) => { const el = document.getElementById('prometheus-graphs'); @@ -9,6 +10,7 @@ export default (props = {}) => { // eslint-disable-next-line no-new new Vue({ el, + store, render(createElement) { return createElement(Dashboard, { props: { diff --git a/app/assets/javascripts/monitoring/services/monitoring_service.js b/app/assets/javascripts/monitoring/services/monitoring_service.js deleted file mode 100644 index 1efa5189996..00000000000 --- a/app/assets/javascripts/monitoring/services/monitoring_service.js +++ /dev/null @@ -1,75 +0,0 @@ -import axios from '../../lib/utils/axios_utils'; -import statusCodes from '../../lib/utils/http_status'; -import { backOff } from '../../lib/utils/common_utils'; -import { s__, __ } from '../../locale'; - -const MAX_REQUESTS = 3; - -function backOffRequest(makeRequestCallback) { - let requestCounter = 0; - return backOff((next, stop) => { - makeRequestCallback() - .then(resp => { - if (resp.status === statusCodes.NO_CONTENT) { - requestCounter += 1; - if (requestCounter < MAX_REQUESTS) { - next(); - } else { - stop(new Error(__('Failed to connect to the prometheus server'))); - } - } else { - stop(resp); - } - }) - .catch(stop); - }); -} - -export default class MonitoringService { - constructor({ metricsEndpoint, deploymentEndpoint, environmentsEndpoint }) { - this.metricsEndpoint = metricsEndpoint; - this.deploymentEndpoint = deploymentEndpoint; - this.environmentsEndpoint = environmentsEndpoint; - } - - getGraphsData(params = {}) { - return backOffRequest(() => axios.get(this.metricsEndpoint, { params })) - .then(resp => resp.data) - .then(response => { - if (!response || !response.data || !response.success) { - throw new Error(s__('Metrics|Unexpected metrics data response from prometheus endpoint')); - } - return response.data; - }); - } - - getDeploymentData() { - if (!this.deploymentEndpoint) { - return Promise.resolve([]); - } - return backOffRequest(() => axios.get(this.deploymentEndpoint)) - .then(resp => resp.data) - .then(response => { - if (!response || !response.deployments) { - throw new Error( - s__('Metrics|Unexpected deployment data response from prometheus endpoint'), - ); - } - return response.deployments; - }); - } - - getEnvironmentsData() { - return axios - .get(this.environmentsEndpoint) - .then(resp => resp.data) - .then(response => { - if (!response || !response.environments) { - throw new Error( - s__('Metrics|There was an error fetching the environments data, please try again'), - ); - } - return response.environments; - }); - } -} diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js new file mode 100644 index 00000000000..63c23e8449d --- /dev/null +++ b/app/assets/javascripts/monitoring/stores/actions.js @@ -0,0 +1,117 @@ +import * as types from './mutation_types'; +import axios from '~/lib/utils/axios_utils'; +import createFlash from '~/flash'; +import statusCodes from '../../lib/utils/http_status'; +import { backOff } from '../../lib/utils/common_utils'; +import { s__, __ } from '../../locale'; + +const MAX_REQUESTS = 3; + +function backOffRequest(makeRequestCallback) { + let requestCounter = 0; + return backOff((next, stop) => { + makeRequestCallback() + .then(resp => { + if (resp.status === statusCodes.NO_CONTENT) { + requestCounter += 1; + if (requestCounter < MAX_REQUESTS) { + next(); + } else { + stop(new Error(__('Failed to connect to the prometheus server'))); + } + } else { + stop(resp); + } + }) + .catch(stop); + }); +} + +export const setGettingStartedEmptyState = ({ commit }) => { + commit(types.SET_GETTING_STARTED_EMPTY_STATE); +}; + +export const setEndpoints = ({ commit }, endpoints) => { + commit(types.SET_ENDPOINTS, endpoints); +}; + +export const requestMetricsData = ({ commit }) => commit(types.REQUEST_METRICS_DATA); +export const receiveMetricsDataSuccess = ({ commit }, data) => + commit(types.RECEIVE_METRICS_DATA_SUCCESS, data); +export const receiveMetricsDataFailure = ({ commit }, error) => + commit(types.RECEIVE_METRICS_DATA_FAILURE, error); +export const receiveDeploymentsDataSuccess = ({ commit }, data) => + commit(types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS, data); +export const receiveDeploymentsDataFailure = ({ commit }) => + commit(types.RECEIVE_DEPLOYMENTS_DATA_FAILURE); +export const receiveEnvironmentsDataSuccess = ({ commit }, data) => + commit(types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS, data); +export const receiveEnvironmentsDataFailure = ({ commit }) => + commit(types.RECEIVE_ENVIRONMENTS_DATA_FAILURE); + +export const fetchData = ({ dispatch }, params) => { + dispatch('fetchMetricsData', params); + dispatch('fetchDeploymentsData'); + dispatch('fetchEnvironmentsData'); +}; + +export const fetchMetricsData = ({ state, dispatch }, params) => { + dispatch('requestMetricsData'); + + return backOffRequest(() => axios.get(state.metricsEndpoint, { params })) + .then(resp => resp.data) + .then(response => { + if (!response || !response.data || !response.success) { + dispatch('receiveMetricsDataFailure', null); + createFlash(s__('Metrics|Unexpected metrics data response from prometheus endpoint')); + } + dispatch('receiveMetricsDataSuccess', response.data); + }) + .catch(error => { + dispatch('receiveMetricsDataFailure', error); + createFlash(s__('Metrics|There was an error while retrieving metrics')); + }); +}; + +export const fetchDeploymentsData = ({ state, dispatch }) => { + if (!state.deploymentEndpoint) { + return Promise.resolve([]); + } + return backOffRequest(() => axios.get(state.deploymentEndpoint)) + .then(resp => resp.data) + .then(response => { + if (!response || !response.deployments) { + createFlash(s__('Metrics|Unexpected deployment data response from prometheus endpoint')); + } + + dispatch('receiveDeploymentsDataSuccess', response.deployments); + }) + .catch(() => { + dispatch('receiveDeploymentsDataFailure'); + createFlash(s__('Metrics|There was an error getting deployment information.')); + }); +}; + +export const fetchEnvironmentsData = ({ state, dispatch }) => { + if (!state.environmentsEndpoint) { + return Promise.resolve([]); + } + return axios + .get(state.environmentsEndpoint) + .then(resp => resp.data) + .then(response => { + if (!response || !response.environments) { + createFlash( + s__('Metrics|There was an error fetching the environments data, please try again'), + ); + } + dispatch('receiveEnvironmentsDataSuccess', response.environments); + }) + .catch(() => { + dispatch('receiveEnvironmentsDataFailure'); + createFlash(s__('Metrics|There was an error getting environments information.')); + }); +}; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/monitoring/stores/index.js b/app/assets/javascripts/monitoring/stores/index.js new file mode 100644 index 00000000000..d58398c54ae --- /dev/null +++ b/app/assets/javascripts/monitoring/stores/index.js @@ -0,0 +1,21 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import * as actions from './actions'; +import mutations from './mutations'; +import state from './state'; + +Vue.use(Vuex); + +export const createStore = () => + new Vuex.Store({ + modules: { + monitoringDashboard: { + namespaced: true, + actions, + mutations, + state, + }, + }, + }); + +export default createStore(); diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js new file mode 100644 index 00000000000..74c4ae64712 --- /dev/null +++ b/app/assets/javascripts/monitoring/stores/mutation_types.js @@ -0,0 +1,15 @@ +export const REQUEST_METRICS_DATA = 'REQUEST_METRICS_DATA'; +export const RECEIVE_METRICS_DATA_SUCCESS = 'RECEIVE_METRICS_DATA_SUCCESS'; +export const RECEIVE_METRICS_DATA_FAILURE = 'RECEIVE_METRICS_DATA_FAILURE'; +export const REQUEST_DEPLOYMENTS_DATA = 'REQUEST_DEPLOYMENTS_DATA'; +export const RECEIVE_DEPLOYMENTS_DATA_SUCCESS = 'RECEIVE_DEPLOYMENTS_DATA_SUCCESS'; +export const RECEIVE_DEPLOYMENTS_DATA_FAILURE = 'RECEIVE_DEPLOYMENTS_DATA_FAILURE'; +export const REQUEST_ENVIRONMENTS_DATA = 'REQUEST_ENVIRONMENTS_DATA'; +export const RECEIVE_ENVIRONMENTS_DATA_SUCCESS = 'RECEIVE_ENVIRONMENTS_DATA_SUCCESS'; +export const RECEIVE_ENVIRONMENTS_DATA_FAILURE = 'RECEIVE_ENVIRONMENTS_DATA_FAILURE'; +export const SET_TIME_WINDOW = 'SET_TIME_WINDOW'; +export const SET_METRICS_ENDPOINT = 'SET_METRICS_ENDPOINT'; +export const SET_ENVIRONMENTS_ENDPOINT = 'SET_ENVIRONMENTS_ENDPOINT'; +export const SET_DEPLOYMENTS_ENDPOINT = 'SET_DEPLOYMENTS_ENDPOINT'; +export const SET_ENDPOINTS = 'SET_ENDPOINTS'; +export const SET_GETTING_STARTED_EMPTY_STATE = 'SET_GETTING_STARTED_EMPTY_STATE'; diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js new file mode 100644 index 00000000000..c1779333d75 --- /dev/null +++ b/app/assets/javascripts/monitoring/stores/mutations.js @@ -0,0 +1,45 @@ +import * as types from './mutation_types'; +import { normalizeMetrics, sortMetrics } from './utils'; + +export default { + [types.REQUEST_METRICS_DATA](state) { + state.emptyState = 'loading'; + state.showEmptyState = true; + }, + [types.RECEIVE_METRICS_DATA_SUCCESS](state, groupData) { + state.groups = groupData.map(group => ({ + ...group, + metrics: normalizeMetrics(sortMetrics(group.metrics)), + })); + + if (!state.groups.length) { + state.emptyState = 'noData'; + } else { + state.showEmptyState = false; + } + }, + [types.RECEIVE_METRICS_DATA_FAILURE](state, error) { + state.emptyState = error ? 'unableToConnect' : 'noData'; + state.showEmptyState = true; + }, + [types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS](state, deployments) { + state.deploymentData = deployments; + }, + [types.RECEIVE_DEPLOYMENTS_DATA_FAILURE](state) { + state.deploymentData = []; + }, + [types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS](state, environments) { + state.environments = environments; + }, + [types.RECEIVE_ENVIRONMENTS_DATA_FAILURE](state) { + state.environments = []; + }, + [types.SET_ENDPOINTS](state, endpoints) { + state.metricsEndpoint = endpoints.metricsEndpoint; + state.environmentsEndpoint = endpoints.environmentsEndpoint; + state.deploymentsEndpoint = endpoints.deploymentsEndpoint; + }, + [types.SET_GETTING_STARTED_EMPTY_STATE](state) { + state.emptyState = 'gettingStarted'; + }, +}; diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js new file mode 100644 index 00000000000..5103122612a --- /dev/null +++ b/app/assets/javascripts/monitoring/stores/state.js @@ -0,0 +1,12 @@ +export default () => ({ + hasMetrics: false, + showPanels: true, + metricsEndpoint: null, + environmentsEndpoint: null, + deploymentsEndpoint: null, + emptyState: 'gettingStarted', + showEmptyState: true, + groups: [], + deploymentData: [], + environments: [], +}); diff --git a/app/assets/javascripts/monitoring/stores/monitoring_store.js b/app/assets/javascripts/monitoring/stores/utils.js index 013fb0d4540..9216554ecbf 100644 --- a/app/assets/javascripts/monitoring/stores/monitoring_store.js +++ b/app/assets/javascripts/monitoring/stores/utils.js @@ -1,12 +1,5 @@ import _ from 'underscore'; -function sortMetrics(metrics) { - return _.chain(metrics) - .sortBy('title') - .sortBy('weight') - .value(); -} - function checkQueryEmptyData(query) { return { ...query, @@ -59,7 +52,13 @@ function groupQueriesByChartInfo(metrics) { return Object.values(metricsByChart); } -function normalizeMetrics(metrics) { +export const sortMetrics = metrics => + _.chain(metrics) + .sortBy('title') + .sortBy('weight') + .value(); + +export const normalizeMetrics = metrics => { const groupedMetrics = groupQueriesByChartInfo(metrics); return groupedMetrics.map(metric => { @@ -81,31 +80,4 @@ function normalizeMetrics(metrics) { queries: removeTimeSeriesNoData(queries), }; }); -} - -export default class MonitoringStore { - constructor() { - this.groups = []; - this.deploymentData = []; - this.environmentsData = []; - } - - storeMetrics(groups = []) { - this.groups = groups.map(group => ({ - ...group, - metrics: normalizeMetrics(sortMetrics(group.metrics)), - })); - } - - storeDeploymentData(deploymentData = []) { - this.deploymentData = deploymentData; - } - - storeEnvironmentsData(environmentsData = []) { - this.environmentsData = environmentsData.filter(environment => !!environment.last_deployment); - } - - getMetricsCount() { - return this.groups.reduce((count, group) => count + group.metrics.length, 0); - } -} +}; diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index 7e8f23d6a96..5a4ff15d198 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -148,12 +148,9 @@ export default { href="#" title="Add reaction" > - <icon - css-classes="link-highlight award-control-icon-neutral" - name="emoji_slightly_smiling_face" - /> - <icon css-classes="link-highlight award-control-icon-positive" name="emoji_smiley" /> - <icon css-classes="link-highlight award-control-icon-super-positive" name="emoji_smiley" /> + <icon css-classes="link-highlight award-control-icon-neutral" name="slight-smile" /> + <icon css-classes="link-highlight award-control-icon-positive" name="smiley" /> + <icon css-classes="link-highlight award-control-icon-super-positive" name="smiley" /> </a> </div> <reply-button diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue index 17e5fcab5b7..941b6d5cab3 100644 --- a/app/assets/javascripts/notes/components/note_awards_list.vue +++ b/app/assets/javascripts/notes/components/note_awards_list.vue @@ -189,13 +189,13 @@ export default { type="button" > <span class="award-control-icon award-control-icon-neutral"> - <icon name="emoji_slightly_smiling_face" /> + <icon name="slight-smile" /> </span> <span class="award-control-icon award-control-icon-positive"> - <icon name="emoji_smiley" /> + <icon name="smiley" /> </span> <span class="award-control-icon award-control-icon-super-positive"> - <icon name="emoji_smiley" /> + <icon name="smiley" /> </span> <i aria-hidden="true" diff --git a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue index a79ef07f1c5..c563514d36b 100644 --- a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue +++ b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue @@ -33,8 +33,7 @@ export default { text() { return sprintf( s__(`Milestones|Promoting %{milestoneTitle} will make it available for all projects inside %{groupName}. - Existing project milestones with the same title will be merged. - This action cannot be reversed.`), + Existing project milestones with the same title will be merged.`), { milestoneTitle: this.milestoneTitle, groupName: this.groupName }, ); }, @@ -72,6 +71,9 @@ export default { <template slot="title"> {{ title }} </template> - {{ text }} + <div> + <p>{{ text }}</p> + <p>{{ s__('Milestones|This action cannot be reversed.') }}</p> + </div> </gl-modal> </template> diff --git a/app/assets/javascripts/pdf/index.vue b/app/assets/javascripts/pdf/index.vue index dc5f9ba9607..6d39abd4a1f 100644 --- a/app/assets/javascripts/pdf/index.vue +++ b/app/assets/javascripts/pdf/index.vue @@ -1,6 +1,6 @@ <script> -import pdfjsLib from 'vendor/pdf'; -import workerSrc from 'vendor/pdf.worker.min'; +import pdfjsLib from 'pdfjs-dist/build/pdf'; +import workerSrc from 'pdfjs-dist/build/pdf.worker.min'; import page from './page/index.vue'; diff --git a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue index 482898b80c4..ebd7a17040a 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue @@ -69,7 +69,9 @@ export default { > <ci-icon :status="group.status" /> - <span class="ci-status-text"> {{ group.name }} </span> + <span class="ci-status-text text-truncate mw-70p gl-pl-1 d-inline-block align-bottom"> + {{ group.name }} + </span> <span class="dropdown-counter-badge"> {{ group.size }} </span> </button> diff --git a/app/assets/javascripts/project_label_subscription.js b/app/assets/javascripts/project_label_subscription.js index d3c604dcee1..5395e14cc79 100644 --- a/app/assets/javascripts/project_label_subscription.js +++ b/app/assets/javascripts/project_label_subscription.js @@ -38,9 +38,9 @@ export default class ProjectLabelSubscription { let newAction; if (oldStatus === 'unsubscribed') { - [newStatus, newAction] = ['subscribed', 'Unsubscribe']; + [newStatus, newAction] = ['subscribed', __('Unsubscribe')]; } else { - [newStatus, newAction] = ['unsubscribed', 'Subscribe']; + [newStatus, newAction] = ['unsubscribed', __('Subscribe')]; } $btn.removeClass('disabled'); diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index 5ee510eb11d..dbe354a547b 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -3,6 +3,7 @@ import $ from 'jquery'; import Api from './api'; import ProjectSelectComboButton from './project_select_combo_button'; +import { s__ } from './locale'; export default function projectSelect() { import(/* webpackChunkName: 'select2' */ 'select2/select2') @@ -21,9 +22,9 @@ export default function projectSelect() { this.includeProjectsInSubgroups = $(select).data('includeProjectsInSubgroups') || false; this.allowClear = $(select).data('allowClear') || false; - placeholder = 'Search for project'; + placeholder = s__('ProjectSelect|Search for project'); if (this.includeGroups) { - placeholder += ' or group'; + placeholder += s__('ProjectSelect| or group'); } $(select).select2({ diff --git a/app/assets/javascripts/reports/components/report_section.vue b/app/assets/javascripts/reports/components/report_section.vue index 420e71f5e86..241185e3126 100644 --- a/app/assets/javascripts/reports/components/report_section.vue +++ b/app/assets/javascripts/reports/components/report_section.vue @@ -156,7 +156,7 @@ export default { <button v-if="isCollapsible" type="button" - class="js-collapse-btn btn float-right btn-sm" + class="js-collapse-btn btn float-right btn-sm qa-expand-report-button" @click="toggleCollapsed" > {{ collapseText }} diff --git a/app/assets/javascripts/repository/components/breadcrumbs.vue b/app/assets/javascripts/repository/components/breadcrumbs.vue new file mode 100644 index 00000000000..6eca015036f --- /dev/null +++ b/app/assets/javascripts/repository/components/breadcrumbs.vue @@ -0,0 +1,61 @@ +<script> +import getRefMixin from '../mixins/get_ref'; +import getProjectShortPath from '../queries/getProjectShortPath.graphql'; + +export default { + apollo: { + projectShortPath: { + query: getProjectShortPath, + }, + }, + mixins: [getRefMixin], + props: { + currentPath: { + type: String, + required: false, + default: '/', + }, + }, + data() { + return { + projectShortPath: '', + }; + }, + computed: { + pathLinks() { + return this.currentPath + .split('/') + .filter(p => p !== '') + .reduce( + (acc, name, i) => { + const path = `${i > 0 ? acc[i].path : ''}/${name}`; + + return acc.concat({ + name, + path, + to: `/tree/${this.ref}${path}`, + }); + }, + [{ name: this.projectShortPath, path: '/', to: `/tree/${this.ref}` }], + ); + }, + }, + methods: { + isLast(i) { + return i === this.pathLinks.length - 1; + }, + }, +}; +</script> + +<template> + <nav :aria-label="__('Files breadcrumb')"> + <ol class="breadcrumb repo-breadcrumb"> + <li v-for="(link, i) in pathLinks" :key="i" class="breadcrumb-item"> + <router-link :to="link.to" :aria-current="isLast(i) ? 'page' : null"> + {{ link.name }} + </router-link> + </li> + </ol> + </nav> +</template> diff --git a/app/assets/javascripts/repository/components/table/header.vue b/app/assets/javascripts/repository/components/table/header.vue new file mode 100644 index 00000000000..9d30aa88155 --- /dev/null +++ b/app/assets/javascripts/repository/components/table/header.vue @@ -0,0 +1,9 @@ +<template> + <thead> + <tr> + <th id="name" scope="col">{{ s__('ProjectFileTree|Name') }}</th> + <th id="last-commit" scope="col" class="d-none d-sm-table-cell">{{ __('Last commit') }}</th> + <th id="last-update" scope="col" class="text-right">{{ __('Last update') }}</th> + </tr> + </thead> +</template> diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue new file mode 100644 index 00000000000..f4df98ac2ff --- /dev/null +++ b/app/assets/javascripts/repository/components/table/index.vue @@ -0,0 +1,144 @@ +<script> +import { GlLoadingIcon } from '@gitlab/ui'; +import createFlash from '~/flash'; +import { sprintf, __ } from '../../../locale'; +import getRefMixin from '../../mixins/get_ref'; +import getFiles from '../../queries/getFiles.graphql'; +import getProjectPath from '../../queries/getProjectPath.graphql'; +import TableHeader from './header.vue'; +import TableRow from './row.vue'; +import ParentRow from './parent_row.vue'; + +const PAGE_SIZE = 100; + +export default { + components: { + GlLoadingIcon, + TableHeader, + TableRow, + ParentRow, + }, + mixins: [getRefMixin], + apollo: { + projectPath: { + query: getProjectPath, + }, + }, + props: { + path: { + type: String, + required: true, + }, + }, + data() { + return { + projectPath: '', + nextPageCursor: '', + entries: { + trees: [], + submodules: [], + blobs: [], + }, + isLoadingFiles: false, + }; + }, + computed: { + tableCaption() { + return sprintf( + __('Files, directories, and submodules in the path %{path} for commit reference %{ref}'), + { path: this.path, ref: this.ref }, + ); + }, + showParentRow() { + return !this.isLoadingFiles && ['', '/'].indexOf(this.path) === -1; + }, + }, + watch: { + $route: function routeChange() { + this.entries.trees = []; + this.entries.submodules = []; + this.entries.blobs = []; + this.nextPageCursor = ''; + this.fetchFiles(); + }, + }, + mounted() { + // We need to wait for `ref` and `projectPath` to be set + this.$nextTick(() => this.fetchFiles()); + }, + methods: { + fetchFiles() { + this.isLoadingFiles = true; + + return this.$apollo + .query({ + query: getFiles, + variables: { + projectPath: this.projectPath, + ref: this.ref, + path: this.path, + nextPageCursor: this.nextPageCursor, + pageSize: PAGE_SIZE, + }, + }) + .then(({ data }) => { + if (!data) return; + + const pageInfo = this.hasNextPage(data.project.repository.tree); + + this.isLoadingFiles = false; + this.entries = Object.keys(this.entries).reduce( + (acc, key) => ({ + ...acc, + [key]: this.normalizeData(key, data.project.repository.tree[key].edges), + }), + {}, + ); + + if (pageInfo && pageInfo.hasNextPage) { + this.nextPageCursor = pageInfo.endCursor; + this.fetchFiles(); + } + }) + .catch(() => createFlash(__('An error occurding while fetching folder content.'))); + }, + normalizeData(key, data) { + return this.entries[key].concat(data.map(({ node }) => node)); + }, + hasNextPage(data) { + return [] + .concat(data.trees.pageInfo, data.submodules.pageInfo, data.blobs.pageInfo) + .find(({ hasNextPage }) => hasNextPage); + }, + }, +}; +</script> + +<template> + <div class="tree-content-holder"> + <div class="table-holder bordered-box"> + <table class="table tree-table qa-file-tree" aria-live="polite"> + <caption class="sr-only"> + {{ + tableCaption + }} + </caption> + <table-header v-once /> + <tbody> + <parent-row v-show="showParentRow" :commit-ref="ref" :path="path" /> + <template v-for="val in entries"> + <table-row + v-for="entry in val" + :id="entry.id" + :key="`${entry.flatPath}-${entry.id}`" + :current-path="path" + :path="entry.flatPath" + :type="entry.type" + /> + </template> + </tbody> + </table> + <gl-loading-icon v-show="isLoadingFiles" class="my-3" size="md" /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/repository/components/table/parent_row.vue b/app/assets/javascripts/repository/components/table/parent_row.vue new file mode 100644 index 00000000000..b4433f00d8a --- /dev/null +++ b/app/assets/javascripts/repository/components/table/parent_row.vue @@ -0,0 +1,37 @@ +<script> +export default { + props: { + commitRef: { + type: String, + required: true, + }, + path: { + type: String, + required: true, + }, + }, + computed: { + parentRoute() { + const splitArray = this.path.split('/'); + splitArray.pop(); + + return { path: `/tree/${this.commitRef}/${splitArray.join('/')}` }; + }, + }, + methods: { + clickRow() { + this.$router.push(this.parentRoute); + }, + }, +}; +</script> + +<template> + <tr v-once @click="clickRow"> + <td colspan="3" class="tree-item-file-name"> + <router-link :to="parentRoute" :aria-label="__('Go to parent')"> + .. + </router-link> + </td> + </tr> +</template> diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue new file mode 100644 index 00000000000..9a264bef87e --- /dev/null +++ b/app/assets/javascripts/repository/components/table/row.vue @@ -0,0 +1,72 @@ +<script> +import { getIconName } from '../../utils/icon'; +import getRefMixin from '../../mixins/get_ref'; + +export default { + mixins: [getRefMixin], + props: { + id: { + type: String, + required: true, + }, + currentPath: { + type: String, + required: true, + }, + path: { + type: String, + required: true, + }, + type: { + type: String, + required: true, + }, + }, + computed: { + routerLinkTo() { + return this.isFolder ? { path: `/tree/${this.ref}/${this.path}` } : null; + }, + iconName() { + return `fa-${getIconName(this.type, this.path)}`; + }, + isFolder() { + return this.type === 'tree'; + }, + isSubmodule() { + return this.type === 'commit'; + }, + linkComponent() { + return this.isFolder ? 'router-link' : 'a'; + }, + fullPath() { + return this.path.replace(new RegExp(`^${this.currentPath}/`), ''); + }, + shortSha() { + return this.id.slice(0, 8); + }, + }, + methods: { + openRow() { + if (this.isFolder) { + this.$router.push(this.routerLinkTo); + } + }, + }, +}; +</script> + +<template> + <tr v-once :class="`file_${id}`" class="tree-item" @click="openRow"> + <td class="tree-item-file-name"> + <i :aria-label="type" role="img" :class="iconName" class="fa fa-fw"></i> + <component :is="linkComponent" :to="routerLinkTo" class="str-truncated"> + {{ fullPath }} + </component> + <template v-if="isSubmodule"> + @ <a href="#" class="commit-sha">{{ shortSha }}</a> + </template> + </td> + <td class="d-none d-sm-table-cell tree-commit"></td> + <td class="tree-time-ago text-right"></td> + </tr> +</template> diff --git a/app/assets/javascripts/repository/fragmentTypes.json b/app/assets/javascripts/repository/fragmentTypes.json new file mode 100644 index 00000000000..949ebca432b --- /dev/null +++ b/app/assets/javascripts/repository/fragmentTypes.json @@ -0,0 +1 @@ +{"__schema":{"types":[{"kind":"INTERFACE","name":"Entry","possibleTypes":[{"name":"Blob"},{"name":"Submodule"},{"name":"TreeEntry"}]}]}} diff --git a/app/assets/javascripts/repository/graphql.js b/app/assets/javascripts/repository/graphql.js index febfcce780c..c64d16ef02a 100644 --- a/app/assets/javascripts/repository/graphql.js +++ b/app/assets/javascripts/repository/graphql.js @@ -1,10 +1,42 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; +import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'; import createDefaultClient from '~/lib/graphql'; +import introspectionQueryResultData from './fragmentTypes.json'; Vue.use(VueApollo); -const defaultClient = createDefaultClient({}); +// We create a fragment matcher so that we can create a fragment from an interface +// Without this, Apollo throws a heuristic fragment matcher warning +const fragmentMatcher = new IntrospectionFragmentMatcher({ + introspectionQueryResultData, +}); + +const defaultClient = createDefaultClient( + {}, + { + cacheConfig: { + fragmentMatcher, + dataIdFromObject: obj => { + // eslint-disable-next-line no-underscore-dangle + switch (obj.__typename) { + // We need to create a dynamic ID for each entry + // Each entry can have the same ID as the ID is a commit ID + // So we create a unique cache ID with the path and the ID + case 'TreeEntry': + case 'Submodule': + case 'Blob': + return `${obj.flatPath}-${obj.id}`; + default: + // If the type doesn't match any of the above we fallback + // to using the default Apollo ID + // eslint-disable-next-line no-underscore-dangle + return obj.id || obj._id; + } + }, + }, + }, +); export default new VueApollo({ defaultClient, diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js index 00b69362312..52f53be045b 100644 --- a/app/assets/javascripts/repository/index.js +++ b/app/assets/javascripts/repository/index.js @@ -1,22 +1,56 @@ import Vue from 'vue'; import createRouter from './router'; import App from './components/app.vue'; +import Breadcrumbs from './components/breadcrumbs.vue'; import apolloProvider from './graphql'; +import { setTitle } from './utils/title'; export default function setupVueRepositoryList() { const el = document.getElementById('js-tree-list'); - const { projectPath, ref } = el.dataset; + const { projectPath, projectShortPath, ref, fullName } = el.dataset; + const router = createRouter(projectPath, ref); apolloProvider.clients.defaultClient.cache.writeData({ data: { projectPath, + projectShortPath, ref, }, }); + router.afterEach(({ params: { pathMatch } }) => { + const isRoot = pathMatch === undefined || pathMatch === '/'; + + setTitle(pathMatch, ref, fullName); + + if (!isRoot) { + document + .querySelectorAll('.js-keep-hidden-on-navigation') + .forEach(elem => elem.classList.add('hidden')); + } + + document + .querySelectorAll('.js-hide-on-navigation') + .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, + }, + }); + }, + }); + return new Vue({ el, - router: createRouter(projectPath, ref), + router, apolloProvider, render(h) { return h(App); diff --git a/app/assets/javascripts/repository/mixins/get_ref.js b/app/assets/javascripts/repository/mixins/get_ref.js new file mode 100644 index 00000000000..b06087d6f42 --- /dev/null +++ b/app/assets/javascripts/repository/mixins/get_ref.js @@ -0,0 +1,14 @@ +import getRef from '../queries/getRef.graphql'; + +export default { + apollo: { + ref: { + query: getRef, + }, + }, + data() { + return { + ref: '', + }; + }, +}; diff --git a/app/assets/javascripts/repository/pages/index.vue b/app/assets/javascripts/repository/pages/index.vue index fdbf195f0f6..2d92e9174ca 100644 --- a/app/assets/javascripts/repository/pages/index.vue +++ b/app/assets/javascripts/repository/pages/index.vue @@ -1,11 +1,9 @@ <script> -import getRef from '../queries/getRef.graphql'; +import FileTable from '../components/table/index.vue'; export default { - apollo: { - ref: { - query: getRef, - }, + components: { + FileTable, }, data() { return { @@ -16,9 +14,5 @@ export default { </script> <template> - <div> - <router-link :to="{ path: `/tree/${ref}/app` }"> - Go to tree - </router-link> - </div> + <file-table path="/" /> </template> diff --git a/app/assets/javascripts/repository/pages/tree.vue b/app/assets/javascripts/repository/pages/tree.vue index f51aafee775..3b898d1aa91 100644 --- a/app/assets/javascripts/repository/pages/tree.vue +++ b/app/assets/javascripts/repository/pages/tree.vue @@ -1,5 +1,10 @@ <script> +import FileTable from '../components/table/index.vue'; + export default { + components: { + FileTable, + }, props: { path: { type: String, @@ -11,5 +16,5 @@ export default { </script> <template> - <div>{{ path }}</div> + <file-table :path="path" /> </template> diff --git a/app/assets/javascripts/repository/queries/getFiles.graphql b/app/assets/javascripts/repository/queries/getFiles.graphql new file mode 100644 index 00000000000..a9b61d28560 --- /dev/null +++ b/app/assets/javascripts/repository/queries/getFiles.graphql @@ -0,0 +1,55 @@ +fragment TreeEntry on Entry { + id + flatPath + type +} + +fragment PageInfo on PageInfo { + hasNextPage + endCursor +} + +query getFiles( + $projectPath: ID! + $path: String + $ref: String! + $pageSize: Int! + $nextPageCursor: String +) { + project(fullPath: $projectPath) { + repository { + tree(path: $path, ref: $ref) { + trees(first: $pageSize, after: $nextPageCursor) { + edges { + node { + ...TreeEntry + } + } + pageInfo { + ...PageInfo + } + } + submodules(first: $pageSize, after: $nextPageCursor) { + edges { + node { + ...TreeEntry + } + } + pageInfo { + ...PageInfo + } + } + blobs(first: $pageSize, after: $nextPageCursor) { + edges { + node { + ...TreeEntry + } + } + pageInfo { + ...PageInfo + } + } + } + } + } +} diff --git a/app/assets/javascripts/repository/queries/getProjectPath.graphql b/app/assets/javascripts/repository/queries/getProjectPath.graphql new file mode 100644 index 00000000000..74e73e07577 --- /dev/null +++ b/app/assets/javascripts/repository/queries/getProjectPath.graphql @@ -0,0 +1,3 @@ +query getProjectPath { + projectPath +} diff --git a/app/assets/javascripts/repository/queries/getProjectShortPath.graphql b/app/assets/javascripts/repository/queries/getProjectShortPath.graphql new file mode 100644 index 00000000000..34eb26598c2 --- /dev/null +++ b/app/assets/javascripts/repository/queries/getProjectShortPath.graphql @@ -0,0 +1,3 @@ +query getProjectShortPath { + projectShortPath @client +} diff --git a/app/assets/javascripts/repository/router.js b/app/assets/javascripts/repository/router.js index b42a96a4ee2..9322c81ab97 100644 --- a/app/assets/javascripts/repository/router.js +++ b/app/assets/javascripts/repository/router.js @@ -12,24 +12,17 @@ export default function createRouter(base, baseRef) { base: joinPaths(gon.relative_url_root || '', base), routes: [ { - path: '/', - name: 'projectRoot', - component: IndexPage, - }, - { path: `/tree/${baseRef}(/.*)?`, name: 'treePath', component: TreePage, props: route => ({ - path: route.params.pathMatch, + path: route.params.pathMatch && route.params.pathMatch.replace(/^\//, ''), }), - beforeEnter(to, from, next) { - document - .querySelectorAll('.js-hide-on-navigation') - .forEach(el => el.classList.add('hidden')); - - next(); - }, + }, + { + path: '/', + name: 'projectRoot', + component: IndexPage, }, ], }); diff --git a/app/assets/javascripts/repository/utils/icon.js b/app/assets/javascripts/repository/utils/icon.js new file mode 100644 index 00000000000..661ebb6edfc --- /dev/null +++ b/app/assets/javascripts/repository/utils/icon.js @@ -0,0 +1,99 @@ +const entryTypeIcons = { + tree: 'folder', + commit: 'archive', +}; + +const fileTypeIcons = [ + { extensions: ['pdf'], name: 'file-pdf-o' }, + { + extensions: [ + 'jpg', + 'jpeg', + 'jif', + 'jfif', + 'jp2', + 'jpx', + 'j2k', + 'j2c', + 'png', + 'gif', + 'tif', + 'tiff', + 'svg', + 'ico', + 'bmp', + ], + name: 'file-image-o', + }, + { + extensions: ['zip', 'zipx', 'tar', 'gz', 'bz', 'bzip', 'xz', 'rar', '7z'], + name: 'file-archive-o', + }, + { extensions: ['mp3', 'wma', 'ogg', 'oga', 'wav', 'flac', 'aac'], name: 'file-audio-o' }, + { + extensions: [ + 'mp4', + 'm4p', + 'm4v', + 'mpg', + 'mp2', + 'mpeg', + 'mpe', + 'mpv', + 'm2v', + 'avi', + 'mkv', + 'flv', + 'ogv', + 'mov', + '3gp', + '3g2', + ], + name: 'file-video-o', + }, + { extensions: ['doc', 'dot', 'docx', 'docm', 'dotx', 'dotm', 'docb'], name: 'file-word-o' }, + { + extensions: [ + 'xls', + 'xlt', + 'xlm', + 'xlsx', + 'xlsm', + 'xltx', + 'xltm', + 'xlsb', + 'xla', + 'xlam', + 'xll', + 'xlw', + ], + name: 'file-excel-o', + }, + { + extensions: [ + 'ppt', + 'pot', + 'pps', + 'pptx', + 'pptm', + 'potx', + 'potm', + 'ppam', + 'ppsx', + 'ppsm', + 'sldx', + 'sldm', + ], + name: 'file-powerpoint-o', + }, +]; + +// eslint-disable-next-line import/prefer-default-export +export const getIconName = (type, path) => { + if (entryTypeIcons[type]) return entryTypeIcons[type]; + + const extension = path.split('.').pop(); + const file = fileTypeIcons.find(t => t.extensions.some(ext => ext === extension)); + + return file ? file.name : 'file-text-o'; +}; diff --git a/app/assets/javascripts/repository/utils/title.js b/app/assets/javascripts/repository/utils/title.js new file mode 100644 index 00000000000..4e194640e92 --- /dev/null +++ b/app/assets/javascripts/repository/utils/title.js @@ -0,0 +1,9 @@ +// eslint-disable-next-line import/prefer-default-export +export const setTitle = (pathMatch, ref, project) => { + if (!pathMatch) return; + + const path = pathMatch.replace(/^\//, ''); + const isEmpty = path === ''; + + document.title = `${isEmpty ? 'Files' : path} · ${ref} · ${project}`; +}; diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index 9a0cdc02952..72e061df573 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -5,7 +5,7 @@ import _ from 'underscore'; import Cookies from 'js-cookie'; import flash from './flash'; import axios from './lib/utils/axios_utils'; -import { __ } from './locale'; +import { sprintf, s__, __ } from './locale'; function Sidebar(currentUser) { this.toggleTodo = this.toggleTodo.bind(this); @@ -101,7 +101,10 @@ Sidebar.prototype.toggleTodo = function(e) { this.todoUpdateDone(data); }) .catch(() => - flash(`There was an error ${ajaxType === 'post' ? 'adding a' : 'deleting the'} todo.`), + flash(sprintf(__('There was an error %{message} todo.')), { + message: + ajaxType === 'post' ? s__('RightSidebar|adding a') : s__('RightSidebar|deleting the'), + }), ); }; diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js index 0a4583b5861..ab43c2139bf 100644 --- a/app/assets/javascripts/search_autocomplete.js +++ b/app/assets/javascripts/search_autocomplete.js @@ -2,7 +2,7 @@ import $ from 'jquery'; import { escape, throttle } from 'underscore'; -import { s__, sprintf } from '~/locale'; +import { s__, __, sprintf } from '~/locale'; import { getIdenticonBackgroundClass, getIdenticonTitle } from '~/helpers/avatar_helper'; import axios from './lib/utils/axios_utils'; import DropdownUtils from './filtered_search/dropdown_utils'; @@ -407,6 +407,7 @@ export class SearchAutocomplete { if (this.searchInput.val() === '') { return this.restoreOriginalState(); } + this.dropdownMenu.removeClass('show'); } restoreOriginalState() { @@ -439,7 +440,7 @@ export class SearchAutocomplete { restoreMenu() { var html; - html = '<ul><li class="dropdown-menu-empty-item"><a>Loading...</a></li></ul>'; + html = `<ul><li class="dropdown-menu-empty-item"><a>${__('Loading...')}</a></li></ul>`; return this.dropdownContent.html(html); } diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue index 10e2c8453e2..35eba266625 100644 --- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue +++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue @@ -194,9 +194,9 @@ export default { v-show="noEmoji" class="js-no-emoji-placeholder no-emoji-placeholder position-relative" > - <icon name="emoji_slightly_smiling_face" css-classes="award-control-icon-neutral" /> - <icon name="emoji_smiley" css-classes="award-control-icon-positive" /> - <icon name="emoji_smile" css-classes="award-control-icon-super-positive" /> + <icon name="slight-smile" css-classes="award-control-icon-neutral" /> + <icon name="smiley" css-classes="award-control-icon-positive" /> + <icon name="smile" css-classes="award-control-icon-super-positive" /> </span> </button> </span> diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js index 7404dfbf22a..70f89152f70 100644 --- a/app/assets/javascripts/star.js +++ b/app/assets/javascripts/star.js @@ -31,7 +31,7 @@ export default class Star { $this.prepend(spriteIcon('star', iconClasses)); } }) - .catch(() => Flash('Star toggle failed. Try again later.')); + .catch(() => Flash(__('Star toggle failed. Try again later.'))); }); } } diff --git a/app/assets/javascripts/subscription_select.js b/app/assets/javascripts/subscription_select.js index ebe1c6dd02d..7206bbd7109 100644 --- a/app/assets/javascripts/subscription_select.js +++ b/app/assets/javascripts/subscription_select.js @@ -1,4 +1,5 @@ import $ from 'jquery'; +import { __ } from './locale'; export default function subscriptionSelect() { $('.js-subscription-event').each((i, element) => { @@ -8,7 +9,7 @@ export default function subscriptionSelect() { selectable: true, fieldName, toggleLabel(selected, el, instance) { - let label = 'Subscription'; + let label = __('Subscription'); const $item = instance.dropdown.find('.is-active'); if ($item.length) { label = $item.text(); diff --git a/app/assets/javascripts/usage_ping_consent.js b/app/assets/javascripts/usage_ping_consent.js index d3d745a3c11..1e7a5fb19c2 100644 --- a/app/assets/javascripts/usage_ping_consent.js +++ b/app/assets/javascripts/usage_ping_consent.js @@ -2,6 +2,7 @@ import $ from 'jquery'; import axios from './lib/utils/axios_utils'; import Flash, { hideFlash } from './flash'; import { parseBoolean } from './lib/utils/common_utils'; +import { __ } from './locale'; export default () => { $('body').on('click', '.js-usage-consent-action', e => { @@ -25,7 +26,7 @@ export default () => { }) .catch(() => { hideConsentMessage(); - Flash('Something went wrong. Try again later.'); + Flash(__('Something went wrong. Try again later.')); }); }); }; diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 8c71615dff2..7e6f02b10af 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -5,7 +5,7 @@ import $ from 'jquery'; import _ from 'underscore'; import axios from './lib/utils/axios_utils'; -import { __ } from './locale'; +import { s__, __, sprintf } from './locale'; import ModalStore from './boards/stores/modal_store'; // TODO: remove eventHub hack after code splitting refactor @@ -157,14 +157,20 @@ function UsersSelect(currentUser, els, options = {}) { .get(0); if (selectedUsers.length === 0) { - return 'Unassigned'; + return s__('UsersSelect|Unassigned'); } else if (selectedUsers.length === 1) { return firstUser.name; } else if (isSelected) { const otherSelected = selectedUsers.filter(s => s !== selectedUser.id); - return `${selectedUser.name} + ${otherSelected.length} more`; + return sprintf(s__('UsersSelect|%{name} + %{length} more'), { + name: selectedUser.name, + length: otherSelected.length, + }); } else { - return `${firstUser.name} + ${selectedUsers.length - 1} more`; + return sprintf(s__('UsersSelect|%{name} + %{length} more'), { + name: firstUser.name, + length: selectedUsers.length - 1, + }); } }; @@ -218,11 +224,11 @@ function UsersSelect(currentUser, els, options = {}) { tooltipTitle = _.escape(user.name); } else { user = { - name: 'Unassigned', + name: s__('UsersSelect|Unassigned'), username: '', avatar: '', }; - tooltipTitle = __('Assignee'); + tooltipTitle = s__('UsersSelect|Assignee'); } $value.html(assigneeTemplate(user)); $collapsedSidebar.attr('title', tooltipTitle).tooltip('_fixTitle'); @@ -233,7 +239,11 @@ function UsersSelect(currentUser, els, options = {}) { '<% if( avatar ) { %> <a class="author-link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>', ); assigneeTemplate = _.template( - '<% if (username) { %> <a class="author-link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>', + `<% if (username) { %> <a class="author-link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> + ${sprintf(s__('UsersSelect|No assignee - %{openingTag} assign yourself %{closingTag}'), { + openingTag: '<a href="#" class="js-assign-yourself">', + closingTag: '</a>', + })}</span> <% } %>`, ); return $dropdown.glDropdown({ showMenuAbove: showMenuAbove, @@ -302,7 +312,7 @@ function UsersSelect(currentUser, els, options = {}) { showDivider += 1; users.unshift({ beforeDivider: true, - name: 'Unassigned', + name: s__('UsersSelect|Unassigned'), id: 0, }); } @@ -310,7 +320,7 @@ function UsersSelect(currentUser, els, options = {}) { showDivider += 1; name = showAnyUser; if (name === true) { - name = 'Any User'; + name = s__('UsersSelect|Any User'); } anyUser = { beforeDivider: true, @@ -596,7 +606,7 @@ function UsersSelect(currentUser, els, options = {}) { showEmailUser = $(select).data('emailUser'); firstUser = $(select).data('firstUser'); return $(select).select2({ - placeholder: 'Search for a user', + placeholder: __('Search for a user'), multiple: $(select).hasClass('multiselect'), minimumInputLength: 0, query: function(query) { @@ -621,7 +631,7 @@ function UsersSelect(currentUser, els, options = {}) { } if (showNullUser) { nullUser = { - name: 'Unassigned', + name: s__('UsersSelect|Unassigned'), id: 0, }; data.results.unshift(nullUser); @@ -629,7 +639,7 @@ function UsersSelect(currentUser, els, options = {}) { if (showAnyUser) { name = showAnyUser; if (name === true) { - name = 'Any User'; + name = s__('UsersSelect|Any User'); } anyUser = { name: name, @@ -645,7 +655,7 @@ function UsersSelect(currentUser, els, options = {}) { ) { var trimmed = query.term.trim(); emailUser = { - name: 'Invite "' + trimmed + '" by email', + name: sprintf(__('Invite "%{trimmed}" by email'), { trimmed }), username: trimmed, id: trimmed, invite: true, @@ -688,7 +698,7 @@ UsersSelect.prototype.initSelection = function(element, callback) { id = $(element).val(); if (id === '0') { nullUser = { - name: 'Unassigned', + name: s__('UsersSelect|Unassigned'), }; return callback(nullUser); } else if (id !== '') { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue index 8f4cae8ae58..ad0464a3a98 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue @@ -49,7 +49,7 @@ export default { required: false, default: () => ({ sourceProjectId: '', - issueId: '', + mergeRequestId: '', appUrl: '', }), }, diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue index b9f5f602117..0686409a785 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue @@ -48,7 +48,7 @@ export default { visualReviewAppMeta() { return { appUrl: this.mr.appUrl, - issueId: this.mr.iid, + mergeRequestId: this.mr.iid, sourceProjectId: this.mr.sourceProjectId, }; }, diff --git a/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue b/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue index 780ecdcdac4..6aad2a26a53 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue @@ -14,7 +14,7 @@ export default { </script> <template> - <p v-once class="mr-info-list mr-links source-branch-removal-status append-bottom-0"> + <p v-once class="mr-info-list mr-links append-bottom-0"> <span class="status-text" v-html="removesBranchText"> </span> <i v-tooltip :title="tooltipTitle" :aria-label="tooltipTitle" class="fa fa-question-circle"> </i> diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index 705ee05e29f..bf175eb5f69 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -333,41 +333,45 @@ export default { <div class="mr-widget-section"> <component :is="componentName" :mr="mr" :service="service" /> - <section v-if="shouldRenderCollaborationStatus" class="mr-info-list mr-links"> - {{ s__('mrWidget|Allows commits from members who can merge to the target branch') }} - </section> + <div class="mr-widget-info"> + <section v-if="shouldRenderCollaborationStatus" class="mr-info-list mr-links"> + <p> + {{ s__('mrWidget|Allows commits from members who can merge to the target branch') }} + </p> + </section> - <mr-widget-related-links - v-if="shouldRenderRelatedLinks" - :state="mr.state" - :related-links="mr.relatedLinks" - /> + <mr-widget-related-links + v-if="shouldRenderRelatedLinks" + :state="mr.state" + :related-links="mr.relatedLinks" + /> - <mr-widget-alert-message - v-if="showMergePipelineForkWarning" - type="warning" - :help-path="mr.mergeRequestPipelinesHelpPath" - > - {{ - s__( - 'mrWidget|Fork merge requests do not create merge request pipelines which validate a post merge result', - ) - }} - </mr-widget-alert-message> + <mr-widget-alert-message + v-if="showMergePipelineForkWarning" + type="warning" + :help-path="mr.mergeRequestPipelinesHelpPath" + > + {{ + s__( + 'mrWidget|Fork merge requests do not create merge request pipelines which validate a post merge result', + ) + }} + </mr-widget-alert-message> - <mr-widget-alert-message - v-if="showTargetBranchAdvancedError" - type="danger" - :help-path="mr.mergeRequestPipelinesHelpPath" - > - {{ - s__( - 'mrWidget|The target branch has advanced, which invalidates the merge request pipeline. Please update the source branch and retry merging', - ) - }} - </mr-widget-alert-message> + <mr-widget-alert-message + v-if="showTargetBranchAdvancedError" + type="danger" + :help-path="mr.mergeRequestPipelinesHelpPath" + > + {{ + s__( + 'mrWidget|The target branch has advanced, which invalidates the merge request pipeline. Please update the source branch and retry merging', + ) + }} + </mr-widget-alert-message> - <source-branch-removal-status v-if="shouldRenderSourceBranchRemovalStatus" /> + <source-branch-removal-status v-if="shouldRenderSourceBranchRemovalStatus" /> + </div> </div> <div v-if="shouldRenderMergeHelp" class="mr-widget-footer"><mr-widget-merge-help /></div> </div> diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue index 4dbfd1ba6f4..a60d5eb491e 100644 --- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue +++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue @@ -71,11 +71,15 @@ export default { </div> <div class="text-secondary"> <div v-if="user.bio" class="js-bio d-flex mb-1"> - <icon name="profile" css-classes="category-icon" /> + <icon name="profile" css-classes="category-icon flex-shrink-0" /> <span class="ml-1">{{ user.bio }}</span> </div> <div v-if="user.organization" class="js-organization d-flex mb-1"> - <icon v-show="!jobInfoIsLoading" name="work" css-classes="category-icon" /> + <icon + v-show="!jobInfoIsLoading" + name="work" + css-classes="category-icon flex-shrink-0" + /> <span class="ml-1">{{ user.organization }}</span> </div> <gl-skeleton-loading @@ -88,7 +92,7 @@ export default { <icon v-show="!locationIsLoading && user.location" name="location" - css-classes="category-icon" + css-classes="category-icon flex-shrink-0" /> <span class="ml-1">{{ user.location }}</span> <gl-skeleton-loading diff --git a/app/assets/stylesheets/components/avatar.scss b/app/assets/stylesheets/components/avatar.scss new file mode 100644 index 00000000000..25ee3ca944d --- /dev/null +++ b/app/assets/stylesheets/components/avatar.scss @@ -0,0 +1,232 @@ +$avatar-sizes: ( + 16: ( + font-size: 10px, + line-height: 16px, + border-radius: $border-radius-small + ), + 18: ( + border-radius: $border-radius-small + ), + 19: ( + border-radius: $border-radius-small + ), + 20: ( + border-radius: $border-radius-small + ), + 24: ( + font-size: 12px, + line-height: 24px, + border-radius: $border-radius-default + ), + 26: ( + font-size: 20px, + line-height: 1.33, + border-radius: $border-radius-default + ), + 32: ( + font-size: 14px, + line-height: 32px, + border-radius: $border-radius-default + ), + 36: ( + border-radius: $border-radius-default + ), + 40: ( + font-size: 16px, + line-height: 38px, + border-radius: $border-radius-default + ), + 46: ( + border-radius: $border-radius-default + ), + 48: ( + font-size: 20px, + line-height: 48px, + border-radius: $border-radius-large + ), + 60: ( + font-size: 32px, + line-height: 58px, + border-radius: $border-radius-large + ), + 64: ( + font-size: 28px, + line-height: 64px, + border-radius: $border-radius-large + ), + 70: ( + font-size: 34px, + line-height: 70px, + border-radius: $border-radius-large + ), + 90: ( + font-size: 36px, + line-height: 88px, + border-radius: $border-radius-large + ), + 96: ( + font-size: 48px, + line-height: 96px, + border-radius: $border-radius-large + ), + 100: ( + font-size: 36px, + line-height: 98px, + border-radius: $border-radius-large + ), + 110: ( + font-size: 40px, + line-height: 108px, + font-weight: $gl-font-weight-normal, + border-radius: $border-radius-large + ), + 140: ( + font-size: 72px, + line-height: 138px, + border-radius: $border-radius-large + ), + 160: ( + font-size: 96px, + line-height: 158px, + border-radius: $border-radius-large + ) +); + +$identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $identicon-blue, $identicon-teal, + $identicon-orange, $gray-darker; + +.avatar-circle { + float: left; + margin-right: 15px; + border-radius: $avatar-radius; + border: 1px solid $gray-normal; + + @each $size, $size-config in $avatar-sizes { + &.s#{$size} { + @include avatar-size(#{$size}px, if($size < 36, 8px, 16px)); + } + } +} + +.avatar { + @extend .avatar-circle; + transition-property: none; + + width: 40px; + height: 40px; + padding: 0; + background: $gray-lightest; + overflow: hidden; + border-color: rgba($black, $gl-avatar-border-opacity); + + &.avatar-inline { + float: none; + display: inline-block; + margin-left: 2px; + flex-shrink: 0; + + &.s16 { + margin-right: 4px; + } + + &.s24 { + margin-right: 4px; + } + } + + &.center { + font-size: 14px; + line-height: 1.8em; + text-align: center; + } + + &.avatar-tile { + border-radius: 0; + border: 0; + } + + &.avatar-placeholder { + border: 0; + } +} + +.identicon { + text-align: center; + vertical-align: top; + color: $gray-800; + background-color: $gray-darker; + + // Sizes + @each $size, $size-config in $avatar-sizes { + $keys: map-keys($size-config); + + &.s#{$size} { + @each $key in $keys { + // We don't want `border-radius` to be included here. + @if ($key != 'border-radius') { + #{$key}: map-get($size-config, #{$key}); + } + } + } + } + + // Background colors + @for $i from 1 through length($identicon-backgrounds) { + &.bg#{$i} { + background-color: nth($identicon-backgrounds, $i); + } + } +} + +.avatar-container { + @extend .avatar-circle; + overflow: hidden; + display: flex; + + a { + width: 100%; + height: 100%; + display: flex; + text-decoration: none; + } + + .avatar { + border-radius: 0; + border: 0; + height: auto; + width: 100%; + margin: 0; + align-self: center; + } + + &.s40 { + min-width: 40px; + min-height: 40px; + } + + &.s64 { + min-width: 64px; + min-height: 64px; + } +} + +.rect-avatar { + border-radius: $border-radius-small; + + @each $size, $size-config in $avatar-sizes { + &.s#{$size} { + border-radius: map-get($size-config, 'border-radius'); + } + } +} + +.avatar-counter { + background-color: $gray-darkest; + color: $white-light; + border: 1px solid $gray-normal; + border-radius: 1em; + font-family: $regular-font; + font-size: 9px; + line-height: 16px; + text-align: center; +} diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index ab9047c54e4..9b0d19b0ef0 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -8,7 +8,6 @@ @import 'framework/animations'; @import 'framework/vue_transitions'; -@import 'framework/avatar'; @import 'framework/asciidoctor'; @import 'framework/banner'; @import 'framework/blocks'; diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss deleted file mode 100644 index 37a729c7a63..00000000000 --- a/app/assets/stylesheets/framework/avatar.scss +++ /dev/null @@ -1,194 +0,0 @@ -@mixin avatar-size($size, $margin-right) { - width: $size; - height: $size; - margin-right: $margin-right; -} - -.avatar-circle { - float: left; - margin-right: 15px; - border-radius: $avatar-radius; - border: 1px solid $gray-normal; - &.s16 { @include avatar-size(16px, 6px); } - &.s18 { @include avatar-size(18px, 6px); } - &.s19 { @include avatar-size(19px, 6px); } - &.s20 { @include avatar-size(20px, 7px); } - &.s24 { @include avatar-size(24px, 8px); } - &.s26 { @include avatar-size(26px, 8px); } - &.s32 { @include avatar-size(32px, 10px); } - &.s36 { @include avatar-size(36px, 10px); } - &.s40 { @include avatar-size(40px, 10px); } - &.s46 { @include avatar-size(46px, 15px); } - &.s48 { @include avatar-size(48px, 10px); } - &.s60 { @include avatar-size(60px, 12px); } - &.s64 { @include avatar-size(64px, 14px); } - &.s70 { @include avatar-size(70px, 14px); } - &.s90 { @include avatar-size(90px, 15px); } - &.s100 { @include avatar-size(100px, 15px); } - &.s110 { @include avatar-size(110px, 15px); } - &.s140 { @include avatar-size(140px, 15px); } - &.s160 { @include avatar-size(160px, 20px); } -} - -.avatar { - @extend .avatar-circle; - transition-property: none; - - width: 40px; - height: 40px; - padding: 0; - background: $gray-lightest; - overflow: hidden; - - &.avatar-inline { - float: none; - display: inline-block; - margin-left: 2px; - flex-shrink: 0; - - &.s16 { margin-right: 4px; } - &.s24 { margin-right: 4px; } - } - - &.center { - font-size: 14px; - line-height: 1.8em; - text-align: center; - } - - &.avatar-tile { - border-radius: 0; - border: 0; - } - - &.avatar-placeholder { - border: 0; - } - - &:not([href]):hover { - border-color: darken($gray-normal, 10%); - } -} - -.identicon { - text-align: center; - vertical-align: top; - color: $gl-gray-700; - background-color: $gray-darker; - - // Sizes - &.s16 { font-size: 12px; - line-height: 1.33; } - - &.s24 { font-size: 13px; - line-height: 1.8; } - - &.s26 { font-size: 20px; - line-height: 1.33; } - - &.s32 { font-size: 20px; - line-height: 30px; } - - &.s40 { font-size: 16px; - line-height: 38px; } - - &.s48 { font-size: 20px; - line-height: 46px; } - - &.s60 { font-size: 32px; - line-height: 58px; } - - &.s64 { font-size: 32px; - line-height: 64px; } - - &.s70 { font-size: 34px; - line-height: 70px; } - - &.s90 { font-size: 36px; - line-height: 88px; } - - &.s100 { font-size: 36px; - line-height: 98px; } - - &.s110 { font-size: 40px; - line-height: 108px; - font-weight: $gl-font-weight-normal; } - - &.s140 { font-size: 72px; - line-height: 138px; } - - &.s160 { font-size: 96px; - line-height: 158px; } - - // Background colors - &.bg1 { background-color: $identicon-red; } - &.bg2 { background-color: $identicon-purple; } - &.bg3 { background-color: $identicon-indigo; } - &.bg4 { background-color: $identicon-blue; } - &.bg5 { background-color: $identicon-teal; } - &.bg6 { background-color: $identicon-orange; } - &.bg7 { background-color: $gray-darker; } -} - -.avatar-container { - @extend .avatar-circle; - overflow: hidden; - display: flex; - - a { - width: 100%; - height: 100%; - display: flex; - text-decoration: none; - } - - .avatar { - border-radius: 0; - border: 0; - height: auto; - width: 100%; - margin: 0; - align-self: center; - } - - &.s40 { min-width: 40px; - min-height: 40px; } - - &.s64 { min-width: 64px; - min-height: 64px; } -} - -.rect-avatar { - border-radius: $border-radius-small; - &.s16 { border-radius: $border-radius-small; } - &.s18 { border-radius: $border-radius-small; } - &.s19 { border-radius: $border-radius-small; } - &.s20 { border-radius: $border-radius-small; } - &.s24 { border-radius: $border-radius-default; } - &.s26 { border-radius: $border-radius-default; } - &.s32 { border-radius: $border-radius-default; } - &.s36 { border-radius: $border-radius-default; } - &.s40 { border-radius: $border-radius-default; } - &.s46 { border-radius: $border-radius-default; } - &.s48 { border-radius: $border-radius-large; } - &.s60 { border-radius: $border-radius-large; } - &.s64 { border-radius: $border-radius-large; } - &.s70 { border-radius: $border-radius-large; } - &.s90 { border-radius: $border-radius-large; } - &.s96 { border-radius: $border-radius-large; } - &.s100 { border-radius: $border-radius-large; } - &.s110 { border-radius: $border-radius-large; } - &.s140 { border-radius: $border-radius-large; } - &.s160 { border-radius: $border-radius-large; } -} - -.avatar-counter { - background-color: $gray-darkest; - color: $white-light; - border: 1px solid $gray-normal; - border-radius: 1em; - font-family: $regular-font; - font-size: 9px; - line-height: 16px; - text-align: center; -} diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss index 648e1944388..a8df7e1bfad 100644 --- a/app/assets/stylesheets/framework/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -151,8 +151,7 @@ outline: 0; .award-control-icon svg { - background: $award-emoji-positive-add-bg; - fill: $award-emoji-positive-add-lines; + fill: $blue-500; } .award-control-icon-neutral { @@ -236,7 +235,7 @@ } path { - fill: $border-gray-normal; + fill: $gray-700; } } diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index ab8f397f3a0..2c720703822 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -21,7 +21,7 @@ } @mixin btn-default { - border-radius: 3px; + border-radius: $border-radius-default; font-size: $gl-font-size; font-weight: $gl-font-weight-normal; padding: $gl-vert-padding $gl-btn-padding; @@ -37,7 +37,7 @@ @include btn-default; } -@mixin btn-outline($background, $text, $border, $hover-background, $hover-text, $hover-border, $active-background, $active-border) { +@mixin btn-outline($background, $text, $border, $hover-background, $hover-text, $hover-border, $active-background, $active-border, $active-text) { background-color: $background; color: $text; border-color: $border; @@ -61,13 +61,22 @@ } } + &:focus { + box-shadow: 0 0 4px 1px $blue-300; + } + &:active { background-color: $active-background; border-color: $active-border; - color: $hover-text; + box-shadow: inset 0 2px 4px 0 rgba($black, 0.2); + color: $active-text; > .icon { - color: $hover-text; + color: $active-text; + } + + &:focus { + box-shadow: inset 0 2px 4px 0 rgba($black, 0.2); } } } @@ -164,21 +173,21 @@ &.btn-inverted { &.btn-success { - @include btn-outline($white-light, $green-600, $green-500, $green-500, $white-light, $green-600, $green-600, $green-700); + @include btn-outline($white-light, $green-600, $green-500, $green-100, $green-700, $green-500, $green-200, $green-600, $green-800); } &.btn-remove, &.btn-danger { - @include btn-outline($white-light, $red-500, $red-500, $red-500, $white-light, $red-600, $red-600, $red-700); + @include btn-outline($white-light, $red-500, $red-500, $red-100, $red-700, $red-500, $red-200, $red-600, $red-800); } &.btn-warning { - @include btn-outline($white-light, $orange-500, $orange-500, $orange-500, $white-light, $orange-600, $orange-600, $orange-700); + @include btn-outline($white-light, $orange-500, $orange-500, $orange-100, $orange-700, $orange-500, $orange-200, $orange-600, $orange-800); } &.btn-primary, &.btn-info { - @include btn-outline($white-light, $blue-500, $blue-500, $blue-500, $white-light, $blue-600, $blue-600, $blue-700); + @include btn-outline($white-light, $blue-500, $blue-500, $blue-100, $blue-700, $blue-500, $blue-200, $blue-600, $blue-800); } } @@ -193,11 +202,11 @@ &.btn-close, &.btn-close-color { - @include btn-outline($white-light, $orange-600, $orange-500, $orange-500, $white-light, $orange-600, $orange-600, $orange-700); + @include btn-outline($white-light, $orange-600, $orange-500, $orange-100, $orange-700, $orange-500, $orange-200, $orange-600, $orange-800); } &.btn-spam { - @include btn-outline($white-light, $red-500, $red-500, $red-500, $white-light, $red-600, $red-600, $red-700); + @include btn-outline($white-light, $red-500, $red-500, $red-100, $red-700, $red-500, $red-200, $red-600, $red-800); } &.btn-danger, @@ -402,7 +411,7 @@ .btn-inverted { &-secondary { - @include btn-outline($white-light, $blue-500, $blue-500, $blue-500, $white-light, $blue-600, $blue-600, $blue-700); + @include btn-outline($white-light, $blue-500, $blue-500, $blue-100, $blue-700, $blue-500, $blue-200, $blue-600, $blue-800); } } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 8fb4027bf97..cd951f67293 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -570,10 +570,10 @@ } .dropdown-menu-close { - right: 5px; + top: $gl-padding-4; + right: $gl-padding-8; width: 20px; height: 20px; - top: -1px; } .dropdown-menu-close-icon { diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 53d3645cd63..17c117188b3 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -241,6 +241,7 @@ */ &.code { padding: 0; + border-radius: 0 0 $border-radius-default $border-radius-default; } .list-inline.previews { diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 5bcfd5d1322..26cbb7f5c13 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -218,7 +218,7 @@ min-width: 200px; padding-right: 25px; padding-left: 0; - height: $input-height; + height: $input-height - 2; line-height: inherit; border-color: transparent; diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index d0f99c2df7e..4a9c73a1bc9 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -280,3 +280,7 @@ label { max-width: $input-lg-width; width: 100%; } + +.input-group-text { + max-height: $input-height; +} diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss index 946f575ac13..741f92110c3 100644 --- a/app/assets/stylesheets/framework/highlight.scss +++ b/app/assets/stylesheets/framework/highlight.scss @@ -8,7 +8,7 @@ pre { padding: 10px 0; border: 0; - border-radius: 0; + border-radius: 0 0 $border-radius-default $border-radius-default; font-family: $monospace-font; font-size: $code-font-size; line-height: 19px; @@ -42,6 +42,7 @@ padding: 10px; text-align: right; float: left; + border-bottom-left-radius: $border-radius-default; a { font-family: $monospace-font; diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 18eb10c1f23..18671f7c4d8 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -325,8 +325,8 @@ line-height: 1; padding: 0; min-width: 16px; - color: $gray-darkest; - fill: $gray-darkest; + color: $gray-600; + fill: $gray-600; .fa { position: relative; @@ -376,3 +376,12 @@ } } } + +/* +* Mixin that handles the size and right margin of avatars. +*/ +@mixin avatar-size($size, $margin-right) { + width: $size; + height: $size; + margin-right: $margin-right; +} diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index e2bb1eb67c0..f75e5b55506 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -53,7 +53,8 @@ flex-direction: row; .btn + .btn:not(.dropdown-toggle-split), - .btn + .btn-group { + .btn + .btn-group, + .btn-group + .btn { margin-left: $grid-size; } @@ -61,7 +62,8 @@ flex-direction: column; .btn + .btn:not(.dropdown-toggle-split), - .btn + .btn-group { + .btn + .btn-group, + .btn-group + .btn { margin-left: 0; margin-top: $grid-size; } diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index 3d5208c3db5..e8176e59c19 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -55,4 +55,5 @@ .discussion .timeline-entry { margin: 0; border-right: 0; + border-radius: $border-radius-default $border-radius-default 0 0; } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 1cf122102cc..28768bdf88f 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -589,6 +589,7 @@ $issue-board-list-difference-md: $issue-board-list-difference-sm + $issue-boards */ $avatar-radius: 50%; $gl-avatar-size: 40px; +$gl-avatar-border-opacity: 0.1; /* * Blame diff --git a/app/assets/stylesheets/framework/variables_overrides.scss b/app/assets/stylesheets/framework/variables_overrides.scss index fb4d3f23cd9..ea96381a098 100644 --- a/app/assets/stylesheets/framework/variables_overrides.scss +++ b/app/assets/stylesheets/framework/variables_overrides.scss @@ -7,6 +7,7 @@ $secondary: $gray-light; $input-disabled-bg: $gray-light; $input-border-color: $gray-200; $input-color: $gl-text-color; +$input-font-size: $gl-font-size; $font-family-sans-serif: $regular-font; $font-family-monospace: $monospace-font; $btn-line-height: 20px; diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss index 809ba6d4953..255383d89c8 100644 --- a/app/assets/stylesheets/pages/clusters.scss +++ b/app/assets/stylesheets/pages/clusters.scss @@ -69,6 +69,8 @@ align-self: flex-start; font-weight: 500; font-size: 20px; + color: $orange-900; + opacity: 1; margin: $gl-padding-8 14px 0 0; } diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index cb5f1a84005..c386493231c 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -21,7 +21,6 @@ .detail-page-header-body { position: relative; - line-height: 35px; display: flex; flex: 1 1; min-width: 0; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index b2b3720fdde..b3a634e23a3 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -494,6 +494,12 @@ table.code { } } + .line_holder:last-of-type { + td:first-child { + border-bottom-left-radius: $border-radius-default; + } + } + &.left-side-selected { td.line_content.parallel.right-side { user-select: none; diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 618f23d81b1..e34628002ac 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -156,6 +156,10 @@ &:hover { background: none; } + + a { + color: $blue-600; + } } } } diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index 22a515cbdaa..297f642681b 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -21,13 +21,6 @@ color: $login-brand-holder-color; } - h1:first-child { - font-weight: $gl-font-weight-normal; - margin-bottom: 0.68em; - margin-top: 0; - font-size: 34px; - } - h3 { font-size: 22px; } @@ -49,8 +42,8 @@ .login-box, .omniauth-container { box-shadow: 0 0 0 1px $border-color; - border-bottom-right-radius: $border-radius-small; - border-bottom-left-radius: $border-radius-small; + border-bottom-right-radius: $border-radius; + border-bottom-left-radius: $border-radius; padding: 15px; .login-heading h3 { @@ -95,7 +88,7 @@ } .omniauth-container { - border-radius: $border-radius-small; + border-radius: $border-radius; font-size: 13px; p { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 44b558dd5ff..ab5a9e170f0 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -87,6 +87,11 @@ padding: $gl-padding; } +.mr-widget-info { + padding-left: $gl-padding-50 - $gl-padding-32; + padding-right: $gl-padding; +} + .mr-state-widget { color: $gl-text-color; @@ -560,6 +565,10 @@ .mr-links { padding-left: $status-icon-size + $gl-btn-padding; + + &:last-child { + padding-bottom: $gl-padding; + } } .mr-info-list { @@ -1030,11 +1039,6 @@ background: $black-transparent; } -.source-branch-removal-status { - padding-left: 50px; - padding-bottom: $gl-padding; -} - .mr-compare { .diff-file .file-title-flex-parent { top: $header-height + 51px; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index f2b67a693c3..50c87e55f56 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -107,6 +107,7 @@ $note-form-margin-left: 72px; &.collapsed { color: $gl-text-color-secondary; + border-radius: 0 0 $border-radius-default $border-radius-default; svg { float: left; @@ -644,7 +645,7 @@ $note-form-margin-left: 72px; display: inline-flex; align-items: center; margin-left: 10px; - color: $gray-darkest; + color: $gray-600; @include notes-media('max', map-get($grid-breakpoints, sm) - 1) { float: none; diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 37071a57bb3..dbf600df9d6 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -261,3 +261,13 @@ input[type='checkbox']:hover { color: $blue-600; } } + +// Disable webkit input icons, link to solution: https://stackoverflow.com/questions/9421551/how-do-i-remove-all-default-webkit-search-field-styling +/* stylelint-disable property-no-vendor-prefix */ +input[type='search']::-webkit-search-decoration, +input[type='search']::-webkit-search-cancel-button, +input[type='search']::-webkit-search-results-button, +input[type='search']::-webkit-search-results-decoration { + -webkit-appearance: none; +} +/* stylelint-enable */ |