diff options
Diffstat (limited to 'app')
110 files changed, 1346 insertions, 802 deletions
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index cace8bb9dba..73ce3e760ab 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -437,7 +437,7 @@ export class AwardsHandler { createAwardButtonForVotesBlock(votesBlock, emojiName) { const buttonHtml = ` - <button class="btn award-control js-emoji-btn has-tooltip active" title="You" data-placement="bottom"> + <button class="btn award-control js-emoji-btn has-tooltip active" title="You"> ${this.emoji.glEmojiTag(emojiName)} <span class="award-control-text js-counter">1</span> </button> diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index fc4779632f9..6ebd1ad109e 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -6,7 +6,13 @@ import Flash from '../flash'; import Poll from '../lib/utils/poll'; import initSettingsPanels from '../settings_panels'; import eventHub from './event_hub'; -import { APPLICATION_STATUS, REQUEST_SUBMITTED, REQUEST_FAILURE } from './constants'; +import { + APPLICATION_STATUS, + REQUEST_SUBMITTED, + REQUEST_FAILURE, + UPGRADE_REQUESTED, + UPGRADE_REQUEST_FAILURE, +} from './constants'; import ClustersService from './services/clusters_service'; import ClustersStore from './stores/clusters_store'; import Applications from './components/applications.vue'; @@ -120,11 +126,17 @@ export default class Clusters { addListeners() { if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken); eventHub.$on('installApplication', this.installApplication); + eventHub.$on('upgradeApplication', data => this.upgradeApplication(data)); + eventHub.$on('upgradeFailed', appId => this.upgradeFailed(appId)); + eventHub.$on('dismissUpgradeSuccess', appId => this.dismissUpgradeSuccess(appId)); } removeListeners() { if (this.showTokenButton) this.showTokenButton.removeEventListener('click', this.showToken); eventHub.$off('installApplication', this.installApplication); + eventHub.$off('upgradeApplication', this.upgradeApplication); + eventHub.$off('upgradeFailed', this.upgradeFailed); + eventHub.$off('dismissUpgradeSuccess', this.dismissUpgradeSuccess); } initPolling() { @@ -245,6 +257,21 @@ export default class Clusters { }); } + upgradeApplication(data) { + const appId = data.id; + this.store.updateAppProperty(appId, 'requestStatus', UPGRADE_REQUESTED); + this.store.updateAppProperty(appId, 'status', APPLICATION_STATUS.UPDATING); + this.service.installApplication(appId, data.params).catch(() => this.upgradeFailed(appId)); + } + + upgradeFailed(appId) { + this.store.updateAppProperty(appId, 'requestStatus', UPGRADE_REQUEST_FAILURE); + } + + dismissUpgradeSuccess(appId) { + this.store.updateAppProperty(appId, 'requestStatus', null); + } + destroy() { this.destroyed = true; diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue index 3c3ce1dec56..5952e93b9a7 100644 --- a/app/assets/javascripts/clusters/components/application_row.vue +++ b/app/assets/javascripts/clusters/components/application_row.vue @@ -1,15 +1,24 @@ <script> /* eslint-disable vue/require-default-prop */ +import { GlLink } from '@gitlab/ui'; +import TimeagoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; import { s__, sprintf } from '../../locale'; import eventHub from '../event_hub'; import identicon from '../../vue_shared/components/identicon.vue'; import loadingButton from '../../vue_shared/components/loading_button.vue'; -import { APPLICATION_STATUS, REQUEST_SUBMITTED, REQUEST_FAILURE } from '../constants'; +import { + APPLICATION_STATUS, + REQUEST_SUBMITTED, + REQUEST_FAILURE, + UPGRADE_REQUESTED, +} from '../constants'; export default { components: { loadingButton, identicon, + TimeagoTooltip, + GlLink, }, props: { id: { @@ -54,6 +63,18 @@ export default { type: String, required: false, }, + version: { + type: String, + required: false, + }, + chartRepo: { + type: String, + required: false, + }, + upgradeAvailable: { + type: Boolean, + required: false, + }, installApplicationRequestParams: { type: Object, required: false, @@ -78,7 +99,8 @@ export default { return ( this.status === APPLICATION_STATUS.INSTALLED || this.status === APPLICATION_STATUS.UPDATED || - this.status === APPLICATION_STATUS.UPDATING + this.status === APPLICATION_STATUS.UPDATING || + this.status === APPLICATION_STATUS.UPDATE_ERRORED ); }, canInstall() { @@ -146,6 +168,69 @@ export default { title: this.title, }); }, + versionLabel() { + if (this.upgradeFailed) { + return s__('ClusterIntegration|Upgrade failed'); + } else if (this.isUpgrading) { + return s__('ClusterIntegration|Upgrading'); + } + + return s__('ClusterIntegration|Upgraded'); + }, + upgradeRequested() { + return this.requestStatus === UPGRADE_REQUESTED; + }, + upgradeSuccessful() { + return this.status === APPLICATION_STATUS.UPDATED; + }, + upgradeFailed() { + if (this.isUpgrading) { + return false; + } + + return this.status === APPLICATION_STATUS.UPDATE_ERRORED; + }, + upgradeFailureDescription() { + return sprintf( + s__( + 'ClusterIntegration|Something went wrong when upgrading %{title}. Please check the logs and try again.', + ), + { + title: this.title, + }, + ); + }, + upgradeSuccessDescription() { + return sprintf(s__('ClusterIntegration|%{title} upgraded successfully.'), { + title: this.title, + }); + }, + upgradeButtonLabel() { + let label; + if (this.upgradeAvailable && !this.upgradeFailed && !this.isUpgrading) { + label = s__('ClusterIntegration|Upgrade'); + } else if (this.isUpgrading) { + label = s__('ClusterIntegration|Upgrading'); + } else if (this.upgradeFailed) { + label = s__('ClusterIntegration|Retry upgrade'); + } + + return label; + }, + isUpgrading() { + // Since upgrading is handled asynchronously on the backend we need this check to prevent any delay on the frontend + return ( + this.status === APPLICATION_STATUS.UPDATING || + (this.upgradeRequested && !this.upgradeSuccessful) + ); + }, + }, + watch: { + status() { + if (this.status === APPLICATION_STATUS.UPDATE_ERRORED) { + eventHub.$emit('upgradeFailed', this.id); + } + }, }, methods: { installClicked() { @@ -154,6 +239,15 @@ export default { params: this.installApplicationRequestParams, }); }, + upgradeClicked() { + eventHub.$emit('upgradeApplication', { + id: this.id, + params: this.installApplicationRequestParams, + }); + }, + dismissUpgradeSuccess() { + eventHub.$emit('dismissUpgradeSuccess', this.id); + }, }, }; </script> @@ -207,6 +301,51 @@ export default { </li> </ul> </div> + + <div + v-if="(upgradeSuccessful || upgradeFailed) && !upgradeAvailable" + class="form-text text-muted label p-0 js-cluster-application-upgrade-details" + > + {{ versionLabel }} + + <span v-if="upgradeSuccessful"> to</span> + + <gl-link + v-if="upgradeSuccessful" + :href="chartRepo" + target="_blank" + class="js-cluster-application-upgrade-version" + > + chart v{{ version }} + </gl-link> + </div> + + <div + v-if="upgradeFailed && !isUpgrading" + class="bs-callout bs-callout-danger cluster-application-banner mt-2 mb-0 js-cluster-application-upgrade-failure-message" + > + {{ upgradeFailureDescription }} + </div> + + <div + v-if="upgradeRequested && upgradeSuccessful" + class="bs-callout bs-callout-success cluster-application-banner mt-2 mb-0 p-0 pl-3" + > + {{ upgradeSuccessDescription }} + + <button class="close cluster-application-banner-close" @click="dismissUpgradeSuccess"> + × + </button> + </div> + + <loading-button + v-if="upgradeAvailable || upgradeFailed || 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 5d19c79570a..0cf187d4189 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -362,6 +362,9 @@ export default { :status-reason="applications.runner.statusReason" :request-status="applications.runner.requestStatus" :request-reason="applications.runner.requestReason" + :version="applications.runner.version" + :chart-repo="applications.runner.chartRepo" + :upgrade-available="applications.runner.upgradeAvailable" :disabled="!helmInstalled" title-link="https://docs.gitlab.com/runner/" > diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js index 360511e8882..39022879d91 100644 --- a/app/assets/javascripts/clusters/constants.js +++ b/app/assets/javascripts/clusters/constants.js @@ -12,15 +12,19 @@ export const APPLICATION_STATUS = { SCHEDULED: 'scheduled', INSTALLING: 'installing', INSTALLED: 'installed', - UPDATED: 'updated', UPDATING: 'updating', + UPDATED: 'updated', + UPDATE_ERRORED: 'update_errored', ERROR: 'errored', }; // These are only used client-side export const REQUEST_SUBMITTED = 'request-submitted'; export const REQUEST_FAILURE = 'request-failure'; +export const UPGRADE_REQUESTED = 'upgrade-requested'; +export const UPGRADE_REQUEST_FAILURE = 'upgrade-request-failure'; export const INGRESS = 'ingress'; export const JUPYTER = 'jupyter'; export const KNATIVE = 'knative'; +export const RUNNER = 'runner'; export const CERT_MANAGER = 'cert_manager'; diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js index 8f74be4e0e6..d309678be27 100644 --- a/app/assets/javascripts/clusters/stores/clusters_store.js +++ b/app/assets/javascripts/clusters/stores/clusters_store.js @@ -1,6 +1,6 @@ import { s__ } from '../../locale'; import { parseBoolean } from '../../lib/utils/common_utils'; -import { INGRESS, JUPYTER, KNATIVE, CERT_MANAGER } from '../constants'; +import { INGRESS, JUPYTER, KNATIVE, CERT_MANAGER, RUNNER } from '../constants'; export default class ClusterStore { constructor() { @@ -40,6 +40,9 @@ export default class ClusterStore { statusReason: null, requestStatus: null, requestReason: null, + version: null, + chartRepo: 'https://gitlab.com/charts/gitlab-runner', + upgradeAvailable: null, }, prometheus: { title: s__('ClusterIntegration|Prometheus'), @@ -100,7 +103,13 @@ export default class ClusterStore { this.state.statusReason = serverState.status_reason; serverState.applications.forEach(serverAppEntry => { - const { name: appId, status, status_reason: statusReason } = serverAppEntry; + const { + name: appId, + status, + status_reason: statusReason, + version, + update_available: upgradeAvailable, + } = serverAppEntry; this.state.applications[appId] = { ...(this.state.applications[appId] || {}), @@ -124,6 +133,9 @@ export default class ClusterStore { serverAppEntry.hostname || this.state.applications.knative.hostname; this.state.applications.knative.externalIp = serverAppEntry.external_ip || this.state.applications.knative.externalIp; + } else if (appId === RUNNER) { + this.state.applications.runner.version = version; + this.state.applications.runner.upgradeAvailable = upgradeAvailable; } }); } diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue index 3ef54752436..0bf2dde8b96 100644 --- a/app/assets/javascripts/diffs/components/compare_versions.vue +++ b/app/assets/javascripts/diffs/components/compare_versions.vue @@ -6,6 +6,7 @@ import { polyfillSticky } from '~/lib/utils/sticky'; import Icon from '~/vue_shared/components/icon.vue'; import CompareVersionsDropdown from './compare_versions_dropdown.vue'; import SettingsDropdown from './settings_dropdown.vue'; +import DiffStats from './diff_stats.vue'; export default { components: { @@ -14,6 +15,7 @@ export default { GlLink, GlButton, SettingsDropdown, + DiffStats, }, directives: { GlTooltip: GlTooltipDirective, @@ -35,8 +37,15 @@ export default { }, }, computed: { - ...mapState('diffs', ['commit', 'showTreeList', 'startVersion', 'latestVersionPath']), - ...mapGetters('diffs', ['hasCollapsedFile']), + ...mapGetters('diffs', ['hasCollapsedFile', 'diffFilesLength']), + ...mapState('diffs', [ + 'commit', + 'showTreeList', + 'startVersion', + 'latestVersionPath', + 'addedLines', + 'removedLines', + ]), comparableDiffs() { return this.mergeRequestDiffs.slice(1); }, @@ -104,6 +113,11 @@ export default { <gl-link :href="commit.commit_url" class="monospace">{{ commit.short_id }}</gl-link> </div> <div class="inline-parallel-buttons d-none d-md-flex ml-auto"> + <diff-stats + :diff-files-length="diffFilesLength" + :added-lines="addedLines" + :removed-lines="removedLines" + /> <gl-button v-if="commit || startVersion" :href="latestVersionPath" diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index b58f704bebb..60586d4a607 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -9,6 +9,7 @@ import { GlTooltipDirective } from '@gitlab/ui'; import { truncateSha } from '~/lib/utils/text_utility'; import { __, s__, sprintf } from '~/locale'; import EditButton from './edit_button.vue'; +import DiffStats from './diff_stats.vue'; export default { components: { @@ -16,6 +17,7 @@ export default { EditButton, Icon, FileIcon, + DiffStats, }, directives: { GlTooltip: GlTooltipDirective, @@ -202,6 +204,7 @@ export default { v-if="!diffFile.submodule && addMergeRequestButtons" class="file-actions d-none d-sm-block" > + <diff-stats :added-lines="diffFile.added_lines" :removed-lines="diffFile.removed_lines" /> <template v-if="diffFile.blob && diffFile.blob.readable_text"> <button :disabled="!diffHasDiscussions(diffFile)" diff --git a/app/assets/javascripts/diffs/components/diff_stats.vue b/app/assets/javascripts/diffs/components/diff_stats.vue new file mode 100644 index 00000000000..2e5855380af --- /dev/null +++ b/app/assets/javascripts/diffs/components/diff_stats.vue @@ -0,0 +1,52 @@ +<script> +import Icon from '~/vue_shared/components/icon.vue'; +import { n__ } from '~/locale'; + +export default { + components: { Icon }, + props: { + addedLines: { + type: Number, + required: true, + }, + removedLines: { + type: Number, + required: true, + }, + diffFilesLength: { + type: Number, + required: false, + default: null, + }, + }, + computed: { + filesText() { + return n__('File', 'Files', this.diffFilesLength); + }, + isCompareVersionsHeader() { + return Boolean(this.diffFilesLength); + }, + }, +}; +</script> + +<template> + <div + class="diff-stats" + :class="{ + 'is-compare-versions-header d-none d-lg-inline-flex': isCompareVersionsHeader, + 'd-inline-flex': !isCompareVersionsHeader, + }" + > + <div v-if="diffFilesLength !== null" class="diff-stats-group"> + <icon name="doc-code" class="diff-stats-icon text-secondary" /> + <strong>{{ diffFilesLength }} {{ filesText }}</strong> + </div> + <div class="diff-stats-group cgreen"> + <icon name="file-addition" class="diff-stats-icon" /> <strong>{{ addedLines }}</strong> + </div> + <div class="diff-stats-group cred"> + <icon name="file-deletion" class="diff-stats-icon" /> <strong>{{ removedLines }}</strong> + </div> + </div> +</template> diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue index a0f09932593..7e00b994541 100644 --- a/app/assets/javascripts/diffs/components/tree_list.vue +++ b/app/assets/javascripts/diffs/components/tree_list.vue @@ -13,18 +13,43 @@ export default { Icon, FileRow, }, + data() { + return { + search: '', + }; + }, computed: { - ...mapState('diffs', ['tree', 'addedLines', 'removedLines', 'renderTreeList']), - ...mapGetters('diffs', ['allBlobs', 'diffFilesLength']), + ...mapState('diffs', ['tree', 'renderTreeList']), + ...mapGetters('diffs', ['allBlobs']), filteredTreeList() { - return this.renderTreeList ? this.tree : this.allBlobs; + const search = this.search.toLowerCase().trim(); + + if (search === '' || this.$options.fuzzyFileFinderEnabled) + return this.renderTreeList ? this.tree : this.allBlobs; + + return this.allBlobs.reduce((acc, folder) => { + const tree = folder.tree.filter(f => f.path.toLowerCase().indexOf(search) >= 0); + + if (tree.length) { + return acc.concat({ + ...folder, + tree, + }); + } + + return acc; + }, []); }, }, methods: { ...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile', 'toggleFileFinder']), + clearSearch() { + this.search = ''; + }, }, shortcutKeyCharacter: `${/Mac/i.test(navigator.userAgent) ? '⌘' : 'Ctrl'}+P`, FileRowStats, + diffTreeFiltering: gon.features && gon.features.diffTreeFiltering, }; </script> @@ -33,17 +58,36 @@ export default { <div class="append-bottom-8 position-relative tree-list-search d-flex"> <div class="flex-fill d-flex"> <icon name="search" class="position-absolute tree-list-icon" /> - <button - type="button" - class="form-control text-left text-secondary" - @click="toggleFileFinder(true)" - > - {{ s__('MergeRequest|Search files') }} - </button> - <span - class="position-absolute text-secondary diff-tree-search-shortcut" - v-html="$options.shortcutKeyCharacter" - ></span> + <template v-if="$options.diffTreeFiltering"> + <input + v-model="search" + :placeholder="s__('MergeRequest|Filter files')" + type="search" + class="form-control" + /> + <button + v-show="search" + :aria-label="__('Clear search')" + type="button" + class="position-absolute bg-transparent tree-list-icon tree-list-clear-icon border-0 p-0" + @click="clearSearch" + > + <icon name="close" /> + </button> + </template> + <template v-else> + <button + type="button" + class="form-control text-left text-secondary" + @click="toggleFileFinder(true)" + > + {{ s__('MergeRequest|Search files') }} + </button> + <span + class="position-absolute text-secondary diff-tree-search-shortcut" + v-html="$options.shortcutKeyCharacter" + ></span> + </template> </div> </div> <div :class="{ 'pt-0 tree-list-blobs': !renderTreeList }" class="tree-list-scroll"> @@ -64,13 +108,6 @@ export default { {{ s__('MergeRequest|No files found') }} </p> </div> - <div v-once class="pt-3 pb-3 text-center"> - {{ n__('%d changed file', '%d changed files', diffFilesLength) }} - <div> - <span class="cgreen"> {{ n__('%d addition', '%d additions', addedLines) }} </span> - <span class="cred"> {{ n__('%d deleted', '%d deletions', removedLines) }} </span> - </div> - </div> </div> </template> @@ -86,7 +123,7 @@ export default { pointer-events: none; } -.tree-list-icon { +.tree-list-icon:not(button) { pointer-events: none; } </style> diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js index 6ee33d9fc6d..47f78a5db54 100644 --- a/app/assets/javascripts/diffs/store/modules/diff_state.js +++ b/app/assets/javascripts/diffs/store/modules/diff_state.js @@ -11,6 +11,8 @@ const storedTreeShow = localStorage.getItem(MR_TREE_SHOW_KEY); export default () => ({ isLoading: true, + addedLines: null, + removedLines: null, endpoint: '', basePath: '', commit: null, diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js index dbfcf8cc921..cb1b1173190 100644 --- a/app/assets/javascripts/due_date_select.js +++ b/app/assets/javascripts/due_date_select.js @@ -64,6 +64,7 @@ class DueDateSelect { this.saveDueDate(true); } }, + firstDay: gon.first_day_of_week, }); calendar.setDate(parsePikadayDate($dueDateInput.val())); @@ -183,6 +184,7 @@ export default class DueDateSelectors { onSelect(dateText) { $datePicker.val(calendar.toString(dateText)); }, + firstDay: gon.first_day_of_week, }); calendar.setDate(parsePikadayDate(datePickerVal)); diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue index bd402c0eea5..6ece8b92a30 100644 --- a/app/assets/javascripts/environments/components/container.vue +++ b/app/assets/javascripts/environments/components/container.vue @@ -22,10 +22,6 @@ export default { type: Object, required: true, }, - canCreateDeployment: { - type: Boolean, - required: true, - }, canReadEnvironment: { type: Boolean, required: true, @@ -51,11 +47,7 @@ export default { <slot name="emptyState"></slot> <div v-if="!isLoading && environments.length > 0" class="table-holder"> - <environment-table - :environments="environments" - :can-create-deployment="canCreateDeployment" - :can-read-environment="canReadEnvironment" - /> + <environment-table :environments="environments" :can-read-environment="canReadEnvironment" /> <table-pagination v-if="pagination && pagination.totalPages > 1" diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index f44806d82a6..503c1b38f71 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -47,12 +47,6 @@ export default { default: () => ({}), }, - canCreateDeployment: { - type: Boolean, - required: false, - default: false, - }, - canReadEnvironment: { type: Boolean, required: false, @@ -151,7 +145,7 @@ export default { }, actions() { - if (!this.model || !this.model.last_deployment || !this.canCreateDeployment) { + if (!this.model || !this.model.last_deployment) { return []; } @@ -561,7 +555,7 @@ export default { /> <rollback-component - v-if="canRetry && canCreateDeployment" + v-if="canRetry" :is-last-deployment="isLastDeployment" :retry-url="retryUrl" /> diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue index 87c1d44dd40..aa2417d3194 100644 --- a/app/assets/javascripts/environments/components/environments_app.vue +++ b/app/assets/javascripts/environments/components/environments_app.vue @@ -24,10 +24,6 @@ export default { type: Boolean, required: true, }, - canCreateDeployment: { - type: Boolean, - required: true, - }, canReadEnvironment: { type: Boolean, required: true, @@ -106,7 +102,6 @@ export default { :is-loading="isLoading" :environments="state.environments" :pagination="state.paginationInformation" - :can-create-deployment="canCreateDeployment" :can-read-environment="canReadEnvironment" @onChangePage="onChangePage" > diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue index 75bdf87137f..e2c304de00a 100644 --- a/app/assets/javascripts/environments/components/environments_table.vue +++ b/app/assets/javascripts/environments/components/environments_table.vue @@ -23,12 +23,6 @@ export default { required: false, default: false, }, - - canCreateDeployment: { - type: Boolean, - required: false, - default: false, - }, }, methods: { folderUrl(model) { @@ -64,7 +58,6 @@ export default { is="environment-item" :key="`environment-item-${i}`" :model="model" - :can-create-deployment="canCreateDeployment" :can-read-environment="canReadEnvironment" /> @@ -79,7 +72,6 @@ export default { v-for="(children, index) in model.children" :key="`env-item-${i}-${index}`" :model="children" - :can-create-deployment="canCreateDeployment" :can-read-environment="canReadEnvironment" /> diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js index 982e550e73c..56e7f69cad6 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js +++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js @@ -18,7 +18,6 @@ export default () => endpoint: environmentsData.environmentsDataEndpoint, folderName: environmentsData.environmentsDataFolderName, cssContainerClass: environmentsData.cssClass, - canCreateDeployment: parseBoolean(environmentsData.environmentsDataCanCreateDeployment), canReadEnvironment: parseBoolean(environmentsData.environmentsDataCanReadEnvironment), }; }, @@ -28,7 +27,6 @@ export default () => endpoint: this.endpoint, folderName: this.folderName, cssContainerClass: this.cssContainerClass, - canCreateDeployment: this.canCreateDeployment, canReadEnvironment: this.canReadEnvironment, }, }); diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue index d6f0b6115a6..80f0e00400b 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_view.vue +++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue @@ -23,10 +23,6 @@ export default { type: String, required: true, }, - canCreateDeployment: { - type: Boolean, - required: true, - }, canReadEnvironment: { type: Boolean, required: true, @@ -55,7 +51,6 @@ export default { :is-loading="isLoading" :environments="state.environments" :pagination="state.paginationInformation" - :can-create-deployment="canCreateDeployment" :can-read-environment="canReadEnvironment" @onChangePage="onChangePage" /> diff --git a/app/assets/javascripts/environments/index.js b/app/assets/javascripts/environments/index.js index d366e7550b7..6af66d0f86e 100644 --- a/app/assets/javascripts/environments/index.js +++ b/app/assets/javascripts/environments/index.js @@ -20,7 +20,6 @@ export default () => helpPagePath: environmentsData.helpPagePath, cssContainerClass: environmentsData.cssClass, canCreateEnvironment: parseBoolean(environmentsData.canCreateEnvironment), - canCreateDeployment: parseBoolean(environmentsData.canCreateDeployment), canReadEnvironment: parseBoolean(environmentsData.canReadEnvironment), }; }, @@ -32,7 +31,6 @@ export default () => helpPagePath: this.helpPagePath, cssContainerClass: this.cssContainerClass, canCreateEnvironment: this.canCreateEnvironment, - canCreateDeployment: this.canCreateDeployment, canReadEnvironment: this.canReadEnvironment, }, }); diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js index 5a2b680c2f7..cdfebd19fa4 100644 --- a/app/assets/javascripts/ide/index.js +++ b/app/assets/javascripts/ide/index.js @@ -6,6 +6,7 @@ import ide from './components/ide.vue'; import store from './stores'; import router from './ide_router'; import { parseBoolean } from '../lib/utils/common_utils'; +import { resetServiceWorkersPublicPath } from '../lib/utils/webpack'; Vue.use(Translate); @@ -60,16 +61,6 @@ export function initIde(el, options = {}) { }); } -// tell webpack to load assets from origin so that web workers don't break -export function resetServiceWorkersPublicPath() { - // __webpack_public_path__ is a global variable that can be used to adjust - // the webpack publicPath setting at runtime. - // see: https://webpack.js.org/guides/public-path/ - const relativeRootPath = (gon && gon.relative_url_root) || ''; - const webpackAssetPath = `${relativeRootPath}/assets/webpack/`; - __webpack_public_path__ = webpackAssetPath; // eslint-disable-line camelcase -} - /** * Start the IDE. * diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index 4d2533d01f1..9336b71cfd7 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -44,6 +44,7 @@ export default class IssuableForm { parse: dateString => parsePikadayDate(dateString), toString: date => pikadayToString(date), onSelect: dateText => $issuableDueDate.val(calendar.toString(dateText)), + firstDay: gon.first_day_of_week, }); calendar.setDate(parsePikadayDate($issuableDueDate.val())); } diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue index e664269b199..58f14bac8c8 100644 --- a/app/assets/javascripts/issue_show/components/description.vue +++ b/app/assets/javascripts/issue_show/components/description.vue @@ -1,6 +1,7 @@ <script> import $ from 'jquery'; -import { __ } from '~/locale'; +import { s__, sprintf } from '~/locale'; +import createFlash from '~/flash'; import animateMixin from '../mixins/animate'; import TaskList from '../../task_list'; import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor'; @@ -91,9 +92,14 @@ export default { }, taskListUpdateError() { - window.Flash( - __( - 'Someone edited this issue at the same time you did. The description has been updated and you will need to make your changes again.', + createFlash( + sprintf( + s__( + 'Someone edited this %{issueType} at the same time you did. The description has been updated and you will need to make your changes again.', + ), + { + issueType: this.issuableType, + }, ), ); diff --git a/app/assets/javascripts/lib/utils/chart_utils.js b/app/assets/javascripts/lib/utils/chart_utils.js new file mode 100644 index 00000000000..0f78756aac8 --- /dev/null +++ b/app/assets/javascripts/lib/utils/chart_utils.js @@ -0,0 +1,83 @@ +const commonTooltips = () => ({ + mode: 'x', + intersect: false, +}); + +const adjustedFontScale = () => ({ + fontSize: 8, +}); + +const yAxesConfig = (shouldAdjustFontSize = false) => ({ + yAxes: [ + { + ticks: { + beginAtZero: true, + ...(shouldAdjustFontSize ? adjustedFontScale() : {}), + }, + }, + ], +}); + +const xAxesConfig = (shouldAdjustFontSize = false) => ({ + xAxes: [ + { + ticks: { + ...(shouldAdjustFontSize ? adjustedFontScale() : {}), + }, + }, + ], +}); + +const commonChartOptions = () => ({ + responsive: true, + maintainAspectRatio: false, + legend: false, +}); + +export const barChartOptions = shouldAdjustFontSize => ({ + ...commonChartOptions(), + scales: { + ...yAxesConfig(shouldAdjustFontSize), + ...xAxesConfig(shouldAdjustFontSize), + }, + tooltips: { + ...commonTooltips(), + displayColors: false, + callbacks: { + title() { + return ''; + }, + label({ xLabel, yLabel }) { + return `${xLabel}: ${yLabel}`; + }, + }, + }, +}); + +export const pieChartOptions = commonChartOptions; + +export const lineChartOptions = ({ width, numberOfPoints, shouldAdjustFontSize }) => ({ + ...commonChartOptions(), + scales: { + ...yAxesConfig(shouldAdjustFontSize), + ...xAxesConfig(shouldAdjustFontSize), + }, + elements: { + point: { + hitRadius: width / (numberOfPoints * 2), + }, + }, + tooltips: { + ...commonTooltips(), + caretSize: 0, + multiKeyBackground: 'rgba(0,0,0,0)', + callbacks: { + labelColor({ datasetIndex }, { config }) { + return { + backgroundColor: config.data.datasets[datasetIndex].backgroundColor, + borderColor: 'rgba(0,0,0,0)', + }; + }, + }, + }, +}); diff --git a/app/assets/javascripts/lib/utils/file_upload.js b/app/assets/javascripts/lib/utils/file_upload.js index b41ffb44971..82ee83e4348 100644 --- a/app/assets/javascripts/lib/utils/file_upload.js +++ b/app/assets/javascripts/lib/utils/file_upload.js @@ -1,6 +1,9 @@ export default (buttonSelector, fileSelector) => { const btn = document.querySelector(buttonSelector); const fileInput = document.querySelector(fileSelector); + + if (!btn || !fileInput) return; + const form = btn.closest('form'); btn.addEventListener('click', () => { diff --git a/app/assets/javascripts/lib/utils/webpack.js b/app/assets/javascripts/lib/utils/webpack.js new file mode 100644 index 00000000000..308ad9784e4 --- /dev/null +++ b/app/assets/javascripts/lib/utils/webpack.js @@ -0,0 +1,10 @@ +// tell webpack to load assets from origin so that web workers don't break +// eslint-disable-next-line import/prefer-default-export +export function resetServiceWorkersPublicPath() { + // __webpack_public_path__ is a global variable that can be used to adjust + // the webpack publicPath setting at runtime. + // see: https://webpack.js.org/guides/public-path/ + const relativeRootPath = (gon && gon.relative_url_root) || ''; + const webpackAssetPath = `${relativeRootPath}/assets/webpack/`; + __webpack_public_path__ = webpackAssetPath; // eslint-disable-line camelcase +} diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 8e10b3ad912..63db4938cd7 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -124,9 +124,6 @@ function deferredInitialisation() { selector: '.has-tooltip, [data-toggle="tooltip"]', trigger: 'hover', boundary: 'viewport', - placement(tip, el) { - return $(el).data('placement') || 'bottom'; - }, }); // Initialize popovers diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js index 0beedcacf33..0dabb28ea66 100644 --- a/app/assets/javascripts/member_expiration_date.js +++ b/app/assets/javascripts/member_expiration_date.js @@ -33,6 +33,7 @@ export default function memberExpirationDate(selector = '.js-access-expiration-d toggleClearInput.call($input); }, + firstDay: gon.first_day_of_week, }); calendar.setDate(parsePikadayDate($input.val())); diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index ac3b47cd218..3b42a154af8 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -2,6 +2,7 @@ import $ from 'jquery'; import { __ } from '~/locale'; +import createFlash from '~/flash'; import TaskList from './task_list'; import MergeRequestTabs from './merge_request_tabs'; import IssuablesHelper from './helpers/issuables_helper'; @@ -40,6 +41,13 @@ function MergeRequest(opts) { document.querySelector('#task_status').innerText = result.task_status; document.querySelector('#task_status_short').innerText = result.task_status_short; }, + onError: () => { + createFlash( + __( + 'Someone edited this merge request at the same time you did. Please refresh the page to see changes.', + ), + ); + }, }); } } diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue index ec0e33a1927..14c02db7bcc 100644 --- a/app/assets/javascripts/monitoring/components/charts/area.vue +++ b/app/assets/javascripts/monitoring/components/charts/area.vue @@ -189,8 +189,8 @@ export default { <template> <div class="prometheus-graph col-12 col-lg-6"> <div class="prometheus-graph-header"> - <h5 class="prometheus-graph-title">{{ graphData.title }}</h5> - <div class="prometheus-graph-widgets"><slot></slot></div> + <h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5> + <div ref="graphWidgets" class="prometheus-graph-widgets"><slot></slot></div> </div> <gl-area-chart ref="areaChart" diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 9c5fd93f7d1..895a57785bc 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -160,7 +160,8 @@ export default { {{ s__('Metrics|Environment') }} <div class="dropdown prepend-left-10"> <button class="dropdown-menu-toggle" data-toggle="dropdown" type="button"> - <span> {{ currentEnvironmentName }} </span> <icon name="chevron-down" /> + <span>{{ currentEnvironmentName }}</span> + <icon name="chevron-down" /> </button> <div v-if="store.environmentsData.length > 0" @@ -172,9 +173,8 @@ export default { :href="environment.metrics_path" :class="{ 'is-active': environment.name == currentEnvironmentName }" class="dropdown-item" + >{{ environment.name }}</a > - {{ environment.name }} - </a> </li> </ul> </div> diff --git a/app/assets/javascripts/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js index e4d72eb8318..9e99aa4f724 100644 --- a/app/assets/javascripts/mr_notes/index.js +++ b/app/assets/javascripts/mr_notes/index.js @@ -7,8 +7,11 @@ import discussionCounter from '../notes/components/discussion_counter.vue'; import initDiscussionFilters from '../notes/discussion_filters'; import store from './stores'; import MergeRequest from '../merge_request'; +import { resetServiceWorkersPublicPath } from '../lib/utils/webpack'; export default function initMrNotes() { + resetServiceWorkersPublicPath(); + const mrShowNode = document.querySelector('.merge-request'); // eslint-disable-next-line no-new new MergeRequest({ diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index 394f2a80a67..91b9e5de374 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -135,6 +135,7 @@ export default { <span v-if="accessLevel" class="note-role user-access-role">{{ accessLevel }}</span> <div v-if="canResolve" class="note-actions-item"> <button + ref="resolveButton" v-gl-tooltip :class="{ 'is-disabled': !resolvable, 'is-active': isResolved }" :title="resolveButtonTitle" @@ -151,10 +152,9 @@ export default { </div> <div v-if="canAwardEmoji" class="note-actions-item"> <a - v-gl-tooltip.bottom + v-gl-tooltip :class="{ 'js-user-authored': isAuthoredByCurrentUser }" class="note-action-button note-emoji-button js-add-award js-note-emoji" - data-position="right" href="#" title="Add reaction" > @@ -175,7 +175,7 @@ export default { /> <div v-if="canEdit" class="note-actions-item"> <button - v-gl-tooltip.bottom + v-gl-tooltip type="button" title="Edit comment" class="note-action-button js-note-edit btn btn-transparent" @@ -186,7 +186,7 @@ export default { </div> <div v-if="showDeleteAction" class="note-actions-item"> <button - v-gl-tooltip.bottom + v-gl-tooltip type="button" title="Delete comment" class="note-action-button js-note-delete btn btn-transparent" @@ -197,7 +197,7 @@ export default { </div> <div v-else-if="shouldShowActionsDropdown" class="dropdown more-actions note-actions-item"> <button - v-gl-tooltip.bottom + v-gl-tooltip type="button" title="More actions" class="note-action-button more-actions-toggle btn btn-transparent" diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue index 3efdd1c5c17..17e5fcab5b7 100644 --- a/app/assets/javascripts/notes/components/note_awards_list.vue +++ b/app/assets/javascripts/notes/components/note_awards_list.vue @@ -171,7 +171,6 @@ export default { :class="getAwardClassBindings(awardList)" :title="awardTitle(awardList)" data-boundary="viewport" - data-placement="bottom" class="btn award-control" type="button" @click="handleAward(awardName)" @@ -187,7 +186,6 @@ export default { title="Add reaction" aria-label="Add reaction" data-boundary="viewport" - data-placement="bottom" type="button" > <span class="award-control-icon award-control-icon-neutral"> diff --git a/app/assets/javascripts/pages/dashboard/projects/index.js b/app/assets/javascripts/pages/dashboard/projects/index.js index 8f98be79640..01001d4f3ff 100644 --- a/app/assets/javascripts/pages/dashboard/projects/index.js +++ b/app/assets/javascripts/pages/dashboard/projects/index.js @@ -1,7 +1,5 @@ import ProjectsList from '~/projects_list'; -import Star from '../../../star'; document.addEventListener('DOMContentLoaded', () => { new ProjectsList(); // eslint-disable-line no-new - new Star('.project-row'); // eslint-disable-line no-new }); diff --git a/app/assets/javascripts/pages/explore/projects/index.js b/app/assets/javascripts/pages/explore/projects/index.js index 8f98be79640..01001d4f3ff 100644 --- a/app/assets/javascripts/pages/explore/projects/index.js +++ b/app/assets/javascripts/pages/explore/projects/index.js @@ -1,7 +1,5 @@ import ProjectsList from '~/projects_list'; -import Star from '../../../star'; document.addEventListener('DOMContentLoaded', () => { new ProjectsList(); // eslint-disable-line no-new - new Star('.project-row'); // eslint-disable-line no-new }); diff --git a/app/assets/javascripts/pages/projects/environments/metrics/index.js b/app/assets/javascripts/pages/projects/environments/metrics/index.js index 0b644780ad4..0d69a689316 100644 --- a/app/assets/javascripts/pages/projects/environments/metrics/index.js +++ b/app/assets/javascripts/pages/projects/environments/metrics/index.js @@ -1,3 +1,3 @@ -import monitoringBundle from '~/monitoring/monitoring_bundle'; +import monitoringBundle from 'ee_else_ce/monitoring/monitoring_bundle'; document.addEventListener('DOMContentLoaded', monitoringBundle); diff --git a/app/assets/javascripts/pages/projects/graphs/charts/index.js b/app/assets/javascripts/pages/projects/graphs/charts/index.js index 3ccad513c05..314519ee442 100644 --- a/app/assets/javascripts/pages/projects/graphs/charts/index.js +++ b/app/assets/javascripts/pages/projects/graphs/charts/index.js @@ -2,65 +2,86 @@ import $ from 'jquery'; import Chart from 'chart.js'; import _ from 'underscore'; +import { barChartOptions, pieChartOptions } from '~/lib/utils/chart_utils'; + document.addEventListener('DOMContentLoaded', () => { const projectChartData = JSON.parse(document.getElementById('projectChartData').innerHTML); - const responsiveChart = (selector, data) => { - const options = { - scaleOverlay: true, - responsive: true, - pointHitDetectionRadius: 2, - maintainAspectRatio: false, - }; + const barChart = (selector, data) => { // get selector by context const ctx = selector.get(0).getContext('2d'); // pointing parent container to make chart.js inherit its width const container = $(selector).parent(); - const generateChart = () => { - selector.attr('width', $(container).width()); - if (window.innerWidth < 768) { - // Scale fonts if window width lower than 768px (iPad portrait) - options.scaleFontSize = 8; - } - return new Chart(ctx).Bar(data, options); - }; - // enabling auto-resizing - $(window).resize(generateChart); - return generateChart(); + selector.attr('width', $(container).width()); + + // Scale fonts if window width lower than 768px (iPad portrait) + const shouldAdjustFontSize = window.innerWidth < 768; + return new Chart(ctx, { + type: 'bar', + data, + options: barChartOptions(shouldAdjustFontSize), + }); + }; + + const pieChart = (context, data) => { + const options = pieChartOptions(); + + return new Chart(context, { + type: 'pie', + data, + options, + }); }; const chartData = data => ({ labels: Object.keys(data), datasets: [ { - fillColor: 'rgba(220,220,220,0.5)', - strokeColor: 'rgba(220,220,220,1)', - barStrokeWidth: 1, - barValueSpacing: 1, - barDatasetSpacing: 1, + backgroundColor: 'rgba(220,220,220,0.5)', + borderColor: 'rgba(220,220,220,1)', + borderWidth: 1, data: _.values(data), }, ], }); + const reorderWeekDays = (weekDays, firstDayOfWeek = 0) => { + if (firstDayOfWeek === 0) { + return weekDays; + } + + return Object.keys(weekDays).reduce((acc, dayName, idx, arr) => { + const reorderedDayName = arr[(idx + firstDayOfWeek) % arr.length]; + + return { + ...acc, + [reorderedDayName]: weekDays[reorderedDayName], + }; + }, {}); + }; + const hourData = chartData(projectChartData.hour); - responsiveChart($('#hour-chart'), hourData); + barChart($('#hour-chart'), hourData); - const dayData = chartData(projectChartData.weekDays); - responsiveChart($('#weekday-chart'), dayData); + const weekDays = reorderWeekDays(projectChartData.weekDays, gon.first_day_of_week); + const dayData = chartData(weekDays); + barChart($('#weekday-chart'), dayData); const monthData = chartData(projectChartData.month); - responsiveChart($('#month-chart'), monthData); + barChart($('#month-chart'), monthData); - const data = projectChartData.languages; + const data = { + datasets: [ + { + data: projectChartData.languages.map(x => x.value), + backgroundColor: projectChartData.languages.map(x => x.color), + hoverBackgroundColor: projectChartData.languages.map(x => x.highlight), + }, + ], + labels: projectChartData.languages.map(x => x.label), + }; const ctx = $('#languages-chart') .get(0) .getContext('2d'); - const options = { - scaleOverlay: true, - responsive: true, - maintainAspectRatio: false, - }; - - new Chart(ctx).Pie(data, options); + pieChart(ctx, data); }); diff --git a/app/assets/javascripts/pages/projects/pipelines/charts/index.js b/app/assets/javascripts/pages/projects/pipelines/charts/index.js index 48353f3b4ef..9fa580d2ba9 100644 --- a/app/assets/javascripts/pages/projects/pipelines/charts/index.js +++ b/app/assets/javascripts/pages/projects/pipelines/charts/index.js @@ -1,29 +1,31 @@ import $ from 'jquery'; import Chart from 'chart.js'; -const options = { - scaleOverlay: true, - responsive: true, - maintainAspectRatio: false, -}; +import { barChartOptions, lineChartOptions } from '~/lib/utils/chart_utils'; + +const SUCCESS_LINE_COLOR = '#1aaa55'; + +const TOTAL_LINE_COLOR = '#707070'; -const buildChart = chartScope => { +const buildChart = (chartScope, shouldAdjustFontSize) => { const data = { labels: chartScope.labels, datasets: [ { - fillColor: '#707070', - strokeColor: '#707070', - pointColor: '#707070', - pointStrokeColor: '#EEE', - data: chartScope.totalValues, + backgroundColor: SUCCESS_LINE_COLOR, + borderColor: SUCCESS_LINE_COLOR, + pointBackgroundColor: SUCCESS_LINE_COLOR, + pointBorderColor: '#fff', + data: chartScope.successValues, + fill: 'origin', }, { - fillColor: '#1aaa55', - strokeColor: '#1aaa55', - pointColor: '#1aaa55', - pointStrokeColor: '#fff', - data: chartScope.successValues, + backgroundColor: TOTAL_LINE_COLOR, + borderColor: TOTAL_LINE_COLOR, + pointBackgroundColor: TOTAL_LINE_COLOR, + pointBorderColor: '#EEE', + data: chartScope.totalValues, + fill: '-1', }, ], }; @@ -31,36 +33,51 @@ const buildChart = chartScope => { .get(0) .getContext('2d'); - new Chart(ctx).Line(data, options); + return new Chart(ctx, { + type: 'line', + data, + options: lineChartOptions({ + width: ctx.canvas.width, + numberOfPoints: chartScope.totalValues.length, + shouldAdjustFontSize, + }), + }); }; -document.addEventListener('DOMContentLoaded', () => { - const chartTimesData = JSON.parse(document.getElementById('pipelinesTimesChartsData').innerHTML); - const chartsData = JSON.parse(document.getElementById('pipelinesChartsData').innerHTML); +const buildBarChart = (chartTimesData, shouldAdjustFontSize) => { const data = { labels: chartTimesData.labels, datasets: [ { - fillColor: 'rgba(220,220,220,0.5)', - strokeColor: 'rgba(220,220,220,1)', - barStrokeWidth: 1, + backgroundColor: 'rgba(220,220,220,0.5)', + borderColor: 'rgba(220,220,220,1)', + borderWidth: 1, barValueSpacing: 1, barDatasetSpacing: 1, data: chartTimesData.values, }, ], }; - - if (window.innerWidth < 768) { - // Scale fonts if window width lower than 768px (iPad portrait) - options.scaleFontSize = 8; - } - - new Chart( + return new Chart( $('#build_timesChart') .get(0) .getContext('2d'), - ).Bar(data, options); + { + type: 'bar', + data, + options: barChartOptions(shouldAdjustFontSize), + }, + ); +}; + +document.addEventListener('DOMContentLoaded', () => { + const chartTimesData = JSON.parse(document.getElementById('pipelinesTimesChartsData').innerHTML); + const chartsData = JSON.parse(document.getElementById('pipelinesChartsData').innerHTML); + + // Scale fonts if window width lower than 768px (iPad portrait) + const shouldAdjustFontSize = window.innerWidth < 768; + + buildBarChart(chartTimesData, shouldAdjustFontSize); - chartsData.forEach(scope => buildChart(scope)); + chartsData.forEach(scope => buildChart(scope, shouldAdjustFontSize)); }); diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js index 8a84ac37dab..afa099d0e0b 100644 --- a/app/assets/javascripts/pages/users/activity_calendar.js +++ b/app/assets/javascripts/pages/users/activity_calendar.js @@ -159,7 +159,7 @@ export default class ActivityCalendar { .append('g') .attr('transform', (group, i) => { _.each(group, (stamp, a) => { - if (a === 0 && stamp.day === 0) { + if (a === 0 && stamp.day === this.firstDayOfWeek) { const month = stamp.date.getMonth(); const x = this.daySizeWithSpace * i + 1 + this.daySizeWithSpace; const lastMonth = _.last(this.months); @@ -205,6 +205,14 @@ export default class ActivityCalendar { y: 29 + this.dayYPos(5), }, ]; + + if (this.firstDayOfWeek === 1) { + days.push({ + text: 'S', + y: 29 + this.dayYPos(7), + }); + } + this.svg .append('g') .selectAll('text') diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js index 1c3fd58ca74..39cd891c111 100644 --- a/app/assets/javascripts/pages/users/user_tabs.js +++ b/app/assets/javascripts/pages/users/user_tabs.js @@ -234,7 +234,7 @@ export default class UserTabs { data, calendarActivitiesPath, utcOffset, - 0, + gon.first_day_of_week, monthsAgo, ); } diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue index 74faa35358d..1ec2784cc5a 100644 --- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue +++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue @@ -134,6 +134,13 @@ export default { >ms / <span title="Invoke Count">{{ currentRequest.details.gc.invokes }}</span> gc </span> </div> + <div + v-if="currentRequest.details && currentRequest.details.tracing" + id="peek-view-trace" + class="view" + > + <a :href="currentRequest.details.tracing.tracing_url"> trace </a> + </div> <request-selector v-if="currentRequest" :current-request="currentRequest" diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index 998554d1be5..d65e73a3f9c 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -115,15 +115,35 @@ const bindEvents = () => { const templates = { rails: { text: 'Ruby on Rails', - icon: '.template-option svg.icon-rails', + icon: '.template-option .icon-rails', }, express: { text: 'NodeJS Express', - icon: '.template-option svg.icon-node-express', + icon: '.template-option .icon-express', }, spring: { text: 'Spring', - icon: '.template-option svg.icon-java-spring', + icon: '.template-option .icon-spring', + }, + hugo: { + text: 'Pages/Hugo', + icon: '.template-option .icon-hugo', + }, + jekyll: { + text: 'Pages/Jekyll', + icon: '.template-option .icon-jekyll', + }, + plainhtml: { + text: 'Pages/Plain HTML', + icon: '.template-option .icon-plainhtml', + }, + gitbook: { + text: 'Pages/GitBook', + icon: '.template-option .icon-gitbook', + }, + hexo: { + text: 'Pages/Hexo', + icon: '.template-option .icon-hexo', }, }; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue new file mode 100644 index 00000000000..a38f25cce35 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue @@ -0,0 +1,40 @@ +<script> +export default { + props: { + value: { + type: String, + required: true, + }, + label: { + type: String, + required: true, + }, + inputId: { + type: String, + required: true, + }, + }, +}; +</script> + +<template> + <li> + <div class="commit-message-editor"> + <div class="d-flex flex-wrap align-items-center justify-content-between"> + <label class="col-form-label" :for="inputId"> + <strong>{{ label }}</strong> + </label> + <slot name="header"></slot> + </div> + <textarea + :id="inputId" + :value="value" + class="form-control js-gfm-input append-bottom-default commit-message-edit" + required="required" + rows="7" + @input="$emit('input', $event.target.value)" + ></textarea> + <slot name="checkbox"></slot> + </div> + </li> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue new file mode 100644 index 00000000000..b3c1c0e329d --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue @@ -0,0 +1,38 @@ +<script> +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; + +export default { + components: { + GlDropdown, + GlDropdownItem, + }, + props: { + commits: { + type: Array, + required: true, + default: () => [], + }, + }, +}; +</script> + +<template> + <div> + <gl-dropdown + right + no-caret + text="Use an existing commit message" + variant="link" + class="mr-commit-dropdown" + > + <gl-dropdown-item + v-for="commit in commits" + :key="commit.short_id" + class="text-nowrap text-truncate" + @click="$emit('input', commit.message)" + > + <span class="monospace mr-2">{{ commit.short_id }}</span> {{ commit.title }} + </gl-dropdown-item> + </gl-dropdown> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue new file mode 100644 index 00000000000..a1d3a09cca4 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue @@ -0,0 +1,91 @@ +<script> +import { GlButton } from '@gitlab/ui'; +import _ from 'underscore'; +import { __, n__, sprintf, s__ } from '~/locale'; +import Icon from '~/vue_shared/components/icon.vue'; + +export default { + components: { + Icon, + GlButton, + }, + props: { + isSquashEnabled: { + type: Boolean, + required: true, + }, + commitsCount: { + type: Number, + required: false, + default: 0, + }, + targetBranch: { + type: String, + required: true, + }, + }, + data() { + return { + expanded: false, + }; + }, + computed: { + collapseIcon() { + return this.expanded ? 'chevron-down' : 'chevron-right'; + }, + commitsCountMessage() { + return n__(__('%d commit'), __('%d commits'), this.isSquashEnabled ? 1 : this.commitsCount); + }, + modifyLinkMessage() { + return this.isSquashEnabled ? __('Modify commit messages') : __('Modify merge commit'); + }, + ariaLabel() { + return this.expanded ? __('Collapse') : __('Expand'); + }, + message() { + return sprintf( + s__( + 'mrWidgetCommitsAdded|%{commitCount} and %{mergeCommitCount} will be added to %{targetBranch}.', + ), + { + commitCount: `<strong class="commits-count-message">${this.commitsCountMessage}</strong>`, + mergeCommitCount: `<strong>${s__('mrWidgetCommitsAdded|1 merge commit')}</strong>`, + targetBranch: `<span class="label-branch">${_.escape(this.targetBranch)}</span>`, + }, + false, + ); + }, + }, + methods: { + toggle() { + this.expanded = !this.expanded; + }, + }, +}; +</script> + +<template> + <div> + <div + class="js-mr-widget-commits-count mr-widget-extension clickable d-flex align-items-center px-3 py-2" + @click="toggle()" + > + <gl-button + :aria-label="ariaLabel" + variant="blank" + class="commit-edit-toggle mr-2" + @click.stop="toggle()" + > + <icon :name="collapseIcon" :size="16" /> + </gl-button> + <span v-if="expanded">{{ __('Collapse') }}</span> + <span v-else> + <span v-html="message"></span> + <gl-button variant="link" class="modify-message-button"> + {{ modifyLinkMessage }} + </gl-button> + </span> + </div> + <div v-show="expanded"><slot></slot></div> + </div> +</template> 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 b8f29649eb5..ce4207864ea 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 @@ -2,17 +2,24 @@ import successSvg from 'icons/_icon_status_success.svg'; import warningSvg from 'icons/_icon_status_warning.svg'; import simplePoll from '~/lib/utils/simple_poll'; +import { __ } from '~/locale'; import MergeRequest from '../../../merge_request'; import Flash from '../../../flash'; import statusIcon from '../mr_widget_status_icon.vue'; import eventHub from '../../event_hub'; import SquashBeforeMerge from './squash_before_merge.vue'; +import CommitsHeader from './commits_header.vue'; +import CommitEdit from './commit_edit.vue'; +import CommitMessageDropdown from './commit_message_dropdown.vue'; export default { name: 'ReadyToMerge', components: { statusIcon, SquashBeforeMerge, + CommitsHeader, + CommitEdit, + CommitMessageDropdown, }, props: { mr: { type: Object, required: true }, @@ -22,27 +29,20 @@ export default { return { removeSourceBranch: this.mr.shouldRemoveSourceBranch, mergeWhenBuildSucceeds: false, - useCommitMessageWithDescription: false, setToMergeWhenPipelineSucceeds: false, - showCommitMessageEditor: false, isMakingRequest: false, isMergingImmediately: false, commitMessage: this.mr.commitMessage, squashBeforeMerge: this.mr.squash, successSvg, warningSvg, + squashCommitMessage: this.mr.squashCommitMessage, }; }, computed: { shouldShowMergeWhenPipelineSucceedsText() { return this.mr.isPipelineActive; }, - commitMessageLinkTitle() { - const withDesc = 'Include description in commit message'; - const withoutDesc = "Don't include description in commit message"; - - return this.useCommitMessageWithDescription ? withoutDesc : withDesc; - }, status() { const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr; @@ -84,9 +84,9 @@ export default { }, mergeButtonText() { if (this.isMergingImmediately) { - return 'Merge in progress'; + return __('Merge in progress'); } else if (this.shouldShowMergeWhenPipelineSucceedsText) { - return 'Merge when pipeline succeeds'; + return __('Merge when pipeline succeeds'); } return 'Merge'; @@ -98,7 +98,7 @@ export default { const { commitMessage } = this; return Boolean( !commitMessage.length || - !this.shouldShowMergeControls() || + !this.shouldShowMergeControls || this.isMakingRequest || this.mr.preventMerge, ); @@ -110,18 +110,14 @@ export default { const { commitsCount, enableSquashBeforeMerge } = this.mr; return enableSquashBeforeMerge && commitsCount > 1; }, - }, - methods: { shouldShowMergeControls() { return this.mr.isMergeAllowed || this.shouldShowMergeWhenPipelineSucceedsText; }, - updateCommitMessage() { - const cmwd = this.mr.commitMessageWithDescription; - this.useCommitMessageWithDescription = !this.useCommitMessageWithDescription; - this.commitMessage = this.useCommitMessageWithDescription ? cmwd : this.mr.commitMessage; - }, - toggleCommitMessageEditor() { - this.showCommitMessageEditor = !this.showCommitMessageEditor; + }, + methods: { + updateMergeCommitMessage(includeDescription) { + const { commitMessageWithDescription, commitMessage } = this.mr; + this.commitMessage = includeDescription ? commitMessageWithDescription : commitMessage; }, handleMergeButtonClick(mergeWhenBuildSucceeds, mergeImmediately) { // TODO: Remove no-param-reassign @@ -139,6 +135,7 @@ export default { merge_when_pipeline_succeeds: this.setToMergeWhenPipelineSucceeds, should_remove_source_branch: this.removeSourceBranch === true, squash: this.squashBeforeMerge, + squash_commit_message: this.squashCommitMessage, }; this.isMakingRequest = true; @@ -158,7 +155,7 @@ export default { }) .catch(() => { this.isMakingRequest = false; - new Flash('Something went wrong. Please try again.'); // eslint-disable-line + new Flash(__('Something went wrong. Please try again.')); // eslint-disable-line }); }, initiateMergePolling() { @@ -194,7 +191,7 @@ export default { } }) .catch(() => { - new Flash('Something went wrong while merging this merge request. Please try again.'); // eslint-disable-line + new Flash(__('Something went wrong while merging this merge request. Please try again.')); // eslint-disable-line }); }, initiateRemoveSourceBranchPolling() { @@ -223,7 +220,7 @@ export default { } }) .catch(() => { - new Flash('Something went wrong while deleting the source branch. Please try again.'); // eslint-disable-line + new Flash(__('Something went wrong while deleting the source branch. Please try again.')); // eslint-disable-line }); }, }, @@ -231,127 +228,136 @@ export default { </script> <template> - <div class="mr-widget-body media"> - <status-icon :status="iconClass" /> - <div class="media-body"> - <div class="mr-widget-body-controls media space-children"> - <span class="btn-group"> - <button - :disabled="isMergeButtonDisabled" - :class="mergeButtonClass" - type="button" - class="qa-merge-button" - @click="handleMergeButtonClick()" - > - <i v-if="isMakingRequest" class="fa fa-spinner fa-spin" aria-hidden="true"></i> - {{ mergeButtonText }} - </button> - <button - v-if="shouldShowMergeOptionsDropdown" - :disabled="isMergeButtonDisabled" - type="button" - class="btn btn-sm btn-info dropdown-toggle js-merge-moment" - data-toggle="dropdown" - aria-label="Select merge moment" - > - <i class="fa fa-chevron-down qa-merge-moment-dropdown" aria-hidden="true"></i> - </button> - <ul - v-if="shouldShowMergeOptionsDropdown" - class="dropdown-menu dropdown-menu-right" - role="menu" - > - <li> - <a - class="merge_when_pipeline_succeeds qa-merge-when-pipeline-succeeds-option" - href="#" - @click.prevent="handleMergeButtonClick(true)" - > - <span class="media"> - <span class="merge-opt-icon" aria-hidden="true" v-html="successSvg"></span> - <span class="media-body merge-opt-title">Merge when pipeline succeeds</span> - </span> - </a> - </li> - <li> - <a - class="accept-merge-request qa-merge-immediately-option" - href="#" - @click.prevent="handleMergeButtonClick(false, true)" - > - <span class="media"> - <span class="merge-opt-icon" aria-hidden="true" v-html="warningSvg"></span> - <span class="media-body merge-opt-title">Merge immediately</span> - </span> - </a> - </li> - </ul> - </span> - <div class="media-body-wrap space-children"> - <template v-if="shouldShowMergeControls()"> - <label v-if="mr.canRemoveSourceBranch"> - <input - id="remove-source-branch-input" - v-model="removeSourceBranch" - :disabled="isRemoveSourceBranchButtonDisabled" - class="js-remove-source-branch-checkbox" - type="checkbox" - /> - Delete source branch - </label> - - <!-- Placeholder for EE extension of this component --> - <squash-before-merge - v-if="shouldShowSquashBeforeMerge" - v-model="squashBeforeMerge" - :help-path="mr.squashBeforeMergeHelpPath" - :is-disabled="isMergeButtonDisabled" - /> - - <span v-if="mr.ffOnlyEnabled" class="js-fast-forward-message"> - Fast-forward merge without a merge commit - </span> + <div> + <div class="mr-widget-body media"> + <status-icon :status="iconClass" /> + <div class="media-body"> + <div class="mr-widget-body-controls media space-children"> + <span class="btn-group"> <button - v-else :disabled="isMergeButtonDisabled" - class="js-modify-commit-message-button btn btn-default btn-sm" + :class="mergeButtonClass" type="button" - @click="toggleCommitMessageEditor" + class="qa-merge-button" + @click="handleMergeButtonClick()" > - Modify commit message + <i v-if="isMakingRequest" class="fa fa-spinner fa-spin" aria-hidden="true"></i> + {{ mergeButtonText }} </button> - </template> - <template v-else> - <span class="bold js-resolve-mr-widget-items-message"> - You can only merge once the items above are resolved - </span> - </template> - </div> - </div> - <div v-if="showCommitMessageEditor" class="prepend-top-default commit-message-editor"> - <div class="form-group clearfix"> - <label class="col-form-label" for="commit-message"> Commit message </label> - <div class="col-sm-10"> - <div class="commit-message-container"> - <div class="max-width-marker"></div> - <textarea - id="commit-message" - v-model="commitMessage" - class="form-control js-commit-message" - required="required" - rows="14" - name="Commit message" - ></textarea> - </div> - <p class="hint"> - Try to keep the first line under 52 characters and the others under 72 - </p> - <div class="hint"> - <a href="#" @click.prevent="updateCommitMessage"> {{ commitMessageLinkTitle }} </a> - </div> + <button + v-if="shouldShowMergeOptionsDropdown" + :disabled="isMergeButtonDisabled" + type="button" + class="btn btn-sm btn-info dropdown-toggle js-merge-moment" + data-toggle="dropdown" + aria-label="Select merge moment" + > + <i class="fa fa-chevron-down qa-merge-moment-dropdown" aria-hidden="true"></i> + </button> + <ul + v-if="shouldShowMergeOptionsDropdown" + class="dropdown-menu dropdown-menu-right" + role="menu" + > + <li> + <a + class="merge_when_pipeline_succeeds qa-merge-when-pipeline-succeeds-option" + href="#" + @click.prevent="handleMergeButtonClick(true)" + > + <span class="media"> + <span class="merge-opt-icon" aria-hidden="true" v-html="successSvg"></span> + <span class="media-body merge-opt-title">{{ + __('Merge when pipeline succeeds') + }}</span> + </span> + </a> + </li> + <li> + <a + class="accept-merge-request qa-merge-immediately-option" + href="#" + @click.prevent="handleMergeButtonClick(false, true)" + > + <span class="media"> + <span class="merge-opt-icon" aria-hidden="true" v-html="warningSvg"></span> + <span class="media-body merge-opt-title">{{ __('Merge immediately') }}</span> + </span> + </a> + </li> + </ul> + </span> + <div class="media-body-wrap space-children"> + <template v-if="shouldShowMergeControls"> + <label v-if="mr.canRemoveSourceBranch"> + <input + id="remove-source-branch-input" + v-model="removeSourceBranch" + :disabled="isRemoveSourceBranchButtonDisabled" + class="js-remove-source-branch-checkbox" + type="checkbox" + /> + {{ __('Delete source branch') }} + </label> + + <!-- Placeholder for EE extension of this component --> + <squash-before-merge + v-if="shouldShowSquashBeforeMerge" + v-model="squashBeforeMerge" + :help-path="mr.squashBeforeMergeHelpPath" + :is-disabled="isMergeButtonDisabled" + /> + </template> + <template v-else> + <span class="bold js-resolve-mr-widget-items-message"> + {{ __('You can only merge once the items above are resolved') }} + </span> + </template> </div> </div> </div> </div> + <template v-if="shouldShowMergeControls"> + <div v-if="mr.ffOnlyEnabled" class="mr-fast-forward-message"> + {{ __('Fast-forward merge without a merge commit') }} + </div> + <template v-else> + <commits-header + :is-squash-enabled="squashBeforeMerge" + :commits-count="mr.commitsCount" + :target-branch="mr.targetBranch" + > + <ul class="border-top content-list commits-list flex-list"> + <commit-edit + v-if="squashBeforeMerge" + v-model="squashCommitMessage" + :label="__('Squash commit message')" + input-id="squash-message-edit" + squash + > + <commit-message-dropdown + slot="header" + v-model="squashCommitMessage" + :commits="mr.commits" + /> + </commit-edit> + <commit-edit + v-model="commitMessage" + :label="__('Merge commit message')" + input-id="merge-message-edit" + > + <label slot="checkbox"> + <input + id="include-description" + type="checkbox" + @change="updateMergeCommitMessage($event.target.checked)" + /> + {{ __('Include merge request description') }} + </label> + </commit-edit> + </ul> + </commits-header> + </template> + </template> </div> </template> 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 57c4dfbe3b7..abbbe19c5ef 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 @@ -315,7 +315,7 @@ export default { :endpoint="mr.testResultsPath" /> - <div class="mr-widget-section"> + <div class="mr-widget-section p-0"> <component :is="componentName" :mr="mr" :service="service" /> <section v-if="shouldRenderCollaborationStatus" class="mr-info-list mr-links"> 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 36cac230d9d..58363f632a9 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 @@ -42,6 +42,8 @@ export default class MergeRequestStore { this.mergePipeline = data.merge_pipeline || {}; this.deployments = this.deployments || data.deployments || []; this.postMergeDeployments = this.postMergeDeployments || []; + this.commits = data.commits_without_merge_commits || []; + this.squashCommitMessage = data.default_squash_commit_message; this.initRebase(data); if (data.issues_links) { diff --git a/app/assets/javascripts/vue_shared/components/pikaday.vue b/app/assets/javascripts/vue_shared/components/pikaday.vue index 8bdb5bf22c2..fa502b9beb9 100644 --- a/app/assets/javascripts/vue_shared/components/pikaday.vue +++ b/app/assets/javascripts/vue_shared/components/pikaday.vue @@ -1,6 +1,7 @@ <script> import Pikaday from 'pikaday'; import { parsePikadayDate, pikadayToString } from '~/lib/utils/datetime_utility'; +import { __ } from '~/locale'; export default { name: 'DatePicker', @@ -8,7 +9,7 @@ export default { label: { type: String, required: false, - default: 'Date picker', + default: __('Date picker'), }, selectedDate: { type: Date, @@ -40,6 +41,7 @@ export default { toString: date => pikadayToString(date), onSelect: this.selected.bind(this), onClose: this.toggled.bind(this), + firstDay: gon.first_day_of_week, }); this.$el.append(this.calendar.el); diff --git a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue index 6c0c7f15943..45f01a6fced 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue @@ -4,6 +4,7 @@ import datePicker from '../pikaday.vue'; import toggleSidebar from './toggle_sidebar.vue'; import collapsedCalendarIcon from './collapsed_calendar_icon.vue'; import { dateInWords } from '../../../lib/utils/datetime_utility'; +import { __ } from '~/locale'; export default { name: 'SidebarDatePicker', @@ -42,7 +43,7 @@ export default { label: { type: String, required: false, - default: 'Date picker', + default: __('Date picker'), }, selectedDate: { type: Date, diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index cb449b642e7..c5c3b66438c 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -391,6 +391,11 @@ img.emoji { .flex-no-shrink { flex-shrink: 0; } .ws-initial { white-space: initial; } .overflow-auto { overflow: auto; } +.d-flex-center { + display: flex; + align-items: center; + justify-content: center; +} /** COMMON SIZING CLASSES **/ .w-0 { width: 0; } @@ -402,6 +407,10 @@ img.emoji { .min-height-0 { min-height: 0; } +.w-3 { width: #{3 * $grid-size}; } + +.h-3 { width: #{3 * $grid-size}; } + /** COMMON SPACING CLASSES **/ .gl-pl-0 { padding-left: 0; } .gl-pl-1 { padding-left: #{0.5 * $grid-size}; } diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 4d8f1ee51a6..6108eaa1ad0 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -52,6 +52,7 @@ position: absolute; top: 5px; right: 15px; + margin-left: auto; .btn { padding: 0 10px; @@ -325,6 +326,7 @@ span.idiff { &, .file-holder & { display: flex; + flex-wrap: wrap; align-items: center; justify-content: space-between; background-color: $gray-light; @@ -367,16 +369,12 @@ span.idiff { margin: 0 10px 0 0; } - .file-actions { - white-space: nowrap; - - .btn { - padding: 0 10px; - font-size: 13px; - line-height: 28px; - display: inline-block; - float: none; - } + .file-actions .btn { + padding: 0 10px; + font-size: 13px; + line-height: 28px; + display: inline-block; + float: none; } @include media-breakpoint-down(xs) { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 7088a6f59dc..96dab609a13 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -243,6 +243,7 @@ $gl-padding-8: 8px; $gl-padding: 16px; $gl-padding-24: 24px; $gl-padding-32: 32px; +$gl-padding-50: 50px; $gl-col-padding: 15px; $gl-input-padding: 10px; $gl-vert-padding: 6px; diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss index ad12cd101b6..809ba6d4953 100644 --- a/app/assets/stylesheets/pages/clusters.scss +++ b/app/assets/stylesheets/pages/clusters.scss @@ -58,6 +58,20 @@ } } +.cluster-application-banner { + height: 45px; + display: flex; + align-items: center; + justify-content: space-between; +} + +.cluster-application-banner-close { + align-self: flex-start; + font-weight: 500; + font-size: 20px; + margin: $gl-padding-8 14px 0 0; +} + .cluster-application-description { flex: 1; } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index a225645c643..e3b98b26a11 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -508,6 +508,25 @@ } } +.diff-stats { + align-items: center; + padding: 0 .25rem; + + .diff-stats-group { + padding: 0 .25rem; + } + + svg.diff-stats-icon { + vertical-align: text-bottom; + } + + &.is-compare-versions-header { + .diff-stats-group { + padding: 0 .5rem; + } + } +} + .file-content .diff-file { margin: 0; border: 0; diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 38a7e199c6a..135730d71e9 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -38,9 +38,7 @@ } .mr-widget-section { - .media { - align-items: center; - } + border-radius: $border-radius-default $border-radius-default 0 0; .code-text { flex: 1; @@ -56,6 +54,11 @@ .mr-widget-extension { border-top: 1px solid $border-color; background-color: $gray-light; + + &.clickable:hover { + background-color: $gl-gray-200; + cursor: pointer; + } } .mr-widget-workflow { @@ -78,6 +81,7 @@ border-top: 0; } +.mr-widget-body, .mr-widget-section, .mr-widget-content, .mr-widget-footer { @@ -87,11 +91,38 @@ .mr-state-widget { color: $gl-text-color; + .commit-message-edit { + border-radius: $border-radius-default; + } + .mr-widget-section, .mr-widget-footer { border-top: solid 1px $border-color; } + .mr-fast-forward-message { + padding-left: $gl-padding-50; + padding-bottom: $gl-padding; + } + + .commits-list { + > li { + padding: $gl-padding; + + @include media-breakpoint-up(md) { + padding-left: $gl-padding-50; + } + } + } + + .mr-commit-dropdown { + .dropdown-menu { + @include media-breakpoint-up(md) { + width: 150%; + } + } + } + .mr-widget-footer { padding: 0; } @@ -405,7 +436,7 @@ } .mr-widget-help { - padding: 10px 16px 10px 48px; + padding: 10px 16px 10px $gl-padding-50; font-style: italic; } @@ -423,10 +454,6 @@ } } -.mr-widget-body-controls { - flex-wrap: wrap; -} - .mr_source_commit, .mr_target_commit { margin-bottom: 0; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 2342c284a5e..66866aedfba 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -507,12 +507,6 @@ } .template-option { - .logo { - .btn-template-icon { - width: 40px !important; - } - } - padding: 16px 0; &:not(:first-child) { @@ -551,9 +545,8 @@ } .selected-icon { - svg { + img { display: none; - top: 7px; height: 20px; width: 20px; } @@ -946,6 +939,11 @@ pre.light-well { .flex-wrapper { min-width: 0; margin-top: -$gl-padding-8; // negative margin required for flex-wrap + flex: 1 1 100%; + + .project-title { + line-height: 20px; + } } p, @@ -984,14 +982,16 @@ pre.light-well { } .controls { - margin-top: $gl-padding-8; + @include media-breakpoint-down(xs) { + margin-top: $gl-padding-8; + } - @include media-breakpoint-down(md) { + @include media-breakpoint-up(sm) { margin-top: 0; } - @include media-breakpoint-down(xs) { - margin-top: $gl-padding-8; + @include media-breakpoint-up(lg) { + flex: 1 1 40%; } .icon-wrapper { @@ -1041,7 +1041,7 @@ pre.light-well { min-height: 40px; min-width: 40px; - .identicon.s64 { + .identicon.s48 { font-size: 16px; } } diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb index b9717b97640..3bd91b71d92 100644 --- a/app/controllers/clusters/clusters_controller.rb +++ b/app/controllers/clusters/clusters_controller.rb @@ -127,6 +127,7 @@ class Clusters::ClustersController < Clusters::BaseController params.require(:cluster).permit( :enabled, :environment_scope, + :base_domain, platform_kubernetes_attributes: [ :namespace ] @@ -136,6 +137,7 @@ class Clusters::ClustersController < Clusters::BaseController :enabled, :name, :environment_scope, + :base_domain, platform_kubernetes_attributes: [ :api_url, :token, diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 97120273d6b..cc2bb99f55b 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -116,8 +116,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController session[:service_tickets][provider] = ticket end + def build_auth_user(auth_user_class) + auth_user_class.new(oauth) + end + def sign_in_user_flow(auth_user_class) - auth_user = auth_user_class.new(oauth) + auth_user = build_auth_user(auth_user_class) user = auth_user.find_and_update! if auth_user.valid_sign_in? diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb index a27e3cceaeb..94002095739 100644 --- a/app/controllers/profiles/preferences_controller.rb +++ b/app/controllers/profiles/preferences_controller.rb @@ -37,6 +37,6 @@ class Profiles::PreferencesController < Profiles::ApplicationController end def preferences_param_names - [:color_scheme_id, :layout, :dashboard, :project_view, :theme_id] + [:color_scheme_id, :layout, :dashboard, :project_view, :theme_id, :first_day_of_week] end end diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 79685e8b675..e9cd475a199 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -11,11 +11,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController before_action :verify_api_request!, only: :terminal_websocket_authorize before_action :expire_etag_cache, only: [:index] - before_action do - push_frontend_feature_flag(:area_chart, project) - end - - # Returns all environments or all folders based on the :nested param def index @environments = project.environments .with_state(params[:scope] || :available) diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb index 54ff7ded8e5..6045ee4e171 100644 --- a/app/controllers/projects/merge_requests/application_controller.rb +++ b/app/controllers/projects/merge_requests/application_controller.rb @@ -34,7 +34,8 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont :task_num, :title, :discussion_locked, - label_ids: [] + label_ids: [], + update_task: [:index, :checked, :line_number, :line_source] ] end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 5cf7fa3422d..46a44841c31 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -16,6 +16,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo before_action :authenticate_user!, only: [:assign_related_issues] before_action :check_user_can_push_to_source_branch!, only: [:rebase] + before_action only: [:show] do + push_frontend_feature_flag(:diff_tree_filtering, default_enabled: true) + end + def index @merge_requests = @issuables diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 3e08c0ccd8f..23af2e0521c 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -305,7 +305,7 @@ class IssuableFinder def use_subquery_for_search? strong_memoize(:use_subquery_for_search) do attempt_group_search_optimizations? && - Feature.enabled?(:use_subquery_for_group_issues_search, default_enabled: false) + Feature.enabled?(:use_subquery_for_group_issues_search, default_enabled: true) end end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index c8e4e2e3df9..e635f608237 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -150,6 +150,7 @@ module ApplicationSettingsHelper :email_author_in_body, :enabled_git_access_protocol, :enforce_terms, + :first_day_of_week, :gitaly_timeout_default, :gitaly_timeout_medium, :gitaly_timeout_fast, @@ -231,7 +232,8 @@ module ApplicationSettingsHelper :web_ide_clientside_preview_enabled, :diff_max_patch_bytes, :commit_email_hostname, - :protected_ci_variables + :protected_ci_variables, + :local_markdown_version ] end diff --git a/app/helpers/auto_devops_helper.rb b/app/helpers/auto_devops_helper.rb index 516c8a353ea..67e7e475920 100644 --- a/app/helpers/auto_devops_helper.rb +++ b/app/helpers/auto_devops_helper.rb @@ -9,41 +9,4 @@ module AutoDevopsHelper !project.repository.gitlab_ci_yml && !project.ci_service end - - def auto_devops_warning_message(project) - if missing_auto_devops_service?(project) - params = { - kubernetes: link_to('Kubernetes cluster', project_clusters_path(project)) - } - - if missing_auto_devops_domain?(project) - _('Auto Review Apps and Auto Deploy need a domain name and a %{kubernetes} to work correctly.') % params - else - _('Auto Review Apps and Auto Deploy need a %{kubernetes} to work correctly.') % params - end - elsif missing_auto_devops_domain?(project) - _('Auto Review Apps and Auto Deploy need a domain name to work correctly.') - end - end - - # rubocop: disable CodeReuse/ActiveRecord - def cluster_ingress_ip(project) - project - .cluster_ingresses - .where("external_ip is not null") - .limit(1) - .pluck(:external_ip) - .first - end - # rubocop: enable CodeReuse/ActiveRecord - - private - - def missing_auto_devops_domain?(project) - !(project.auto_devops || project.build_auto_devops)&.has_domain? - end - - def missing_auto_devops_service?(project) - !project.deployment_platform&.active? - end end diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb index b3935ae350d..365b94f5a3e 100644 --- a/app/helpers/environments_helper.rb +++ b/app/helpers/environments_helper.rb @@ -11,7 +11,6 @@ module EnvironmentsHelper { "endpoint" => folder_project_environments_path(@project, @folder, format: :json), "folder-name" => @folder, - "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s, "can-read-environment" => can?(current_user, :read_environment, @project).to_s } end diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index f4f46b0fe96..bc1742e8167 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -43,6 +43,17 @@ module PreferencesHelper ] end + def first_day_of_week_choices + [ + [_('Sunday'), 0], + [_('Monday'), 1] + ] + end + + def first_day_of_week_choices_with_default + first_day_of_week_choices.unshift([_('System default (%{default})') % { default: default_first_day_of_week }, nil]) + end + def user_application_theme @user_application_theme ||= Gitlab::Themes.for_user(current_user).css_class end @@ -66,4 +77,8 @@ module PreferencesHelper def excluded_dashboard_choices ['operations'] end + + def default_first_day_of_week + first_day_of_week_choices.rassoc(Gitlab::CurrentSettings.first_day_of_week).first + end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 88746375c67..daadf9427ba 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -193,6 +193,10 @@ class ApplicationSetting < ActiveRecord::Base allow_nil: true, numericality: { only_integer: true, greater_than_or_equal_to: 1.day.seconds } + validates :local_markdown_version, + allow_nil: true, + numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than: 65536 } + SUPPORTED_KEY_TYPES.each do |type| validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type } end @@ -246,6 +250,7 @@ class ApplicationSetting < ActiveRecord::Base dsa_key_restriction: 0, ecdsa_key_restriction: 0, ed25519_key_restriction: 0, + first_day_of_week: 0, gitaly_timeout_default: 55, gitaly_timeout_fast: 10, gitaly_timeout_medium: 30, @@ -303,7 +308,8 @@ class ApplicationSetting < ActiveRecord::Base usage_stats_set_by_user_id: nil, diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES, commit_email_hostname: default_commit_email_hostname, - protected_ci_variables: false + protected_ci_variables: false, + local_markdown_version: 0 } end diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb index 26bf73f4dd8..52c440ffb2f 100644 --- a/app/models/clusters/applications/prometheus.rb +++ b/app/models/clusters/applications/prometheus.rb @@ -53,11 +53,11 @@ module Clusters end def upgrade_command(values) - ::Gitlab::Kubernetes::Helm::UpgradeCommand.new( - name, + ::Gitlab::Kubernetes::Helm::InstallCommand.new( + name: name, version: VERSION, - chart: chart, rbac: cluster.platform_kubernetes_rbac?, + chart: chart, files: files_with_replaced_values(values) ) end diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index a2c48973fa5..7025fc2cc02 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -18,6 +18,7 @@ module Clusters Applications::Knative.application_name => Applications::Knative }.freeze DEFAULT_ENVIRONMENT = '*'.freeze + KUBE_INGRESS_BASE_DOMAIN = 'KUBE_INGRESS_BASE_DOMAIN'.freeze belongs_to :user @@ -49,7 +50,7 @@ module Clusters validates :name, cluster_name: true validates :cluster_type, presence: true - validates :domain, allow_nil: true, hostname: { allow_numeric_hostname: true, require_valid_tld: true } + validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true, require_valid_tld: true } validate :restrict_modification, on: :update validate :no_groups, unless: :group_type? @@ -65,6 +66,9 @@ module Clusters delegate :available?, to: :application_ingress, prefix: true, allow_nil: true delegate :available?, to: :application_prometheus, prefix: true, allow_nil: true delegate :available?, to: :application_knative, prefix: true, allow_nil: true + delegate :external_ip, to: :application_ingress, prefix: true, allow_nil: true + + alias_attribute :base_domain, :domain enum cluster_type: { instance_type: 1, @@ -193,8 +197,42 @@ module Clusters project_type? end + def kube_ingress_domain + @kube_ingress_domain ||= domain.presence || instance_domain || legacy_auto_devops_domain + end + + def predefined_variables + Gitlab::Ci::Variables::Collection.new.tap do |variables| + break variables unless kube_ingress_domain + + variables.append(key: KUBE_INGRESS_BASE_DOMAIN, value: kube_ingress_domain) + end + end + private + def instance_domain + @instance_domain ||= Gitlab::CurrentSettings.auto_devops_domain + end + + # To keep backward compatibility with AUTO_DEVOPS_DOMAIN + # environment variable, we need to ensure KUBE_INGRESS_BASE_DOMAIN + # is set if AUTO_DEVOPS_DOMAIN is set on any of the following options: + # ProjectAutoDevops#Domain, project variables or group variables, + # as the AUTO_DEVOPS_DOMAIN is needed for CI_ENVIRONMENT_URL + # + # This method should is scheduled to be removed on + # https://gitlab.com/gitlab-org/gitlab-ce/issues/56959 + def legacy_auto_devops_domain + if project_type? + project&.auto_devops&.domain.presence || + project.variables.find_by(key: 'AUTO_DEVOPS_DOMAIN')&.value.presence || + project.group&.variables&.find_by(key: 'AUTO_DEVOPS_DOMAIN')&.value.presence + elsif group_type? + group.variables.find_by(key: 'AUTO_DEVOPS_DOMAIN')&.value.presence + end + end + def restrict_modification if provider&.on_creation? errors.add(:base, "cannot modify during creation") diff --git a/app/models/clusters/concerns/application_status.rb b/app/models/clusters/concerns/application_status.rb index a556dd5ad8b..5c0164831bc 100644 --- a/app/models/clusters/concerns/application_status.rb +++ b/app/models/clusters/concerns/application_status.rb @@ -20,7 +20,7 @@ module Clusters state :update_errored, value: 6 event :make_scheduled do - transition [:installable, :errored] => :scheduled + transition [:installable, :errored, :installed, :updated, :update_errored] => :scheduled end event :make_installing do @@ -29,18 +29,16 @@ module Clusters event :make_installed do transition [:installing] => :installed + transition [:updating] => :updated end event :make_errored do - transition any => :errored + transition any - [:updating] => :errored + transition [:updating] => :update_errored end event :make_updating do - transition [:installed, :updated, :update_errored] => :updating - end - - event :make_updated do - transition [:updating] => :updated + transition [:installed, :updated, :update_errored, :scheduled] => :updating end event :make_update_errored do @@ -74,6 +72,10 @@ module Clusters end end + def updateable? + installed? || updated? || update_errored? + end + def available? installed? || updated? end diff --git a/app/models/clusters/concerns/application_version.rb b/app/models/clusters/concerns/application_version.rb index e355de23df6..db94e8e08c9 100644 --- a/app/models/clusters/concerns/application_version.rb +++ b/app/models/clusters/concerns/application_version.rb @@ -12,6 +12,10 @@ module Clusters end end end + + def update_available? + version != self.class.const_get(:VERSION) + end end end end diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 8f3424db295..46d0898014e 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -43,6 +43,7 @@ module Clusters # We expect to be `active?` only when enabled and cluster is created (the api_url is assigned) validates :api_url, url: true, presence: true validates :token, presence: true + validates :ca_cert, certificate: true, allow_blank: true, if: :ca_cert_changed? validate :prevent_modification, on: :update @@ -98,6 +99,8 @@ module Clusters .append(key: 'KUBE_NAMESPACE', value: actual_namespace) .append(key: 'KUBECONFIG', value: kubeconfig, public: false, file: true) end + + variables.concat(cluster.predefined_variables) end end diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb index 5fa6f79bdaa..1a8570b80c3 100644 --- a/app/models/concerns/cache_markdown_field.rb +++ b/app/models/concerns/cache_markdown_field.rb @@ -115,7 +115,28 @@ module CacheMarkdownField end def latest_cached_markdown_version - CacheMarkdownField::CACHE_COMMONMARK_VERSION + @latest_cached_markdown_version ||= (CacheMarkdownField::CACHE_COMMONMARK_VERSION << 16) | local_version + end + + def local_version + # because local_markdown_version is stored in application_settings which + # uses cached_markdown_version too, we check explicitly to avoid + # endless loop + return local_markdown_version if has_attribute?(:local_markdown_version) + + settings = Gitlab::CurrentSettings.current_application_settings + + # Following migrations are not properly isolated and + # use real models (by calling .ghost method), in these migrations + # local_markdown_version attribute doesn't exist yet, so we + # use a default value: + # db/migrate/20170825104051_migrate_issues_to_ghost_user.rb + # db/migrate/20171114150259_merge_requests_author_id_foreign_key.rb + if settings.respond_to?(:local_markdown_version) + settings.local_markdown_version + else + 0 + end end included do diff --git a/app/models/project.rb b/app/models/project.rb index 8f746f6e094..c72d3a3b725 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -147,7 +147,6 @@ class Project < ActiveRecord::Base has_one :pipelines_email_service has_one :irker_service has_one :pivotaltracker_service - has_one :hipchat_service has_one :flowdock_service has_one :assembla_service has_one :asana_service @@ -2074,6 +2073,10 @@ class Project < ActiveRecord::Base pool_repository&.link_repository(repository) end + def has_pool_repository? + pool_repository.present? + end + private def merge_requests_allowing_collaboration(source_branch = nil) diff --git a/app/models/project_auto_devops.rb b/app/models/project_auto_devops.rb index 2253ad7b543..e353a6443c4 100644 --- a/app/models/project_auto_devops.rb +++ b/app/models/project_auto_devops.rb @@ -24,6 +24,12 @@ class ProjectAutoDevops < ActiveRecord::Base domain.present? || instance_domain.present? end + # From 11.8, AUTO_DEVOPS_DOMAIN has been replaced by KUBE_INGRESS_BASE_DOMAIN. + # See Clusters::Cluster#predefined_variables and https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24580 + # for more info. + # + # Suppport AUTO_DEVOPS_DOMAIN is scheduled to be removed on + # https://gitlab.com/gitlab-org/gitlab-ce/issues/52363 def predefined_variables Gitlab::Ci::Variables::Collection.new.tap do |variables| if has_domain? diff --git a/app/models/project_services/deployment_service.rb b/app/models/project_services/deployment_service.rb index 6dae4f3a4a6..80aa2101509 100644 --- a/app/models/project_services/deployment_service.rb +++ b/app/models/project_services/deployment_service.rb @@ -11,7 +11,7 @@ class DeploymentService < Service %w() end - def predefined_variables + def predefined_variables(project:) [] end diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb deleted file mode 100644 index a69b7b4c4b6..00000000000 --- a/app/models/project_services/hipchat_service.rb +++ /dev/null @@ -1,311 +0,0 @@ -# frozen_string_literal: true - -class HipchatService < Service - include ActionView::Helpers::SanitizeHelper - - MAX_COMMITS = 3 - HIPCHAT_ALLOWED_TAGS = %w[ - a b i strong em br img pre code - table th tr td caption colgroup col thead tbody tfoot - ul ol li dl dt dd - ].freeze - - prop_accessor :token, :room, :server, :color, :api_version - boolean_accessor :notify_only_broken_pipelines, :notify - validates :token, presence: true, if: :activated? - - def initialize_properties - if properties.nil? - self.properties = {} - self.notify_only_broken_pipelines = true - end - end - - def title - 'HipChat' - end - - def description - 'Private group chat and IM' - end - - def self.to_param - 'hipchat' - end - - def fields - [ - { type: 'text', name: 'token', placeholder: 'Room token', required: true }, - { type: 'text', name: 'room', placeholder: 'Room name or ID' }, - { type: 'checkbox', name: 'notify' }, - { type: 'select', name: 'color', choices: %w(yellow red green purple gray random) }, - { type: 'text', name: 'api_version', - placeholder: 'Leave blank for default (v2)' }, - { type: 'text', name: 'server', - placeholder: 'Leave blank for default. https://hipchat.example.com' }, - { type: 'checkbox', name: 'notify_only_broken_pipelines' } - ] - end - - def self.supported_events - %w(push issue confidential_issue merge_request note confidential_note tag_push pipeline) - end - - def execute(data) - return unless supported_events.include?(data[:object_kind]) - - message = create_message(data) - return unless message.present? - - gate[room].send('GitLab', message, message_options(data)) # rubocop:disable GitlabSecurity/PublicSend - end - - def test(data) - begin - result = execute(data) - rescue StandardError => error - return { success: false, result: error } - end - - { success: true, result: result } - end - - private - - def gate - options = { api_version: api_version.present? ? api_version : 'v2' } - options[:server_url] = server unless server.blank? - @gate ||= HipChat::Client.new(token, options) - end - - def message_options(data = nil) - { notify: notify.present? && Gitlab::Utils.to_boolean(notify), color: message_color(data) } - end - - def create_message(data) - object_kind = data[:object_kind] - - case object_kind - when "push", "tag_push" - create_push_message(data) - when "issue" - create_issue_message(data) unless update?(data) - when "merge_request" - create_merge_request_message(data) unless update?(data) - when "note" - create_note_message(data) - when "pipeline" - create_pipeline_message(data) if should_pipeline_be_notified?(data) - end - end - - def render_line(text) - markdown(text.lines.first.chomp, pipeline: :single_line) if text - end - - def create_push_message(push) - ref_type = Gitlab::Git.tag_ref?(push[:ref]) ? 'tag' : 'branch' - ref = Gitlab::Git.ref_name(push[:ref]) - - before = push[:before] - after = push[:after] - - message = [] - message << "#{push[:user_name]} " - - if Gitlab::Git.blank_ref?(before) - message << "pushed new #{ref_type} <a href=\""\ - "#{project_url}/commits/#{CGI.escape(ref)}\">#{ref}</a>"\ - " to #{project_link}\n" - elsif Gitlab::Git.blank_ref?(after) - message << "removed #{ref_type} <b>#{ref}</b> from <a href=\"#{project.web_url}\">#{project_name}</a> \n" - else - message << "pushed to #{ref_type} <a href=\""\ - "#{project.web_url}/commits/#{CGI.escape(ref)}\">#{ref}</a> " - message << "of <a href=\"#{project.web_url}\">#{project.full_name.gsub!(/\s/, '')}</a> " - message << "(<a href=\"#{project.web_url}/compare/#{before}...#{after}\">Compare changes</a>)" - - push[:commits].take(MAX_COMMITS).each do |commit| - message << "<br /> - #{render_line(commit[:message])} (<a href=\"#{commit[:url]}\">#{commit[:id][0..5]}</a>)" - end - - if push[:commits].count > MAX_COMMITS - message << "<br />... #{push[:commits].count - MAX_COMMITS} more commits" - end - end - - message.join - end - - def markdown(text, options = {}) - return "" unless text - - context = { - project: project, - pipeline: :email - } - - Banzai.render(text, context) - - context.merge!(options) - - html = Banzai.render_and_post_process(text, context) - sanitized_html = sanitize(html, tags: HIPCHAT_ALLOWED_TAGS, attributes: %w[href title alt]) - - sanitized_html.truncate(200, separator: ' ', omission: '...') - end - - def create_issue_message(data) - user_name = data[:user][:name] - - obj_attr = data[:object_attributes] - obj_attr = HashWithIndifferentAccess.new(obj_attr) - title = render_line(obj_attr[:title]) - state = obj_attr[:state] - issue_iid = obj_attr[:iid] - issue_url = obj_attr[:url] - description = obj_attr[:description] - - issue_link = "<a href=\"#{issue_url}\">issue ##{issue_iid}</a>" - - message = ["#{user_name} #{state} #{issue_link} in #{project_link}: <b>#{title}</b>"] - message << "<pre>#{markdown(description)}</pre>" - - message.join - end - - def create_merge_request_message(data) - user_name = data[:user][:name] - - obj_attr = data[:object_attributes] - obj_attr = HashWithIndifferentAccess.new(obj_attr) - merge_request_id = obj_attr[:iid] - state = obj_attr[:state] - description = obj_attr[:description] - title = render_line(obj_attr[:title]) - - merge_request_url = "#{project_url}/merge_requests/#{merge_request_id}" - merge_request_link = "<a href=\"#{merge_request_url}\">merge request !#{merge_request_id}</a>" - message = ["#{user_name} #{state} #{merge_request_link} in " \ - "#{project_link}: <b>#{title}</b>"] - - message << "<pre>#{markdown(description)}</pre>" - message.join - end - - def format_title(title) - "<b>#{render_line(title)}</b>" - end - - def create_note_message(data) - data = HashWithIndifferentAccess.new(data) - user_name = data[:user][:name] - - obj_attr = HashWithIndifferentAccess.new(data[:object_attributes]) - note = obj_attr[:note] - note_url = obj_attr[:url] - noteable_type = obj_attr[:noteable_type] - commit_id = nil - - case noteable_type - when "Commit" - commit_attr = HashWithIndifferentAccess.new(data[:commit]) - commit_id = commit_attr[:id] - subject_desc = commit_id - subject_desc = Commit.truncate_sha(subject_desc) - subject_type = "commit" - title = format_title(commit_attr[:message]) - when "Issue" - subj_attr = HashWithIndifferentAccess.new(data[:issue]) - subject_id = subj_attr[:iid] - subject_desc = "##{subject_id}" - subject_type = "issue" - title = format_title(subj_attr[:title]) - when "MergeRequest" - subj_attr = HashWithIndifferentAccess.new(data[:merge_request]) - subject_id = subj_attr[:iid] - subject_desc = "!#{subject_id}" - subject_type = "merge request" - title = format_title(subj_attr[:title]) - when "Snippet" - subj_attr = HashWithIndifferentAccess.new(data[:snippet]) - subject_id = subj_attr[:id] - subject_desc = "##{subject_id}" - subject_type = "snippet" - title = format_title(subj_attr[:title]) - end - - subject_html = "<a href=\"#{note_url}\">#{subject_type} #{subject_desc}</a>" - message = ["#{user_name} commented on #{subject_html} in #{project_link}: "] - message << title - - message << "<pre>#{markdown(note, ref: commit_id)}</pre>" - message.join - end - - def create_pipeline_message(data) - pipeline_attributes = data[:object_attributes] - pipeline_id = pipeline_attributes[:id] - ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch' - ref = pipeline_attributes[:ref] - user_name = (data[:user] && data[:user][:name]) || 'API' - status = pipeline_attributes[:status] - duration = pipeline_attributes[:duration] - - branch_link = "<a href=\"#{project_url}/commits/#{CGI.escape(ref)}\">#{ref}</a>" - pipeline_url = "<a href=\"#{project_url}/pipelines/#{pipeline_id}\">##{pipeline_id}</a>" - - "#{project_link}: Pipeline #{pipeline_url} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status(status)} in #{duration} second(s)" - end - - def message_color(data) - pipeline_status_color(data) || color || 'yellow' - end - - def pipeline_status_color(data) - return unless data && data[:object_kind] == 'pipeline' - - case data[:object_attributes][:status] - when 'success' - 'green' - else - 'red' - end - end - - def project_name - project.full_name.gsub(/\s/, '') - end - - def project_url - project.web_url - end - - def project_link - "<a href=\"#{project_url}\">#{project_name}</a>" - end - - def update?(data) - data[:object_attributes][:action] == 'update' - end - - def humanized_status(status) - case status - when 'success' - 'passed' - else - status - end - end - - def should_pipeline_be_notified?(data) - case data[:object_attributes][:status] - when 'success' - !notify_only_broken_pipelines? - when 'failed' - true - else - false - end - end -end diff --git a/app/models/service.rb b/app/models/service.rb index 9dcb0aab0a3..3461e0bfe70 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -255,7 +255,6 @@ class Service < ActiveRecord::Base external_wiki flowdock hangouts_chat - hipchat irker jira kubernetes diff --git a/app/models/user.rb b/app/models/user.rb index 9c091ac366c..24101eda0b1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -228,6 +228,9 @@ class User < ApplicationRecord delegate :path, to: :namespace, allow_nil: true, prefix: true delegate :notes_filter_for, to: :user_preference delegate :set_notes_filter, to: :user_preference + delegate :first_day_of_week, :first_day_of_week=, to: :user_preference + + accepts_nested_attributes_for :user_preference, update_only: true state_machine :state, initial: :active do event :block do diff --git a/app/serializers/cluster_application_entity.rb b/app/serializers/cluster_application_entity.rb index 62b23a889c8..02df1480828 100644 --- a/app/serializers/cluster_application_entity.rb +++ b/app/serializers/cluster_application_entity.rb @@ -8,4 +8,5 @@ class ClusterApplicationEntity < Grape::Entity expose :external_ip, if: -> (e, _) { e.respond_to?(:external_ip) } expose :hostname, if: -> (e, _) { e.respond_to?(:hostname) } expose :email, if: -> (e, _) { e.respond_to?(:email) } + expose :update_available?, as: :update_available, if: -> (e, _) { e.respond_to?(:update_available?) } end diff --git a/app/serializers/deployment_entity.rb b/app/serializers/deployment_entity.rb index aa1d9e6292c..34ae06278c8 100644 --- a/app/serializers/deployment_entity.rb +++ b/app/serializers/deployment_entity.rb @@ -24,6 +24,12 @@ class DeploymentEntity < Grape::Entity expose :user, using: UserEntity expose :commit, using: CommitEntity expose :deployable, using: JobEntity - expose :manual_actions, using: JobEntity - expose :scheduled_actions, using: JobEntity + expose :manual_actions, using: JobEntity, if: -> (*) { can_create_deployment? } + expose :scheduled_actions, using: JobEntity, if: -> (*) { can_create_deployment? } + + private + + def can_create_deployment? + can?(request.current_user, :create_deployment, request.project) + end end diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index f764536e762..e95ba09c006 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -160,7 +160,8 @@ module Auth ## # We still support legacy pipeline triggers which do not have associated # actor. New permissions model and new triggers are always associated with - # an actor, so this should be improved in 10.0 version of GitLab. + # an actor. So this should be improved once + # https://gitlab.com/gitlab-org/gitlab-ce/issues/37452 is resolved. # def build_can_push?(requested_project) # Build can push only to the project from which it originates diff --git a/app/services/clusters/applications/check_installation_progress_service.rb b/app/services/clusters/applications/check_installation_progress_service.rb index 21ec26ea233..c592d608b89 100644 --- a/app/services/clusters/applications/check_installation_progress_service.rb +++ b/app/services/clusters/applications/check_installation_progress_service.rb @@ -4,7 +4,7 @@ module Clusters module Applications class CheckInstallationProgressService < BaseHelmService def execute - return unless app.installing? + return unless operation_in_progress? case installation_phase when Gitlab::Kubernetes::Pod::SUCCEEDED @@ -16,11 +16,16 @@ module Clusters end rescue Kubeclient::HttpError => e log_error(e) - app.make_errored!("Kubernetes error: #{e.error_code}") unless app.errored? + + app.make_errored!("Kubernetes error: #{e.error_code}") end private + def operation_in_progress? + app.installing? || app.updating? + end + def on_success app.make_installed! ensure @@ -28,13 +33,13 @@ module Clusters end def on_failed - app.make_errored!("Installation failed. Check pod logs for #{install_command.pod_name} for more details.") + app.make_errored!("Operation failed. Check pod logs for #{pod_name} for more details.") end def check_timeout if timeouted? begin - app.make_errored!("Installation timed out. Check pod logs for #{install_command.pod_name} for more details.") + app.make_errored!("Operation timed out. Check pod logs for #{pod_name} for more details.") end else ClusterWaitForAppInstallationWorker.perform_in( @@ -42,20 +47,24 @@ module Clusters end end + def pod_name + install_command.pod_name + end + def timeouted? Time.now.utc - app.updated_at.to_time.utc > ClusterWaitForAppInstallationWorker::TIMEOUT end def remove_installation_pod - helm_api.delete_pod!(install_command.pod_name) + helm_api.delete_pod!(pod_name) end def installation_phase - helm_api.status(install_command.pod_name) + helm_api.status(pod_name) end def installation_errors - helm_api.log(install_command.pod_name) + helm_api.log(pod_name) end end end diff --git a/app/services/clusters/applications/schedule_installation_service.rb b/app/services/clusters/applications/schedule_installation_service.rb index d75ba70c27e..15c93f1e79b 100644 --- a/app/services/clusters/applications/schedule_installation_service.rb +++ b/app/services/clusters/applications/schedule_installation_service.rb @@ -10,6 +10,18 @@ module Clusters end def execute + application.updateable? ? schedule_upgrade : schedule_install + end + + private + + def schedule_upgrade + application.make_scheduled! + + ClusterUpgradeAppWorker.perform_async(application.name, application.id) + end + + def schedule_install application.make_scheduled! ClusterInstallAppWorker.perform_async(application.name, application.id) diff --git a/app/services/clusters/applications/upgrade_service.rb b/app/services/clusters/applications/upgrade_service.rb new file mode 100644 index 00000000000..a0ece1d2635 --- /dev/null +++ b/app/services/clusters/applications/upgrade_service.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Clusters + module Applications + class UpgradeService < BaseHelmService + def execute + return unless app.scheduled? + + begin + app.make_updating! + + # install_command works with upgrades too + # as it basically does `helm upgrade --install` + helm_api.update(install_command) + + ClusterWaitForAppInstallationWorker.perform_in( + ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id) + rescue Kubeclient::HttpError => e + log_error(e) + app.make_update_errored!("Kubernetes error: #{e.error_code}") + rescue StandardError => e + log_error(e) + app.make_update_errored!("Can't start upgrade process.") + end + end + end + end +end diff --git a/app/services/labels/update_service.rb b/app/services/labels/update_service.rb index e563447c64c..be33947d0eb 100644 --- a/app/services/labels/update_service.rb +++ b/app/services/labels/update_service.rb @@ -8,6 +8,7 @@ module Labels # returns the updated label def execute(label) + params[:name] = params.delete(:new_name) if params.key?(:new_name) params[:color] = convert_color_name_to_hex if params[:color].present? label.update(params) diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 86a04587f79..8112c2a4299 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -21,7 +21,7 @@ module MergeRequests end handle_wip_event(merge_request) - update(merge_request) + update_task_event(merge_request) || update(merge_request) end # rubocop:disable Metrics/AbcSize @@ -83,6 +83,11 @@ module MergeRequests end # rubocop:enable Metrics/AbcSize + def handle_task_changes(merge_request) + todo_service.mark_pending_todos_as_done(merge_request, current_user) + todo_service.update_merge_request(merge_request, current_user) + end + def merge_from_quick_action(merge_request) last_diff_sha = params.delete(:merge) return unless merge_request.mergeable_with_quick_action?(current_user, last_diff_sha: last_diff_sha) diff --git a/app/services/task_list_toggle_service.rb b/app/services/task_list_toggle_service.rb index b5c4cd3235d..af24bc0524c 100644 --- a/app/services/task_list_toggle_service.rb +++ b/app/services/task_list_toggle_service.rb @@ -32,7 +32,8 @@ class TaskListToggleService source_line_index = line_number - 1 markdown_task = source_lines[source_line_index] - return unless markdown_task == line_source + # The source in the DB could be using either \n or \r\n line endings + return unless markdown_task.chomp == line_source return unless source_checkbox = Taskable::ITEM_PATTERN.match(markdown_task) currently_checked = TaskList::Item.new(source_checkbox[1]).complete? diff --git a/app/views/admin/application_settings/_localization.html.haml b/app/views/admin/application_settings/_localization.html.haml new file mode 100644 index 00000000000..95d016a94a5 --- /dev/null +++ b/app/views/admin/application_settings/_localization.html.haml @@ -0,0 +1,11 @@ += form_for @application_setting, url: admin_application_settings_path(anchor: 'js-localization-settings'), html: { class: 'fieldset-form' } do |f| + = form_errors(@application_setting) + + %fieldset + .form-group + = f.label :first_day_of_week, _('Default first day of the week'), class: 'label-bold' + = f.select :first_day_of_week, first_day_of_week_choices, {}, class: 'form-control' + .form-text.text-muted + = _('Default first day of the week in calendars and date pickers.') + + = f.submit _('Save changes'), class: "btn btn-success" diff --git a/app/views/admin/application_settings/preferences.html.haml b/app/views/admin/application_settings/preferences.html.haml index 00000b86ab7..c468d69d7b8 100644 --- a/app/views/admin/application_settings/preferences.html.haml +++ b/app/views/admin/application_settings/preferences.html.haml @@ -56,3 +56,14 @@ = _('Configure Gitaly timeouts.') .settings-content = render 'gitaly' + +%section.settings.as-localization.no-animate#js-localization-settings{ class: ('expanded' if expanded_by_default?) } + .settings-header + %h4 + = _('Localization') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded_by_default? ? _('Collapse') : _('Expand') + %p + = _('Various localization settings.') + .settings-content + = render 'localization' diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml index a758a63dfb3..8d9c083d223 100644 --- a/app/views/award_emoji/_awards_block.html.haml +++ b/app/views/award_emoji/_awards_block.html.haml @@ -3,7 +3,7 @@ - awards_sort(grouped_emojis).each do |emoji, awards| %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: [(award_state_class(awardable, awards, current_user))], - data: { placement: "bottom", title: award_user_list(awards, current_user) } } + data: { title: award_user_list(awards, current_user) } } = emoji_icon(emoji) %span.award-control-text.js-counter = awards.count @@ -12,7 +12,7 @@ .award-menu-holder.js-award-holder %button.btn.award-control.has-tooltip.js-add-award{ type: 'button', 'aria-label': _('Add reaction'), - data: { title: _('Add reaction'), placement: "bottom" } } + data: { title: _('Add reaction') } } %span{ class: "award-control-icon award-control-icon-neutral" }= custom_icon('emoji_slightly_smiling_face') %span{ class: "award-control-icon award-control-icon-positive" }= custom_icon('emoji_smiley') %span{ class: "award-control-icon award-control-icon-super-positive" }= custom_icon('emoji_smile') diff --git a/app/views/clusters/clusters/_form.html.haml b/app/views/clusters/clusters/_form.html.haml index 4c47e11927e..7acd9ce0562 100644 --- a/app/views/clusters/clusters/_form.html.haml +++ b/app/views/clusters/clusters/_form.html.haml @@ -20,12 +20,27 @@ .form-text.text-muted= s_("ClusterIntegration|Choose which of your environments will use this cluster.") - else = text_field_tag :environment_scope, '*', class: 'col-md-6 form-control disabled', placeholder: s_('ClusterIntegration|Environment scope'), disabled: true - - environment_scope_url = 'https://docs.gitlab.com/ee/user/project/clusters/#setting-the-environment-scope-premium' + - environment_scope_url = help_page_path('user/project/clusters/index', anchor: 'base-domain') - environment_scope_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: environment_scope_url } .form-text.text-muted %code * = s_("ClusterIntegration| is the default environment scope for this cluster. This means that all jobs, regardless of their environment, will use this cluster. %{environment_scope_start}More information%{environment_scope_end}").html_safe % { environment_scope_start: environment_scope_start, environment_scope_end: '</a>'.html_safe } + .form-group + %h5= s_('ClusterIntegration|Base domain') + = field.text_field :base_domain, class: 'col-md-6 form-control js-select-on-focus' + .form-text.text-muted + - auto_devops_url = help_page_path('topics/autodevops/index') + - auto_devops_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: auto_devops_url } + = s_('ClusterIntegration|Specifying a domain will allow you to use Auto Review Apps and Auto Deploy stages for %{auto_devops_start}Auto DevOps%{auto_devops_end}. The domain should have a wildcard DNS configured matching the domain.').html_safe % { auto_devops_start: auto_devops_start, auto_devops_end: '</a>'.html_safe } + - if @cluster.application_ingress_external_ip.present? + = s_('ClusterIntegration|Alternatively') + %code #{@cluster.application_ingress_external_ip}.nip.io + = s_('ClusterIntegration| can be used instead of a custom domain.') + - custom_domain_url = help_page_path('user/project/clusters/index', anchor: 'pointing-your-dns-at-the-cluster-ip') + - custom_domain_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: custom_domain_url } + = s_('ClusterIntegration| %{custom_domain_start}More information%{custom_domain_end}.').html_safe % { custom_domain_start: custom_domain_start, custom_domain_end: '</a>'.html_safe } + - if can?(current_user, :update_cluster, @cluster) .form-group = field.submit _('Save changes'), class: 'btn btn-success' diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index c1616810185..1a9aca1f6bf 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -60,5 +60,21 @@ = f.select :project_view, project_view_choices, {}, class: 'form-control' .form-text.text-muted Choose what content you want to see on a project’s overview page. + + .col-sm-12 + %hr + + .col-lg-4.profile-settings-sidebar + %h4.prepend-top-0 + = _('Localization') + %p + = _('Customize language and region related settings.') + = succeed '.' do + = link_to _('Learn more'), help_page_path('user/profile/preferences', anchor: 'localization'), target: '_blank' + .col-lg-8 + .form-group + = f.label :first_day_of_week, class: 'label-bold' do + = _('First day of the week') + = f.select :first_day_of_week, first_day_of_week_choices_with_default, {}, class: 'form-control' .form-group - = f.submit 'Save changes', class: 'btn btn-success' + = f.submit _('Save changes'), class: 'btn btn-success' diff --git a/app/views/projects/blob/viewers/_loading.html.haml b/app/views/projects/blob/viewers/_loading.html.haml index 120c0540335..df1f3e4e01b 100644 --- a/app/views/projects/blob/viewers/_loading.html.haml +++ b/app/views/projects/blob/viewers/_loading.html.haml @@ -1,2 +1,2 @@ .text-center.prepend-top-default.append-bottom-default - = icon('spinner spin 2x', 'aria-hidden' => 'true', 'aria-label' => 'Loading content…') + = icon('spinner spin 2x', 'aria-hidden' => 'true', 'aria-label' => 'Loading content…', class: 'qa-spinner') diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index d66de7ab698..99cbbc11acd 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -2,7 +2,6 @@ - page_title _("Environments") #environments-list-view{ data: { environments_data: environments_list_data, - "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s, "can-read-environment" => can?(current_user, :read_environment, @project).to_s, "can-create-environment" => can?(current_user, :create_environment, @project).to_s, "new-environment-path" => new_project_environment_path(@project), diff --git a/app/views/projects/graphs/charts.html.haml b/app/views/projects/graphs/charts.html.haml index b0e22a35fe1..60160f521ad 100644 --- a/app/views/projects/graphs/charts.html.haml +++ b/app/views/projects/graphs/charts.html.haml @@ -55,23 +55,23 @@ #{@commits_graph.authors} = (_("Authors: %{authors}") % { authors: "<strong>#{authors}</strong>" }).html_safe .col-md-6 + %p.slead + = _("Commits per day of month") %div - %p.slead - = _("Commits per day of month") %canvas#month-chart .row .col-md-6 .col-md-6 + %p.slead + = _("Commits per weekday") %div - %p.slead - = _("Commits per weekday") %canvas#weekday-chart .row .col-md-6 .col-md-6 + %p.slead + = _("Commits per day hour (UTC)") %div - %p.slead - = _("Commits per day hour (UTC)") %canvas#hour-chart -# haml-lint:disable InlineJavaScript diff --git a/app/views/projects/issues/_merge_requests_status.html.haml b/app/views/projects/issues/_merge_requests_status.html.haml index 43e4c8db93f..90838a75214 100644 --- a/app/views/projects/issues/_merge_requests_status.html.haml +++ b/app/views/projects/issues/_merge_requests_status.html.haml @@ -12,11 +12,14 @@ - mr_status_class = 'closed' - else - mr_status_date = merge_request.created_at - - mr_status_title = _('Opened') + - mr_status_title = mr_status_date ? _('Opened') : _('Open') - mr_status_icon = 'issue-open-m' - mr_status_class = 'open' -- mr_status_tooltip = "<div><span class=\"bold\">#{mr_status_title}</span> #{time_ago_in_words(mr_status_date)} ago</div><span class=\"text-tertiary\">#{l(mr_status_date.to_time, format: time_format)}</span>" +- if mr_status_date + - mr_status_tooltip = "<div><span class=\"bold\">#{mr_status_title}</span> #{time_ago_in_words(mr_status_date)} ago</div><span class=\"text-tertiary\">#{l(mr_status_date.to_time, format: time_format)}</span>" +- else + - mr_status_tooltip = "<div><span class=\"bold\">#{mr_status_title}</span></div>" %span.mr-status-wrapper.suggestion-help-hover{ class: css_class, data: { toggle: 'tooltip', placement: 'bottom', html: 'true', title: mr_status_tooltip } } = sprite_icon(mr_status_icon, size: 16, css_class: "merge-request-status #{mr_status_class}") diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 8275996b522..ff7c36c2d5b 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -16,6 +16,9 @@ = _('A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}.').html_safe % { among_other_things_link: among_other_things_link } %p = _('All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings.') + %p + - pages_getting_started_guide = link_to _('Pages getting started guide'), help_page_path("user/project/pages/getting_started_part_two", anchor: "fork-a-project-to-get-started-from"), target: '_blank' + = _('Information about additional Pages templates and how to install them can be found in our %{pages_getting_started_guide}.').html_safe % { pages_getting_started_guide: pages_getting_started_guide } .md = brand_new_project_guidelines %p diff --git a/app/views/projects/pipelines/charts/_pipeline_times.haml b/app/views/projects/pipelines/charts/_pipeline_times.haml index c23fe6ff170..c0ac79ed5f8 100644 --- a/app/views/projects/pipelines/charts/_pipeline_times.haml +++ b/app/views/projects/pipelines/charts/_pipeline_times.haml @@ -1,7 +1,7 @@ -%div - %p.light - = _("Commit duration in minutes for last 30 commits") +%p.light + = _("Commit duration in minutes for last 30 commits") +%div %canvas#build_timesChart{ height: 200 } -# haml-lint:disable InlineJavaScript diff --git a/app/views/projects/pipelines/charts/_pipelines.haml b/app/views/projects/pipelines/charts/_pipelines.haml index 14b3d47a9c2..47f1f074210 100644 --- a/app/views/projects/pipelines/charts/_pipelines.haml +++ b/app/views/projects/pipelines/charts/_pipelines.haml @@ -13,18 +13,21 @@ %p.light = _("Pipelines for last week") (#{date_from_to(Date.today - 7.days, Date.today)}) - %canvas#weekChart{ height: 200 } + %div + %canvas#weekChart{ height: 200 } .prepend-top-default %p.light = _("Pipelines for last month") (#{date_from_to(Date.today - 30.days, Date.today)}) - %canvas#monthChart{ height: 200 } + %div + %canvas#monthChart{ height: 200 } .prepend-top-default %p.light = _("Pipelines for last year") - %canvas#yearChart.padded{ height: 250 } + %div + %canvas#yearChart.padded{ height: 250 } -# haml-lint:disable InlineJavaScript %script#pipelinesChartsData{ type: "application/json" } diff --git a/app/views/projects/project_templates/_built_in_templates.html.haml b/app/views/projects/project_templates/_built_in_templates.html.haml index 2a0ce4bd16b..6159f1c3542 100644 --- a/app/views/projects/project_templates/_built_in_templates.html.haml +++ b/app/views/projects/project_templates/_built_in_templates.html.haml @@ -1,7 +1,7 @@ - Gitlab::ProjectTemplate.all.each do |template| .template-option.d-flex.align-items-center - .logo.append-right-10 - = custom_icon(template.logo, size: 40) + .logo.append-right-10.px-1 + = image_tag template.logo, size: 32, class: "btn-template-icon icon-#{template.name}" .description %strong = template.title diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml index 5ec5a06396e..8c4d1c32ebe 100644 --- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml +++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml @@ -4,10 +4,6 @@ = form_errors(@project) %fieldset.builds-feature.js-auto-devops-settings .form-group - - message = auto_devops_warning_message(@project) - - if message - %p.auto-devops-warning-message.settings-message.text-center - = message.html_safe = f.fields_for :auto_devops_attributes, @auto_devops do |form| .card.auto-devops-card .card-body @@ -21,19 +17,12 @@ = s_('CICD|The Auto DevOps pipeline will run if no alternative CI configuration file is found.') = link_to _('More information'), help_page_path('topics/autodevops/index.md'), target: '_blank' .card-footer.js-extra-settings{ class: @project.auto_devops_enabled? || 'hidden' } - = form.label :domain do - %strong= _('Domain') - = form.text_field :domain, class: 'form-control', placeholder: 'domain.com' - .form-text.text-muted - = s_('CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.') - - if cluster_ingress_ip = cluster_ingress_ip(@project) - = s_('%{nip_domain} can be used as an alternative to a custom domain.').html_safe % { nip_domain: "<code>#{cluster_ingress_ip}.nip.io</code>".html_safe } - = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'auto-devops-base-domain'), target: '_blank' - + %p.settings-message.text-center + - kubernetes_cluster_link = help_page_path('user/project/clusters/index') + - kubernetes_cluster_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: kubernetes_cluster_link } + = s_('CICD|You must add a %{kubernetes_cluster_start}Kubernetes cluster integration%{kubernetes_cluster_end} to this project with a domain in order for your deployment strategy to work correctly.').html_safe % { kubernetes_cluster_start: kubernetes_cluster_start, kubernetes_cluster_end: '</a>'.html_safe } %label.prepend-top-10 %strong= s_('CICD|Deployment strategy') - %p.settings-message.text-center - = s_('CICD|Deployment strategy needs a domain name to work correctly.') .form-check = form.radio_button :deploy_strategy, 'continuous', class: 'form-check-input' = form.label :deploy_strategy_continuous, class: 'form-check-label' do diff --git a/app/views/shared/icons/_express.svg b/app/views/shared/icons/_express.svg deleted file mode 100644 index a51e81e5568..00000000000 --- a/app/views/shared/icons/_express.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="27" height="32" viewBox="0 0 27 32" class="btn-template-icon icon-node-express"><g fill="none" fill-rule="evenodd"><path d="M-3 0h32v32H-3z"/><path fill="#353535" d="M1.192 16.267c.04 2.065.288 3.982.745 5.75.456 1.767 1.16 3.307 2.115 4.618.953 1.31 2.185 2.343 3.694 3.098 1.51.755 3.357 1.132 5.54 1.132 3.22 0 5.89-.844 8.016-2.532 2.125-1.69 3.446-4.22 3.962-7.597h1.192c-.437 3.575-1.847 6.345-4.23 8.312-2.384 1.966-5.324 2.95-8.82 2.95-2.383.04-4.42-.338-6.107-1.133-1.69-.794-3.07-1.917-4.142-3.367-1.073-1.45-1.867-3.158-2.383-5.124C.258 20.408 0 18.294 0 16.028c0-2.542.377-4.806 1.132-6.792C1.887 7.25 2.88 5.57 4.112 4.2 5.34 2.83 6.77 1.79 8.4 1.074 10.03.358 11.698 0 13.406 0c2.383 0 4.44.457 6.167 1.37 1.728.914 3.138 2.126 4.23 3.635 1.093 1.51 1.887 3.238 2.384 5.184.496 1.945.705 3.97.625 6.077H1.193zm24.43-1.192c0-1.867-.26-3.645-.775-5.333-.516-1.688-1.28-3.168-2.294-4.44-1.013-1.27-2.274-2.273-3.784-3.008-1.51-.735-3.258-1.102-5.244-1.102-1.67 0-3.228.317-4.678.953-1.45.636-2.72 1.56-3.813 2.77-1.092 1.212-1.976 2.672-2.652 4.38-.675 1.708-1.072 3.635-1.19 5.78h24.43z"/></g></svg> diff --git a/app/views/shared/icons/_rails.svg b/app/views/shared/icons/_rails.svg deleted file mode 100644 index 852bd183cc7..00000000000 --- a/app/views/shared/icons/_rails.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="32" height="20" viewBox="0 0 32 20" class="btn-template-icon icon-rails"><g fill="none" fill-rule="evenodd"><path d="M0-6h32v32H0z"/><path fill="#c00" fill-rule="nonzero" d="M.985 19.636s.422-4.163 3.375-9.087c2.954-4.924 7.99-8.65 12.083-9.017 8.144-.816 15.46 6.485 15.46 6.485s-.24.168-.494.38C23.42 2.49 18.54 5.274 17.005 6.02c-7.033 3.925-4.91 13.616-4.91 13.616H.987zM24.137 2.32c-.45-.182-.9-.35-1.364-.505l.056-.93c.885.254 1.237.423 1.363.493l-.056.943zM22.8 5.304c.45.028.915.084 1.393.183l-.056.872c-.464-.1-.928-.155-1.392-.17l.056-.885zM17.597.913c-.407 0-.815.015-1.223.058l-.268-.83c.465-.056.915-.084 1.35-.084l.282.858h-.14zm.676 5.178c.35-.154.76-.31 1.237-.45l.31.93c-.41.125-.817.294-1.225.49l-.323-.97zm-6.386-3.7c-.366.184-.718.395-1.083.62l-.647-.985c.38-.225.745-.42 1.097-.604l.633.97zm2.883 6.33c.252-.323.548-.646.87-.942l.634.957c-.31.323-.59.647-.83 1L14.77 8.72zm-2.04 4.53c.112-.506.24-1.027.422-1.547l1.012.802c-.14.548-.24 1.097-.295 1.645l-1.14-.9zM6.57 6.57c-.34.35-.662.73-.958 1.11L4.53 6.752c.323-.352.674-.704 1.04-1.055l1 .872zm-4.25 6.286c-.224.52-.52 1.21-.702 1.688L0 13.954c.14-.38.436-1.084.703-1.69l1.618.592zm10.2 3.967l1.518.548c.084.663.21 1.28.337 1.83l-1.688-.605c-.07-.422-.14-1.027-.168-1.772z"/></g></svg> diff --git a/app/views/shared/icons/_spring.svg b/app/views/shared/icons/_spring.svg deleted file mode 100644 index ccf18749029..00000000000 --- a/app/views/shared/icons/_spring.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" class="btn-template-icon icon-java-spring"><g fill="none" fill-rule="evenodd"><path d="M0 0h32v32H0z"/><path fill="#70AD51" d="M5.466 27.993c.586.473 1.446.385 1.918-.202.475-.585.386-1.445-.2-1.92-.585-.474-1.444-.383-1.92.202-.45.555-.392 1.356.115 1.844l-.266-.234C1.972 24.762 0 20.597 0 15.978 0 7.168 7.168 0 15.98 0c4.48 0 8.53 1.857 11.435 4.836.66-.898 1.232-1.902 1.7-3.015 2.036 6.118 3.233 11.26 2.795 15.31-.592 8.274-7.508 14.83-15.93 14.83-3.912 0-7.496-1.416-10.276-3.757l-.238-.21zm23.58-4.982c4.01-5.336 1.775-13.965-.085-19.48-1.657 3.453-5.738 6.094-9.262 6.93-3.303.788-6.226.142-9.283 1.318-6.97 2.68-6.86 10.992-3.02 12.86.002 0 .23.124.227.12 0-.002 5.644-1.122 8.764-2.274 4.56-1.684 9.566-5.835 11.213-10.657-.877 5.015-5.182 9.84-9.507 12.056-2.302 1.182-4.092 1.445-7.88 2.756-.464.158-.828.314-.828.314.96-.16 1.917-.212 1.917-.212 5.393-.255 13.807 1.516 17.745-3.73z"/></g></svg> diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index e1564d57426..df17ae95e2a 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -12,21 +12,20 @@ - css_class += " no-description" if project.description.blank? && !show_last_commit_as_description - cache_key = project_list_cache_key(project) - updated_tooltip = time_ago_with_tooltip(project.last_activity_date) -- css_details_class = compact_mode ? "d-flex flex-column flex-sm-row flex-md-row align-items-sm-center" : "align-items-center flex-md-fill flex-lg-column d-sm-flex d-lg-block" -- css_controls_class = compact_mode ? "" : "align-items-md-end align-items-lg-center flex-lg-row" +- css_controls_class = compact_mode ? "" : "flex-lg-row justify-content-lg-between" %li.project-row.d-flex{ class: css_class } = cache(cache_key) do - if avatar - .avatar-container.s64.flex-grow-0.flex-shrink-0 + .avatar-container.s48.flex-grow-0.flex-shrink-0 = link_to project_path(project), class: dom_class(project) do - if project.creator && use_creator_avatar - = image_tag avatar_icon_for_user(project.creator, 64), class: "avatar s65", alt:'' + = image_tag avatar_icon_for_user(project.creator, 48), class: "avatar s65", alt:'' - else - = project_icon(project, alt: '', class: 'avatar project-avatar s64', width: 64, height: 64) - .project-details.flex-sm-fill{ class: css_details_class } - .flex-wrapper.flex-fill - .d-flex.align-items-center.flex-wrap + = project_icon(project, alt: '', class: 'avatar project-avatar s48', width: 48, height: 48) + .project-details.d-sm-flex.flex-sm-fill.align-items-center + .flex-wrapper + .d-flex.align-items-center.flex-wrap.project-title %h2.d-flex.prepend-top-8 = link_to project_path(project), class: 'text-plain' do %span.project-full-name.append-right-8>< @@ -52,13 +51,13 @@ %span.user-access-role.d-block= Gitlab::Access.human_access(access) - if show_last_commit_as_description - .description.d-none.d-sm-block.prepend-top-8.append-right-default + .description.d-none.d-sm-block.append-right-default = link_to_markdown(project.commit.title, project_commit_path(project, project.commit), class: "commit-row-message") - elsif project.description.present? - .description.d-none.d-sm-block.prepend-top-8.append-right-default + .description.d-none.d-sm-block.append-right-default = markdown_field(project, :description) - .controls.d-flex.flex-row.flex-sm-column.flex-md-column.align-items-center.align-items-sm-end.flex-wrap.flex-shrink-0{ class: css_controls_class } + .controls.d-flex.flex-sm-column.align-items-center.align-items-sm-end.flex-wrap.flex-shrink-0{ class: css_controls_class } .icon-container.d-flex.align-items-center - if project.archived %span.d-flex.icon-wrapper.badge.badge-warning archived @@ -74,13 +73,13 @@ = number_with_delimiter(project.forks_count) - if show_merge_request_count?(disabled: !merge_requests, compact_mode: compact_mode) = link_to project_merge_requests_path(project), - class: "d-none d-lg-flex align-items-center icon-wrapper merge-requests has-tooltip", + class: "d-none d-xl-flex align-items-center icon-wrapper merge-requests has-tooltip", title: _('Merge Requests'), data: { container: 'body', placement: 'top' } do = sprite_icon('git-merge', size: 14, css_class: 'append-right-4') = number_with_delimiter(project.open_merge_requests_count) - if show_issue_count?(disabled: !issues, compact_mode: compact_mode) = link_to project_issues_path(project), - class: "d-none d-lg-flex align-items-center icon-wrapper issues has-tooltip", + class: "d-none d-xl-flex align-items-center icon-wrapper issues has-tooltip", title: _('Issues'), data: { container: 'body', placement: 'top' } do = sprite_icon('issues', size: 14, css_class: 'append-right-4') = number_with_delimiter(project.open_issues_count) @@ -89,19 +88,3 @@ = render_project_pipeline_status(project.pipeline_status, tooltip_placement: 'top') .updated-note %span Updated #{updated_tooltip} - - .d-none.d-lg-flex.align-item-stretch - - unless compact_mode - - if current_user - %button.star-button.btn.btn-default.d-flex.align-items-center.star-btn.toggle-star{ type: "button", data: { endpoint: toggle_star_project_path(project, :json) } } - - if current_user.starred?(project) - = sprite_icon('star', { css_class: 'icon' }) - %span.starred= s_('ProjectOverview|Unstar') - - else - = sprite_icon('star-o', { css_class: 'icon' }) - %span= s_('ProjectOverview|Star') - - - else - = link_to new_user_session_path, class: 'btn btn-default has-tooltip count-badge-button d-flex align-items-center star-btn', title: s_('ProjectOverview|You must sign in to star a project') do - = sprite_icon('star-o', { css_class: 'icon' }) - %span= s_('ProjectOverview|Star') diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 85c123c2704..410411b1294 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -23,6 +23,7 @@ - cronjob:prune_web_hook_logs - gcp_cluster:cluster_install_app +- gcp_cluster:cluster_upgrade_app - gcp_cluster:cluster_provision - gcp_cluster:cluster_wait_for_app_installation - gcp_cluster:wait_for_cluster_creation diff --git a/app/workers/cluster_upgrade_app_worker.rb b/app/workers/cluster_upgrade_app_worker.rb new file mode 100644 index 00000000000..d1a538859b4 --- /dev/null +++ b/app/workers/cluster_upgrade_app_worker.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class ClusterUpgradeAppWorker + include ApplicationWorker + include ClusterQueue + include ClusterApplications + + def perform(app_name, app_id) + find_application(app_name, app_id) do |app| + Clusters::Applications::UpgradeService.new(app).execute + end + end +end |