diff options
Diffstat (limited to 'app/assets')
184 files changed, 2445 insertions, 1243 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/api.js b/app/assets/javascripts/api.js index e583a8affd4..7cebb88f3a4 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -12,7 +12,7 @@ const Api = { groupProjectsPath: '/api/:version/groups/:id/projects.json', projectsPath: '/api/:version/projects.json', projectPath: '/api/:version/projects/:id', - projectLabelsPath: '/:namespace_path/:project_path/labels', + projectLabelsPath: '/:namespace_path/:project_path/-/labels', projectMergeRequestsPath: '/api/:version/projects/:id/merge_requests', projectMergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid', projectMergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes', diff --git a/app/assets/javascripts/batch_comments/mixins/resolved_status.js b/app/assets/javascripts/batch_comments/mixins/resolved_status.js index 96ee9f62ba4..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 comment 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/behaviors/shortcuts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js index 670f66b005e..c8eb96a625c 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js @@ -37,7 +37,7 @@ export default class ShortcutsIssuable extends Shortcuts { } // Sanity check: Make sure the selected text comes from a discussion : it can either contain a message... - let foundMessage = !!documentFragment.querySelector('.md'); + let foundMessage = Boolean(documentFragment.querySelector('.md')); // ... Or come from a message if (!foundMessage) { diff --git a/app/assets/javascripts/boards/components/board_blank_state.vue b/app/assets/javascripts/boards/components/board_blank_state.vue index 47a46502bff..1cbd31729cd 100644 --- a/app/assets/javascripts/boards/components/board_blank_state.vue +++ b/app/assets/javascripts/boards/components/board_blank_state.vue @@ -1,6 +1,5 @@ <script> /* global ListLabel */ -import _ from 'underscore'; import Cookies from 'js-cookie'; import boardsStore from '../stores/boards_store'; @@ -29,8 +28,6 @@ export default { }); }); - boardsStore.state.lists = _.sortBy(boardsStore.state.lists, 'position'); - // Save the labels gl.boardService .generateDefaultLists() diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index c9972d051aa..b1a8b13f3ac 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -142,8 +142,10 @@ export default { const card = this.$refs.issue[e.oldIndex]; card.showDetail = false; - boardsStore.moving.list = card.list; - boardsStore.moving.issue = boardsStore.moving.list.findIssue(+e.item.dataset.issueId); + + const { list } = card; + const issue = list.findIssue(Number(e.item.dataset.issueId)); + boardsStore.startMoving(list, issue); sortableStart(); }, diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue index dc1bdc23b5e..63dc99db086 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.vue +++ b/app/assets/javascripts/boards/components/board_new_issue.vue @@ -72,7 +72,7 @@ export default { // Need this because our jQuery very kindly disables buttons on ALL form submissions $(this.$refs.submitButton).enable(); - boardsStore.detail.issue = issue; + boardsStore.setIssueDetail(issue); boardsStore.detail.list = this.list; }) .catch(() => { diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue index 17de7b2cf1e..a8516f178fc 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.vue +++ b/app/assets/javascripts/boards/components/issue_card_inner.vue @@ -6,7 +6,6 @@ import Icon from '~/vue_shared/components/icon.vue'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; import issueCardInner from 'ee_else_ce/boards/mixins/issue_card_inner'; import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; -import eventHub from '../eventhub'; import IssueDueDate from './issue_due_date.vue'; import IssueTimeEstimate from './issue_time_estimate.vue'; import boardsStore from '../stores/boards_store'; @@ -136,23 +135,7 @@ export default { const labelTitle = encodeURIComponent(label.title); const filter = `label_name[]=${labelTitle}`; - this.applyFilter(filter); - }, - applyFilter(filter) { - const filterPath = boardsStore.filter.path.split('&'); - const filterIndex = filterPath.indexOf(filter); - - if (filterIndex === -1) { - filterPath.push(filter); - } else { - filterPath.splice(filterIndex, 1); - } - - boardsStore.filter.path = filterPath.join('&'); - - boardsStore.updateFiltersUrl(); - - eventHub.$emit('updateTokens'); + boardsStore.toggleFilter(filter); }, labelStyle(label) { return { diff --git a/app/assets/javascripts/boards/components/modal/index.vue b/app/assets/javascripts/boards/components/modal/index.vue index 8e09e265cfb..defa1f75ba2 100644 --- a/app/assets/javascripts/boards/components/modal/index.vue +++ b/app/assets/javascripts/boards/components/modal/index.vue @@ -124,7 +124,7 @@ export default { data.issues.forEach(issueObj => { const issue = new ListIssue(issueObj); const foundSelectedIssue = ModalStore.findSelectedIssue(issue); - issue.selected = !!foundSelectedIssue; + issue.selected = Boolean(foundSelectedIssue); this.issues.push(issue); }); diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue index a2b8a0af236..4ab2b17301f 100644 --- a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue +++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue @@ -48,7 +48,7 @@ export default Vue.extend({ list.removeIssue(issue); }); - boardsStore.detail.issue = {}; + boardsStore.clearDetailIssue(); }, /** * Build the default patch request. diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index 4995a8d9367..e9cab3e3bba 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -1,5 +1,4 @@ import $ from 'jquery'; -import _ from 'underscore'; import Vue from 'vue'; import Flash from '~/flash'; @@ -106,18 +105,23 @@ export default () => { gl.boardService .all() .then(res => res.data) - .then(data => { - data.forEach(board => { - const list = boardsStore.addList(board, this.defaultAvatar); - - if (list.type === 'closed') { - list.position = Infinity; - } else if (list.type === 'backlog') { - list.position = -1; + .then(lists => { + lists.forEach(listObj => { + let { position } = listObj; + if (listObj.list_type === 'closed') { + position = Infinity; + } else if (listObj.list_type === 'backlog') { + position = -1; } - }); - this.state.lists = _.sortBy(this.state.lists, 'position'); + boardsStore.addList( + { + ...listObj, + position, + }, + this.defaultAvatar, + ); + }); boardsStore.addBlankState(); this.loading = false; @@ -164,10 +168,10 @@ export default () => { }); } - boardsStore.detail.issue = newIssue; + boardsStore.setIssueDetail(newIssue); }, clearDetailIssue() { - boardsStore.detail.issue = {}; + boardsStore.clearDetailIssue(); }, toggleSubscription(id) { const { issue } = boardsStore.detail; diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index 7e5d0e0f888..08aecfab8a4 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -37,8 +37,8 @@ class List { this.type = obj.list_type; const typeInfo = this.getTypeInfo(this.type); - this.preset = !!typeInfo.isPreset; - this.isExpandable = !!typeInfo.isExpandable; + this.preset = Boolean(typeInfo.isPreset); + this.isExpandable = Boolean(typeInfo.isExpandable); this.isExpanded = true; this.page = 1; this.loading = true; diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 70861fbf2b3..f72ab189015 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -8,6 +8,7 @@ import Cookies from 'js-cookie'; import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee'; import { getUrlParamsArray, parseBoolean } from '~/lib/utils/common_utils'; import { __ } from '~/locale'; +import eventHub from '../eventhub'; const boardsStore = { disabled: false, @@ -45,7 +46,7 @@ const boardsStore = { }, addList(listObj, defaultAvatar) { const list = new List(listObj, defaultAvatar); - this.state.lists.push(list); + this.state.lists = _.sortBy([...this.state.lists, list], 'position'); return list; }, @@ -82,8 +83,6 @@ const boardsStore = { title: __('Welcome to your Issue Board!'), position: 0, }); - - this.state.lists = _.sortBy(this.state.lists, 'position'); }, removeBlankState() { this.removeList('blank'); @@ -111,6 +110,11 @@ const boardsStore = { }); listFrom.update(); }, + + startMoving(list, issue) { + Object.assign(this.moving, { list, issue }); + }, + moveIssueToList(listFrom, listTo, issue, newIndex) { const issueTo = listTo.findIssue(issue.id); const issueLists = issue.getLists(); @@ -185,9 +189,35 @@ const boardsStore = { findListByLabelId(id) { return this.state.lists.find(list => list.type === 'label' && list.label.id === id); }, + + toggleFilter(filter) { + const filterPath = this.filter.path.split('&'); + const filterIndex = filterPath.indexOf(filter); + + if (filterIndex === -1) { + filterPath.push(filter); + } else { + filterPath.splice(filterIndex, 1); + } + + this.filter.path = filterPath.join('&'); + + this.updateFiltersUrl(); + + eventHub.$emit('updateTokens'); + }, + updateFiltersUrl() { window.history.pushState(null, null, `?${this.filter.path}`); }, + + clearDetailIssue() { + this.setIssueDetail({}); + }, + + setIssueDetail(issueDetail) { + this.detail.issue = issueDetail; + }, }; BoardsStoreEE.initEESpecific(boardsStore); diff --git a/app/assets/javascripts/branches/branches_delete_modal.js b/app/assets/javascripts/branches/branches_delete_modal.js index f34496f84c6..f4c3fa185d8 100644 --- a/app/assets/javascripts/branches/branches_delete_modal.js +++ b/app/assets/javascripts/branches/branches_delete_modal.js @@ -23,7 +23,7 @@ class DeleteModal { const branchData = e.currentTarget.dataset; this.branchName = branchData.branchName || ''; this.deletePath = branchData.deletePath || ''; - this.isMerged = !!branchData.isMerged; + this.isMerged = Boolean(branchData.isMerged); this.updateModal(); } diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index 561b6bdd9f1..bc2e71b99f2 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'); @@ -305,8 +353,10 @@ export default class Clusters { saveKnativeDomain(data) { const appId = data.id; - this.store.updateAppProperty(appId, 'status', APPLICATION_STATUS.UPDATING); - this.service.updateApplication(appId, data.params); + this.store.updateApplication(appId); + this.service.updateApplication(appId, data.params).catch(() => { + this.store.notifyUpdateFailure(appId); + }); } setKnativeHostname(data) { diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue index 5f7675bb432..7b173be599a 100644 --- a/app/assets/javascripts/clusters/components/application_row.vue +++ b/app/assets/javascripts/clusters/components/application_row.vue @@ -89,6 +89,10 @@ export default { type: Boolean, required: false, }, + updateable: { + type: Boolean, + default: true, + }, updateSuccessful: { type: Boolean, required: false, @@ -138,7 +142,7 @@ export default { ); }, hasLogo() { - return !!this.logoUrl; + return Boolean(this.logoUrl); }, identiconId() { // generate a deterministic integer id for the identicon background @@ -326,36 +330,38 @@ export default { </ul> </div> - <div - v-if="shouldShowUpgradeDetails" - class="form-text text-muted label p-0 js-cluster-application-upgrade-details" - > - {{ versionLabel }} - <span v-if="updateSuccessful">to</span> - - <gl-link - v-if="updateSuccessful" - :href="chartRepo" - target="_blank" - class="js-cluster-application-upgrade-version" - >chart v{{ version }}</gl-link + <div v-if="updateable"> + <div + v-if="shouldShowUpgradeDetails" + class="form-text text-muted label p-0 js-cluster-application-upgrade-details" > - </div> + {{ versionLabel }} + <span v-if="updateSuccessful">to</span> - <div - v-if="updateFailed && !isUpgrading" - class="bs-callout bs-callout-danger cluster-application-banner mt-2 mb-0 js-cluster-application-upgrade-failure-message" - > - {{ upgradeFailureDescription }} + <gl-link + v-if="updateSuccessful" + :href="chartRepo" + target="_blank" + class="js-cluster-application-upgrade-version" + >chart v{{ version }}</gl-link + > + </div> + + <div + v-if="updateFailed && !isUpgrading" + class="bs-callout bs-callout-danger cluster-application-banner mt-2 mb-0 js-cluster-application-upgrade-failure-message" + > + {{ upgradeFailureDescription }} + </div> + <loading-button + v-if="upgradeAvailable || updateFailed || isUpgrading" + class="btn btn-primary js-cluster-application-upgrade-button mt-2" + :loading="isUpgrading" + :disabled="isUpgrading" + :label="upgradeButtonLabel" + @click="upgradeClicked" + /> </div> - <loading-button - v-if="upgradeAvailable || updateFailed || isUpgrading" - class="btn btn-primary js-cluster-application-upgrade-button mt-2" - :loading="isUpgrading" - :disabled="isUpgrading" - :label="upgradeButtonLabel" - @click="upgradeClicked" - /> </div> <div :class="{ 'section-25': showManageButton, 'section-15': !showManageButton }" diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index 73760da9b98..2d129245d37 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -15,6 +15,7 @@ import prometheusLogo from 'images/cluster_app_logos/prometheus.png'; import { s__, sprintf } from '../../locale'; import applicationRow from './application_row.vue'; import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; +import KnativeDomainEditor from './knative_domain_editor.vue'; import { CLUSTER_TYPE, APPLICATION_STATUS, INGRESS } from '../constants'; import LoadingButton from '~/vue_shared/components/loading_button.vue'; import eventHub from '~/clusters/event_hub'; @@ -25,6 +26,7 @@ export default { clipboardButton, LoadingButton, GlLoadingIcon, + KnativeDomainEditor, }, props: { type: { @@ -154,64 +156,21 @@ export default { knative() { return this.applications.knative; }, - knativeInstalled() { - return ( - this.knative.status === APPLICATION_STATUS.INSTALLED || - this.knativeUpgrading || - this.knativeUpgradeFailed || - this.knative.status === APPLICATION_STATUS.UPDATED - ); - }, - knativeUpgrading() { - return ( - this.knative.status === APPLICATION_STATUS.UPDATING || - this.knative.status === APPLICATION_STATUS.SCHEDULED - ); - }, - knativeUpgradeFailed() { - return this.knative.status === APPLICATION_STATUS.UPDATE_ERRORED; - }, - knativeExternalEndpoint() { - return this.knative.externalIp || this.knative.externalHostname; - }, - knativeDescription() { - return sprintf( - _.escape( - s__( - `ClusterIntegration|Installing Knative may incur additional costs. Learn more about %{pricingLink}.`, - ), - ), - { - pricingLink: `<strong><a href="https://cloud.google.com/compute/pricing#lb" - target="_blank" rel="noopener noreferrer"> - ${_.escape(s__('ClusterIntegration|pricing'))}</a></strong>`, - }, - false, - ); - }, - canUpdateKnativeEndpoint() { - return this.knativeExternalEndpoint && !this.knativeUpgradeFailed && !this.knativeUpgrading; - }, - knativeHostname: { - get() { - return this.knative.hostname; - }, - set(hostname) { - eventHub.$emit('setKnativeHostname', { - id: 'knative', - hostname, - }); - }, - }, }, created() { this.helmInstallIllustration = helmInstallIllustration; }, methods: { - saveKnativeDomain() { + saveKnativeDomain(hostname) { eventHub.$emit('saveKnativeDomain', { id: 'knative', - params: { hostname: this.knative.hostname }, + params: { hostname }, + }); + }, + setKnativeHostname(hostname) { + eventHub.$emit('setKnativeHostname', { + id: 'knative', + hostname, }); }, }, @@ -318,9 +277,9 @@ export default { generated endpoint in order to access your application after it has been deployed.`) }} - <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{ - __('More information') - }}</a> + <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer"> + {{ __('More information') }} + </a> </p> </div> @@ -330,9 +289,9 @@ export default { the process of being assigned. Please check your Kubernetes cluster or Quotas on Google Kubernetes Engine if it takes a long time.`) }} - <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{ - __('More information') - }}</a> + <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer"> + {{ __('More information') }} + </a> </p> </template> <template v-if="!ingressInstalled"> @@ -361,9 +320,9 @@ export default { <div slot="description"> <p v-html="certManagerDescription"></p> <div class="form-group"> - <label for="cert-manager-issuer-email">{{ - s__('ClusterIntegration|Issuer Email') - }}</label> + <label for="cert-manager-issuer-email"> + {{ s__('ClusterIntegration|Issuer Email') }} + </label> <div class="input-group"> <input v-model="applications.cert_manager.email" @@ -491,9 +450,9 @@ export default { s__(`ClusterIntegration|Replace this with your own hostname if you want. If you do so, point hostname to Ingress IP Address from above.`) }} - <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{ - __('More information') - }}</a> + <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer"> + {{ __('More information') }} + </a> </p> </div> </template> @@ -514,6 +473,7 @@ export default { :uninstallable="applications.knative.uninstallable" :uninstall-successful="applications.knative.uninstallSuccessful" :uninstall-failed="applications.knative.uninstallFailed" + :updateable="false" :disabled="!helmInstalled" v-bind="applications.knative" title-link="https://github.com/knative/docs" @@ -525,9 +485,9 @@ export default { s__(`ClusterIntegration|You must have an RBAC-enabled cluster to install Knative.`) }} - <a :href="helpPath" target="_blank" rel="noopener noreferrer">{{ - __('More information') - }}</a> + <a :href="helpPath" target="_blank" rel="noopener noreferrer"> + {{ __('More information') }} + </a> </p> <br /> </span> @@ -540,83 +500,13 @@ export default { }} </p> - <div class="row"> - <template v-if="knativeInstalled || (helmInstalled && rbac)"> - <div - :class="{ 'col-md-6': knativeInstalled, 'col-12': helmInstalled && rbac }" - class="form-group col-sm-12 mb-0" - > - <label for="knative-domainname"> - <strong>{{ s__('ClusterIntegration|Knative Domain Name:') }}</strong> - </label> - <input - id="knative-domainname" - v-model="knativeHostname" - type="text" - class="form-control js-knative-domainname" - /> - </div> - </template> - <template v-if="knativeInstalled"> - <div class="form-group col-sm-12 col-md-6 pl-md-0 mb-0 mt-3 mt-md-0"> - <label for="knative-endpoint"> - <strong>{{ s__('ClusterIntegration|Knative Endpoint:') }}</strong> - </label> - <div v-if="knativeExternalEndpoint" class="input-group"> - <input - id="knative-endpoint" - :value="knativeExternalEndpoint" - type="text" - class="form-control js-knative-endpoint" - readonly - /> - <span class="input-group-append"> - <clipboard-button - :text="knativeExternalEndpoint" - :title="s__('ClusterIntegration|Copy Knative Endpoint to clipboard')" - class="input-group-text js-knative-endpoint-clipboard-btn" - /> - </span> - </div> - <div v-else class="input-group"> - <input type="text" class="form-control js-endpoint" readonly /> - <gl-loading-icon - class="position-absolute align-self-center ml-2 js-knative-ip-loading-icon" - /> - </div> - </div> - - <p class="form-text text-muted col-12"> - {{ - s__( - `ClusterIntegration|To access your application after deployment, point a wildcard DNS to the Knative Endpoint.`, - ) - }} - <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{ - __('More information') - }}</a> - </p> - - <p - v-if="!knativeExternalEndpoint" - class="settings-message js-no-knative-endpoint-message mt-2 mr-3 mb-0 ml-3" - > - {{ - s__(`ClusterIntegration|The endpoint is in - the process of being assigned. Please check your Kubernetes - cluster or Quotas on Google Kubernetes Engine if it takes a long time.`) - }} - </p> - - <button - v-if="canUpdateKnativeEndpoint" - class="btn btn-success js-knative-save-domain-button mt-3 ml-3" - @click="saveKnativeDomain" - > - {{ s__('ClusterIntegration|Save changes') }} - </button> - </template> - </div> + <knative-domain-editor + v-if="knative.installed || (helmInstalled && rbac)" + :knative="knative" + :ingress-dns-help-path="ingressDnsHelpPath" + @save="saveKnativeDomain" + @set="setKnativeHostname" + /> </div> </application-row> </div> diff --git a/app/assets/javascripts/clusters/components/knative_domain_editor.vue b/app/assets/javascripts/clusters/components/knative_domain_editor.vue new file mode 100644 index 00000000000..480228619a5 --- /dev/null +++ b/app/assets/javascripts/clusters/components/knative_domain_editor.vue @@ -0,0 +1,150 @@ +<script> +import LoadingButton from '~/vue_shared/components/loading_button.vue'; +import ClipboardButton from '../../vue_shared/components/clipboard_button.vue'; +import { GlLoadingIcon } from '@gitlab/ui'; +import { s__ } from '~/locale'; + +import { APPLICATION_STATUS } from '~/clusters/constants'; + +const { UPDATING, UNINSTALLING } = APPLICATION_STATUS; + +export default { + components: { + LoadingButton, + ClipboardButton, + GlLoadingIcon, + }, + props: { + knative: { + type: Object, + required: true, + }, + ingressDnsHelpPath: { + type: String, + default: '', + }, + }, + computed: { + saveButtonDisabled() { + return [UNINSTALLING, UPDATING].includes(this.knative.status); + }, + saving() { + return [UPDATING].includes(this.knative.status); + }, + saveButtonLabel() { + return this.saving ? this.__('Saving') : this.__('Save changes'); + }, + knativeInstalled() { + return this.knative.installed; + }, + knativeExternalEndpoint() { + return this.knative.externalIp || this.knative.externalHostname; + }, + knativeUpdateSuccessful() { + return this.knative.updateSuccessful; + }, + knativeHostname: { + get() { + return this.knative.hostname; + }, + set(hostname) { + this.$emit('set', hostname); + }, + }, + }, + watch: { + knativeUpdateSuccessful(updateSuccessful) { + if (updateSuccessful) { + this.$toast.show(s__('ClusterIntegration|Knative domain name was updated successfully.')); + } + }, + }, +}; +</script> + +<template> + <div class="row"> + <div + v-if="knative.updateFailed" + class="bs-callout bs-callout-danger cluster-application-banner col-12 mt-2 mb-2 js-cluster-knative-domain-name-failure-message" + > + {{ s__('ClusterIntegration|Something went wrong while updating Knative domain name.') }} + </div> + + <template> + <div + :class="{ 'col-md-6': knativeInstalled, 'col-12': !knativeInstalled }" + class="form-group col-sm-12 mb-0" + > + <label for="knative-domainname"> + <strong>{{ s__('ClusterIntegration|Knative Domain Name:') }}</strong> + </label> + <input + id="knative-domainname" + v-model="knativeHostname" + type="text" + class="form-control js-knative-domainname" + /> + </div> + </template> + <template v-if="knativeInstalled"> + <div class="form-group col-sm-12 col-md-6 pl-md-0 mb-0 mt-3 mt-md-0"> + <label for="knative-endpoint"> + <strong>{{ s__('ClusterIntegration|Knative Endpoint:') }}</strong> + </label> + <div v-if="knativeExternalEndpoint" class="input-group"> + <input + id="knative-endpoint" + :value="knativeExternalEndpoint" + type="text" + class="form-control js-knative-endpoint" + readonly + /> + <span class="input-group-append"> + <clipboard-button + :text="knativeExternalEndpoint" + :title="s__('ClusterIntegration|Copy Knative Endpoint to clipboard')" + class="input-group-text js-knative-endpoint-clipboard-btn" + /> + </span> + </div> + <div v-else class="input-group"> + <input type="text" class="form-control js-endpoint" readonly /> + <gl-loading-icon + class="position-absolute align-self-center ml-2 js-knative-ip-loading-icon" + /> + </div> + </div> + + <p class="form-text text-muted col-12"> + {{ + s__( + `ClusterIntegration|To access your application after deployment, point a wildcard DNS to the Knative Endpoint.`, + ) + }} + <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer"> + {{ __('More information') }} + </a> + </p> + + <p + v-if="!knativeExternalEndpoint" + class="settings-message js-no-knative-endpoint-message mt-2 mr-3 mb-0 ml-3" + > + {{ + s__(`ClusterIntegration|The endpoint is in + the process of being assigned. Please check your Kubernetes + cluster or Quotas on Google Kubernetes Engine if it takes a long time.`) + }} + </p> + + <loading-button + class="btn-success js-knative-save-domain-button mt-3 ml-3" + :loading="saving" + :disabled="saveButtonDisabled" + :label="saveButtonLabel" + @click="$emit('save', knativeHostname)" + /> + </template> + </div> +</template> diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js index 1b4d7e8372c..89e61c10a46 100644 --- a/app/assets/javascripts/clusters/stores/clusters_store.js +++ b/app/assets/javascripts/clusters/stores/clusters_store.js @@ -77,6 +77,8 @@ export default class ClusterStore { isEditingHostName: false, externalIp: null, externalHostname: null, + updateSuccessful: false, + updateFailed: false, }, }, }; diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js index a0ca44caa07..d0cc4897aeb 100644 --- a/app/assets/javascripts/commons/polyfills.js +++ b/app/assets/javascripts/commons/polyfills.js @@ -1,19 +1,21 @@ // ECMAScript polyfills -import 'core-js/fn/array/fill'; -import 'core-js/fn/array/find'; -import 'core-js/fn/array/find-index'; -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/promise'; -import 'core-js/fn/promise/finally'; -import 'core-js/fn/string/code-point-at'; -import 'core-js/fn/string/from-code-point'; -import 'core-js/fn/string/includes'; -import 'core-js/fn/symbol'; -import 'core-js/es6/map'; -import 'core-js/es6/weak-map'; +import 'core-js/es/array/fill'; +import 'core-js/es/array/find'; +import 'core-js/es/array/find-index'; +import 'core-js/es/array/from'; +import 'core-js/es/array/includes'; +import 'core-js/es/object/assign'; +import 'core-js/es/object/values'; +import 'core-js/es/object/entries'; +import 'core-js/es/promise'; +import 'core-js/es/promise/finally'; +import 'core-js/es/string/code-point-at'; +import 'core-js/es/string/from-code-point'; +import 'core-js/es/string/includes'; +import 'core-js/es/symbol'; +import 'core-js/es/map'; +import 'core-js/es/weak-map'; +import 'core-js/modules/web.url'; // Browser polyfills import 'formdata-polyfill'; diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js index 37a3ceb5341..5bfe158ceda 100644 --- a/app/assets/javascripts/compare_autocomplete.js +++ b/app/assets/javascripts/compare_autocomplete.js @@ -40,7 +40,7 @@ export default function initCompareAutocomplete(limitTo = null, clickHandler = ( }, selectable: true, filterable: true, - filterRemote: !!$dropdown.data('refsUrl'), + filterRemote: Boolean($dropdown.data('refsUrl')), fieldName: $dropdown.data('fieldName'), filterInput: 'input[type="search"]', renderRow: function(ref) { diff --git a/app/assets/javascripts/create_item_dropdown.js b/app/assets/javascripts/create_item_dropdown.js index 916b190f469..fa0f04c7d82 100644 --- a/app/assets/javascripts/create_item_dropdown.js +++ b/app/assets/javascripts/create_item_dropdown.js @@ -12,7 +12,7 @@ export default class CreateItemDropdown { this.fieldName = options.fieldName; this.onSelect = options.onSelect || (() => {}); this.getDataOption = options.getData; - this.getDataRemote = !!options.filterRemote; + this.getDataRemote = Boolean(options.filterRemote); this.createNewItemFromValueOption = options.createNewItemFromValue; this.$dropdown = options.$dropdown; this.$dropdownContainer = this.$dropdown.parent(); diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue index a767379d662..bd7259ce3ee 100644 --- a/app/assets/javascripts/diffs/components/commit_item.vue +++ b/app/assets/javascripts/diffs/components/commit_item.vue @@ -69,7 +69,7 @@ export default { :link-href="authorUrl" :img-src="authorAvatar" :img-alt="authorName" - :img-size="36" + :img-size="40" class="avatar-cell d-none d-sm-block" /> <div class="commit-detail flex-list"> diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index 392de1c9f23..eb9f1465945 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -240,7 +240,7 @@ export default { css-class="btn-default btn-transparent btn-clipboard" /> - <small v-if="isModeChanged" ref="fileMode"> + <small v-if="isModeChanged" ref="fileMode" class="mr-1"> {{ diffFile.a_mode }} → {{ diffFile.b_mode }} </small> @@ -254,16 +254,17 @@ export default { <diff-stats :added-lines="diffFile.added_lines" :removed-lines="diffFile.removed_lines" /> <div class="btn-group" role="group"> <template v-if="diffFile.blob && diffFile.blob.readable_text"> - <button - :disabled="!diffHasDiscussions(diffFile)" - :class="{ active: hasExpandedDiscussions }" - :title="s__('MergeRequests|Toggle comments for this file')" - class="js-btn-vue-toggle-comments btn" - type="button" - @click="handleToggleDiscussions" - > - <icon name="comment" /> - </button> + <span v-gl-tooltip.hover :title="s__('MergeRequests|Toggle comments for this file')"> + <gl-button + :disabled="!diffHasDiscussions(diffFile)" + :class="{ active: hasExpandedDiscussions }" + class="js-btn-vue-toggle-comments btn" + type="button" + @click="handleToggleDiscussions" + > + <icon name="comment" /> + </gl-button> + </span> <edit-button v-if="!diffFile.deleted_file" diff --git a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue index 0c0a0faa59d..7cf3d90d468 100644 --- a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue +++ b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue @@ -86,7 +86,6 @@ export default { :key="note.id" :img-src="note.author.avatar_url" :tooltip-text="getTooltipText(note)" - :size="19" class="diff-comment-avatar js-diff-comment-avatar" @click.native="toggleDiscussions" /> diff --git a/app/assets/javascripts/diffs/components/edit_button.vue b/app/assets/javascripts/diffs/components/edit_button.vue index f0cc5de4b33..dcb79cd5e16 100644 --- a/app/assets/javascripts/diffs/components/edit_button.vue +++ b/app/assets/javascripts/diffs/components/edit_button.vue @@ -38,7 +38,7 @@ export default { <template> <gl-button - v-gl-tooltip.bottom + v-gl-tooltip.top :href="editPath" :title="__('Edit file')" class="js-edit-blob" diff --git a/app/assets/javascripts/error_tracking_settings/store/getters.js b/app/assets/javascripts/error_tracking_settings/store/getters.js index a008b181907..d77e5f15469 100644 --- a/app/assets/javascripts/error_tracking_settings/store/getters.js +++ b/app/assets/javascripts/error_tracking_settings/store/getters.js @@ -2,10 +2,10 @@ import _ from 'underscore'; import { __, s__, sprintf } from '~/locale'; import { getDisplayName } from '../utils'; -export const hasProjects = state => !!state.projects && state.projects.length > 0; +export const hasProjects = state => Boolean(state.projects) && state.projects.length > 0; export const isProjectInvalid = (state, getters) => - !!state.selectedProject && + Boolean(state.selectedProject) && getters.hasProjects && !state.projects.some(project => _.isMatch(state.selectedProject, project)); 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/frequent_items/store/actions.js b/app/assets/javascripts/frequent_items/store/actions.js index 3dd89a82a42..ba62ab67e50 100644 --- a/app/assets/javascripts/frequent_items/store/actions.js +++ b/app/assets/javascripts/frequent_items/store/actions.js @@ -51,7 +51,7 @@ export const fetchSearchedItems = ({ state, dispatch }, searchQuery) => { const params = { simple: true, per_page: 20, - membership: !!gon.current_user_id, + membership: Boolean(gon.current_user_id), }; if (state.namespace === 'projects') { diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index f437954881c..0af9aabd8cf 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -1,4 +1,5 @@ import $ from 'jquery'; +import 'at.js'; import _ from 'underscore'; import glRegexp from './lib/utils/regexp'; import AjaxCache from './lib/utils/ajax_cache'; diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 1c6b18c0e03..18fa6265108 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -307,8 +307,8 @@ GitLabDropdown = (function() { // Set Defaults this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT); this.noFilterInput = this.options.noFilterInput || this.getElement(NO_FILTER_INPUT); - this.highlight = !!this.options.highlight; - this.icon = !!this.options.icon; + this.highlight = Boolean(this.options.highlight); + this.icon = Boolean(this.options.icon); this.filterInputBlur = this.options.filterInputBlur != null ? this.options.filterInputBlur : true; // If no input is passed create a default one @@ -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/gl_form.js b/app/assets/javascripts/gl_form.js index 5a6d44ef838..a66555838ba 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -13,7 +13,7 @@ export default class GLForm { const dataSources = (gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources) || {}; Object.keys(this.enableGFM).forEach(item => { if (item !== 'emojis') { - this.enableGFM[item] = !!dataSources[item]; + this.enableGFM[item] = Boolean(dataSources[item]); } }); // Before we start, we should clean up any previous data for this form 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/components/preview/clientside.vue b/app/assets/javascripts/ide/components/preview/clientside.vue index c98dda00817..6999746f115 100644 --- a/app/assets/javascripts/ide/components/preview/clientside.vue +++ b/app/assets/javascripts/ide/components/preview/clientside.vue @@ -105,7 +105,7 @@ export default { .then(() => { this.initManager('#ide-preview', this.sandboxOpts, { fileResolver: { - isFile: p => Promise.resolve(!!this.entries[createPathWithExt(p)]), + isFile: p => Promise.resolve(Boolean(this.entries[createPathWithExt(p)])), readFile: p => this.loadFileContent(createPathWithExt(p)).then(content => content), }, }); diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue index 99f1d4a573d..5201c33b1b4 100644 --- a/app/assets/javascripts/ide/components/repo_commit_section.vue +++ b/app/assets/javascripts/ide/components/repo_commit_section.vue @@ -30,7 +30,7 @@ export default { ...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommittedChanges', 'activeFile']), ...mapGetters('commit', ['discardDraftButtonDisabled']), showStageUnstageArea() { - return !!(this.someUncommittedChanges || this.lastCommitMsg || !this.unusedSeal); + return Boolean(this.someUncommittedChanges || this.lastCommitMsg || !this.unusedSeal); }, activeFileKey() { return this.activeFile ? this.activeFile.key : null; diff --git a/app/assets/javascripts/ide/lib/editor_options.js b/app/assets/javascripts/ide/lib/editor_options.js index e35595ab1fd..dac2a8e8b51 100644 --- a/app/assets/javascripts/ide/lib/editor_options.js +++ b/app/assets/javascripts/ide/lib/editor_options.js @@ -11,7 +11,7 @@ export const defaultEditorOptions = { export default [ { - readOnly: model => !!model.file.file_lock, + readOnly: model => Boolean(model.file.file_lock), quickSuggestions: model => !(model.language === 'markdown'), }, ]; 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..5a736805fdc 100644 --- a/app/assets/javascripts/ide/stores/getters.js +++ b/app/assets/javascripts/ide/stores/getters.js @@ -36,12 +36,16 @@ 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}`]; -export const hasChanges = state => !!state.changedFiles.length || !!state.stagedFiles.length; +export const hasChanges = state => + Boolean(state.changedFiles.length) || Boolean(state.stagedFiles.length); -export const hasMergeRequest = state => !!state.currentMergeRequestId; +export const hasMergeRequest = state => Boolean(state.currentMergeRequestId); export const allBlobs = state => Object.keys(state.entries) @@ -67,7 +71,7 @@ export const isCommitModeActive = state => state.currentActivityView === activit export const isReviewModeActive = state => state.currentActivityView === activityBarViews.review; export const someUncommittedChanges = state => - !!(state.changedFiles.length || state.stagedFiles.length); + Boolean(state.changedFiles.length || state.stagedFiles.length); export const getChangesInFolder = state => path => { const changedFilesCount = state.changedFiles.filter(f => filePathMatches(f.path, path)).length; diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index c2760eb1554..77ea2084877 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -102,7 +102,7 @@ export const updateFilesAfterCommit = ({ commit, dispatch, rootState, rootGetter eventHub.$emit(`editor.update.model.content.${file.key}`, { content: file.content, - changed: !!changedFile, + changed: Boolean(changedFile), }); }); }; @@ -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/modules/pipelines/getters.js b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js index ef7cd4ff8e8..1d127d915d7 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/getters.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js @@ -1,6 +1,6 @@ import { states } from './constants'; -export const hasLatestPipeline = state => !state.isLoadingPipeline && !!state.latestPipeline; +export const hasLatestPipeline = state => !state.isLoadingPipeline && Boolean(state.latestPipeline); export const pipelineFailed = state => state.latestPipeline && state.latestPipeline.details.status.text === states.failed; 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.js b/app/assets/javascripts/ide/stores/mutations.js index 344b189decf..ae42b87c9a7 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -142,7 +142,7 @@ export default { Object.assign(state.entries[file.path], { raw: file.content, - changed: !!changedFile, + changed: Boolean(changedFile), staged: false, prevPath: '', moved: false, 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/image_diff/helpers/comment_indicator_helper.js b/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js index 05000c73052..7051a968dac 100644 --- a/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js +++ b/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js @@ -14,7 +14,7 @@ export function addCommentIndicator(containerEl, { x, y }) { export function removeCommentIndicator(imageFrameEl) { const commentIndicatorEl = imageFrameEl.querySelector('.comment-indicator'); const imageEl = imageFrameEl.querySelector('img'); - const willRemove = !!commentIndicatorEl; + const willRemove = Boolean(commentIndicatorEl); let meta = {}; if (willRemove) { diff --git a/app/assets/javascripts/image_diff/image_diff.js b/app/assets/javascripts/image_diff/image_diff.js index 3587f073a00..26c1b0ec7be 100644 --- a/app/assets/javascripts/image_diff/image_diff.js +++ b/app/assets/javascripts/image_diff/image_diff.js @@ -6,8 +6,8 @@ import { isImageLoaded } from '../lib/utils/image_utility'; export default class ImageDiff { constructor(el, options) { this.el = el; - this.canCreateNote = !!(options && options.canCreateNote); - this.renderCommentBadge = !!(options && options.renderCommentBadge); + this.canCreateNote = Boolean(options && options.canCreateNote); + this.renderCommentBadge = Boolean(options && options.renderCommentBadge); this.$noteContainer = $('.note-container', this.el); this.imageBadges = []; } diff --git a/app/assets/javascripts/image_diff/view_types.js b/app/assets/javascripts/image_diff/view_types.js index ab0a595571f..1a5123de220 100644 --- a/app/assets/javascripts/image_diff/view_types.js +++ b/app/assets/javascripts/image_diff/view_types.js @@ -5,5 +5,5 @@ export const viewTypes = { }; export function isValidViewType(validate) { - return !!Object.getOwnPropertyNames(viewTypes).find(viewType => viewType === validate); + return Boolean(Object.getOwnPropertyNames(viewTypes).find(viewType => viewType === validate)); } diff --git a/app/assets/javascripts/issuable_index.js b/app/assets/javascripts/issuable_index.js index f51c7a2f990..16f88cddce3 100644 --- a/app/assets/javascripts/issuable_index.js +++ b/app/assets/javascripts/issuable_index.js @@ -12,7 +12,7 @@ export default class IssuableIndex { } initBulkUpdate(pagePrefix) { const userCanBulkUpdate = $('.issues-bulk-update').length > 0; - const alreadyInitialized = !!this.bulkUpdateSidebar; + const alreadyInitialized = Boolean(this.bulkUpdateSidebar); if (userCanBulkUpdate && !alreadyInitialized) { IssuableBulkUpdateActions.init({ diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index ab0b4231255..e88ca4747c5 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -156,7 +156,7 @@ export default { return this.store.formState; }, hasUpdated() { - return !!this.state.updatedAt; + return Boolean(this.state.updatedAt); }, issueChanged() { const { diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue index d2f33dc31a7..1e1dce5f4fc 100644 --- a/app/assets/javascripts/issue_show/components/title.vue +++ b/app/assets/javascripts/issue_show/components/title.vue @@ -71,7 +71,7 @@ export default { 'issue-realtime-pre-pulse': preAnimation, 'issue-realtime-trigger-pulse': pulseAnimation, }" - class="title" + class="title qa-title" dir="auto" v-html="titleHtml" ></h2> 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/components/stages_dropdown.vue b/app/assets/javascripts/jobs/components/stages_dropdown.vue index 6e92b599b0a..cb073a9b04d 100644 --- a/app/assets/javascripts/jobs/components/stages_dropdown.vue +++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue @@ -2,6 +2,7 @@ import _ from 'underscore'; import { GlLink } from '@gitlab/ui'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; +import PipelineLink from '~/vue_shared/components/ci_pipeline_link.vue'; import Icon from '~/vue_shared/components/icon.vue'; export default { @@ -9,6 +10,7 @@ export default { CiIcon, Icon, GlLink, + PipelineLink, }, props: { pipeline: { @@ -48,9 +50,12 @@ export default { <ci-icon :status="pipeline.details.status" class="vertical-align-middle" /> <span class="font-weight-bold">{{ s__('Job|Pipeline') }}</span> - <gl-link :href="pipeline.path" class="js-pipeline-path link-commit qa-pipeline-path" - >#{{ pipeline.id }}</gl-link - > + <pipeline-link + :href="pipeline.path" + :pipeline-id="pipeline.id" + :pipeline-iid="pipeline.iid" + class="js-pipeline-path link-commit qa-pipeline-path" + /> <template v-if="hasRef"> {{ s__('Job|for') }} 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 c6dd21cd2d4..7064731a5ea 100644 --- a/app/assets/javascripts/label_manager.js +++ b/app/assets/javascripts/label_manager.js @@ -53,7 +53,7 @@ export default class LabelManager { toggleEmptyState($label, $btn, action) { this.emptyState.classList.toggle( 'hidden', - !!this.prioritizedLabels[0].querySelector(':scope > li'), + Boolean(this.prioritizedLabels[0].querySelector(':scope > li')), ); } diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js index 498c2348ca2..5857f9e22ae 100644 --- a/app/assets/javascripts/lib/graphql.js +++ b/app/assets/javascripts/lib/graphql.js @@ -1,24 +1,32 @@ import { ApolloClient } from 'apollo-client'; import { InMemoryCache } from 'apollo-cache-inmemory'; import { createUploadLink } from 'apollo-upload-client'; +import { ApolloLink } from 'apollo-link'; +import { BatchHttpLink } from 'apollo-link-batch-http'; 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, '/'); } + const httpOptions = { + uri, + headers: { + [csrf.headerKey]: csrf.token, + }, + }; + return new ApolloClient({ - link: createUploadLink({ - uri, - headers: { - [csrf.headerKey]: csrf.token, - }, - }), - cache: new InMemoryCache(), + link: ApolloLink.split( + operation => operation.getContext().hasUpload, + createUploadLink(httpOptions), + new BatchHttpLink(httpOptions), + ), + cache: new InMemoryCache(config.cacheConfig), resolvers, }); }; diff --git a/app/assets/javascripts/lib/utils/accessor.js b/app/assets/javascripts/lib/utils/accessor.js index 1d18992af63..39cffedcac6 100644 --- a/app/assets/javascripts/lib/utils/accessor.js +++ b/app/assets/javascripts/lib/utils/accessor.js @@ -2,7 +2,7 @@ function isPropertyAccessSafe(base, property) { let safe; try { - safe = !!base[property]; + safe = Boolean(base[property]); } catch (error) { safe = false; } diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index b236daff1e0..cc5e12aa467 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -94,6 +94,8 @@ export const handleLocationHash = () => { const fixedNav = document.querySelector('.navbar-gitlab'); const performanceBar = document.querySelector('#js-peek'); const topPadding = 8; + const diffFileHeader = document.querySelector('.js-file-title'); + const versionMenusContainer = document.querySelector('.mr-version-menus-container'); let adjustment = 0; if (fixedNav) adjustment -= fixedNav.offsetHeight; @@ -114,6 +116,14 @@ export const handleLocationHash = () => { adjustment -= performanceBar.offsetHeight; } + if (diffFileHeader) { + adjustment -= diffFileHeader.offsetHeight; + } + + if (versionMenusContainer) { + adjustment -= versionMenusContainer.offsetHeight; + } + if (isInMRPage()) { adjustment -= topPadding; } diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 4d6327840db..d3e6851496b 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())); } @@ -500,7 +513,7 @@ export const stringifyTime = (timeObject, fullNameFormat = false) => { const reducedTime = _.reduce( timeObject, (memo, unitValue, unitName) => { - const isNonZero = !!unitValue; + const isNonZero = Boolean(unitValue); if (fullNameFormat && isNonZero) { // Remove traling 's' if unit value is singular 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/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index 84a617acb42..b7922e29bb0 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -223,9 +223,9 @@ export function insertMarkdownText({ return tag.replace(textPlaceholder, val); } if (val.indexOf(tag) === 0) { - return '' + val.replace(tag, ''); + return String(val.replace(tag, '')); } else { - return '' + tag + val; + return String(tag) + val; } }) .join('\n'); @@ -233,7 +233,7 @@ export function insertMarkdownText({ } else if (tag.indexOf(textPlaceholder) > -1) { textToInsert = tag.replace(textPlaceholder, selected); } else { - textToInsert = '' + startChar + tag + selected + (wrap ? tag : ' '); + textToInsert = String(startChar) + tag + selected + (wrap ? tag : ' '); } if (removedFirstNewLine) { diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index bdfd06fc250..b5474fc5c71 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -121,4 +121,40 @@ export function webIDEUrl(route = undefined) { return returnUrl; } +/** + * Returns current base URL + */ +export function getBaseURL() { + const { protocol, host } = window.location; + return `${protocol}//${host}`; +} + +/** + * Returns true if url is an absolute or root-relative URL + * + * @param {String} url + */ +export function isAbsoluteOrRootRelative(url) { + return /^(https?:)?\//.test(url); +} + +/** + * Checks if the provided URL is a safe URL (absolute http(s) or root-relative URL) + * + * @param {String} url that will be checked + * @returns {Boolean} + */ +export function isSafeURL(url) { + if (!isAbsoluteOrRootRelative(url)) { + return false; + } + + try { + const parsedUrl = new URL(url, getBaseURL()); + return ['http:', 'https:'].includes(parsedUrl.protocol); + } catch (e) { + return false; + } +} + export { join as joinPaths } from 'path'; 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..def810e879a 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'; @@ -40,7 +38,7 @@ export default { GlModalDirective, }, props: { - externalDashboardPath: { + externalDashboardUrl: { type: String, required: false, default: '', @@ -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" @@ -274,7 +248,7 @@ export default { > </gl-dropdown> </div> - <div v-if="showTimeWindowDropdown" class="d-flex align-items-center"> + <div v-if="showTimeWindowDropdown" class="d-flex align-items-center prepend-left-8"> <strong>{{ s__('Metrics|Show last') }}</strong> <gl-dropdown class="prepend-left-10 js-time-window-dropdown" @@ -325,10 +299,11 @@ export default { </gl-modal> </div> <gl-button - v-if="externalDashboardPath.length" + v-if="externalDashboardUrl.length" class="js-external-dashboard-link prepend-left-8" variant="primary" - :href="externalDashboardPath" + :href="externalDashboardUrl" + target="_blank" > {{ __('View full dashboard') }} <icon name="external-link" /> @@ -336,7 +311,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 +320,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 +337,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..3fd9e07fa8b --- /dev/null +++ b/app/assets/javascripts/monitoring/stores/mutation_types.js @@ -0,0 +1,12 @@ +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_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/mr_notes/stores/getters.js b/app/assets/javascripts/mr_notes/stores/getters.js index b10e9f9f9f1..e48cfcd9564 100644 --- a/app/assets/javascripts/mr_notes/stores/getters.js +++ b/app/assets/javascripts/mr_notes/stores/getters.js @@ -1,5 +1,5 @@ export default { isLoggedIn(state, getters) { - return !!getters.getUserData.id; + return Boolean(getters.getUserData.id); }, }; diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue index c7cfc0f0f3b..307e56708e0 100644 --- a/app/assets/javascripts/notes/components/discussion_counter.vue +++ b/app/assets/javascripts/notes/components/discussion_counter.vue @@ -57,7 +57,7 @@ export default { class="line-resolve-btn is-disabled" type="button" > - <icon name="check-circle" /> + <icon :name="allResolved ? 'check-circle-filled' : 'check-circle'" /> </span> <span class="line-resolve-text"> {{ resolvedDiscussionsCount }}/{{ resolvableDiscussionsCount }} diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue index 5b6163a6214..228bb652597 100644 --- a/app/assets/javascripts/notes/components/discussion_notes.vue +++ b/app/assets/javascripts/notes/components/discussion_notes.vue @@ -49,7 +49,7 @@ export default { computed: { ...mapGetters(['userCanReply']), hasReplies() { - return !!this.replies.length; + return Boolean(this.replies.length); }, replies() { return this.discussion.notes.slice(1); diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index 7e8f23d6a96..c9c40cb6acf 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -135,7 +135,7 @@ export default { @click="onResolve" > <template v-if="!isResolving"> - <icon name="check-circle" /> + <icon :name="isResolved ? 'check-circle-filled' : 'check-circle'" /> </template> <gl-loading-icon v-else inline /> </button> @@ -147,13 +147,11 @@ export default { class="note-action-button note-emoji-button js-add-award js-note-emoji" href="#" title="Add reaction" + data-position="right" > - <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/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 47d74c2f892..aa80e25a3e0 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -10,7 +10,7 @@ import Flash from '../../flash'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import noteHeader from './note_header.vue'; import noteActions from './note_actions.vue'; -import noteBody from './note_body.vue'; +import NoteBody from './note_body.vue'; import eventHub from '../event_hub'; import noteable from '../mixins/noteable'; import resolvable from '../mixins/resolvable'; @@ -21,7 +21,7 @@ export default { userAvatarLink, noteHeader, noteActions, - noteBody, + NoteBody, TimelineEntryItem, }, mixins: [noteable, resolvable, draftMixin], @@ -75,7 +75,7 @@ export default { }; }, canReportAsAbuse() { - return !!this.note.report_abuse_path && this.author.id !== this.getUserData.id; + return Boolean(this.note.report_abuse_path) && this.author.id !== this.getUserData.id; }, noteAnchorId() { return `note_${this.note.id}`; @@ -209,7 +209,10 @@ export default { // we need to do this to prevent noteForm inconsistent content warning // this is something we intentionally do so we need to recover the content this.note.note = noteText; - this.$refs.noteBody.note.note = noteText; + const { noteBody } = this.$refs; + if (noteBody) { + noteBody.note.note = noteText; + } }, }, }; diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index 0f1976db37d..4d00e957973 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -127,6 +127,9 @@ export default { initUserPopovers(this.$el.querySelectorAll('.js-user-link')); }); }, + beforeDestroy() { + this.stopPolling(); + }, methods: { ...mapActions([ 'setLoadingState', @@ -144,6 +147,7 @@ export default { 'expandDiscussion', 'startTaskList', 'convertToDiscussion', + 'stopPolling', ]), fetchNotes() { if (this.isFetching) return null; diff --git a/app/assets/javascripts/notes/mixins/issuable_state.js b/app/assets/javascripts/notes/mixins/issuable_state.js index 97f3ea0d5de..ded0ac3cfa9 100644 --- a/app/assets/javascripts/notes/mixins/issuable_state.js +++ b/app/assets/javascripts/notes/mixins/issuable_state.js @@ -1,11 +1,11 @@ export default { methods: { isConfidential(issue) { - return !!issue.confidential; + return Boolean(issue.confidential); }, isLocked(issue) { - return !!issue.discussion_locked; + return Boolean(issue.discussion_locked); }, hasWarning(issue) { diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index 2d150e64ef7..d7982be3e4b 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -20,7 +20,7 @@ export const getNoteableData = state => state.noteableData; export const getNoteableDataByProp = state => prop => state.noteableData[prop]; -export const userCanReply = state => !!state.noteableData.current_user.can_create_note; +export const userCanReply = state => Boolean(state.noteableData.current_user.can_create_note); export const openState = state => state.noteableData.state; diff --git a/app/assets/javascripts/operation_settings/components/external_dashboard.vue b/app/assets/javascripts/operation_settings/components/external_dashboard.vue index 0a87d193b72..ed518611d0b 100644 --- a/app/assets/javascripts/operation_settings/components/external_dashboard.vue +++ b/app/assets/javascripts/operation_settings/components/external_dashboard.vue @@ -1,4 +1,5 @@ <script> +import { mapState, mapActions } from 'vuex'; import { GlButton, GlFormGroup, GlFormInput, GlLink } from '@gitlab/ui'; export default { @@ -8,26 +9,34 @@ export default { GlFormInput, GlLink, }, - props: { - externalDashboardPath: { - type: String, - required: false, - default: '', - }, - externalDashboardHelpPagePath: { - type: String, - required: true, + computed: { + ...mapState([ + 'externalDashboardHelpPagePath', + 'externalDashboardUrl', + 'operationsSettingsEndpoint', + ]), + userDashboardUrl: { + get() { + return this.externalDashboardUrl; + }, + set(url) { + this.setExternalDashboardUrl(url); + }, }, }, + methods: { + ...mapActions(['setExternalDashboardUrl', 'updateExternalDashboardUrl']), + }, }; </script> <template> - <section class="settings expanded"> + <section class="settings no-animate"> <div class="settings-header"> <h4 class="js-section-header"> {{ s__('ExternalMetrics|External Dashboard') }} </h4> + <gl-button class="js-settings-toggle">{{ __('Expand') }}</gl-button> <p class="js-section-sub-header"> {{ s__( @@ -44,11 +53,12 @@ export default { :description="s__('ExternalMetrics|Enter the URL of the dashboard you want to link to')" > <gl-form-input - :value="externalDashboardPath" + v-model="userDashboardUrl" placeholder="https://my-org.gitlab.io/my-dashboards" + @keydown.enter.native.prevent="updateExternalDashboardUrl" /> </gl-form-group> - <gl-button variant="success"> + <gl-button variant="success" @click="updateExternalDashboardUrl"> {{ __('Save Changes') }} </gl-button> </form> diff --git a/app/assets/javascripts/operation_settings/index.js b/app/assets/javascripts/operation_settings/index.js index 1171f3ece9f..6946578e6d2 100644 --- a/app/assets/javascripts/operation_settings/index.js +++ b/app/assets/javascripts/operation_settings/index.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import store from './store'; import ExternalDashboardForm from './components/external_dashboard.vue'; export default () => { @@ -14,13 +15,9 @@ export default () => { return new Vue({ el, + store: store(el.dataset), render(createElement) { - return createElement(ExternalDashboardForm, { - props: { - ...el.dataset, - expanded: false, - }, - }); + return createElement(ExternalDashboardForm); }, }); }; diff --git a/app/assets/javascripts/operation_settings/store/actions.js b/app/assets/javascripts/operation_settings/store/actions.js new file mode 100644 index 00000000000..ec05b0c76cf --- /dev/null +++ b/app/assets/javascripts/operation_settings/store/actions.js @@ -0,0 +1,38 @@ +import axios from '~/lib/utils/axios_utils'; +import { __ } from '~/locale'; +import createFlash from '~/flash'; +import { refreshCurrentPage } from '~/lib/utils/url_utility'; +import * as mutationTypes from './mutation_types'; + +export const setExternalDashboardUrl = ({ commit }, url) => + commit(mutationTypes.SET_EXTERNAL_DASHBOARD_URL, url); + +export const updateExternalDashboardUrl = ({ state, dispatch }) => + axios + .patch(state.operationsSettingsEndpoint, { + project: { + metrics_setting_attributes: { + external_dashboard_url: state.externalDashboardUrl, + }, + }, + }) + .then(() => dispatch('receiveExternalDashboardUpdateSuccess')) + .catch(error => dispatch('receiveExternalDashboardUpdateError', error)); + +export const receiveExternalDashboardUpdateSuccess = () => { + /** + * The operations_controller currently handles successful requests + * by creating a flash banner messsage to notify the user. + */ + refreshCurrentPage(); +}; + +export const receiveExternalDashboardUpdateError = (_, error) => { + const { response } = error; + const message = response.data && response.data.message ? response.data.message : ''; + + createFlash(`${__('There was an error saving your changes.')} ${message}`, 'alert'); +}; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/operation_settings/store/index.js b/app/assets/javascripts/operation_settings/store/index.js new file mode 100644 index 00000000000..e96bb1e8aad --- /dev/null +++ b/app/assets/javascripts/operation_settings/store/index.js @@ -0,0 +1,16 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import createState from './state'; +import * as actions from './actions'; +import mutations from './mutations'; + +Vue.use(Vuex); + +export const createStore = initialState => + new Vuex.Store({ + state: createState(initialState), + actions, + mutations, + }); + +export default createStore; diff --git a/app/assets/javascripts/operation_settings/store/mutation_types.js b/app/assets/javascripts/operation_settings/store/mutation_types.js new file mode 100644 index 00000000000..237d2b6122f --- /dev/null +++ b/app/assets/javascripts/operation_settings/store/mutation_types.js @@ -0,0 +1,3 @@ +/* eslint-disable import/prefer-default-export */ + +export const SET_EXTERNAL_DASHBOARD_URL = 'SET_EXTERNAL_DASHBOARD_URL'; diff --git a/app/assets/javascripts/operation_settings/store/mutations.js b/app/assets/javascripts/operation_settings/store/mutations.js new file mode 100644 index 00000000000..64bb33bb89f --- /dev/null +++ b/app/assets/javascripts/operation_settings/store/mutations.js @@ -0,0 +1,7 @@ +import * as types from './mutation_types'; + +export default { + [types.SET_EXTERNAL_DASHBOARD_URL](state, url) { + state.externalDashboardUrl = url; + }, +}; diff --git a/app/assets/javascripts/operation_settings/store/state.js b/app/assets/javascripts/operation_settings/store/state.js new file mode 100644 index 00000000000..72167141c48 --- /dev/null +++ b/app/assets/javascripts/operation_settings/store/state.js @@ -0,0 +1,5 @@ +export default (initialState = {}) => ({ + externalDashboardUrl: initialState.externalDashboardUrl || '', + operationsSettingsEndpoint: initialState.operationsSettingsEndpoint, + externalDashboardHelpPagePath: initialState.externalDashboardHelpPagePath, +}); diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue index bd4309e47ad..bb490919a9a 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue @@ -29,7 +29,7 @@ export default { // The text input is editable when there's a custom interval, or when it's // a preset interval and the user clicks the 'custom' radio button isEditable() { - return !!(this.customInputEnabled || !this.intervalIsPreset); + return Boolean(this.customInputEnabled || !this.intervalIsPreset); }, }, watch: { diff --git a/app/assets/javascripts/pages/projects/settings/operations/show/index.js b/app/assets/javascripts/pages/projects/settings/operations/show/index.js index 5270a7924ec..98e19705976 100644 --- a/app/assets/javascripts/pages/projects/settings/operations/show/index.js +++ b/app/assets/javascripts/pages/projects/settings/operations/show/index.js @@ -1,7 +1,9 @@ import mountErrorTrackingForm from '~/error_tracking_settings'; import mountOperationSettings from '~/operation_settings'; +import initSettingsPanels from '~/settings_panels'; document.addEventListener('DOMContentLoaded', () => { mountErrorTrackingForm(); mountOperationSettings(); + initSettingsPanels(); }); 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/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue index b2e365e5cde..f3a71ee434c 100644 --- a/app/assets/javascripts/pipelines/components/header_component.vue +++ b/app/assets/javascripts/pipelines/components/header_component.vue @@ -83,6 +83,8 @@ export default { v-if="shouldRenderContent" :status="status" :item-id="pipeline.id" + :item-iid="pipeline.iid" + :item-id-tooltip="__('Pipeline ID (IID)')" :time="pipeline.created_at" :user="pipeline.user" :actions="actions" diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue index c41ecab1294..00c02e15562 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue @@ -2,6 +2,7 @@ import { GlLink, GlTooltipDirective } from '@gitlab/ui'; import _ from 'underscore'; import { __, sprintf } from '~/locale'; +import PipelineLink from '~/vue_shared/components/ci_pipeline_link.vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import popover from '~/vue_shared/directives/popover'; @@ -19,6 +20,7 @@ export default { components: { UserAvatarLink, GlLink, + PipelineLink, }, directives: { GlTooltip: GlTooltipDirective, @@ -59,10 +61,13 @@ export default { }; </script> <template> - <div class="table-section section-10 d-none d-sm-none d-md-block pipeline-tags"> - <gl-link :href="pipeline.path" class="js-pipeline-url-link"> - <span class="pipeline-id">#{{ pipeline.id }}</span> - </gl-link> + <div class="table-section section-10 d-none d-sm-none d-md-block pipeline-tags section-wrap"> + <pipeline-link + :href="pipeline.path" + :pipeline-id="pipeline.id" + :pipeline-iid="pipeline.iid" + class="js-pipeline-url-link" + /> <div class="label-container"> <span v-if="pipeline.flags.latest" diff --git a/app/assets/javascripts/profile/account/index.js b/app/assets/javascripts/profile/account/index.js index 59c13e1a042..f0d9642a2b2 100644 --- a/app/assets/javascripts/profile/account/index.js +++ b/app/assets/javascripts/profile/account/index.js @@ -35,7 +35,7 @@ export default () => { return createElement('delete-account-modal', { props: { actionUrl: deleteAccountModalEl.dataset.actionUrl, - confirmWithPassword: !!deleteAccountModalEl.dataset.confirmWithPassword, + confirmWithPassword: Boolean(deleteAccountModalEl.dataset.confirmWithPassword), username: deleteAccountModalEl.dataset.username, }, }); diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js index 6e3800021b4..8dd37aee7e1 100644 --- a/app/assets/javascripts/profile/profile.js +++ b/app/assets/javascripts/profile/profile.js @@ -39,6 +39,7 @@ export default class Profile { bindEvents() { $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm); + $('.js-group-notification-email').on('change', this.submitForm); $('#user_notification_email').on('change', this.submitForm); $('#user_notified_of_own_activity').on('change', this.submitForm); this.form.on('submit', this.onSubmitForm); diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/store/actions.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/actions.js index 4834a856271..f05ad7773a2 100644 --- a/app/assets/javascripts/projects/gke_cluster_dropdowns/store/actions.js +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/actions.js @@ -57,7 +57,7 @@ export const validateProjectBilling = ({ dispatch, commit, state }) => resp => { const { billingEnabled } = resp.result; - commit(types.SET_PROJECT_BILLING_STATUS, !!billingEnabled); + commit(types.SET_PROJECT_BILLING_STATUS, Boolean(billingEnabled)); dispatch('setIsValidatingProjectBilling', false); resolve(); }, diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/store/getters.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/getters.js index e39f02d0894..f9e2e2f74fb 100644 --- a/app/assets/javascripts/projects/gke_cluster_dropdowns/store/getters.js +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/getters.js @@ -1,3 +1,3 @@ -export const hasProject = state => !!state.selectedProject.projectId; -export const hasZone = state => !!state.selectedZone; -export const hasMachineType = state => !!state.selectedMachineType; +export const hasProject = state => Boolean(state.selectedProject.projectId); +export const hasZone = state => Boolean(state.selectedZone); +export const hasMachineType = state => Boolean(state.selectedMachineType); diff --git a/app/assets/javascripts/registry/stores/mutations.js b/app/assets/javascripts/registry/stores/mutations.js index 1ac699c538f..8ace6657ad1 100644 --- a/app/assets/javascripts/registry/stores/mutations.js +++ b/app/assets/javascripts/registry/stores/mutations.js @@ -9,7 +9,7 @@ export default { [types.SET_REPOS_LIST](state, list) { Object.assign(state, { repos: list.map(el => ({ - canDelete: !!el.destroy_path, + canDelete: Boolean(el.destroy_path), destroyPath: el.destroy_path, id: el.id, isLoading: false, @@ -42,7 +42,7 @@ export default { location: element.location, createdAt: element.created_at, destroyPath: element.destroy_path, - canDelete: !!element.destroy_path, + canDelete: Boolean(element.destroy_path), })); }, 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/index.vue b/app/assets/javascripts/repository/components/table/index.vue index 7119861c7b3..cccde1bb278 100644 --- a/app/assets/javascripts/repository/components/table/index.vue +++ b/app/assets/javascripts/repository/components/table/index.vue @@ -1,25 +1,27 @@ <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: { - files: { - query: getFiles, - variables() { - return { - ref: this.ref, - path: this.path, - }; - }, + projectPath: { + query: getProjectPath, }, }, props: { @@ -30,7 +32,14 @@ export default { }, data() { return { - files: [], + projectPath: '', + nextPageCursor: '', + entries: { + trees: [], + submodules: [], + blobs: [], + }, + isLoadingFiles: false, }; }, computed: { @@ -40,8 +49,66 @@ export default { { path: this.path, ref: this.ref }, ); }, - isLoadingFiles() { - return this.$apollo.queries.files.loading; + 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 occurred 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); }, }, }; @@ -56,10 +123,22 @@ export default { tableCaption }} </caption> - <table-header /> - <tbody></tbody> + <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-if="isLoadingFiles" class="my-3" size="md" /> + <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..3c39f404226 --- /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 class="tree-item"> + <td colspan="3" class="tree-item-file-name" @click.self="clickRow"> + <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 0aedc73fc12..c64d16ef02a 100644 --- a/app/assets/javascripts/repository/graphql.js +++ b/app/assets/javascripts/repository/graphql.js @@ -1,16 +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({ - Query: { - files() { - return []; +// 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/pages/tree.vue b/app/assets/javascripts/repository/pages/tree.vue index 413102b2cd3..3b898d1aa91 100644 --- a/app/assets/javascripts/repository/pages/tree.vue +++ b/app/assets/javascripts/repository/pages/tree.vue @@ -2,7 +2,7 @@ import FileTable from '../components/table/index.vue'; export default { - component: { + components: { FileTable, }, props: { diff --git a/app/assets/javascripts/repository/queries/getFiles.graphql b/app/assets/javascripts/repository/queries/getFiles.graphql index 5ceaf67ea82..a9b61d28560 100644 --- a/app/assets/javascripts/repository/queries/getFiles.graphql +++ b/app/assets/javascripts/repository/queries/getFiles.graphql @@ -1,8 +1,55 @@ -query getFiles($path: String!, $ref: String!) { - files(path: $path, ref: $ref) @client { - id - name - fullPath - type +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 72e061df573..930c0d5e958 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -82,9 +82,9 @@ Sidebar.prototype.toggleTodo = function(e) { ajaxType = $this.data('deletePath') ? 'delete' : 'post'; if ($this.data('deletePath')) { - url = '' + $this.data('deletePath'); + url = String($this.data('deletePath')); } else { - url = '' + $this.data('createPath'); + url = String($this.data('createPath')); } $this.tooltip('hide'); diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js index 0a4583b5861..6aca4067ba7 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'; @@ -379,7 +379,7 @@ export class SearchAutocomplete { } } } - this.wrap.toggleClass('has-value', !!e.target.value); + this.wrap.toggleClass('has-value', Boolean(e.target.value)); } onSearchInputFocus() { @@ -396,7 +396,7 @@ export class SearchAutocomplete { onClearInputClick(e) { e.preventDefault(); - this.wrap.toggleClass('has-value', !!e.target.value); + this.wrap.toggleClass('has-value', Boolean(e.target.value)); return this.searchInput.val('').focus(); } @@ -405,8 +405,9 @@ export class SearchAutocomplete { this.wrap.removeClass('search-active'); // If input is blank then restore state if (this.searchInput.val() === '') { - return this.restoreOriginalState(); + 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/serverless/components/functions.vue b/app/assets/javascripts/serverless/components/functions.vue index f9b4e789563..94341050b86 100644 --- a/app/assets/javascripts/serverless/components/functions.vue +++ b/app/assets/javascripts/serverless/components/functions.vue @@ -4,6 +4,7 @@ import { GlLoadingIcon } from '@gitlab/ui'; import FunctionRow from './function_row.vue'; import EnvironmentRow from './environment_row.vue'; import EmptyState from './empty_state.vue'; +import { CHECKING_INSTALLED } from '../constants'; export default { components: { @@ -13,10 +14,6 @@ export default { GlLoadingIcon, }, props: { - installed: { - type: Boolean, - required: true, - }, clustersPath: { type: String, required: true, @@ -31,8 +28,15 @@ export default { }, }, computed: { - ...mapState(['isLoading', 'hasFunctionData']), + ...mapState(['installed', 'isLoading', 'hasFunctionData']), ...mapGetters(['getFunctions']), + + checkingInstalled() { + return this.installed === CHECKING_INSTALLED; + }, + isInstalled() { + return this.installed === true; + }, }, created() { this.fetchFunctions({ @@ -47,15 +51,16 @@ export default { <template> <section id="serverless-functions"> - <div v-if="installed"> + <gl-loading-icon + v-if="checkingInstalled" + :size="2" + class="prepend-top-default append-bottom-default" + /> + + <div v-else-if="isInstalled"> <div v-if="hasFunctionData"> - <gl-loading-icon - v-if="isLoading" - :size="2" - class="prepend-top-default append-bottom-default" - /> - <template v-else> - <div class="groups-list-tree-container"> + <template> + <div class="groups-list-tree-container js-functions-wrapper"> <ul class="content-list group-list-tree"> <environment-row v-for="(env, index) in getFunctions" @@ -66,6 +71,11 @@ export default { </ul> </div> </template> + <gl-loading-icon + v-if="isLoading" + :size="2" + class="prepend-top-default append-bottom-default js-functions-loader" + /> </div> <div v-else class="empty-state js-empty-state"> <div class="text-content"> diff --git a/app/assets/javascripts/serverless/constants.js b/app/assets/javascripts/serverless/constants.js index 35f77205f2c..2fa15e56ccb 100644 --- a/app/assets/javascripts/serverless/constants.js +++ b/app/assets/javascripts/serverless/constants.js @@ -1,3 +1,7 @@ export const MAX_REQUESTS = 3; // max number of times to retry export const X_INTERVAL = 5; // Reflects the number of verticle bars on the x-axis + +export const CHECKING_INSTALLED = 'checking'; // The backend is still determining whether or not Knative is installed + +export const TIMEOUT = 'timeout'; diff --git a/app/assets/javascripts/serverless/serverless_bundle.js b/app/assets/javascripts/serverless/serverless_bundle.js index 2d3f086ffee..ed3b633d766 100644 --- a/app/assets/javascripts/serverless/serverless_bundle.js +++ b/app/assets/javascripts/serverless/serverless_bundle.js @@ -45,7 +45,7 @@ export default class Serverless { }, }); } else { - const { statusPath, clustersPath, helpPath, installed } = document.querySelector( + const { statusPath, clustersPath, helpPath } = document.querySelector( '.js-serverless-functions-page', ).dataset; @@ -56,7 +56,6 @@ export default class Serverless { render(createElement) { return createElement(Functions, { props: { - installed: installed !== undefined, clustersPath, helpPath, statusPath, diff --git a/app/assets/javascripts/serverless/store/actions.js b/app/assets/javascripts/serverless/store/actions.js index 826501c9022..a0a9fdf7ace 100644 --- a/app/assets/javascripts/serverless/store/actions.js +++ b/app/assets/javascripts/serverless/store/actions.js @@ -3,13 +3,18 @@ import axios from '~/lib/utils/axios_utils'; import statusCodes from '~/lib/utils/http_status'; import { backOff } from '~/lib/utils/common_utils'; import createFlash from '~/flash'; -import { MAX_REQUESTS } from '../constants'; +import { __ } from '~/locale'; +import { MAX_REQUESTS, CHECKING_INSTALLED, TIMEOUT } from '../constants'; export const requestFunctionsLoading = ({ commit }) => commit(types.REQUEST_FUNCTIONS_LOADING); export const receiveFunctionsSuccess = ({ commit }, data) => commit(types.RECEIVE_FUNCTIONS_SUCCESS, data); -export const receiveFunctionsNoDataSuccess = ({ commit }) => - commit(types.RECEIVE_FUNCTIONS_NODATA_SUCCESS); +export const receiveFunctionsPartial = ({ commit }, data) => + commit(types.RECEIVE_FUNCTIONS_PARTIAL, data); +export const receiveFunctionsTimeout = ({ commit }, data) => + commit(types.RECEIVE_FUNCTIONS_TIMEOUT, data); +export const receiveFunctionsNoDataSuccess = ({ commit }, data) => + commit(types.RECEIVE_FUNCTIONS_NODATA_SUCCESS, data); export const receiveFunctionsError = ({ commit }, error) => commit(types.RECEIVE_FUNCTIONS_ERROR, error); @@ -25,18 +30,25 @@ export const receiveMetricsError = ({ commit }, error) => export const fetchFunctions = ({ dispatch }, { functionsPath }) => { let retryCount = 0; + const functionsPartiallyFetched = data => { + if (data.functions !== null && data.functions.length) { + dispatch('receiveFunctionsPartial', data); + } + }; + dispatch('requestFunctionsLoading'); backOff((next, stop) => { axios .get(functionsPath) .then(response => { - if (response.status === statusCodes.NO_CONTENT) { + if (response.data.knative_installed === CHECKING_INSTALLED) { retryCount += 1; if (retryCount < MAX_REQUESTS) { + functionsPartiallyFetched(response.data); next(); } else { - stop(null); + stop(TIMEOUT); } } else { stop(response.data); @@ -45,10 +57,13 @@ export const fetchFunctions = ({ dispatch }, { functionsPath }) => { .catch(stop); }) .then(data => { - if (data !== null) { + if (data === TIMEOUT) { + dispatch('receiveFunctionsTimeout'); + createFlash(__('Loading functions timed out. Please reload the page to try again.')); + } else if (data.functions !== null && data.functions.length) { dispatch('receiveFunctionsSuccess', data); } else { - dispatch('receiveFunctionsNoDataSuccess'); + dispatch('receiveFunctionsNoDataSuccess', data); } }) .catch(error => { diff --git a/app/assets/javascripts/serverless/store/mutation_types.js b/app/assets/javascripts/serverless/store/mutation_types.js index 25b2f7ac38a..b8fa9ea1a01 100644 --- a/app/assets/javascripts/serverless/store/mutation_types.js +++ b/app/assets/javascripts/serverless/store/mutation_types.js @@ -1,5 +1,7 @@ export const REQUEST_FUNCTIONS_LOADING = 'REQUEST_FUNCTIONS_LOADING'; export const RECEIVE_FUNCTIONS_SUCCESS = 'RECEIVE_FUNCTIONS_SUCCESS'; +export const RECEIVE_FUNCTIONS_PARTIAL = 'RECEIVE_FUNCTIONS_PARTIAL'; +export const RECEIVE_FUNCTIONS_TIMEOUT = 'RECEIVE_FUNCTIONS_TIMEOUT'; export const RECEIVE_FUNCTIONS_NODATA_SUCCESS = 'RECEIVE_FUNCTIONS_NODATA_SUCCESS'; export const RECEIVE_FUNCTIONS_ERROR = 'RECEIVE_FUNCTIONS_ERROR'; diff --git a/app/assets/javascripts/serverless/store/mutations.js b/app/assets/javascripts/serverless/store/mutations.js index 991f32a275d..2685a5b11ff 100644 --- a/app/assets/javascripts/serverless/store/mutations.js +++ b/app/assets/javascripts/serverless/store/mutations.js @@ -5,12 +5,23 @@ export default { state.isLoading = true; }, [types.RECEIVE_FUNCTIONS_SUCCESS](state, data) { - state.functions = data; + state.functions = data.functions; + state.installed = data.knative_installed; state.isLoading = false; state.hasFunctionData = true; }, - [types.RECEIVE_FUNCTIONS_NODATA_SUCCESS](state) { + [types.RECEIVE_FUNCTIONS_PARTIAL](state, data) { + state.functions = data.functions; + state.installed = true; + state.isLoading = true; + state.hasFunctionData = true; + }, + [types.RECEIVE_FUNCTIONS_TIMEOUT](state) { + state.isLoading = false; + }, + [types.RECEIVE_FUNCTIONS_NODATA_SUCCESS](state, data) { state.isLoading = false; + state.installed = data.knative_installed; state.hasFunctionData = false; }, [types.RECEIVE_FUNCTIONS_ERROR](state, error) { diff --git a/app/assets/javascripts/serverless/store/state.js b/app/assets/javascripts/serverless/store/state.js index afc3f37d7ba..fdd29299749 100644 --- a/app/assets/javascripts/serverless/store/state.js +++ b/app/assets/javascripts/serverless/store/state.js @@ -1,5 +1,6 @@ export default () => ({ error: null, + installed: 'checking', isLoading: true, // functions 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/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue index c03b2a68c78..d84d5344935 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue @@ -49,10 +49,10 @@ export default { }, computed: { hasTimeSpent() { - return !!this.timeSpent; + return Boolean(this.timeSpent); }, hasTimeEstimate() { - return !!this.timeEstimate; + return Boolean(this.timeEstimate); }, showComparisonState() { return this.hasTimeEstimate && this.hasTimeSpent; @@ -67,7 +67,7 @@ export default { return !this.hasTimeEstimate && !this.hasTimeSpent; }, showHelpState() { - return !!this.showHelp; + return Boolean(this.showHelp); }, }, created() { 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/test_utils/index.js b/app/assets/javascripts/test_utils/index.js index a55a338eea8..1e75ee60671 100644 --- a/app/assets/javascripts/test_utils/index.js +++ b/app/assets/javascripts/test_utils/index.js @@ -1,5 +1,5 @@ -import 'core-js/es6/map'; -import 'core-js/es6/set'; +import 'core-js/es/map'; +import 'core-js/es/set'; import simulateDrag from './simulate_drag'; import simulateInput from './simulate_input'; 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/visual_review_toolbar/index.js b/app/assets/javascripts/visual_review_toolbar/index.js new file mode 100644 index 00000000000..91d0382feac --- /dev/null +++ b/app/assets/javascripts/visual_review_toolbar/index.js @@ -0,0 +1,2 @@ +import './styles/toolbar.css'; +import 'vendor/visual_review_toolbar'; diff --git a/app/assets/javascripts/visual_review_toolbar/styles/toolbar.css b/app/assets/javascripts/visual_review_toolbar/styles/toolbar.css new file mode 100644 index 00000000000..342b3599a44 --- /dev/null +++ b/app/assets/javascripts/visual_review_toolbar/styles/toolbar.css @@ -0,0 +1,149 @@ +/* + As a standalone script, the toolbar has its own css + */ + +#gitlab-collapse > * { + pointer-events: none; +} + +#gitlab-form-wrapper { + display: flex; + flex-direction: column; + width: 100% +} + +#gitlab-review-container { + max-width: 22rem; + max-height: 22rem; + overflow: scroll; + position: fixed; + bottom: 1rem; + right: 1rem; + display: flex; + flex-direction: row-reverse; + padding: 1rem; + background-color: #fff; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, + 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', + 'Noto Color Emoji'; + font-size: .8rem; + font-weight: 400; + color: #2e2e2e; +} + +.gitlab-open-wrapper { + max-width: 22rem; + max-height: 22rem; +} + +.gitlab-closed-wrapper { + max-width: 3.4rem; + max-height: 3.4rem; +} + +.gitlab-button { + cursor: pointer; + transition: background-color 100ms linear, border-color 100ms linear, color 100ms linear, box-shadow 100ms linear; +} + +.gitlab-button-secondary { + background: none #fff; + margin: 0 .5rem; + border: 1px solid #e3e3e3; +} + +.gitlab-button-secondary:hover { + background-color: #f0f0f0; + border-color: #e3e3e3; + color: #2e2e2e; +} + +.gitlab-button-secondary:active { + color: #2e2e2e; + background-color: #e1e1e1; + border-color: #dadada; +} + +.gitlab-button-success:hover { + color: #fff; + background-color: #137e3f; + border-color: #127339; +} + +.gitlab-button-success:active { + background-color: #168f48; + border-color: #12753a; + color: #fff; +} + +.gitlab-button-success { + background-color: #1aaa55; + border: 1px solid #168f48; + color: #fff; +} + +.gitlab-button-wide { + width: 100%; +} + +.gitlab-button-wrapper { + margin-top: 1rem; + display: flex; + align-items: baseline; + justify-content: flex-end; +} + +.gitlab-collapse { + width: 2.4rem; + height: 2.2rem; + margin-left: 1rem; + padding: .5rem; +} + +.gitlab-collapse-closed { + align-self: center; +} + +.gitlab-checkbox-label { + padding: 0 .2rem; +} + +.gitlab-checkbox-wrapper { + display: flex; + align-items: baseline; +} + +.gitlab-label { + font-weight: 600; + display: inline-block; + width: 100%; +} + +.gitlab-link { + color: #1b69b6; + text-decoration: none; + background-color: transparent; + background-image: none; +} + +.gitlab-message { + padding: .25rem 0; + margin: 0; + line-height: 1.2rem; +} + +.gitlab-metadata-note { + font-size: .7rem; + line-height: 1rem; + color: #666; + margin-bottom: 0; +} + +.gitlab-input { + width: 100%; + border: 1px solid #dfdfdf; + border-radius: 4px; + padding: .1rem .2rem; + min-height: 2rem; + max-width: 17rem; +} 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..abe5bdd2901 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: '', }), }, @@ -77,16 +77,16 @@ export default { return this.deployment.external_url; }, hasExternalUrls() { - return !!(this.deployment.external_url && this.deployment.external_url_formatted); + return Boolean(this.deployment.external_url && this.deployment.external_url_formatted); }, hasDeploymentTime() { - return !!(this.deployment.deployed_at && this.deployment.deployed_at_formatted); + return Boolean(this.deployment.deployed_at && this.deployment.deployed_at_formatted); }, hasDeploymentMeta() { - return !!(this.deployment.url && this.deployment.name); + return Boolean(this.deployment.url && this.deployment.name); }, hasMetrics() { - return !!this.deployment.metrics_url; + return Boolean(this.deployment.metrics_url); }, deployedText() { return this.$options.deployedTextMap[this.deployment.status]; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue index 040315b3c66..19a222462b3 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue @@ -37,7 +37,7 @@ export default { </script> <template> - <div class="m-3 ml-5" :class="messageClass"> + <div class="m-3 ml-7" :class="messageClass"> <slot></slot> <gl-link v-if="helpPath" :href="helpPath" target="_blank"> <icon :size="16" name="question-o" class="align-middle" /> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue index f5fa68308bc..c377c16fb13 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue @@ -5,6 +5,7 @@ import { sprintf, __ } from '~/locale'; import PipelineStage from '~/pipelines/components/stage.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; import Icon from '~/vue_shared/components/icon.vue'; +import PipelineLink from '~/vue_shared/components/ci_pipeline_link.vue'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; import mrWidgetPipelineMixin from 'ee_else_ce/vue_merge_request_widget/mixins/mr_widget_pipeline'; @@ -16,6 +17,7 @@ export default { Icon, TooltipOnTruncate, GlLink, + PipelineLink, LinkedPipelinesMiniList: () => import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'), }, @@ -112,9 +114,12 @@ export default { <div class="media-body"> <div class="font-weight-bold js-pipeline-info-container"> {{ s__('Pipeline|Pipeline') }} - <gl-link :href="pipeline.path" class="pipeline-id font-weight-normal pipeline-number" - >#{{ pipeline.id }}</gl-link - > + <pipeline-link + :href="pipeline.path" + :pipeline-id="pipeline.id" + :pipeline-iid="pipeline.iid" + class="pipeline-id pipeline-iid font-weight-normal" + /> {{ pipeline.details.status.label }} <template v-if="hasCommitInfo"> {{ s__('Pipeline|for') }} 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..03a15ba81ed 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, }; }, @@ -56,7 +56,7 @@ export default { return this.isPostMerge ? this.mr.mergePipeline : this.mr.pipeline; }, showVisualReviewAppLink() { - return !!(this.mr.visualReviewFF && this.mr.visualReviewAppAvailable); + return Boolean(this.mr.visualReviewFF && this.mr.visualReviewAppAvailable); }, }, }; 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/components/states/mr_widget_auto_merge_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue index a3a44dd8e99..83e7d6db9fa 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue @@ -35,9 +35,7 @@ export default { <status-icon status="warning" /> <div class="media-body space-children"> <span class="bold"> - <template v-if="mr.mergeError" - >{{ mr.mergeError }}.</template - > + <template v-if="mr.mergeError">{{ mr.mergeError }}</template> {{ s__('mrWidget|This merge request failed to be merged automatically') }} </span> <button diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue index 1b3af2fccf2..88e1ccbaf35 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue @@ -57,7 +57,7 @@ export default { removeSourceBranch() { const options = { sha: this.mr.sha, - merge_when_pipeline_succeeds: true, + auto_merge_strategy: 'merge_when_pipeline_succeeds', should_remove_source_branch: true, }; @@ -85,7 +85,7 @@ export default { <h4 class="d-flex align-items-start"> <span class="append-right-10"> {{ s__('mrWidget|Set by') }} - <mr-widget-author :author="mr.setToMWPSBy" /> + <mr-widget-author :author="mr.setToAutoMergeBy" /> {{ s__('mrWidget|to be merged automatically when the pipeline succeeds') }} </span> <a diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue index 851939d5d4e..615d59a7b8e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -31,7 +31,7 @@ export default { return { removeSourceBranch: this.mr.shouldRemoveSourceBranch, mergeWhenBuildSucceeds: false, - setToMergeWhenPipelineSucceeds: false, + autoMergeStrategy: undefined, isMakingRequest: false, isMergingImmediately: false, commitMessage: this.mr.commitMessage, @@ -42,7 +42,7 @@ export default { }; }, computed: { - shouldShowMergeWhenPipelineSucceedsText() { + shouldShowAutoMergeText() { return this.mr.isPipelineActive; }, status() { @@ -87,7 +87,7 @@ export default { mergeButtonText() { if (this.isMergingImmediately) { return __('Merge in progress'); - } else if (this.shouldShowMergeWhenPipelineSucceedsText) { + } else if (this.shouldShowAutoMergeText) { return __('Merge when pipeline succeeds'); } @@ -104,7 +104,7 @@ export default { return enableSquashBeforeMerge && commitsCount > 1; }, shouldShowMergeControls() { - return this.mr.isMergeAllowed || this.shouldShowMergeWhenPipelineSucceedsText; + return this.mr.isMergeAllowed || this.shouldShowAutoMergeText; }, shouldShowSquashEdit() { return this.squashBeforeMerge && this.shouldShowSquashBeforeMerge; @@ -126,12 +126,12 @@ export default { this.isMergingImmediately = true; } - this.setToMergeWhenPipelineSucceeds = mergeWhenBuildSucceeds === true; + this.autoMergeStrategy = mergeWhenBuildSucceeds ? 'merge_when_pipeline_succeeds' : undefined; const options = { sha: this.mr.sha, commit_message: this.commitMessage, - merge_when_pipeline_succeeds: this.setToMergeWhenPipelineSucceeds, + auto_merge_strategy: this.autoMergeStrategy, should_remove_source_branch: this.removeSourceBranch === true, squash: this.squashBeforeMerge, squash_commit_message: this.squashCommitMessage, @@ -330,6 +330,7 @@ export default { :commits-count="mr.commitsCount" :target-branch="mr.targetBranch" :is-fast-forward-enabled="mr.ffOnlyEnabled" + :class="{ 'border-bottom': mr.mergeError }" > <ul class="border-top content-list commits-list flex-list"> <commit-edit diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue index b1f5655a15a..accb9d9fef1 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue @@ -29,8 +29,8 @@ export default { </script> <template> - <div class="accept-control inline"> - <label class="merge-param-checkbox"> + <div class="inline"> + <label> <input :checked="value" :disabled="isDisabled" 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..d02bb2f341d 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 @@ -1,6 +1,6 @@ <script> import _ from 'underscore'; -import { __ } from '~/locale'; +import { sprintf, s__, __ } from '~/locale'; import Project from '~/pages/projects/project'; import SmartInterval from '~/smart_interval'; import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_store'; @@ -97,7 +97,7 @@ export default { return this.mr.hasCI; }, shouldRenderRelatedLinks() { - return !!this.mr.relatedLinks && !this.mr.isNothingToMergeState; + return Boolean(this.mr.relatedLinks) && !this.mr.isNothingToMergeState; }, shouldRenderSourceBranchRemovalStatus() { return ( @@ -125,6 +125,11 @@ export default { this.mr.pipeline.target_sha !== this.mr.targetBranchSha, ); }, + mergeError() { + return sprintf(s__('mrWidget|Merge failed: %{mergeError}. Please try again.'), { + mergeError: this.mr.mergeError, + }); + }, }, watch: { state(newVal, oldVal) { @@ -333,41 +338,49 @@ 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> + <mr-widget-alert-message v-if="mr.mergeError" type="danger"> + {{ mergeError }} + </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_merge_request_widget/stores/get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js index 0cc4fd59f5e..3ab229567f6 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js @@ -23,8 +23,8 @@ export default function deviseState(data) { return stateKey.pipelineBlocked; } else if (this.isSHAMismatch) { return stateKey.shaMismatch; - } else if (this.mergeWhenPipelineSucceeds) { - return this.mergeError ? stateKey.autoMergeFailed : stateKey.mergeWhenPipelineSucceeds; + } else if (this.autoMergeEnabled) { + return this.mergeError ? stateKey.autoMergeFailed : stateKey.autoMergeEnabled; } else if (!this.canMerge) { return stateKey.notAllowedToMerge; } else if (this.canBeMerged) { diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index 45708d78886..32badb0fb08 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -61,7 +61,7 @@ export default class MergeRequestStore { this.updatedAt = data.updated_at; this.metrics = MergeRequestStore.buildMetrics(data.metrics); - this.setToMWPSBy = MergeRequestStore.formatUserObject(data.merge_user || {}); + this.setToAutoMergeBy = MergeRequestStore.formatUserObject(data.merge_user || {}); this.mergeUserId = data.merge_user_id; this.currentUserId = gon.current_user_id; this.sourceBranchPath = data.source_branch_path; @@ -70,15 +70,16 @@ export default class MergeRequestStore { this.targetBranchPath = data.target_branch_commits_path; this.targetBranchTreePath = data.target_branch_tree_path; this.conflictResolutionPath = data.conflict_resolution_path; - this.cancelAutoMergePath = data.cancel_merge_when_pipeline_succeeds_path; + this.cancelAutoMergePath = data.cancel_auto_merge_path; this.removeWIPPath = data.remove_wip_path; this.sourceBranchRemoved = !data.source_branch_exists; this.shouldRemoveSourceBranch = data.remove_source_branch || false; this.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false; - this.mergeWhenPipelineSucceeds = data.merge_when_pipeline_succeeds || false; + this.autoMergeEnabled = Boolean(data.auto_merge_enabled); + this.autoMergeStrategy = data.auto_merge_strategy; this.mergePath = data.merge_path; this.ffOnlyEnabled = data.ff_only_enabled; - this.shouldBeRebased = !!data.should_be_rebased; + this.shouldBeRebased = Boolean(data.should_be_rebased); this.statusPath = data.status_path; this.emailPatchesPath = data.email_patches_path; this.plainDiffPath = data.plain_diff_path; @@ -91,9 +92,9 @@ export default class MergeRequestStore { this.isOpen = data.state === 'opened'; this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false; this.canRemoveSourceBranch = currentUser.can_remove_source_branch || false; - this.canMerge = !!data.merge_path; + this.canMerge = Boolean(data.merge_path); this.canCreateIssue = currentUser.can_create_issue || false; - this.canCancelAutomaticMerge = !!data.cancel_merge_when_pipeline_succeeds_path; + this.canCancelAutomaticMerge = Boolean(data.cancel_auto_merge_path); this.isSHAMismatch = this.sha !== data.diff_head_sha; this.canBeMerged = data.can_be_merged || false; this.isMergeAllowed = data.mergeable || false; diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js index e080ce5c229..48bc6a867f4 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js @@ -13,7 +13,7 @@ const stateToComponentMap = { unresolvedDiscussions: 'mr-widget-unresolved-discussions', pipelineBlocked: 'mr-widget-pipeline-blocked', pipelineFailed: 'mr-widget-pipeline-failed', - mergeWhenPipelineSucceeds: 'mr-widget-merge-when-pipeline-succeeds', + autoMergeEnabled: 'mr-widget-merge-when-pipeline-succeeds', failedToMerge: 'mr-widget-failed-to-merge', autoMergeFailed: 'mr-widget-auto-merge-failed', shaMismatch: 'sha-mismatch', @@ -45,7 +45,7 @@ export const stateKey = { pipelineBlocked: 'pipelineBlocked', shaMismatch: 'shaMismatch', autoMergeFailed: 'autoMergeFailed', - mergeWhenPipelineSucceeds: 'mergeWhenPipelineSucceeds', + autoMergeEnabled: 'autoMergeEnabled', notAllowedToMerge: 'notAllowedToMerge', readyToMerge: 'readyToMerge', rebase: 'rebase', diff --git a/app/assets/javascripts/vue_shared/components/ci_pipeline_link.vue b/app/assets/javascripts/vue_shared/components/ci_pipeline_link.vue new file mode 100644 index 00000000000..eae4c06467c --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/ci_pipeline_link.vue @@ -0,0 +1,32 @@ +<script> +import { GlLink, GlTooltipDirective } from '@gitlab/ui'; + +export default { + components: { + GlLink, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + href: { + type: String, + required: true, + }, + pipelineId: { + type: Number, + required: true, + }, + pipelineIid: { + type: Number, + required: true, + }, + }, +}; +</script> +<template> + <gl-link v-gl-tooltip :href="href" :title="__('Pipeline ID (IID)')"> + <span class="pipeline-id">#{{ pipelineId }}</span> + <span class="pipeline-iid">(#{{ pipelineIid }})</span> + </gl-link> +</template> diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue index 3f45dc7853b..0bac63b1062 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -37,6 +37,16 @@ export default { type: Number, required: true, }, + itemIid: { + type: Number, + required: false, + default: null, + }, + itemIdTooltip: { + type: String, + required: false, + default: '', + }, time: { type: String, required: true, @@ -85,7 +95,12 @@ export default { <section class="header-main-content"> <ci-icon-badge :status="status" /> - <strong> {{ itemName }} #{{ itemId }} </strong> + <strong v-gl-tooltip :title="itemIdTooltip"> + {{ itemName }} #{{ itemId }} + <template v-if="itemIid" + >(#{{ itemIid }})</template + > + </strong> <template v-if="shouldRenderTriggeredLabel"> triggered @@ -96,9 +111,8 @@ export default { <timeago-tooltip :time="time" /> - by - <template v-if="user"> + by <gl-link v-gl-tooltip :href="user.path" diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue index 3b57b5e8da4..d6c398c8946 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue @@ -33,37 +33,36 @@ export default { <div class="comment-toolbar clearfix"> <div class="toolbar-text"> <template v-if="!hasQuickActionsDocsPath && markdownDocsPath"> - <gl-link :href="markdownDocsPath" target="_blank" tabindex="-1"> - Markdown is supported - </gl-link> + <gl-link :href="markdownDocsPath" target="_blank" tabindex="-1" + >Markdown is supported</gl-link + > </template> <template v-if="hasQuickActionsDocsPath && markdownDocsPath"> - <gl-link :href="markdownDocsPath" target="_blank" tabindex="-1"> Markdown </gl-link> - and - <gl-link :href="quickActionsDocsPath" target="_blank" tabindex="-1"> - quick actions - </gl-link> + <gl-link :href="markdownDocsPath" target="_blank" tabindex="-1">Markdown</gl-link> and + <gl-link :href="quickActionsDocsPath" target="_blank" tabindex="-1">quick actions</gl-link> are supported </template> </div> <span v-if="canAttachFile" class="uploading-container"> <span class="uploading-progress-container hide"> - <i class="fa fa-file-image-o toolbar-button-icon" aria-hidden="true"> </i> - <span class="attaching-file-message"></span> <span class="uploading-progress">0%</span> + <i class="fa fa-file-image-o toolbar-button-icon" aria-hidden="true"></i> + <span class="attaching-file-message"></span> + <span class="uploading-progress">0%</span> <span class="uploading-spinner"> - <i class="fa fa-spinner fa-spin toolbar-button-icon" aria-hidden="true"> </i> + <i class="fa fa-spinner fa-spin toolbar-button-icon" aria-hidden="true"></i> </span> </span> <span class="uploading-error-container hide"> <span class="uploading-error-icon"> - <i class="fa fa-file-image-o toolbar-button-icon" aria-hidden="true"> </i> + <i class="fa fa-file-image-o toolbar-button-icon" aria-hidden="true"></i> </span> <span class="uploading-error-message"></span> <button class="retry-uploading-link" type="button">Try again</button> or <button class="attach-new-file markdown-selector" type="button">attach a new file</button> </span> - <button class="markdown-selector button-attach-file" tabindex="-1" type="button"> - <i class="fa fa-file-image-o toolbar-button-icon" aria-hidden="true"> </i> Attach a file + <button class="markdown-selector button-attach-file btn-link" tabindex="-1" type="button"> + <i class="fa fa-file-image-o toolbar-button-icon" aria-hidden="true"></i + ><span class="text-attach-file">Attach a file</span> </button> <button class="btn btn-default btn-sm hide button-cancel-uploading-files" type="button"> Cancel diff --git a/app/assets/javascripts/vue_shared/components/pikaday.vue b/app/assets/javascripts/vue_shared/components/pikaday.vue index fa502b9beb9..8104d919bf6 100644 --- a/app/assets/javascripts/vue_shared/components/pikaday.vue +++ b/app/assets/javascripts/vue_shared/components/pikaday.vue @@ -34,7 +34,7 @@ export default { format: 'yyyy-mm-dd', container: this.$el, defaultDate: this.selectedDate, - setDefaultDate: !!this.selectedDate, + setDefaultDate: Boolean(this.selectedDate), minDate: this.minDate, maxDate: this.maxDate, parse: dateString => parsePikadayDate(dateString), diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.vue b/app/assets/javascripts/vue_shared/components/table_pagination.vue index 8e0b08032f7..9cce9a4e542 100644 --- a/app/assets/javascripts/vue_shared/components/table_pagination.vue +++ b/app/assets/javascripts/vue_shared/components/table_pagination.vue @@ -121,7 +121,7 @@ export default { this.change(1); break; default: - this.change(+text); + this.change(Number(text)); break; } }, 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/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss index 93377b8dd91..7f6384f4eea 100644 --- a/app/assets/stylesheets/bootstrap_migration.scss +++ b/app/assets/stylesheets/bootstrap_migration.scss @@ -22,7 +22,9 @@ body, .form-control, .search form { // Override default font size used in non-csslab UI - font-size: 14px; + // Use rem to keep default font-size at 14px on body so 1rem still + // fits 8px grid, but also allow users to change browser font size + font-size: .875rem; } legend { diff --git a/app/assets/stylesheets/components/avatar.scss b/app/assets/stylesheets/components/avatar.scss new file mode 100644 index 00000000000..1afa5ed90f4 --- /dev/null +++ b/app/assets/stylesheets/components/avatar.scss @@ -0,0 +1,202 @@ +$avatar-sizes: ( + 16: ( + font-size: 10px, + line-height: 16px, + border-radius: $border-radius-small + ), + 18: ( + 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 + ), + 40: ( + font-size: 16px, + line-height: 38px, + 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 + ), + 90: ( + font-size: 36px, + line-height: 88px, + border-radius: $border-radius-large + ), + 100: ( + font-size: 36px, + line-height: 98px, + 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: $gl-padding; + 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 < 48, 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/errors.scss b/app/assets/stylesheets/errors.scss index 658e0ff638e..8c32b6c8985 100644 --- a/app/assets/stylesheets/errors.scss +++ b/app/assets/stylesheets/errors.scss @@ -17,7 +17,7 @@ body { text-align: center; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; margin: auto; - font-size: 14px; + font-size: .875rem; } h1 { 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..7760c48cb92 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 { @@ -233,10 +232,7 @@ height: $default-icon-size; width: $default-icon-size; border-radius: 50%; - } - - 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..97a763671ba 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -1,12 +1,12 @@ @mixin btn-comment-icon { border-radius: 50%; background: $white-light; - padding: 1px 5px; + padding: 1px; font-size: 12px; color: $blue-500; + border: 1px solid $blue-500; width: 24px; height: 24px; - border: 1px solid $blue-500; &:hover, &.inverted { @@ -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, @@ -330,6 +339,8 @@ svg { top: auto; + width: 16px; + height: 16px; } } @@ -402,7 +413,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..ef6f0633150 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 { @@ -328,7 +329,7 @@ span.idiff { background-color: $gray-light; border-bottom: 1px solid $border-color; border-top: 1px solid $border-color; - padding: 5px $gl-padding; + padding: $gl-padding-8 $gl-padding; margin: 0; border-radius: $border-radius-default $border-radius-default 0 0; @@ -365,10 +366,6 @@ span.idiff { color: $gl-text-color; } - small { - margin: 0 10px 0 0; - } - .file-actions .btn { padding: 0 10px; font-size: 13px; 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..2a601afff53 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -27,10 +27,16 @@ input[type='text'].danger { } label { + font-weight: $gl-font-weight-bold; + &.inline-label { margin: 0; } + &.form-check-label { + font-weight: $gl-font-weight-normal; + } + &.label-bold { font-weight: $gl-font-weight-bold; } @@ -41,14 +47,6 @@ label { margin: 0; } -.form-label { - @extend label; -} - -.form-control-label { - @extend .col-md-2; -} - .inline-input-group { width: 250px; } @@ -81,44 +79,14 @@ label { margin-left: 0; margin-right: 0; - .form-control-label { - font-weight: $gl-font-weight-bold; - padding-top: 4px; - } - .form-control { height: 29px; background: $white-light; font-family: $monospace-font; } - .input-group-prepend .btn, - .input-group-append .btn { - padding: 3px $gl-btn-padding; - background-color: $gray-light; - border: 1px solid $border-color; - } - - .text-block { - line-height: 0.8; - padding-top: 9px; - - code { - line-height: 1.8; - } - - img { - margin-right: $gl-padding; - } - } - @include media-breakpoint-down(xs) { padding: 0 $gl-padding; - - .form-control-label, - .text-block { - padding-left: 0; - } } } @@ -140,19 +108,6 @@ label { } } -.select-wrapper { - position: relative; - - .fa-chevron-down { - position: absolute; - font-size: 10px; - right: 10px; - top: 12px; - color: $gray-darkest; - pointer-events: none; - } -} - .select-control { padding-left: 10px; padding-right: 10px; @@ -175,12 +130,6 @@ label { margin-top: 35px; } -.form-group .form-control-label, -.form-group .form-control-label-full-width { - font-weight: $gl-font-weight-normal; -} - - .form-control::placeholder { color: $gl-text-color-tertiary; } @@ -224,7 +173,8 @@ label { border: 1px solid $green-600; &:focus { - box-shadow: 0 0 0 1px $green-600 inset, 0 1px 1px $gl-field-focus-shadow inset, 0 0 4px 0 $green-600; + box-shadow: 0 0 0 1px $green-600 inset, 0 1px 1px $gl-field-focus-shadow inset, + 0 0 4px 0 $green-600; border: 0 none; } } @@ -233,7 +183,8 @@ label { border: 1px solid $red-500; &:focus { - box-shadow: 0 0 0 1px $red-500 inset, 0 1px 1px $gl-field-focus-shadow inset, 0 0 4px 0 $gl-field-focus-shadow-error; + box-shadow: 0 0 0 1px $red-500 inset, 0 1px 1px $gl-field-focus-shadow inset, + 0 0 4px 0 $gl-field-focus-shadow-error; border: 0 none; } } @@ -259,16 +210,26 @@ label { } } -.input-icon-wrapper { +.input-icon-wrapper, +.select-wrapper { position: relative; +} - .input-icon-right { - position: absolute; - right: 0.8em; - top: 50%; - transform: translateY(-50%); - color: $gray-600; - } +.select-wrapper > .fa-chevron-down { + position: absolute; + font-size: 10px; + right: 10px; + top: 12px; + color: $gray-darkest; + pointer-events: none; +} + +.input-icon-wrapper > .input-icon-right { + position: absolute; + right: 0.8em; + top: 50%; + transform: translateY(-50%); + color: $gray-600; } .input-md { @@ -280,3 +241,21 @@ label { max-width: $input-lg-width; width: 100%; } + +.input-group-text { + max-height: $input-height; +} + +.gl-form-checkbox { + align-items: baseline; + + &.form-check-inline .form-check-input { + align-self: flex-start; + margin-right: $gl-padding-8; + height: 1.5 * $gl-font-size; + } + + .help-text { + margin-bottom: 0; + } +} 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..df40149f0a6 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,17 @@ } } } + +/* +* Mixin that handles the size and right margin of avatars. +*/ +@mixin avatar-size($size, $margin-right) { + width: $size; + height: $size; + margin-right: $margin-right; +} + +@mixin code-icon-size() { + width: $gl-font-size * $code-line-height * 0.9; + height: $gl-font-size * $code-line-height * 0.9; +} diff --git a/app/assets/stylesheets/framework/snippets.scss b/app/assets/stylesheets/framework/snippets.scss index 36ab38f1c9d..3ab83f4c8e6 100644 --- a/app/assets/stylesheets/framework/snippets.scss +++ b/app/assets/stylesheets/framework/snippets.scss @@ -22,6 +22,10 @@ .snippet-file-content { border-radius: 3px; + + .file-title-flex-parent .btn-clipboard { + line-height: 28px; + } } .snippet-header { 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/commits.scss b/app/assets/stylesheets/pages/commits.scss index 66cd113db84..77a36e59b03 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -154,8 +154,6 @@ } .avatar-cell { - width: 46px; - img { margin-right: 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..5e5d298f8f2 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -15,7 +15,6 @@ position: sticky; top: $mr-file-header-top; z-index: 102; - height: $mr-version-controls-height; &::before { content: ''; @@ -494,6 +493,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; @@ -609,10 +614,9 @@ table.code { .diff-comment-avatar-holders { position: absolute; - height: 19px; - width: 19px; - margin-left: -15px; + margin-left: -$gl-padding; z-index: 100; + @include code-icon-size(); &:hover { .diff-comment-avatar, @@ -646,26 +650,28 @@ table.code { .diff-comments-more-count { position: absolute; left: 0; - width: 19px; - height: 19px; margin-right: 0; border-color: $white-light; cursor: pointer; transition: all 0.1s ease-out; + @include code-icon-size(); @for $i from 1 through 4 { &:nth-child(#{$i}) { z-index: (4 - $i); } } + + .avatar { + @include code-icon-size(); + } } .diff-comments-more-count { - width: 19px; - min-width: 19px; padding-left: 0; padding-right: 0; overflow: hidden; + @include code-icon-size(); } .diff-comments-more-count, @@ -674,12 +680,15 @@ table.code { } .diff-notes-collapse { - width: 24px; - height: 24px; + border: 0; border-radius: 50%; padding: 0; transition: transform 0.1s ease-out; z-index: 100; + display: flex; + justify-content: center; + align-items: center; + @include code-icon-size(); .collapse-icon { height: 50%; 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/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 04c66006027..4ba74d34664 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -218,7 +218,7 @@ .title { color: $gl-text-color; - margin-bottom: 10px; + margin-bottom: $gl-padding-8; line-height: 1; .avatar { @@ -604,7 +604,6 @@ .participants-list { display: flex; flex-wrap: wrap; - margin: -7px; } .user-list { @@ -614,7 +613,7 @@ .participants-author { display: inline-block; - padding: 7px; + padding: 0 $gl-padding-8 $gl-padding-8 0; &:nth-of-type(7n) { padding-right: 0; @@ -641,7 +640,6 @@ .participants-more, .user-list-more { - margin-top: 5px; margin-left: 5px; a, diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 13288d8bad1..11e8a32389f 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -456,8 +456,9 @@ // Don't hide the overflow in system messages .system-note-message, -.issuable-detail, +.issuable-details, .md-preview-holder, +.referenced-commands, .note-body { .scoped-label-wrapper { .badge { 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..77b40fe2d30 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; @@ -180,46 +185,6 @@ } } } - - .accept-control { - display: inline-block; - float: left; - margin: 0; - margin-left: 20px; - padding: 5px; - padding-top: 8px; - line-height: 20px; - - &.right { - float: right; - padding-right: 0; - } - - .modify-merge-commit-link { - padding: 0; - background-color: transparent; - border: 0; - color: $gl-text-color; - - &:hover, - &:focus { - text-decoration: underline; - } - } - - .merge-param-checkbox { - margin: 0; - } - - a .fa-question-circle { - color: $gl-text-color-secondary; - - &:hover, - &:focus { - color: $link-hover-color; - } - } - } } .ci-widget { @@ -402,12 +367,6 @@ width: 100%; text-align: center; } - - .accept-control { - width: 100%; - text-align: center; - margin: 0; - } } .commit-message-editor { @@ -560,6 +519,10 @@ .mr-links { padding-left: $status-icon-size + $gl-btn-padding; + + &:last-child { + padding-bottom: $gl-padding; + } } .mr-info-list { @@ -1030,11 +993,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/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 3343b55d24b..8c7b124dd33 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -334,7 +334,7 @@ table { .toolbar-button-icon { position: relative; top: 1px; - margin-right: 3px; + margin-right: $gl-padding-4; color: inherit; font-size: 16px; } @@ -461,6 +461,15 @@ table { border: 0; font-size: 14px; line-height: 16px; + + &:hover, + &:focus { + text-decoration: none; + + .text-attach-file { + text-decoration: underline; + } + } } .markdown-selector { diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index fcb57db590a..32477c20db6 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -437,7 +437,9 @@ $note-form-margin-left: 72px; .diff-file { .is-over { .add-diff-note { - display: inline-block; + display: inline-flex; + justify-content: center; + align-items: center; } } @@ -645,7 +647,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; @@ -741,7 +743,7 @@ $note-form-margin-left: 72px; .add-diff-note { @include btn-comment-icon; opacity: 0; - margin-left: -50px; + margin-left: -52px; position: absolute; top: 50%; transform: translateY(-50%); @@ -822,6 +824,7 @@ $note-form-margin-left: 72px; .line-resolve-btn { margin-right: 5px; + color: $gray-darkest; svg { vertical-align: middle; @@ -836,7 +839,6 @@ $note-form-margin-left: 72px; background-color: transparent; border: 0; outline: 0; - color: $gray-darkest; transition: color $general-hover-transition-duration $general-hover-transition-curve; &.is-disabled { @@ -900,10 +902,6 @@ $note-form-margin-left: 72px; .diff-comment-form { display: block; } - - .add-diff-note svg { - margin-top: 4px; - } } .discussion-filter-container { diff --git a/app/assets/stylesheets/pages/notifications.scss b/app/assets/stylesheets/pages/notifications.scss index e98cb444f0a..e1cbf0e6654 100644 --- a/app/assets/stylesheets/pages/notifications.scss +++ b/app/assets/stylesheets/pages/notifications.scss @@ -4,6 +4,34 @@ .dropdown-menu { @extend .dropdown-menu-right; } + + @include media-breakpoint-down(sm) { + .notification-dropdown { + width: 100%; + } + + .notification-form { + display: block; + } + + .notifications-btn, + .btn-group { + width: 100%; + } + + .table-section { + border-top: 0; + min-height: unset; + + &:not(:first-child) { + padding-top: 0; + } + } + + .update-notifications { + width: 100%; + } + } } .notification { 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 */ |