diff options
196 files changed, 2575 insertions, 1106 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 3eefcb9dd5b..9084fa2f716 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -1.0.0 +1.1.0 @@ -235,7 +235,7 @@ gem 'asana', '~> 0.8.1' gem 'ruby-fogbugz', '~> 0.2.1' # Kubernetes integration -gem 'kubeclient', '~> 3.1.0' +gem 'kubeclient', '~> 4.0.0' # Sanitize user input gem 'sanitize', '~> 4.6' @@ -425,7 +425,7 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 0.123.0', require: 'gitaly' +gem 'gitaly-proto', '~> 1.1.0', require: 'gitaly' gem 'grpc', '~> 1.15.0' gem 'google-protobuf', '~> 3.6' diff --git a/Gemfile.lock b/Gemfile.lock index 20231696795..f622c6292b2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -273,7 +273,7 @@ GEM gettext_i18n_rails (>= 0.7.1) po_to_json (>= 1.0.0) rails (>= 3.2.0) - gitaly-proto (0.123.0) + gitaly-proto (1.1.0) grpc (~> 1.0) github-markup (1.7.0) gitlab-default_value_for (3.1.1) @@ -367,14 +367,14 @@ GEM html2text (0.2.0) nokogiri (~> 1.6) htmlentities (4.3.4) - http (2.2.2) + http (3.3.0) addressable (~> 2.3) http-cookie (~> 1.0) - http-form_data (~> 1.0.1) + http-form_data (~> 2.0) http_parser.rb (~> 0.6.0) http-cookie (1.0.3) domain_name (~> 0.5) - http-form_data (1.0.3) + http-form_data (2.1.1) http_parser.rb (0.6.0) httparty (0.13.7) json (~> 1.8) @@ -418,8 +418,8 @@ GEM kgio (2.10.0) knapsack (1.17.0) rake - kubeclient (3.1.0) - http (~> 2.2.2) + kubeclient (4.0.0) + http (~> 3.0) recursive-open-struct (~> 1.0, >= 1.0.4) rest-client (~> 2.0) launchy (2.4.3) @@ -1006,7 +1006,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.3) - gitaly-proto (~> 0.123.0) + gitaly-proto (~> 1.1.0) github-markup (~> 1.7.0) gitlab-default_value_for (~> 3.1.1) gitlab-markup (~> 1.6.5) @@ -1042,7 +1042,7 @@ DEPENDENCIES jwt (~> 1.5.6) kaminari (~> 1.0) knapsack (~> 1.17) - kubeclient (~> 3.1.0) + kubeclient (~> 4.0.0) letter_opener_web (~> 1.3.0) license_finder (~> 5.4) licensee (~> 8.9) diff --git a/Gemfile.rails4.lock b/Gemfile.rails4.lock index caaa886987b..2542e085815 100644 --- a/Gemfile.rails4.lock +++ b/Gemfile.rails4.lock @@ -272,7 +272,7 @@ GEM gettext_i18n_rails (>= 0.7.1) po_to_json (>= 1.0.0) rails (>= 3.2.0) - gitaly-proto (0.123.0) + gitaly-proto (1.1.0) grpc (~> 1.0) github-markup (1.7.0) gitlab-markup (1.6.5) @@ -364,14 +364,14 @@ GEM html2text (0.2.0) nokogiri (~> 1.6) htmlentities (4.3.4) - http (2.2.2) + http (3.3.0) addressable (~> 2.3) http-cookie (~> 1.0) - http-form_data (~> 1.0.1) + http-form_data (~> 2.0) http_parser.rb (~> 0.6.0) http-cookie (1.0.3) domain_name (~> 0.5) - http-form_data (1.0.3) + http-form_data (2.1.1) http_parser.rb (0.6.0) httparty (0.13.7) json (~> 1.8) @@ -415,8 +415,8 @@ GEM kgio (2.10.0) knapsack (1.17.0) rake - kubeclient (3.1.0) - http (~> 2.2.2) + kubeclient (4.0.0) + http (~> 3.0) recursive-open-struct (~> 1.0, >= 1.0.4) rest-client (~> 2.0) launchy (2.4.3) @@ -998,7 +998,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.3) - gitaly-proto (~> 0.123.0) + gitaly-proto (~> 1.1.0) github-markup (~> 1.7.0) gitlab-markup (~> 1.6.5) gitlab-sidekiq-fetcher @@ -1033,7 +1033,7 @@ DEPENDENCIES jwt (~> 1.5.6) kaminari (~> 1.0) knapsack (~> 1.17) - kubeclient (~> 3.1.0) + kubeclient (~> 4.0.0) letter_opener_web (~> 1.3.0) license_finder (~> 5.4) licensee (~> 8.9) diff --git a/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js b/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js index a303e504cc7..55c68139ded 100644 --- a/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js +++ b/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js @@ -1,11 +1,11 @@ import $ from 'jquery'; -import { convertPermissionToBoolean } from '~/lib/utils/common_utils'; +import { parseBoolean } from '~/lib/utils/common_utils'; import GfmAutoComplete from '~/gfm_auto_complete'; export default function initGFMInput() { $('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => { const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources); - const enableGFM = convertPermissionToBoolean(el.dataset.supportsAutocomplete); + const enableGFM = parseBoolean(el.dataset.supportsAutocomplete); gfm.setup($(el), { emojis: true, diff --git a/app/assets/javascripts/behaviors/secret_values.js b/app/assets/javascripts/behaviors/secret_values.js index f6bf62d734e..5b92608d536 100644 --- a/app/assets/javascripts/behaviors/secret_values.js +++ b/app/assets/javascripts/behaviors/secret_values.js @@ -1,5 +1,5 @@ import { n__ } from '../locale'; -import { convertPermissionToBoolean } from '../lib/utils/common_utils'; +import { parseBoolean } from '../lib/utils/common_utils'; export default class SecretValues { constructor({ @@ -16,7 +16,7 @@ export default class SecretValues { this.revealButton = this.container.querySelector('.js-secret-value-reveal-button'); if (this.revealButton) { - const isRevealed = convertPermissionToBoolean(this.revealButton.dataset.secretRevealStatus); + const isRevealed = parseBoolean(this.revealButton.dataset.secretRevealStatus); this.updateDom(isRevealed); this.revealButton.addEventListener('click', this.onRevealButtonClicked.bind(this)); @@ -24,9 +24,7 @@ export default class SecretValues { } onRevealButtonClicked() { - const previousIsRevealed = convertPermissionToBoolean( - this.revealButton.dataset.secretRevealStatus, - ); + const previousIsRevealed = parseBoolean(this.revealButton.dataset.secretRevealStatus); this.updateDom(!previousIsRevealed); } diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js index 8b5a3c1c69d..eade1283513 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js @@ -4,6 +4,7 @@ import Mousetrap from 'mousetrap'; import axios from '../../lib/utils/axios_utils'; import { refreshCurrentPage, visitUrl } from '../../lib/utils/url_utility'; import findAndFollowLink from '../../lib/utils/navigation_utility'; +import { parseBoolean } from '~/lib/utils/common_utils'; const defaultStopCallback = Mousetrap.stopCallback; Mousetrap.stopCallback = (e, element, combo) => { @@ -61,7 +62,7 @@ export default class Shortcuts { static onTogglePerfBar(e) { e.preventDefault(); const performanceBarCookieName = 'perf_bar_enabled'; - if (Cookies.get(performanceBarCookieName) === 'true') { + if (parseBoolean(Cookies.get(performanceBarCookieName))) { Cookies.set(performanceBarCookieName, 'false', { path: '/' }); } else { Cookies.set(performanceBarCookieName, 'true', { path: '/' }); diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js index ec27ae8c291..9f547471170 100644 --- a/app/assets/javascripts/blob_edit/blob_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_bundle.js @@ -16,9 +16,17 @@ export default () => { const filePath = editBlobForm.data('blobFilename'); const currentAction = $('.js-file-title').data('currentAction'); const projectId = editBlobForm.data('project-id'); + const commitButton = $('.js-commit-button'); + + commitButton.on('click', () => { + window.onbeforeunload = null; + }); new EditBlob(`${urlRoot}${assetsPath}`, filePath, currentAction, projectId); new NewCommitForm(editBlobForm); + + // returning here blocks page navigation + window.onbeforeunload = () => ''; } if (uploadBlobForm.length) { diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index 61a3072ac27..f88e9b55988 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -24,7 +24,7 @@ import BoardSidebar from './components/board_sidebar'; import initNewListDropdown from './components/new_list_dropdown'; import BoardAddIssuesModal from './components/modal/index.vue'; import '~/vue_shared/vue_resource_interceptor'; -import { NavigationType } from '~/lib/utils/common_utils'; +import { NavigationType, parseBoolean } from '~/lib/utils/common_utils'; let issueBoardsApp; @@ -60,7 +60,7 @@ export default () => { boardsEndpoint: $boardApp.dataset.boardsEndpoint, listsEndpoint: $boardApp.dataset.listsEndpoint, boardId: $boardApp.dataset.boardId, - disabled: $boardApp.dataset.disabled === 'true', + disabled: parseBoolean($boardApp.dataset.disabled), issueLinkBase: $boardApp.dataset.issueLinkBase, rootPath: $boardApp.dataset.rootPath, bulkUpdatePath: $boardApp.dataset.bulkUpdatePath, diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index eefe14a1d79..cf88a973d33 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -5,7 +5,7 @@ import $ from 'jquery'; import _ from 'underscore'; import Vue from 'vue'; import Cookies from 'js-cookie'; -import { getUrlParamsArray } from '~/lib/utils/common_utils'; +import { getUrlParamsArray, parseBoolean } from '~/lib/utils/common_utils'; const boardsStore = { disabled: false, @@ -78,7 +78,7 @@ const boardsStore = { }); }, welcomeIsHidden() { - return Cookies.get('issue_board_welcome_hidden') === 'true'; + return parseBoolean(Cookies.get('issue_board_welcome_hidden')); }, removeList(id, type = 'blank') { const list = this.findList('id', id, type); diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js index 97a1645aa51..b2c88e8c14e 100644 --- a/app/assets/javascripts/build_artifacts.js +++ b/app/assets/javascripts/build_artifacts.js @@ -2,7 +2,7 @@ import $ from 'jquery'; import { visitUrl } from './lib/utils/url_utility'; -import { convertPermissionToBoolean } from './lib/utils/common_utils'; +import { parseBoolean } from './lib/utils/common_utils'; export default class BuildArtifacts { constructor() { @@ -22,7 +22,7 @@ export default class BuildArtifacts { // eslint-disable-next-line class-methods-use-this setupEntryClick() { return $('.tree-holder').on('click', 'tr[data-link]', function() { - visitUrl(this.dataset.link, convertPermissionToBoolean(this.dataset.externalLink)); + visitUrl(this.dataset.link, parseBoolean(this.dataset.externalLink)); }); } // eslint-disable-next-line class-methods-use-this diff --git a/app/assets/javascripts/ci_variable_list/ajax_variable_list.js b/app/assets/javascripts/ci_variable_list/ajax_variable_list.js index dffabbfe1b8..592e1fd1c31 100644 --- a/app/assets/javascripts/ci_variable_list/ajax_variable_list.js +++ b/app/assets/javascripts/ci_variable_list/ajax_variable_list.js @@ -2,7 +2,7 @@ import _ from 'underscore'; import axios from '../lib/utils/axios_utils'; import { s__ } from '../locale'; import Flash from '../flash'; -import { convertPermissionToBoolean } from '../lib/utils/common_utils'; +import { parseBoolean } from '../lib/utils/common_utils'; import statusCodes from '../lib/utils/http_status'; import VariableList from './ci_variable_list'; @@ -101,7 +101,7 @@ export default class AjaxVariableList { // If we submitted a row that was destroyed, remove it so we don't try // to destroy it again which would cause a BE error const destroyInput = row.querySelector('.js-ci-variable-input-destroy'); - if (convertPermissionToBoolean(destroyInput.value)) { + if (parseBoolean(destroyInput.value)) { row.remove(); // Update the ID input so any future edits and `_destroy` will apply on the BE } else { diff --git a/app/assets/javascripts/ci_variable_list/ci_variable_list.js b/app/assets/javascripts/ci_variable_list/ci_variable_list.js index 7bdc18ce03e..ee0f7cda189 100644 --- a/app/assets/javascripts/ci_variable_list/ci_variable_list.js +++ b/app/assets/javascripts/ci_variable_list/ci_variable_list.js @@ -1,5 +1,5 @@ import $ from 'jquery'; -import { convertPermissionToBoolean } from '../lib/utils/common_utils'; +import { parseBoolean } from '../lib/utils/common_utils'; import { s__ } from '../locale'; import setupToggleButtons from '../toggle_buttons'; import CreateItemDropdown from '../create_item_dropdown'; @@ -150,7 +150,7 @@ export default class VariableList { removeRow(row) { const $row = $(row); - const isPersisted = convertPermissionToBoolean($row.attr('data-is-persisted')); + const isPersisted = parseBoolean($row.attr('data-is-persisted')); if (isPersisted) { $row.hide(); diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index 8354c28778c..a37cb4def28 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -168,6 +168,9 @@ export default { knativeInstalled() { return this.applications.knative.status === APPLICATION_STATUS.INSTALLED; }, + knativeExternalIp() { + return this.applications.knative.externalIp; + }, }, created() { this.helmInstallIllustration = helmInstallIllustration; @@ -408,12 +411,11 @@ export default { <div slot="description"> <p> {{ - s__(`ClusterIntegration|A Knative build extends Kubernetes - and utilizes existing Kubernetes primitives to provide you with - the ability to run on-cluster container builds from source. - For example, you can write a build that uses Kubernetes-native - resources to obtain your source code from a repository, - build it into container a image, and then run that image.`) + s__(`ClusterIntegration|Knative (pronounced kay-nay-tiv) extends + Kubernetes to provide a set of middleware components that are + essential to build modern, source-centric, and container-based + applications that can run anywhere: on premises, in the cloud, or + even in a third-party data center.`) }} </p> @@ -444,6 +446,49 @@ export default { /> </div> </template> + <template v-if="knativeInstalled"> + <div class="form-group"> + <label for="knative-ip-address"> + {{ s__('ClusterIntegration|Knative IP Address:') }} + </label> + <div v-if="knativeExternalIp" class="input-group"> + <input + id="knative-ip-address" + :value="knativeExternalIp" + type="text" + class="form-control js-ip-address" + readonly + /> + <span class="input-group-append"> + <clipboard-button + :text="knativeExternalIp" + :title="s__('ClusterIntegration|Copy Knative IP Address to clipboard')" + class="input-group-text js-clipboard-btn" + /> + </span> + </div> + <input v-else type="text" class="form-control js-ip-address" readonly value="?" /> + </div> + + <p v-if="!knativeExternalIp" class="settings-message js-no-ip-message"> + {{ + s__(`ClusterIntegration|The IP address is in + the process of being assigned. Please check your Kubernetes + cluster or Quotas on Google Kubernetes Engine if it takes a long time.`) + }} + </p> + + <p> + {{ + s__(`ClusterIntegration|Point a wildcard DNS to this + generated IP address in order to access + your application after it has been deployed.`) + }} + <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer"> + {{ __('More information') }} + </a> + </p> + </template> </div> </application-row> </div> diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js index 3678be59d24..2d69da8eaec 100644 --- a/app/assets/javascripts/clusters/stores/clusters_store.js +++ b/app/assets/javascripts/clusters/stores/clusters_store.js @@ -60,6 +60,7 @@ export default class ClusterStore { requestStatus: null, requestReason: null, hostname: null, + externalIp: null, }, }, }; @@ -111,6 +112,8 @@ export default class ClusterStore { } else if (appId === KNATIVE) { this.state.applications.knative.hostname = serverAppEntry.hostname || this.state.applications.knative.hostname; + this.state.applications.knative.externalIp = + serverAppEntry.external_ip || this.state.applications.knative.externalIp; } }); } diff --git a/app/assets/javascripts/contextual_sidebar.js b/app/assets/javascripts/contextual_sidebar.js index dff0adba25a..10f02739ec8 100644 --- a/app/assets/javascripts/contextual_sidebar.js +++ b/app/assets/javascripts/contextual_sidebar.js @@ -2,6 +2,7 @@ import $ from 'jquery'; import Cookies from 'js-cookie'; import _ from 'underscore'; import bp from './breakpoints'; +import { parseBoolean } from '~/lib/utils/common_utils'; export default class ContextualSidebar { constructor() { @@ -78,7 +79,7 @@ export default class ContextualSidebar { if (breakpoint === 'sm' || breakpoint === 'md') { this.toggleCollapsedSidebar(true); } else if (breakpoint === 'lg') { - const collapse = Cookies.get('sidebar_collapsed') === 'true'; + const collapse = parseBoolean(Cookies.get('sidebar_collapsed')); this.toggleCollapsedSidebar(collapse); } } diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue index ec4a4aa1d6d..f40a7b25fde 100644 --- a/app/assets/javascripts/diffs/components/tree_list.vue +++ b/app/assets/javascripts/diffs/components/tree_list.vue @@ -1,7 +1,7 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; import { GlTooltipDirective } from '@gitlab/ui'; -import { convertPermissionToBoolean } from '~/lib/utils/common_utils'; +import { parseBoolean } from '~/lib/utils/common_utils'; import Icon from '~/vue_shared/components/icon.vue'; import FileRow from '~/vue_shared/components/file_row.vue'; import FileRowStats from './file_row_stats.vue'; @@ -18,8 +18,7 @@ export default { }, data() { const treeListStored = localStorage.getItem(treeListStorageKey); - const renderTreeList = - treeListStored !== null ? convertPermissionToBoolean(treeListStored) : true; + const renderTreeList = treeListStored !== null ? parseBoolean(treeListStored) : true; return { search: '', diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js index 085e255f1d3..2f59a3822f4 100644 --- a/app/assets/javascripts/diffs/store/modules/diff_state.js +++ b/app/assets/javascripts/diffs/store/modules/diff_state.js @@ -1,6 +1,7 @@ import Cookies from 'js-cookie'; import { getParameterValues } from '~/lib/utils/url_utility'; import bp from '~/breakpoints'; +import { parseBoolean } from '~/lib/utils/common_utils'; import { INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME, MR_TREE_SHOW_KEY } from '../../constants'; const viewTypeFromQueryString = getParameterValues('view')[0]; @@ -22,7 +23,7 @@ export default () => ({ tree: [], treeEntries: {}, showTreeList: - storedTreeShow === null ? bp.getBreakpointSize() !== 'xs' : storedTreeShow === 'true', + storedTreeShow === null ? bp.getBreakpointSize() !== 'xs' : parseBoolean(storedTreeShow), currentDiffFileId: '', projectPath: '', commentForms: [], diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js index f044d31c776..3cf6e4ad14d 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js +++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import environmentsFolderApp from './environments_folder_view.vue'; -import { convertPermissionToBoolean } from '../../lib/utils/common_utils'; +import { parseBoolean } from '../../lib/utils/common_utils'; import Translate from '../../vue_shared/translate'; Vue.use(Translate); @@ -18,8 +18,8 @@ export default () => endpoint: environmentsData.endpoint, folderName: environmentsData.folderName, cssContainerClass: environmentsData.cssClass, - canCreateDeployment: convertPermissionToBoolean(environmentsData.canCreateDeployment), - canReadEnvironment: convertPermissionToBoolean(environmentsData.canReadEnvironment), + canCreateDeployment: parseBoolean(environmentsData.canCreateDeployment), + canReadEnvironment: parseBoolean(environmentsData.canReadEnvironment), }; }, render(createElement) { diff --git a/app/assets/javascripts/environments/index.js b/app/assets/javascripts/environments/index.js index 5b6833fb15d..d366e7550b7 100644 --- a/app/assets/javascripts/environments/index.js +++ b/app/assets/javascripts/environments/index.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import environmentsComponent from './components/environments_app.vue'; -import { convertPermissionToBoolean } from '../lib/utils/common_utils'; +import { parseBoolean } from '../lib/utils/common_utils'; import Translate from '../vue_shared/translate'; Vue.use(Translate); @@ -19,9 +19,9 @@ export default () => newEnvironmentPath: environmentsData.newEnvironmentPath, helpPagePath: environmentsData.helpPagePath, cssContainerClass: environmentsData.cssClass, - canCreateEnvironment: convertPermissionToBoolean(environmentsData.canCreateEnvironment), - canCreateDeployment: convertPermissionToBoolean(environmentsData.canCreateDeployment), - canReadEnvironment: convertPermissionToBoolean(environmentsData.canReadEnvironment), + canCreateEnvironment: parseBoolean(environmentsData.canCreateEnvironment), + canCreateDeployment: parseBoolean(environmentsData.canCreateDeployment), + canReadEnvironment: parseBoolean(environmentsData.canReadEnvironment), }; }, render(createElement) { diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js index 0f68f05b523..928f1fe409f 100644 --- a/app/assets/javascripts/groups/index.js +++ b/app/assets/javascripts/groups/index.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import { parseBoolean } from '~/lib/utils/common_utils'; import Translate from '../vue_shared/translate'; import GroupFilterableList from './groups_filterable_list'; import GroupsStore from './store/groups_store'; @@ -38,7 +39,7 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => { }, data() { const { dataset } = dataEl || this.$options.el; - const hideProjects = dataset.hideProjects === 'true'; + const hideProjects = parseBoolean(dataset.hideProjects); const service = new GroupsService(endpoint || dataset.endpoint); const store = new GroupsStore(hideProjects); diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js index 175d0b8498b..2fa7a219ea8 100644 --- a/app/assets/javascripts/header.js +++ b/app/assets/javascripts/header.js @@ -4,6 +4,7 @@ import Translate from '~/vue_shared/translate'; import { highCountTrim } from '~/lib/utils/text_utility'; import SetStatusModalTrigger from './set_status_modal/set_status_modal_trigger.vue'; import SetStatusModalWrapper from './set_status_modal/set_status_modal_wrapper.vue'; +import { parseBoolean } from '~/lib/utils/common_utils'; /** * Updates todo counter when todos are toggled. @@ -36,7 +37,7 @@ document.addEventListener('DOMContentLoaded', () => { const { hasStatus } = this.$options.el.dataset; return { - hasStatus: hasStatus === 'true', + hasStatus: parseBoolean(hasStatus), }; }, render(createElement) { diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js index 7a5a227db30..fbf944499d5 100644 --- a/app/assets/javascripts/ide/index.js +++ b/app/assets/javascripts/ide/index.js @@ -4,7 +4,7 @@ import Translate from '~/vue_shared/translate'; import ide from './components/ide.vue'; import store from './stores'; import router from './ide_router'; -import { convertPermissionToBoolean } from '../lib/utils/common_utils'; +import { parseBoolean } from '../lib/utils/common_utils'; Vue.use(Translate); @@ -40,7 +40,7 @@ export function initIde(el, options = {}) { webIDEHelpPagePath: el.dataset.webIdeHelpPagePath, }); this.setInitialData({ - clientsidePreviewEnabled: convertPermissionToBoolean(el.dataset.clientsidePreviewEnabled), + clientsidePreviewEnabled: parseBoolean(el.dataset.clientsidePreviewEnabled), ...extraInitialData(el), }); }, diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js index f1beb1a8ea5..1ffd5c61282 100644 --- a/app/assets/javascripts/importer_status.js +++ b/app/assets/javascripts/importer_status.js @@ -3,7 +3,7 @@ import _ from 'underscore'; import { __, sprintf } from './locale'; import axios from './lib/utils/axios_utils'; import flash from './flash'; -import { convertPermissionToBoolean } from './lib/utils/common_utils'; +import { parseBoolean } from './lib/utils/common_utils'; class ImporterStatus { constructor({ jobsUrl, importUrl, ciCdOnly }) { @@ -141,7 +141,7 @@ function initImporterStatus() { return new ImporterStatus({ jobsUrl: data.jobsImportPath, importUrl: data.importPath, - ciCdOnly: convertPermissionToBoolean(data.ciCdOnly), + ciCdOnly: parseBoolean(data.ciCdOnly), }); } } diff --git a/app/assets/javascripts/issuable_suggestions/components/app.vue b/app/assets/javascripts/issuable_suggestions/components/app.vue new file mode 100644 index 00000000000..eea0701312b --- /dev/null +++ b/app/assets/javascripts/issuable_suggestions/components/app.vue @@ -0,0 +1,96 @@ +<script> +import _ from 'underscore'; +import { GlTooltipDirective } from '@gitlab/ui'; +import { __ } from '~/locale'; +import Icon from '~/vue_shared/components/icon.vue'; +import Suggestion from './item.vue'; +import query from '../queries/issues.graphql'; + +export default { + components: { + Suggestion, + Icon, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + projectPath: { + type: String, + required: true, + }, + search: { + type: String, + required: true, + }, + }, + apollo: { + issues: { + query, + debounce: 250, + skip() { + return this.isSearchEmpty; + }, + update: data => data.project.issues.edges.map(({ node }) => node), + variables() { + return { + fullPath: this.projectPath, + search: this.search, + }; + }, + }, + }, + data() { + return { + issues: [], + loading: 0, + }; + }, + computed: { + isSearchEmpty() { + return _.isEmpty(this.search); + }, + showSuggestions() { + return !this.isSearchEmpty && this.issues.length && !this.loading; + }, + }, + watch: { + search() { + if (this.isSearchEmpty) { + this.issues = []; + } + }, + }, + helpText: __( + 'These existing issues have a similar title. It might be better to comment there instead of creating another similar issue.', + ), +}; +</script> + +<template> + <div v-show="showSuggestions" class="form-group row issuable-suggestions"> + <div v-once class="col-form-label col-sm-2 pt-0"> + {{ __('Similar issues') }} + <icon + v-gl-tooltip.bottom + :title="$options.helpText" + :aria-label="$options.helpText" + name="question-o" + class="text-secondary suggestion-help-hover" + /> + </div> + <div class="col-sm-10"> + <ul class="list-unstyled m-0"> + <li + v-for="(suggestion, index) in issues" + :key="suggestion.id" + :class="{ + 'append-bottom-default': index !== issues.length - 1, + }" + > + <suggestion :suggestion="suggestion" /> + </li> + </ul> + </div> + </div> +</template> diff --git a/app/assets/javascripts/issuable_suggestions/components/item.vue b/app/assets/javascripts/issuable_suggestions/components/item.vue new file mode 100644 index 00000000000..9a16b486bf5 --- /dev/null +++ b/app/assets/javascripts/issuable_suggestions/components/item.vue @@ -0,0 +1,137 @@ +<script> +import _ from 'underscore'; +import { GlLink, GlTooltip, GlTooltipDirective } from '@gitlab/ui'; +import { __ } from '~/locale'; +import Icon from '~/vue_shared/components/icon.vue'; +import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; +import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import timeago from '~/vue_shared/mixins/timeago'; + +export default { + components: { + GlTooltip, + GlLink, + Icon, + UserAvatarImage, + TimeagoTooltip, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + mixins: [timeago], + props: { + suggestion: { + type: Object, + required: true, + }, + }, + computed: { + isOpen() { + return this.suggestion.state === 'opened'; + }, + isClosed() { + return this.suggestion.state === 'closed'; + }, + counts() { + return [ + { + id: _.uniqueId(), + icon: 'thumb-up', + tooltipTitle: __('Upvotes'), + count: this.suggestion.upvotes, + }, + { + id: _.uniqueId(), + icon: 'comment', + tooltipTitle: __('Comments'), + count: this.suggestion.userNotesCount, + }, + ].filter(({ count }) => count); + }, + stateIcon() { + return this.isClosed ? 'issue-close' : 'issue-open-m'; + }, + stateTitle() { + return this.isClosed ? __('Closed') : __('Opened'); + }, + closedOrCreatedDate() { + return this.suggestion.closedAt || this.suggestion.createdAt; + }, + hasUpdated() { + return this.suggestion.updatedAt !== this.suggestion.createdAt; + }, + }, +}; +</script> + +<template> + <div class="suggestion-item"> + <div class="d-flex align-items-center"> + <icon + v-if="suggestion.confidential" + v-gl-tooltip.bottom + :title="__('Confidential')" + name="eye-slash" + class="suggestion-help-hover mr-1 suggestion-confidential" + /> + <gl-link :href="suggestion.webUrl" target="_blank" class="suggestion bold str-truncated-100"> + {{ suggestion.title }} + </gl-link> + </div> + <div class="text-secondary suggestion-footer"> + <icon + ref="state" + :name="stateIcon" + :class="{ + 'suggestion-state-open': isOpen, + 'suggestion-state-closed': isClosed, + }" + class="suggestion-help-hover" + /> + <gl-tooltip :target="() => $refs.state" placement="bottom"> + <span class="d-block"> + <span class="bold"> {{ stateTitle }} </span> {{ timeFormated(closedOrCreatedDate) }} + </span> + <span class="text-tertiary">{{ tooltipTitle(closedOrCreatedDate) }}</span> + </gl-tooltip> + #{{ suggestion.iid }} • + <timeago-tooltip + :time="suggestion.createdAt" + tooltip-placement="bottom" + class="suggestion-help-hover" + /> + by + <gl-link :href="suggestion.author.webUrl"> + <user-avatar-image + :img-src="suggestion.author.avatarUrl" + :size="16" + css-classes="mr-0 float-none" + tooltip-placement="bottom" + class="d-inline-block" + > + <span class="bold d-block">{{ __('Author') }}</span> {{ suggestion.author.name }} + <span class="text-tertiary">@{{ suggestion.author.username }}</span> + </user-avatar-image> + </gl-link> + <template v-if="hasUpdated"> + • {{ __('updated') }} + <timeago-tooltip + :time="suggestion.updatedAt" + tooltip-placement="bottom" + class="suggestion-help-hover" + /> + </template> + <span class="suggestion-counts"> + <span + v-for="{ count, icon, tooltipTitle, id } in counts" + :key="id" + v-gl-tooltip.bottom + :title="tooltipTitle" + class="suggestion-help-hover prepend-left-8 text-tertiary" + > + <icon :name="icon" /> {{ count }} + </span> + </span> + </div> + </div> +</template> diff --git a/app/assets/javascripts/issuable_suggestions/index.js b/app/assets/javascripts/issuable_suggestions/index.js new file mode 100644 index 00000000000..2c80cf1797a --- /dev/null +++ b/app/assets/javascripts/issuable_suggestions/index.js @@ -0,0 +1,38 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import defaultClient from '~/lib/graphql'; +import App from './components/app.vue'; + +Vue.use(VueApollo); + +export default function() { + const el = document.getElementById('js-suggestions'); + const issueTitle = document.getElementById('issue_title'); + const { projectPath } = el.dataset; + const apolloProvider = new VueApollo({ + defaultClient, + }); + + return new Vue({ + el, + apolloProvider, + data() { + return { + search: issueTitle.value, + }; + }, + mounted() { + issueTitle.addEventListener('input', () => { + this.search = issueTitle.value; + }); + }, + render(h) { + return h(App, { + props: { + projectPath, + search: this.search, + }, + }); + }, + }); +} diff --git a/app/assets/javascripts/issuable_suggestions/queries/issues.graphql b/app/assets/javascripts/issuable_suggestions/queries/issues.graphql new file mode 100644 index 00000000000..2384b381344 --- /dev/null +++ b/app/assets/javascripts/issuable_suggestions/queries/issues.graphql @@ -0,0 +1,26 @@ +query issueSuggestion($fullPath: ID!, $search: String) { + project(fullPath: $fullPath) { + issues(search: $search, sort: updated_desc, first: 5) { + edges { + node { + iid + title + confidential + userNotesCount + upvotes + webUrl + state + closedAt + createdAt + updatedAt + author { + name + username + avatarUrl + webUrl + } + } + } + } + } +} diff --git a/app/assets/javascripts/landing.js b/app/assets/javascripts/landing.js index 8c0950ad5d5..bfb4d9ce67b 100644 --- a/app/assets/javascripts/landing.js +++ b/app/assets/javascripts/landing.js @@ -1,4 +1,5 @@ import Cookies from 'js-cookie'; +import { parseBoolean } from '~/lib/utils/common_utils'; class Landing { constructor(landingElement, dismissButton, cookieName) { @@ -30,7 +31,7 @@ class Landing { } isDismissed() { - return Cookies.get(this.cookieName) === 'true'; + return parseBoolean(Cookies.get(this.cookieName)); } } diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js new file mode 100644 index 00000000000..20a0f142d9e --- /dev/null +++ b/app/assets/javascripts/lib/graphql.js @@ -0,0 +1,9 @@ +import ApolloClient from 'apollo-boost'; +import csrf from '~/lib/utils/csrf'; + +export default new ApolloClient({ + uri: `${gon.relative_url_root}/api/graphql`, + headers: { + [csrf.headerKey]: csrf.token, + }, +}); diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 3186ae9c133..040d0bc659e 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -421,12 +421,28 @@ export const historyPushState = newUrl => { }; /** + * Returns true for a String "true" and false otherwise. + * This is the opposite of Boolean(...).toString() + * + * @param {String} value + * @returns {Boolean} + */ +export const parseBoolean = value => value === 'true'; + +/** * Converts permission provided as strings to booleans. * * @param {String} string * @returns {Boolean} */ -export const convertPermissionToBoolean = permission => permission === 'true'; +export const convertPermissionToBoolean = permission => { + if (process.env.NODE_ENV !== 'production') { + // eslint-disable-next-line no-console + console.warn('convertPermissionToBoolean is deprecated! Please use parseBoolean instead.'); + } + + return parseBoolean(permission); +}; /** * Back Off exponential algorithm diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index d8255181574..b0dc5697018 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -8,7 +8,12 @@ import flash from './flash'; import BlobForkSuggestion from './blob/blob_fork_suggestion'; import initChangesDropdown from './init_changes_dropdown'; import bp from './breakpoints'; -import { parseUrlPathname, handleLocationHash, isMetaClick } from './lib/utils/common_utils'; +import { + parseUrlPathname, + handleLocationHash, + isMetaClick, + parseBoolean, +} from './lib/utils/common_utils'; import { isInVueNoteablePage } from './lib/utils/dom_utils'; import { getLocationHash } from './lib/utils/url_utility'; import Diff from './diff'; @@ -212,6 +217,28 @@ export default class MergeRequestTabs { } this.eventHub.$emit('MergeRequestTabChange', this.getCurrentAction()); + } else if (action === this.currentAction) { + // ContentTop is used to handle anything at the top of the page before the main content + const mainContentContainer = document.querySelector('.content-wrapper'); + const tabContentContainer = document.querySelector('.tab-content'); + + if (mainContentContainer && tabContentContainer) { + const mainContentTop = mainContentContainer.getBoundingClientRect().top; + const tabContentTop = tabContentContainer.getBoundingClientRect().top; + + // 51px is the height of the navbar buttons, e.g. `Discussion | Commits | Changes` + const scrollDestination = tabContentTop - mainContentTop - 51; + + // scrollBehavior is only available in browsers that support scrollToOptions + if ('scrollBehavior' in document.documentElement.style) { + window.scrollTo({ + top: scrollDestination, + behavior: 'smooth', + }); + } else { + window.scrollTo(0, scrollDestination); + } + } } } @@ -440,7 +467,7 @@ export default class MergeRequestTabs { // Expand the issuable sidebar unless the user explicitly collapsed it expandView() { - if (Cookies.get('collapsed_gutter') === 'true') { + if (parseBoolean(Cookies.get('collapsed_gutter'))) { return; } const $gutterIcon = $('.js-sidebar-toggle i:visible'); diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue index 23e1e734b37..0e141d02ead 100644 --- a/app/assets/javascripts/monitoring/components/empty_state.vue +++ b/app/assets/javascripts/monitoring/components/empty_state.vue @@ -44,9 +44,9 @@ export default { title: 'Get started with performance monitoring', description: `Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments.`, - buttonText: 'Install Prometheus on clusters', + buttonText: 'Install on clusters', buttonPath: this.clustersPath, - secondaryButtonText: 'Configure existing Prometheus', + secondaryButtonText: 'Configure existing installation', secondaryButtonPath: this.settingsPath, }, loading: { @@ -88,26 +88,32 @@ export default { </script> <template> - <div class="prometheus-state"> - <div class="state-svg svg-content"><img :src="currentState.svgUrl" /></div> - <h4 class="state-title">{{ currentState.title }}</h4> - <p class="state-description"> - {{ currentState.description }} - <a v-if="showButtonDescription" :href="settingsPath"> Prometheus server </a> - </p> - <div class="state-button"> - <a v-if="currentState.buttonPath" :href="currentState.buttonPath" class="btn btn-success"> - {{ currentState.buttonText }} - </a> + <div class="row empty-state js-empty-state"> + <div class="col-12"> + <div class="state-svg svg-content"><img :src="currentState.svgUrl" /></div> </div> - <div class="state-button"> - <a - v-if="currentState.secondaryButtonPath" - :href="currentState.secondaryButtonPath" - class="btn" - > - {{ currentState.secondaryButtonText }} - </a> + + <div class="col-12"> + <div class="text-content"> + <h4 class="state-title text-center">{{ currentState.title }}</h4> + <p class="state-description"> + {{ currentState.description }} + <a v-if="showButtonDescription" :href="settingsPath"> Prometheus server </a> + </p> + + <div class="text-center"> + <a v-if="currentState.buttonPath" :href="currentState.buttonPath" class="btn btn-success"> + {{ currentState.buttonText }} + </a> + <a + v-if="currentState.secondaryButtonPath" + :href="currentState.secondaryButtonPath" + class="btn" + > + {{ currentState.secondaryButtonText }} + </a> + </div> + </div> </div> </div> </template> diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js index 41270e015d4..9d78b5ea110 100644 --- a/app/assets/javascripts/monitoring/monitoring_bundle.js +++ b/app/assets/javascripts/monitoring/monitoring_bundle.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import { convertPermissionToBoolean } from '~/lib/utils/common_utils'; +import { parseBoolean } from '~/lib/utils/common_utils'; import Dashboard from './components/dashboard.vue'; export default () => { @@ -13,7 +13,7 @@ export default () => { return createElement(Dashboard, { props: { ...el.dataset, - hasMetrics: convertPermissionToBoolean(el.dataset.hasMetrics), + hasMetrics: parseBoolean(el.dataset.hasMetrics), }, }); }, diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js index cba6759ebf5..ee1a5274ff7 100644 --- a/app/assets/javascripts/namespace_select.js +++ b/app/assets/javascripts/namespace_select.js @@ -3,10 +3,11 @@ import $ from 'jquery'; import Api from './api'; import { mergeUrlParams } from './lib/utils/url_utility'; +import { parseBoolean } from '~/lib/utils/common_utils'; export default class NamespaceSelect { constructor(opts) { - const isFilter = opts.dropdown.dataset.isFilter === 'true'; + const isFilter = parseBoolean(opts.dropdown.dataset.isFilter); const fieldName = opts.dropdown.dataset.fieldName || 'namespace_id'; $(opts.dropdown).glDropdown({ diff --git a/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js b/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js index d9cf62db3f7..674b807edbe 100644 --- a/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js +++ b/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js @@ -1,5 +1,6 @@ import $ from 'jquery'; import { truncate } from '../../../lib/utils/text_utility'; +import { parseBoolean } from '~/lib/utils/common_utils'; const MAX_MESSAGE_LENGTH = 500; const MESSAGE_CELL_SELECTOR = '.abuse-reports .message'; @@ -26,7 +27,7 @@ export default class AbuseReports { const $messageCellElement = $(this); const originalMessage = $messageCellElement.data('originalMessage'); if (!originalMessage) return; - if ($messageCellElement.data('messageTruncated') === 'true') { + if (parseBoolean($messageCellElement.data('messageTruncated'))) { $messageCellElement.data('messageTruncated', 'false'); $messageCellElement.text(originalMessage); } else { diff --git a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js index 417935e2ad0..10cd8ecfbc9 100644 --- a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js +++ b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js @@ -1,9 +1,10 @@ import $ from 'jquery'; import U2FRegister from '~/u2f/register'; +import { parseBoolean } from '~/lib/utils/common_utils'; document.addEventListener('DOMContentLoaded', () => { const twoFactorNode = document.querySelector('.js-two-factor-auth'); - const skippable = twoFactorNode.dataset.twoFactorSkippable === 'true'; + const skippable = parseBoolean(twoFactorNode.dataset.twoFactorSkippable); if (skippable) { const button = `<a class="btn btn-sm btn-warning float-right" data-method="patch" href="${ twoFactorNode.dataset.two_factor_skip_url diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js index f477424811d..6fc982967eb 100644 --- a/app/assets/javascripts/pages/projects/commit/show/index.js +++ b/app/assets/javascripts/pages/projects/commit/show/index.js @@ -11,6 +11,8 @@ import initDiffNotes from '~/diff_notes/diff_notes_bundle'; import { fetchCommitMergeRequests } from '~/commit_merge_requests'; document.addEventListener('DOMContentLoaded', () => { + const hasPerfBar = document.querySelector('.with-performance-bar'); + const performanceHeight = hasPerfBar ? 35 : 0; new Diff(); new ZenMode(); new ShortcutsNavigation(); @@ -18,8 +20,7 @@ document.addEventListener('DOMContentLoaded', () => { container: '.js-commit-pipeline-graph', }).bindEvents(); initNotes(); - const stickyBarPaddingTop = 16; - initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - stickyBarPaddingTop); + initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight + performanceHeight); $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); fetchCommitMergeRequests(); initDiffNotes(); diff --git a/app/assets/javascripts/pages/projects/issues/form.js b/app/assets/javascripts/pages/projects/issues/form.js index 197bfa8a394..02a56685a35 100644 --- a/app/assets/javascripts/pages/projects/issues/form.js +++ b/app/assets/javascripts/pages/projects/issues/form.js @@ -7,6 +7,7 @@ import LabelsSelect from '~/labels_select'; import MilestoneSelect from '~/milestone_select'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import IssuableTemplateSelectors from '~/templates/issuable_template_selectors'; +import initSuggestions from '~/issuable_suggestions'; export default () => { new ShortcutsNavigation(); @@ -15,4 +16,8 @@ export default () => { new LabelsSelect(); new MilestoneSelect(); new IssuableTemplateSelectors(); + + if (gon.features.issueSuggestions && gon.features.graphql) { + initSuggestions(); + } }; diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue index 1edd076604c..22512a6f12a 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue @@ -3,6 +3,7 @@ import Vue from 'vue'; import Cookies from 'js-cookie'; import Translate from '../../../../../vue_shared/translate'; import illustrationSvg from '../icons/intro_illustration.svg'; +import { parseBoolean } from '~/lib/utils/common_utils'; Vue.use(Translate); @@ -13,7 +14,7 @@ export default { data() { return { docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl, - calloutDismissed: Cookies.get(cookieKey) === 'true', + calloutDismissed: parseBoolean(Cookies.get(cookieKey)), }; }, created() { diff --git a/app/assets/javascripts/pages/projects/pipelines/index/index.js b/app/assets/javascripts/pages/projects/pipelines/index/index.js index fc337a7609b..fd72d2ddbe0 100644 --- a/app/assets/javascripts/pages/projects/pipelines/index/index.js +++ b/app/assets/javascripts/pages/projects/pipelines/index/index.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import PipelinesStore from '../../../../pipelines/stores/pipelines_store'; import pipelinesComponent from '../../../../pipelines/components/pipelines.vue'; import Translate from '../../../../vue_shared/translate'; -import { convertPermissionToBoolean } from '../../../../lib/utils/common_utils'; +import { parseBoolean } from '../../../../lib/utils/common_utils'; Vue.use(Translate); @@ -33,8 +33,8 @@ document.addEventListener( noPipelinesSvgPath: this.dataset.noPipelinesSvgPath, autoDevopsPath: this.dataset.helpAutoDevopsPath, newPipelinePath: this.dataset.newPipelinePath, - canCreatePipeline: convertPermissionToBoolean(this.dataset.canCreatePipeline), - hasGitlabCi: convertPermissionToBoolean(this.dataset.hasGitlabCi), + canCreatePipeline: parseBoolean(this.dataset.canCreatePipeline), + hasGitlabCi: parseBoolean(this.dataset.hasGitlabCi), ciLintPath: this.dataset.ciLintPath, resetCachePath: this.dataset.resetCachePath, }, diff --git a/app/assets/javascripts/performance_bar/services/performance_bar_service.js b/app/assets/javascripts/performance_bar/services/performance_bar_service.js index 3a496fa2ed8..d8c23c82f7f 100644 --- a/app/assets/javascripts/performance_bar/services/performance_bar_service.js +++ b/app/assets/javascripts/performance_bar/services/performance_bar_service.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import _ from 'underscore'; import axios from '../../lib/utils/axios_utils'; +import { parseBoolean } from '~/lib/utils/common_utils'; let vueResourceInterceptor; @@ -41,7 +42,8 @@ export default class PerformanceBarService { // Vue Resource. const requestUrl = (response.config || response).url; const apiRequest = requestUrl && requestUrl.match(/^\/api\//); - const cachedResponse = response.headers && response.headers['x-gitlab-from-cache'] === 'true'; + const cachedResponse = + response.headers && parseBoolean(response.headers['x-gitlab-from-cache']); const fireCallback = requestUrl !== peekUrl && requestId && !apiRequest && !cachedResponse; return [fireCallback, requestId, requestUrl]; diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js index 8704a655b28..deacff5abe7 100644 --- a/app/assets/javascripts/profile/profile.js +++ b/app/assets/javascripts/profile/profile.js @@ -1,6 +1,7 @@ import $ from 'jquery'; import axios from '~/lib/utils/axios_utils'; import flash from '../flash'; +import { parseBoolean } from '~/lib/utils/common_utils'; export default class Profile { constructor({ form } = {}) { @@ -80,7 +81,7 @@ export default class Profile { setRepoRadio() { const multiEditRadios = $('input[name="user[multi_file]"]'); - if (this.newRepoActivated || this.newRepoActivated === 'true') { + if (parseBoolean(this.newRepoActivated)) { multiEditRadios.filter('[value=on]').prop('checked', true); } else { multiEditRadios.filter('[value=off]').prop('checked', true); diff --git a/app/assets/javascripts/toggle_buttons.js b/app/assets/javascripts/toggle_buttons.js index d83ffc7e211..bcb44bf7acf 100644 --- a/app/assets/javascripts/toggle_buttons.js +++ b/app/assets/javascripts/toggle_buttons.js @@ -1,7 +1,7 @@ import $ from 'jquery'; import Flash from './flash'; import { __ } from './locale'; -import { convertPermissionToBoolean } from './lib/utils/common_utils'; +import { parseBoolean } from './lib/utils/common_utils'; /* example HAML: @@ -18,7 +18,7 @@ function updateToggle(toggle, isOn) { } function onToggleClicked(toggle, input, clickCallback) { - const previousIsOn = convertPermissionToBoolean(input.value); + const previousIsOn = parseBoolean(input.value); // Visually change the toggle and start loading updateToggle(toggle, !previousIsOn); @@ -51,7 +51,7 @@ export default function setupToggleButtons(container, clickCallback = () => {}) toggles.forEach(toggle => { const input = toggle.querySelector('.js-project-feature-toggle-input'); - const isOn = convertPermissionToBoolean(input.value); + const isOn = parseBoolean(input.value); // Get the visible toggle in sync with the hidden input updateToggle(toggle, isOn); diff --git a/app/assets/javascripts/usage_ping_consent.js b/app/assets/javascripts/usage_ping_consent.js index 05607f09a7e..d3d745a3c11 100644 --- a/app/assets/javascripts/usage_ping_consent.js +++ b/app/assets/javascripts/usage_ping_consent.js @@ -1,7 +1,7 @@ import $ from 'jquery'; import axios from './lib/utils/axios_utils'; import Flash, { hideFlash } from './flash'; -import { convertPermissionToBoolean } from './lib/utils/common_utils'; +import { parseBoolean } from './lib/utils/common_utils'; export default () => { $('body').on('click', '.js-usage-consent-action', e => { @@ -11,8 +11,8 @@ export default () => { const { url, checkEnabled, pingEnabled } = e.target.dataset; const data = { application_setting: { - version_check_enabled: convertPermissionToBoolean(checkEnabled), - usage_ping_enabled: convertPermissionToBoolean(pingEnabled), + version_check_enabled: parseBoolean(checkEnabled), + usage_ping_enabled: parseBoolean(pingEnabled), }, }; diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index dca89981d81..ce5d36a340f 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -802,11 +802,7 @@ @include media-breakpoint-down(xs) { .navbar-gitlab { - li.header-projects, - li.header-groups, - li.header-more, - li.header-new, - li.header-user { + li.dropdown { position: static; } } diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss index 7a4c3914fb0..afa85f0e4ae 100644 --- a/app/assets/stylesheets/framework/flash.scss +++ b/app/assets/stylesheets/framework/flash.scss @@ -32,16 +32,16 @@ margin: 0; } + .flash-text, + .flash-action { + display: inline-block; + } + .flash-alert { @extend .alert; background-color: $red-500; margin: 0; - .flash-text, - .flash-action { - display: inline-block; - } - .flash-action { margin-left: 5px; text-decoration: none; diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index d1ce3a582bb..39410ac56af 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -110,6 +110,10 @@ } } + .navbar-collapse > ul.nav > li:not(.d-none) { + margin: 0 2px; + } + &.menu-expanded { @include media-breakpoint-down(xs) { .title-container { @@ -117,7 +121,7 @@ } .navbar-collapse { - display: block; + display: flex; } } } @@ -209,7 +213,7 @@ > a { will-change: color; - margin: 4px 2px; + margin: 4px 0; padding: 6px 8px; height: 32px; @@ -455,14 +459,11 @@ color: $indigo-900; font-weight: $gl-font-weight-bold; line-height: 18px; + margin: 4px 0 4px 2px; &:hover { background-color: $white-light; } - - @include media-breakpoint-down(xs) { - margin-top: $gl-padding-4; - } } .navbar-nav { @@ -509,12 +510,7 @@ margin-right: -10px; .nav > li:not(.d-none) { - display: table-cell !important; - width: 25%; - - a { - margin-right: 8px; - } + flex: 1; } } } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 715af4aa4ba..5405f20a760 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -10,22 +10,32 @@ position: -webkit-sticky; position: sticky; top: 92px; + margin-left: -1px; + border-left: 1px solid $border-color; z-index: 102; + &.is-commit { + top: $header-height + 36px; + + .with-performance-bar & { + top: $header-height + 36px + $performance-bar-height; + } + } + &::before { content: ''; position: absolute; top: -1px; - left: -10px; + left: -11px; width: 10px; height: calc(100% + 1px); background: $white-light; - border-right: 1px solid $border-color; + pointer-events: none; } - } - .with-performance-bar & { - top: 127px; + .with-performance-bar & { + top: 127px; + } } a:hover { @@ -701,15 +711,14 @@ } @include media-breakpoint-up(sm) { - top: 24px; + position: -webkit-sticky; + position: sticky; + top: $header-height; background-color: $white-light; + z-index: 200; - &.diff-files-changed-merge-request { - position: sticky; - top: 90px; - z-index: 200; - margin: $gl-padding 0; - padding: 0; + .with-performance-bar & { + top: $header-height + $performance-bar-height; } &.is-stuck { @@ -734,14 +743,6 @@ } } -@include media-breakpoint-up(sm) { - .with-performance-bar { - .diff-files-changed.diff-files-changed-merge-request { - top: 76px + $performance-bar-height; - } - } -} - .diff-file-changes { max-width: 560px; width: 100%; diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 347fcad771a..75166ffcada 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -203,21 +203,6 @@ stroke: $gray-darkest; } -.prometheus-state { - max-width: 460px; - margin: 10px auto; - text-align: center; - - .state-svg { - max-width: 80vw; - margin: 0 auto; - } - - .state-button { - padding: $gl-padding / 2; - } -} - .prometheus-graphs { .environments { .dropdown-menu-toggle { diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 78c5ae9ae63..5b5f486ea63 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -938,3 +938,37 @@ } } } + +.issuable-suggestions svg { + vertical-align: sub; +} + +.suggestion-item a { + color: initial; +} + +.suggestion-confidential { + color: $orange-600; +} + +.suggestion-state-open { + color: $green-500; +} + +.suggestion-state-closed { + color: $blue-500; +} + +.suggestion-help-hover { + cursor: help; +} + +.suggestion-footer { + font-size: 12px; + line-height: 15px; + + .avatar { + margin-top: -3px; + border: 0; + } +} diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 085ff27e6ef..4fda2964fd5 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -393,6 +393,14 @@ $note-form-margin-left: 72px; border-top: 1px solid $border-color; border-radius: 0; + @media (min-width: map-get($grid-breakpoints, md)) { + top: 91px; + + .with-performance-bar & { + top: 126px; + } + } + &:hover { background-color: $gray-light; } diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 308f666394c..d6d7110355b 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -38,6 +38,8 @@ class Projects::IssuesController < Projects::ApplicationController # Allow create a new branch and empty WIP merge request from current issue before_action :authorize_create_merge_request_from!, only: [:create_merge_request] + before_action :set_suggested_issues_feature_flags, only: [:new] + respond_to :html def index @@ -263,4 +265,9 @@ class Projects::IssuesController < Projects::ApplicationController # 3. https://gitlab.com/gitlab-org/gitlab-ce/issues/42426 Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42422') end + + def set_suggested_issues_feature_flags + push_frontend_feature_flag(:graphql) + push_frontend_feature_flag(:issue_suggestions) + end end diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb new file mode 100644 index 00000000000..4ab3c13787a --- /dev/null +++ b/app/graphql/resolvers/issues_resolver.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Resolvers + class IssuesResolver < BaseResolver + extend ActiveSupport::Concern + + argument :search, GraphQL::STRING_TYPE, + required: false + argument :sort, Types::Sort, + required: false, + default_value: 'created_desc' + + type Types::IssueType, null: true + + alias_method :project, :object + + def resolve(**args) + # Will need to be be made group & namespace aware with + # https://gitlab.com/gitlab-org/gitlab-ce/issues/54520 + args[:project_id] = project.id + + IssuesFinder.new(context[:current_user], args).execute + end + end +end diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb new file mode 100644 index 00000000000..a8f2f7914a8 --- /dev/null +++ b/app/graphql/types/issue_type.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Types + class IssueType < BaseObject + expose_permissions Types::PermissionTypes::Issue + + graphql_name 'Issue' + + present_using IssuePresenter + + field :iid, GraphQL::ID_TYPE, null: false + field :title, GraphQL::STRING_TYPE, null: false + field :description, GraphQL::STRING_TYPE, null: true + field :state, GraphQL::STRING_TYPE, null: false + + field :author, Types::UserType, + null: false, + resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, obj.author_id).find } do + authorize :read_user + end + + field :assignees, Types::UserType.connection_type, null: true + + field :labels, Types::LabelType.connection_type, null: true + field :milestone, Types::MilestoneType, + null: true, + resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Milestone, obj.milestone_id).find } do + authorize :read_milestone + end + + field :due_date, Types::TimeType, null: true + field :confidential, GraphQL::BOOLEAN_TYPE, null: false + field :discussion_locked, GraphQL::BOOLEAN_TYPE, + null: false, + resolve: -> (obj, _args, _ctx) { !!obj.discussion_locked } + + field :upvotes, GraphQL::INT_TYPE, null: false + field :downvotes, GraphQL::INT_TYPE, null: false + field :user_notes_count, GraphQL::INT_TYPE, null: false + field :web_url, GraphQL::STRING_TYPE, null: false + + field :closed_at, Types::TimeType, null: true + + field :created_at, Types::TimeType, null: false + field :updated_at, Types::TimeType, null: false + end +end diff --git a/app/graphql/types/label_type.rb b/app/graphql/types/label_type.rb new file mode 100644 index 00000000000..ccd466edc1a --- /dev/null +++ b/app/graphql/types/label_type.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Types + class LabelType < BaseObject + graphql_name 'Label' + + field :description, GraphQL::STRING_TYPE, null: true + field :title, GraphQL::STRING_TYPE, null: false + field :color, GraphQL::STRING_TYPE, null: false + field :text_color, GraphQL::STRING_TYPE, null: false + end +end diff --git a/app/graphql/types/milestone_type.rb b/app/graphql/types/milestone_type.rb new file mode 100644 index 00000000000..af31b572c9a --- /dev/null +++ b/app/graphql/types/milestone_type.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Types + class MilestoneType < BaseObject + graphql_name 'Milestone' + + field :description, GraphQL::STRING_TYPE, null: true + field :title, GraphQL::STRING_TYPE, null: false + field :state, GraphQL::STRING_TYPE, null: false + + field :due_date, Types::TimeType, null: true + field :start_date, Types::TimeType, null: true + + field :created_at, Types::TimeType, null: false + field :updated_at, Types::TimeType, null: false + end +end diff --git a/app/graphql/types/order.rb b/app/graphql/types/order.rb new file mode 100644 index 00000000000..c5e1cc406b4 --- /dev/null +++ b/app/graphql/types/order.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Types + class Types::Order < Types::BaseEnum + value "id", "Created at date" + value "updated_at", "Updated at date" + end +end diff --git a/app/graphql/types/permission_types/issue.rb b/app/graphql/types/permission_types/issue.rb new file mode 100644 index 00000000000..199540c7d6d --- /dev/null +++ b/app/graphql/types/permission_types/issue.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Types + module PermissionTypes + class Issue < BasePermissionType + description 'Check permissions for the current user on a issue' + graphql_name 'IssuePermissions' + + abilities :read_issue, :admin_issue, + :update_issue, :create_note, + :reopen_issue + end + end +end diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index 7b879608b34..050706f97be 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -73,6 +73,11 @@ module Types authorize :read_merge_request end + field :issues, + Types::IssueType.connection_type, + null: true, + resolver: Resolvers::IssuesResolver + field :pipelines, Types::Ci::PipelineType.connection_type, null: false, diff --git a/app/graphql/types/sort.rb b/app/graphql/types/sort.rb new file mode 100644 index 00000000000..1f756fdab69 --- /dev/null +++ b/app/graphql/types/sort.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Types + class Types::Sort < Types::BaseEnum + value "updated_desc", "Updated at descending order" + value "updated_asc", "Updated at ascending order" + value "created_desc", "Created at descending order" + value "created_asc", "Created at ascending order" + end +end diff --git a/app/graphql/types/user_type.rb b/app/graphql/types/user_type.rb new file mode 100644 index 00000000000..a13e65207df --- /dev/null +++ b/app/graphql/types/user_type.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Types + class UserType < BaseObject + graphql_name 'User' + + present_using UserPresenter + + field :name, GraphQL::STRING_TYPE, null: false + field :username, GraphQL::STRING_TYPE, null: false + field :avatar_url, GraphQL::STRING_TYPE, null: false + field :web_url, GraphQL::STRING_TYPE, null: false + end +end diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb index 108874b75a6..7c84bd734bb 100644 --- a/app/models/ci/build_trace_chunk.rb +++ b/app/models/ci/build_trace_chunk.rb @@ -76,7 +76,7 @@ module Ci raise ArgumentError, 'Offset is out of range' if offset > size || offset < 0 raise ArgumentError, 'Chunk size overflow' if CHUNK_SIZE < (offset + new_data.bytesize) - in_lock(*lock_params) do # Write opetation is atomic + in_lock(*lock_params) do # Write operation is atomic unsafe_set_data!(data.byteslice(0, offset) + new_data) end @@ -100,7 +100,7 @@ module Ci end def persist_data! - in_lock(*lock_params) do # Write opetation is atomic + in_lock(*lock_params) do # Write operation is atomic unsafe_persist_to!(self.class.persistable_store) end end diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb index bd0286ee3f9..8f8790585a3 100644 --- a/app/models/clusters/applications/ingress.rb +++ b/app/models/clusters/applications/ingress.rb @@ -51,6 +51,10 @@ module Clusters ClusterWaitForIngressIpAddressWorker.perform_async(name, id) end + + def ingress_service + cluster.kubeclient.get_service('ingress-nginx-ingress-controller', Gitlab::Kubernetes::Helm::NAMESPACE) + end end end end diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb index c66d5ce54db..c0aaa8dce20 100644 --- a/app/models/clusters/applications/knative.rb +++ b/app/models/clusters/applications/knative.rb @@ -6,9 +6,7 @@ module Clusters VERSION = '0.1.3'.freeze REPOSITORY = 'https://storage.googleapis.com/triggermesh-charts'.freeze - # This is required for helm version <= 2.10.x in order to support - # Setting up CRDs - ISTIO_CRDS = 'https://storage.googleapis.com/triggermesh-charts/istio-crds.yaml'.freeze + FETCH_IP_ADDRESS_DELAY = 30.seconds self.table_name = 'clusters_applications_knative' @@ -16,6 +14,16 @@ module Clusters include ::Clusters::Concerns::ApplicationStatus include ::Clusters::Concerns::ApplicationVersion include ::Clusters::Concerns::ApplicationData + include AfterCommitQueue + + state_machine :status do + before_transition any => [:installed] do |application| + application.run_after_commit do + ClusterWaitForIngressIpAddressWorker.perform_in( + FETCH_IP_ADDRESS_DELAY, application.name, application.id) + end + end + end default_value_for :version, VERSION @@ -36,19 +44,23 @@ module Clusters rbac: cluster.platform_kubernetes_rbac?, chart: chart, files: files, - repository: REPOSITORY, - preinstall: install_script + repository: REPOSITORY ) end - def client - cluster.platform_kubernetes.kubeclient.knative_client + def schedule_status_update + return unless installed? + return if external_ip + + ClusterWaitForIngressIpAddressWorker.perform_async(name, id) end - private + def ingress_service + cluster.kubeclient.get_service('knative-ingressgateway', 'istio-system') + end - def install_script - ["/usr/bin/kubectl apply -f #{ISTIO_CRDS}"] + def client + cluster.platform_kubernetes.kubeclient.knative_client end end end diff --git a/app/models/project.rb b/app/models/project.rb index 4d1917b9ab2..39978d8a4c4 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -88,9 +88,6 @@ class Project < ActiveRecord::Base after_create :create_project_feature, unless: :project_feature - after_create -> { SiteStatistic.track(STATISTICS_ATTRIBUTE) } - before_destroy -> { SiteStatistic.untrack(STATISTICS_ATTRIBUTE) } - after_create :create_ci_cd_settings, unless: :ci_cd_settings, if: proc { ProjectCiCdSetting.available? } @@ -1394,7 +1391,7 @@ class Project < ActiveRecord::Base def change_head(branch) if repository.branch_exists?(branch) repository.before_change_head - repository.raw_repository.write_ref('HEAD', "refs/heads/#{branch}", shell: false) + repository.raw_repository.write_ref('HEAD', "refs/heads/#{branch}") repository.copy_gitattributes(branch) repository.after_change_head reload_default_branch diff --git a/app/models/repository.rb b/app/models/repository.rb index a77fa8f2ce7..427dac99b79 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -259,7 +259,7 @@ class Repository next if kept_around?(sha) # This will still fail if the file is corrupted (e.g. 0 bytes) - raw_repository.write_ref(keep_around_ref_name(sha), sha, shell: false) + raw_repository.write_ref(keep_around_ref_name(sha), sha) rescue Gitlab::Git::CommandError => ex Rails.logger.error "Unable to create keep-around reference for repository #{disk_path}: #{ex}" end diff --git a/app/models/site_statistic.rb b/app/models/site_statistic.rb deleted file mode 100644 index 3a7912ed53a..00000000000 --- a/app/models/site_statistic.rb +++ /dev/null @@ -1,76 +0,0 @@ -# frozen_string_literal: true - -class SiteStatistic < ActiveRecord::Base - # prevents the creation of multiple rows - default_value_for :id, 1 - - COUNTER_ATTRIBUTES = %w(repositories_count).freeze - REQUIRED_SCHEMA_VERSION = 20180629153018 - - # Tracks specific attribute - # - # @param [String] raw_attribute must be one of the values listed in COUNTER_ATTRIBUTES - def self.track(raw_attribute) - with_statistics_available(raw_attribute) do |attribute| - SiteStatistic.update_all(["#{attribute} = #{attribute}+1"]) - end - end - - # Untracks specific attribute - # - # @param [String] raw_attribute must be one of the values listed in COUNTER_ATTRIBUTES - def self.untrack(raw_attribute) - with_statistics_available(raw_attribute) do |attribute| - SiteStatistic.update_all(["#{attribute} = #{attribute}-1 WHERE #{attribute} > 0"]) - end - end - - # Wrapper for track/untrack operations with basic validations and enforced requirements - # - # @param [String] raw_attribute must be one of the values listed in COUNTER_ATTRIBUTES - # @yield [String] attribute quoted to be used inside SQL / Arel query - def self.with_statistics_available(raw_attribute) - unless raw_attribute.in?(COUNTER_ATTRIBUTES) - raise ArgumentError, "Invalid attribute: '#{raw_attribute}' to '#{caller_locations(1, 1)[0].label}' method. " \ - "Valid attributes are: #{COUNTER_ATTRIBUTES.join(', ')}" - end - - return unless available? - - self.fetch # make sure record exists - - attribute = self.connection.quote_column_name(raw_attribute) - - # will be running on its own transaction context - yield(attribute) - end - - # Returns a site statistic record with tracked information - # - # @return [SiteStatistic] record with tracked information - def self.fetch - transaction(requires_new: true) do - SiteStatistic.first_or_create! - end - rescue ActiveRecord::RecordNotUnique - retry - end - - # Return whether required schema change is available - # - # This is needed in order to degrade gracefully when testing schema migrations - # - # @return [Boolean] whether schema is available - def self.available? - @available_flag ||= ActiveRecord::Migrator.current_version >= REQUIRED_SCHEMA_VERSION - end - - # Resets cached column information - # - # This is called during schema migration specs, in order to reset internal cache state - def self.reset_column_information - @available_flag = nil - - super - end -end diff --git a/app/models/user.rb b/app/models/user.rb index 01eba7e0426..dbd754dd25a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -363,7 +363,7 @@ class User < ActiveRecord::Base from_users = from_users.confirmed if confirmed from_emails = joins(:emails).where(emails: { email: emails }) - from_emails = from_emails.confirmed if confirmed + from_emails = from_emails.confirmed.merge(Email.confirmed) if confirmed items = [from_users, from_emails] diff --git a/app/policies/milestone_policy.rb b/app/policies/milestone_policy.rb new file mode 100644 index 00000000000..ac4f5b08504 --- /dev/null +++ b/app/policies/milestone_policy.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class MilestonePolicy < BasePolicy + delegate { @subject.project } +end diff --git a/app/presenters/issue_presenter.rb b/app/presenters/issue_presenter.rb new file mode 100644 index 00000000000..c12a202efbc --- /dev/null +++ b/app/presenters/issue_presenter.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class IssuePresenter < Gitlab::View::Presenter::Delegated + presents :issue + + def web_url + Gitlab::UrlBuilder.build(issue) + end +end diff --git a/app/presenters/user_presenter.rb b/app/presenters/user_presenter.rb new file mode 100644 index 00000000000..14ef53e9ec8 --- /dev/null +++ b/app/presenters/user_presenter.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class UserPresenter < Gitlab::View::Presenter::Delegated + presents :user + + def web_url + Gitlab::Routing.url_helpers.user_url(user) + end +end diff --git a/app/services/clusters/applications/base_helm_service.rb b/app/services/clusters/applications/base_helm_service.rb index 270a8eb24f4..e86ca8cf1d0 100644 --- a/app/services/clusters/applications/base_helm_service.rb +++ b/app/services/clusters/applications/base_helm_service.rb @@ -11,6 +11,25 @@ module Clusters protected + def log_error(error) + meta = { + exception: error.class.name, + error_code: error.respond_to?(:error_code) ? error.error_code : nil, + service: self.class.name, + app_id: app.id, + project_ids: app.cluster.project_ids, + group_ids: app.cluster.group_ids, + message: error.message + } + + logger.error(meta) + Gitlab::Sentry.track_acceptable_exception(error, extra: meta) + end + + def logger + @logger ||= Gitlab::Kubernetes::Logger.build + end + def cluster app.cluster end diff --git a/app/services/clusters/applications/check_ingress_ip_address_service.rb b/app/services/clusters/applications/check_ingress_ip_address_service.rb index f32e73e8b1c..0ec06e776a7 100644 --- a/app/services/clusters/applications/check_ingress_ip_address_service.rb +++ b/app/services/clusters/applications/check_ingress_ip_address_service.rb @@ -30,7 +30,7 @@ module Clusters def service strong_memoize(:ingress_service) do - kubeclient.get_service('ingress-nginx-ingress-controller', Gitlab::Kubernetes::Helm::NAMESPACE) + app.ingress_service end end end diff --git a/app/services/clusters/applications/check_installation_progress_service.rb b/app/services/clusters/applications/check_installation_progress_service.rb index ca0f7b30053..21ec26ea233 100644 --- a/app/services/clusters/applications/check_installation_progress_service.rb +++ b/app/services/clusters/applications/check_installation_progress_service.rb @@ -15,8 +15,7 @@ module Clusters check_timeout end rescue Kubeclient::HttpError => e - Rails.logger.error("Kubernetes error: #{e.error_code} #{e.message}") - Gitlab::Sentry.track_acceptable_exception(e, extra: { scope: 'kubernetes', app_id: app.id }) + log_error(e) app.make_errored!("Kubernetes error: #{e.error_code}") unless app.errored? end @@ -29,17 +28,13 @@ module Clusters end def on_failed - app.make_errored!('Installation failed') - ensure - remove_installation_pod + app.make_errored!("Installation failed. Check pod logs for #{install_command.pod_name} for more details.") end def check_timeout if timeouted? begin - app.make_errored!('Installation timed out') - ensure - remove_installation_pod + app.make_errored!("Installation timed out. Check pod logs for #{install_command.pod_name} for more details.") end else ClusterWaitForAppInstallationWorker.perform_in( @@ -53,9 +48,6 @@ module Clusters def remove_installation_pod helm_api.delete_pod!(install_command.pod_name) - rescue => e - Rails.logger.error("Kubernetes error: #{e.class.name} #{e.message}") - # no-op end def installation_phase diff --git a/app/services/clusters/applications/install_service.rb b/app/services/clusters/applications/install_service.rb index f4385748c43..5a65dc4ef59 100644 --- a/app/services/clusters/applications/install_service.rb +++ b/app/services/clusters/applications/install_service.rb @@ -13,12 +13,10 @@ module Clusters ClusterWaitForAppInstallationWorker.perform_in( ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id) rescue Kubeclient::HttpError => e - Rails.logger.error("Kubernetes error: #{e.error_code} #{e.message}") - Gitlab::Sentry.track_acceptable_exception(e, extra: { scope: 'kubernetes', app_id: app.id }) + log_error(e) app.make_errored!("Kubernetes error: #{e.error_code}") rescue StandardError => e - Rails.logger.error "Can't start installation process: #{e.class.name} #{e.message}" - Gitlab::Sentry.track_acceptable_exception(e, extra: { scope: 'kubernetes', app_id: app.id }) + log_error(e) app.make_errored!("Can't start installation process.") end end diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 474ef25cef7..b7d69539eb7 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -71,7 +71,7 @@ = link_to admin_impersonation_path, class: 'nav-link impersonation-btn', method: :delete, title: _('Stop impersonation'), aria: { label: _('Stop impersonation') }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do = icon('user-secret') - if header_link?(:sign_in) - %li.nav-item.m-auto + %li.nav-item %div - sign_in_text = allow_signup? ? _('Sign in / Register') : _('Sign in') = link_to sign_in_text, new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in' diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml index e134f416c70..5cb8aebadb3 100644 --- a/app/views/layouts/header/_new_dropdown.haml +++ b/app/views/layouts/header/_new_dropdown.haml @@ -7,15 +7,14 @@ - if @group&.persisted? - create_group_project = can?(current_user, :create_projects, @group) - create_group_subgroup = can?(current_user, :create_subgroup, @group) + - if create_group_project || create_group_subgroup %li.dropdown-bold-header = _('This group') - if create_group_project - %li.header-new-group-project - = link_to _('New project'), new_project_path(namespace_id: @group.id) + %li= link_to _('New project'), new_project_path(namespace_id: @group.id) - if create_group_subgroup - %li - = link_to _('New subgroup'), new_group_path(parent_id: @group.id) + %li= link_to _('New subgroup'), new_group_path(parent_id: @group.id) %li.divider %li.dropdown-bold-header GitLab @@ -23,25 +22,20 @@ - create_project_issue = show_new_issue_link?(@project) - merge_project = merge_request_source_project_for_project(@project) - create_project_snippet = can?(current_user, :create_project_snippet, @project) + - if create_project_issue || merge_project || create_project_snippet %li.dropdown-bold-header = _('This project') - if create_project_issue - %li - = link_to _('New issue'), new_project_issue_path(@project) + %li= link_to _('New issue'), new_project_issue_path(@project) - if merge_project - %li - = link_to _('New merge request'), project_new_merge_request_path(merge_project) + %li= link_to _('New merge request'), project_new_merge_request_path(merge_project) - if create_project_snippet - %li.header-new-project-snippet - = link_to _('New snippet'), new_project_snippet_path(@project) + %li= link_to _('New snippet'), new_project_snippet_path(@project) %li.divider %li.dropdown-bold-header GitLab - if current_user.can_create_project? - %li - = link_to _('New project'), new_project_path, class: 'qa-global-new-project-link' + %li= link_to _('New project'), new_project_path, class: 'qa-global-new-project-link' - if current_user.can_create_group? - %li - = link_to _('New group'), new_group_path - %li - = link_to _('New snippet'), new_snippet_path + %li= link_to _('New group'), new_group_path + %li= link_to _('New snippet'), new_snippet_path diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml index 8181ee9eea1..af8887b0c39 100644 --- a/app/views/projects/branches/new.html.haml +++ b/app/views/projects/branches/new.html.haml @@ -13,7 +13,7 @@ .form-group.row = label_tag :branch_name, nil, class: 'col-form-label col-sm-2' .col-sm-10 - = text_field_tag :branch_name, params[:branch_name], required: true, autofocus: true, class: 'form-control js-branch-name' + = text_field_tag :branch_name, params[:branch_name], required: true, autofocus: true, class: 'form-control js-branch-name monospace' .form-text.text-muted.text-danger.js-branch-name-error .form-group.row = label_tag :ref, 'Create from', class: 'col-form-label col-sm-2' diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index 541ae905246..79e32949db9 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -13,7 +13,7 @@ = render "ci_menu" - else .block-connector - = render "projects/diffs/diffs", diffs: @diffs, environment: @environment + = render "projects/diffs/diffs", diffs: @diffs, environment: @environment, is_commit: true .limited-width-notes = render "shared/notes/notes_with_form", :autocomplete => true diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 9de3c2db6e7..cc2d0d3b2d8 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -2,9 +2,9 @@ - show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true) - can_create_note = !@diff_notes_disabled && can?(current_user, :create_note, diffs.project) - diff_files = diffs.diff_files -- merge_request = local_assigns.fetch(:merge_request, false) +- is_commit = local_assigns.fetch(:is_commit, false) -.content-block.oneline-block.files-changed.diff-files-changed.js-diff-files-changed{ class: ("diff-files-changed-merge-request" if merge_request) } +.content-block.oneline-block.files-changed.diff-files-changed.js-diff-files-changed .files-changed-inner .inline-parallel-buttons.d-none.d-sm-none.d-md-block - if !diffs_expanded? && diff_files.any? { |diff_file| diff_file.collapsed? } @@ -25,4 +25,4 @@ = render 'projects/diffs/warning', diff_files: diffs .files{ data: { can_create_note: can_create_note } } - = render partial: 'projects/diffs/file', collection: diff_files, as: :diff_file, locals: { project: diffs.project, environment: environment } + = render partial: 'projects/diffs/file', collection: diff_files, as: :diff_file, locals: { project: diffs.project, environment: environment, is_commit: is_commit } diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 1f90acaabcc..5565ae1d98b 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -1,10 +1,11 @@ - environment = local_assigns.fetch(:environment, nil) +- is_commit = local_assigns.fetch(:is_commit, false) - file_hash = hexdigest(diff_file.file_path) - image_diff = diff_file.rich_viewer && diff_file.rich_viewer.partial_name == 'image' - image_replaced = diff_file.old_content_sha && diff_file.old_content_sha != diff_file.content_sha .diff-file.file-holder{ id: file_hash, data: diff_file_html_data(project, diff_file.file_path, diff_file.content_sha) } - .js-file-title.file-title-flex-parent + .js-file-title.file-title-flex-parent{ class: is_commit ? "is-commit" : "" } .file-header-content = render "projects/diffs/file_header", diff_file: diff_file, url: "##{file_hash}" diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index 515499956a2..4ebb029e48b 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -21,7 +21,7 @@ window.gl.mrWidgetData = #{serialize_issuable(@merge_request, serializer: 'widget')} window.gl.mrWidgetData.squash_before_merge_help_path = '#{help_page_path("user/project/merge_requests/squash_and_merge")}'; - window.gl.mrWidgetData.troubleshooting_docs_path = '#{help_page_path('user/project/merge_requests', anchor: 'troubleshooting')}'; + window.gl.mrWidgetData.troubleshooting_docs_path = '#{help_page_path('user/project/merge_requests/index.md', anchor: 'troubleshooting')}'; #js-vue-mr-widget.mr-widget diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index eede8704564..10e3b01096a 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -4,10 +4,10 @@ - header_title "Projects", dashboard_projects_path - active_tab = local_assigns.fetch(:active_tab, 'blank') -.project-edit-container +.project-edit-container.prepend-top-default .project-edit-errors = render 'projects/errors' - .row.prepend-top-default + .row .col-lg-3.profile-settings-sidebar %h4.prepend-top-0 = _('New project') diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index b33c758b464..1618655182c 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -17,6 +17,8 @@ = render 'shared/issuable/form/template_selector', issuable: issuable = render 'shared/issuable/form/title', issuable: issuable, form: form, has_wip_commits: commits && commits.detect(&:work_in_progress?) +- if Feature.enabled?(:issue_suggestions) && Feature.enabled?(:graphql) + #js-suggestions{ data: { project_path: @project.full_path } } = render 'shared/form_elements/description', model: issuable, form: form, project: project diff --git a/changelogs/unreleased/51792-dont-delete-failed-install-pods.yml b/changelogs/unreleased/51792-dont-delete-failed-install-pods.yml new file mode 100644 index 00000000000..7a900cbb86e --- /dev/null +++ b/changelogs/unreleased/51792-dont-delete-failed-install-pods.yml @@ -0,0 +1,5 @@ +--- +title: Don't remove failed install pods after installing GitLab managed applications +merge_request: 23350 +author: +type: changed diff --git a/changelogs/unreleased/52276-jump-to-top-in-merge-request.yml b/changelogs/unreleased/52276-jump-to-top-in-merge-request.yml new file mode 100644 index 00000000000..3dc95441eec --- /dev/null +++ b/changelogs/unreleased/52276-jump-to-top-in-merge-request.yml @@ -0,0 +1,5 @@ +--- +title: Allow user to scroll to top of tab on MR page +merge_request: +author: +type: added diff --git a/changelogs/unreleased/52828-inconsistency-in-fonts-used-for-branch-name-and-create-from-fields-when-creating-new-branch-from-ui.yml b/changelogs/unreleased/52828-inconsistency-in-fonts-used-for-branch-name-and-create-from-fields-when-creating-new-branch-from-ui.yml new file mode 100644 index 00000000000..8132dde8636 --- /dev/null +++ b/changelogs/unreleased/52828-inconsistency-in-fonts-used-for-branch-name-and-create-from-fields-when-creating-new-branch-from-ui.yml @@ -0,0 +1,5 @@ +--- +title: Make new branch form fields' fonts consistent +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/53728-warn-in-web-editor-when-user-navigates-away.yml b/changelogs/unreleased/53728-warn-in-web-editor-when-user-navigates-away.yml new file mode 100644 index 00000000000..8377fdc6133 --- /dev/null +++ b/changelogs/unreleased/53728-warn-in-web-editor-when-user-navigates-away.yml @@ -0,0 +1,5 @@ +--- +title: Prevent user from navigating away from file edit without commit +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/53763-fix-encrypt-columns-data-loss.yml b/changelogs/unreleased/53763-fix-encrypt-columns-data-loss.yml new file mode 100644 index 00000000000..44362a8622e --- /dev/null +++ b/changelogs/unreleased/53763-fix-encrypt-columns-data-loss.yml @@ -0,0 +1,5 @@ +--- +title: Correctly handle data-loss scenarios when encrypting columns +merge_request: 23306 +author: +type: fixed diff --git a/changelogs/unreleased/53778-remove-site-statistics.yml b/changelogs/unreleased/53778-remove-site-statistics.yml new file mode 100644 index 00000000000..fe006e43671 --- /dev/null +++ b/changelogs/unreleased/53778-remove-site-statistics.yml @@ -0,0 +1,5 @@ +--- +title: Removed Site Statistics optimization as it was causing problems +merge_request: 23314 +author: +type: removed diff --git a/changelogs/unreleased/53874-navbar-lowres.yml b/changelogs/unreleased/53874-navbar-lowres.yml new file mode 100644 index 00000000000..3b31b8f93fe --- /dev/null +++ b/changelogs/unreleased/53874-navbar-lowres.yml @@ -0,0 +1,5 @@ +--- +title: "Fix overlapping navbar separator and overflowing navbar dropdown on small displays" +merge_request: 23126 +author: Thomas Pathier +type: fix diff --git a/changelogs/unreleased/53988-remove-notes-index-on-updated-at.yml b/changelogs/unreleased/53988-remove-notes-index-on-updated-at.yml new file mode 100644 index 00000000000..f0bbf69736d --- /dev/null +++ b/changelogs/unreleased/53988-remove-notes-index-on-updated-at.yml @@ -0,0 +1,5 @@ +--- +title: Remove index for notes on updated_at +merge_request: 23356 +author: +type: performance diff --git a/changelogs/unreleased/_acet-fix-flash-styling.yml b/changelogs/unreleased/_acet-fix-flash-styling.yml new file mode 100644 index 00000000000..57354c04899 --- /dev/null +++ b/changelogs/unreleased/_acet-fix-flash-styling.yml @@ -0,0 +1,5 @@ +--- +title: Fix flash notice styling for fluid layout +merge_request: 23382 +author: +type: fixed diff --git a/changelogs/unreleased/bvl-use-shell-writeref.yml b/changelogs/unreleased/bvl-use-shell-writeref.yml new file mode 100644 index 00000000000..682d428e8c5 --- /dev/null +++ b/changelogs/unreleased/bvl-use-shell-writeref.yml @@ -0,0 +1,5 @@ +--- +title: Avoid creating invalid refs using rugged, shelling out for writing refs +merge_request: 23286 +author: +type: fixed diff --git a/changelogs/unreleased/ce-54109-fix_user_by_any_email.yml b/changelogs/unreleased/ce-54109-fix_user_by_any_email.yml new file mode 100644 index 00000000000..eb5d2e3244c --- /dev/null +++ b/changelogs/unreleased/ce-54109-fix_user_by_any_email.yml @@ -0,0 +1,5 @@ +--- +title: Respect confirmed flag on secondary emails +merge_request: 23181 +author: +type: fixed diff --git a/changelogs/unreleased/gt-update-env-metrics-empty-state.yml b/changelogs/unreleased/gt-update-env-metrics-empty-state.yml new file mode 100644 index 00000000000..a05dc07e65c --- /dev/null +++ b/changelogs/unreleased/gt-update-env-metrics-empty-state.yml @@ -0,0 +1,5 @@ +--- +title: Update environments metrics empty state +merge_request: 23074 +author: George Tsiolis +type: changed diff --git a/changelogs/unreleased/lock-trace-writes.yml b/changelogs/unreleased/lock-trace-writes.yml new file mode 100644 index 00000000000..9c5239081b9 --- /dev/null +++ b/changelogs/unreleased/lock-trace-writes.yml @@ -0,0 +1,5 @@ +--- +title: Lock writes to trace stream +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/sh-handle-string-null-bytes.yml b/changelogs/unreleased/sh-handle-string-null-bytes.yml new file mode 100644 index 00000000000..edc045274e3 --- /dev/null +++ b/changelogs/unreleased/sh-handle-string-null-bytes.yml @@ -0,0 +1,5 @@ +--- +title: Gracefully handle references with null bytes +merge_request: 23365 +author: +type: fixed diff --git a/changelogs/unreleased/triggermesh-phase2-external-ip.yml b/changelogs/unreleased/triggermesh-phase2-external-ip.yml new file mode 100644 index 00000000000..582c8f6df2e --- /dev/null +++ b/changelogs/unreleased/triggermesh-phase2-external-ip.yml @@ -0,0 +1,5 @@ +--- +title: Add an external IP address to the knative cluster application page +merge_request: +author: Chris Baumbauer +type: fixed diff --git a/changelogs/unreleased/triggermesh-phase2-knative-description.yml b/changelogs/unreleased/triggermesh-phase2-knative-description.yml new file mode 100644 index 00000000000..c6cee1984d5 --- /dev/null +++ b/changelogs/unreleased/triggermesh-phase2-knative-description.yml @@ -0,0 +1,5 @@ +--- +title: Modify the wording for the knative cluster application to match upstream +merge_request: 23289 +author: Chris Baumbauer +type: fixed diff --git a/changelogs/unreleased/upgrade_kubeclient_400.yml b/changelogs/unreleased/upgrade_kubeclient_400.yml new file mode 100644 index 00000000000..edb38710e6a --- /dev/null +++ b/changelogs/unreleased/upgrade_kubeclient_400.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade kubeclient to 4.0.0 +merge_request: 23261 +author: Praveen Arimbrathodiyil @pravi +type: other diff --git a/config/initializers/attr_encrypted_no_db_connection.rb b/config/initializers/attr_encrypted_no_db_connection.rb index e007666b852..7ad458929db 100644 --- a/config/initializers/attr_encrypted_no_db_connection.rb +++ b/config/initializers/attr_encrypted_no_db_connection.rb @@ -1,7 +1,18 @@ module AttrEncrypted module Adapters module ActiveRecord - module DBConnectionQuerier + module GitlabMonkeyPatches + # Prevent attr_encrypted from defining virtual accessors for encryption + # data when the code and schema are out of sync. See this issue for more + # details: https://github.com/attr-encrypted/attr_encrypted/issues/332 + def attribute_instance_methods_as_symbols_available? + false + end + + # Prevent attr_encrypted from checking out a database connection + # indefinitely. The result of this method is only used when the former + # is true, but it is called unconditionally, so there is still value to + # ensuring the connection is released def attribute_instance_methods_as_symbols # Use with_connection so the connection doesn't stay pinned to the thread. connected = ::ActiveRecord::Base.connection_pool.with_connection(&:active?) rescue false @@ -15,7 +26,16 @@ module AttrEncrypted end end end - prepend DBConnectionQuerier end end end + +# As of v3.1.0, the attr_encrypted gem defines the AttrEncrypted and +# AttrEncrypted::Adapters::ActiveRecord modules, and uses "extend" to mix them +# into the ActiveRecord::Base class. This intervention overrides utility methods +# defined by attr_encrypted to fix two bugs, as detailed above. +# +# The methods are used here: https://github.com/attr-encrypted/attr_encrypted/blob/3.1.0/lib/attr_encrypted.rb#L145-158 +ActiveSupport.on_load(:active_record) do + extend AttrEncrypted::Adapters::ActiveRecord::GitlabMonkeyPatches +end diff --git a/config/initializers/kubeclient.rb b/config/initializers/kubeclient.rb index 2d9f439fdc0..f8fe1156aaa 100644 --- a/config/initializers/kubeclient.rb +++ b/config/initializers/kubeclient.rb @@ -1,19 +1,4 @@ class Kubeclient::Client - # We need to monkey patch this method until - # https://github.com/abonas/kubeclient/pull/323 is merged - def proxy_url(kind, name, port, namespace = '') - discover unless @discovered - entity_name_plural = - if %w[services pods nodes].include?(kind.to_s) - kind.to_s - else - @entities[kind.to_s].resource_name - end - - ns_prefix = build_namespace_prefix(namespace) - rest_client["#{ns_prefix}#{entity_name_plural}/#{name}:#{port}/proxy"].url - end - # Monkey patch to set `max_redirects: 0`, so that kubeclient # does not follow redirects and expose internal services. # See https://gitlab.com/gitlab-org/gitlab-ce/issues/53158 diff --git a/config/webpack.config.js b/config/webpack.config.js index 9ecae9790fd..b9044e13f50 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -84,7 +84,7 @@ module.exports = { }, resolve: { - extensions: ['.js'], + extensions: ['.js', '.gql', '.graphql'], alias: { '~': path.join(ROOT_PATH, 'app/assets/javascripts'), emojis: path.join(ROOT_PATH, 'fixtures/emojis'), @@ -101,6 +101,11 @@ module.exports = { strictExportPresence: true, rules: [ { + type: 'javascript/auto', + test: /\.mjs$/, + use: [], + }, + { test: /\.js$/, exclude: path => /node_modules|vendor[\\/]assets/.test(path) && !/\.vue\.js/.test(path), loader: 'babel-loader', @@ -122,6 +127,11 @@ module.exports = { }, }, { + test: /\.(graphql|gql)$/, + exclude: /node_modules/, + loader: 'graphql-tag/loader', + }, + { test: /\.svg$/, loader: 'raw-loader', }, diff --git a/db/migrate/20181116050532_knative_external_ip.rb b/db/migrate/20181116050532_knative_external_ip.rb new file mode 100644 index 00000000000..f1f903fb692 --- /dev/null +++ b/db/migrate/20181116050532_knative_external_ip.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class KnativeExternalIp < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :clusters_applications_knative, :external_ip, :string + end +end diff --git a/db/migrate/20181123042307_drop_site_statistics.rb b/db/migrate/20181123042307_drop_site_statistics.rb new file mode 100644 index 00000000000..8986374ef65 --- /dev/null +++ b/db/migrate/20181123042307_drop_site_statistics.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class DropSiteStatistics < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + drop_table :site_statistics + end + + def down + create_table :site_statistics do |t| + t.integer :repositories_count, default: 0, null: false + end + + execute('INSERT INTO site_statistics (id) VALUES(1)') + end +end diff --git a/db/migrate/20181126153547_remove_notes_index_on_updated_at.rb b/db/migrate/20181126153547_remove_notes_index_on_updated_at.rb new file mode 100644 index 00000000000..d7ca46b50e4 --- /dev/null +++ b/db/migrate/20181126153547_remove_notes_index_on_updated_at.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveNotesIndexOnUpdatedAt < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + remove_concurrent_index(*index_arguments) + end + + def down + add_concurrent_index(*index_arguments) + end + + private + + def index_arguments + [ + :notes, + [:updated_at], + { + name: 'index_notes_on_updated_at' + } + ] + end +end diff --git a/db/schema.rb b/db/schema.rb index acabd7b442b..9c9c19aa897 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20181126150622) do +ActiveRecord::Schema.define(version: 20181126153547) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -698,6 +698,7 @@ ActiveRecord::Schema.define(version: 20181126150622) do t.string "version", null: false t.string "hostname" t.text "status_reason" + t.string "external_ip" t.index ["cluster_id"], name: "index_clusters_applications_knative_on_cluster_id", unique: true, using: :btree end @@ -1392,7 +1393,6 @@ ActiveRecord::Schema.define(version: 20181126150622) do t.index ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree t.index ["noteable_type"], name: "index_notes_on_noteable_type", using: :btree t.index ["project_id", "noteable_type"], name: "index_notes_on_project_id_and_noteable_type", using: :btree - t.index ["updated_at"], name: "index_notes_on_updated_at", using: :btree end create_table "notification_settings", force: :cascade do |t| @@ -1885,10 +1885,6 @@ ActiveRecord::Schema.define(version: 20181126150622) do t.index ["name"], name: "index_shards_on_name", unique: true, using: :btree end - create_table "site_statistics", force: :cascade do |t| - t.integer "repositories_count", default: 0, null: false - end - create_table "snippets", force: :cascade do |t| t.string "title" t.text "content" diff --git a/doc/README.md b/doc/README.md index c1971b2b905..bf93c73843f 100644 --- a/doc/README.md +++ b/doc/README.md @@ -58,7 +58,7 @@ The following sections provide links to documentation for each DevOps stage: | [Release](#release) | Application release and delivery features. | | [Configure](#configure) | Application and infrastructure configuration tools. | | [Monitor](#monitor) | Application monitoring and metrics features. | -| [Secure](#secure) | Security capability feature. | +| [Secure](#secure) | Security capability features. | <div align="right"> <a type="button" class="btn btn-default" href="#overview"> diff --git a/doc/administration/high_availability/gitlab.md b/doc/administration/high_availability/gitlab.md index f16ae835ced..2ca860bd763 100644 --- a/doc/administration/high_availability/gitlab.md +++ b/doc/administration/high_availability/gitlab.md @@ -118,7 +118,7 @@ need some extra configuration. gitlab_rails['db_key_base'] = 'bf2e47b68d6cafaef1d767e628b619365becf27571e10f196f98dc85e7771042b9203199d39aff91fcb6837c8ed83f2a912b278da50999bb11a2fbc0fba52964' ``` -1. Run `touch /etc/gitlab/skip-auto-migrations` to prevent database migrations +1. Run `touch /etc/gitlab/skip-auto-reconfigure` to prevent database migrations from running on upgrade. Only the primary GitLab application server should handle migrations. diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md index a9ba40c870c..833c1f367dd 100644 --- a/doc/administration/high_availability/redis.md +++ b/doc/administration/high_availability/redis.md @@ -336,7 +336,7 @@ The prerequisites for a HA Redis setup are the following: 1. To prevent database migrations from running on upgrade, run: ``` - sudo touch /etc/gitlab/skip-auto-migrations + sudo touch /etc/gitlab/skip-auto-reconfigure ``` Only the primary GitLab application server should handle migrations. @@ -458,7 +458,7 @@ multiple machines with the Sentinel daemon. 1. To prevent database migrations from running on upgrade, run: ``` - sudo touch /etc/gitlab/skip-auto-migrations + sudo touch /etc/gitlab/skip-auto-reconfigure ``` Only the primary GitLab application server should handle migrations. diff --git a/doc/administration/logs.md b/doc/administration/logs.md index 7e5a3eb9ccd..698f4caab3a 100644 --- a/doc/administration/logs.md +++ b/doc/administration/logs.md @@ -126,6 +126,25 @@ It contains information about [integrations](../user/project/integrations/projec {"severity":"INFO","time":"2018-09-06T17:15:16.365Z","service_class":"JiraService","project_id":3,"project_path":"namespace2/project2","message":"Successfully posted","client_url":"http://jira.example.net"} ``` +## `kubernetes.log` + +Introduced in GitLab 11.6. This file lives in +`/var/log/gitlab/gitlab-rails/kubernetes.log` for Omnibus GitLab +packages or in `/home/git/gitlab/log/kubernetes.log` for +installations from source. + +It logs information related to the Kubernetes Integration including errors +during installing cluster applications on your GitLab managed Kubernetes +clusters. + +Each line contains a JSON line that can be ingested by Elasticsearch, Splunk, +etc. For example: + +```json +{"severity":"ERROR","time":"2018-11-23T15:14:54.652Z","exception":"Kubeclient::HttpError","error_code":401,"service":"Clusters::Applications::CheckInstallationProgressService","app_id":14,"project_ids":[1],"group_ids":[],"message":"Unauthorized"} +{"severity":"ERROR","time":"2018-11-23T15:42:11.647Z","exception":"Kubeclient::HttpError","error_code":null,"service":"Clusters::Applications::InstallService","app_id":2,"project_ids":[19],"group_ids":[],"message":"SSL_connect returned=1 errno=0 state=error: certificate verify failed (unable to get local issuer certificate)"} +``` + ## `githost.log` This file lives in `/var/log/gitlab/gitlab-rails/githost.log` for diff --git a/doc/ci/interactive_web_terminal/index.md b/doc/ci/interactive_web_terminal/index.md index 1ddc1bf4d7e..2c799e83a5f 100644 --- a/doc/ci/interactive_web_terminal/index.md +++ b/doc/ci/interactive_web_terminal/index.md @@ -1,4 +1,4 @@ -# Getting started with interactive web terminals +# Getting started with interactive web terminals **[CORE ONLY]** > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/50144) in GitLab 11.3. @@ -6,10 +6,9 @@ Interactive web terminals give the user access to a terminal in GitLab for running one-off commands for their CI pipeline. NOTE: **Note:** -This is not available for the shared Runners on GitLab.com. -To make use of this feature, you need to provide your -[own Runner](https://docs.gitlab.com/runner/install/) and properly -[configure it](#configuration). +GitLab.com does not support interactive web terminal at the moment. Please +follow [this issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/52611) for +progress. ## Configuration diff --git a/doc/development/db_dump.md b/doc/development/db_dump.md index e4ff72aa349..97762a62a80 100644 --- a/doc/development/db_dump.md +++ b/doc/development/db_dump.md @@ -13,7 +13,7 @@ large database imports. ``` # On STAGING echo "postgresql['checkpoint_segments'] = 64" | sudo tee -a /etc/gitlab/gitlab.rb -sudo touch /etc/gitlab/skip-auto-migrations +sudo touch /etc/gitlab/skip-auto-reconfigure sudo gitlab-ctl reconfigure sudo gitlab-ctl stop unicorn sudo gitlab-ctl stop sidekiq diff --git a/doc/development/logging.md b/doc/development/logging.md index abd08c420da..5c1d96b9e0c 100644 --- a/doc/development/logging.md +++ b/doc/development/logging.md @@ -75,7 +75,7 @@ To create a new file: module Import class Logger < ::Gitlab::JsonLogger def self.file_name_noext - 'importer_json' + 'importer' end end end @@ -105,7 +105,7 @@ To create a new file: ```ruby # GOOD - logger.info("Unable to create project", project_id: project.id) + logger.info(message: "Unable to create project", project_id: project.id) ``` 1. Be sure to create a common base structure of your log messages. For example, @@ -118,13 +118,13 @@ To create a new file: ```ruby # BAD - logger.info("Import error", error: 1) - logger.info("Import error", error: "I/O failure") + logger.info(message: "Import error", error: 1) + logger.info(message: "Import error", error: "I/O failure") ``` ```ruby # GOOD - logger.info("Import error", error_code: 1, error: "I/O failure") + logger.info(message: "Import error", error_code: 1, error: "I/O failure") ``` ## Additional steps with new log files diff --git a/doc/development/ui_guide.md b/doc/development/ui_guide.md index cec937da99b..1e84bf608f4 100644 --- a/doc/development/ui_guide.md +++ b/doc/development/ui_guide.md @@ -2,4 +2,4 @@ redirect_to: 'https://design.gitlab.com/' --- -The content of this documented was moved into the [GitLab Design System](https://design.gitlab.com/). +The content of this document was moved into the [GitLab Design System](https://design.gitlab.com/). diff --git a/doc/development/ux_guide/animation.md b/doc/development/ux_guide/animation.md index 3ebbd87d05f..583ff19bc69 100644 --- a/doc/development/ux_guide/animation.md +++ b/doc/development/ux_guide/animation.md @@ -2,4 +2,4 @@ redirect_to: 'https://design.gitlab.com/foundations/motion' --- -The content of this documented was moved into the [GitLab Design System](https://design.gitlab.com). +The content of this document was moved into the [GitLab Design System](https://design.gitlab.com). diff --git a/doc/development/ux_guide/basics.md b/doc/development/ux_guide/basics.md index cec937da99b..1e84bf608f4 100644 --- a/doc/development/ux_guide/basics.md +++ b/doc/development/ux_guide/basics.md @@ -2,4 +2,4 @@ redirect_to: 'https://design.gitlab.com/' --- -The content of this documented was moved into the [GitLab Design System](https://design.gitlab.com/). +The content of this document was moved into the [GitLab Design System](https://design.gitlab.com/). diff --git a/doc/development/ux_guide/components.md b/doc/development/ux_guide/components.md index cec937da99b..1e84bf608f4 100644 --- a/doc/development/ux_guide/components.md +++ b/doc/development/ux_guide/components.md @@ -2,4 +2,4 @@ redirect_to: 'https://design.gitlab.com/' --- -The content of this documented was moved into the [GitLab Design System](https://design.gitlab.com/). +The content of this document was moved into the [GitLab Design System](https://design.gitlab.com/). diff --git a/doc/development/ux_guide/copy.md b/doc/development/ux_guide/copy.md index cec937da99b..1e84bf608f4 100644 --- a/doc/development/ux_guide/copy.md +++ b/doc/development/ux_guide/copy.md @@ -2,4 +2,4 @@ redirect_to: 'https://design.gitlab.com/' --- -The content of this documented was moved into the [GitLab Design System](https://design.gitlab.com/). +The content of this document was moved into the [GitLab Design System](https://design.gitlab.com/). diff --git a/doc/development/ux_guide/features.md b/doc/development/ux_guide/features.md index cec937da99b..1e84bf608f4 100644 --- a/doc/development/ux_guide/features.md +++ b/doc/development/ux_guide/features.md @@ -2,4 +2,4 @@ redirect_to: 'https://design.gitlab.com/' --- -The content of this documented was moved into the [GitLab Design System](https://design.gitlab.com/). +The content of this document was moved into the [GitLab Design System](https://design.gitlab.com/). diff --git a/doc/development/ux_guide/illustrations.md b/doc/development/ux_guide/illustrations.md index fa67d7c2d74..ed072b6515f 100644 --- a/doc/development/ux_guide/illustrations.md +++ b/doc/development/ux_guide/illustrations.md @@ -2,4 +2,4 @@ redirect_to: 'https://design.gitlab.com/foundations/illustration/' --- -The content of this documented was moved into the [GitLab Design System](https://design.gitlab.com/). +The content of this document was moved into the [GitLab Design System](https://design.gitlab.com/). diff --git a/doc/development/ux_guide/index.md b/doc/development/ux_guide/index.md index cec937da99b..1e84bf608f4 100644 --- a/doc/development/ux_guide/index.md +++ b/doc/development/ux_guide/index.md @@ -2,4 +2,4 @@ redirect_to: 'https://design.gitlab.com/' --- -The content of this documented was moved into the [GitLab Design System](https://design.gitlab.com/). +The content of this document was moved into the [GitLab Design System](https://design.gitlab.com/). diff --git a/doc/development/ux_guide/principles.md b/doc/development/ux_guide/principles.md index cec937da99b..1e84bf608f4 100644 --- a/doc/development/ux_guide/principles.md +++ b/doc/development/ux_guide/principles.md @@ -2,4 +2,4 @@ redirect_to: 'https://design.gitlab.com/' --- -The content of this documented was moved into the [GitLab Design System](https://design.gitlab.com/). +The content of this document was moved into the [GitLab Design System](https://design.gitlab.com/). diff --git a/doc/development/ux_guide/resources.md b/doc/development/ux_guide/resources.md index 895dd5fe831..baec235a8dd 100644 --- a/doc/development/ux_guide/resources.md +++ b/doc/development/ux_guide/resources.md @@ -2,4 +2,4 @@ redirect_to: 'https://design.gitlab.com/resources/design-resources' --- -The content of this documented was moved into the [GitLab Design System](https://design.gitlab.com/). +The content of this document was moved into the [GitLab Design System](https://design.gitlab.com/). diff --git a/doc/development/ux_guide/surfaces.md b/doc/development/ux_guide/surfaces.md index cec937da99b..1e84bf608f4 100644 --- a/doc/development/ux_guide/surfaces.md +++ b/doc/development/ux_guide/surfaces.md @@ -2,4 +2,4 @@ redirect_to: 'https://design.gitlab.com/' --- -The content of this documented was moved into the [GitLab Design System](https://design.gitlab.com/). +The content of this document was moved into the [GitLab Design System](https://design.gitlab.com/). diff --git a/doc/development/ux_guide/tips.md b/doc/development/ux_guide/tips.md index 964ea6aae7d..1e84bf608f4 100644 --- a/doc/development/ux_guide/tips.md +++ b/doc/development/ux_guide/tips.md @@ -2,4 +2,4 @@ redirect_to: 'https://design.gitlab.com/' --- -The content of this documented was moved into the [GitLab Design System](https://design.gitlab.com/).
\ No newline at end of file +The content of this document was moved into the [GitLab Design System](https://design.gitlab.com/). diff --git a/doc/development/ux_guide/users.md b/doc/development/ux_guide/users.md index 9258073cc14..11bac11257c 100644 --- a/doc/development/ux_guide/users.md +++ b/doc/development/ux_guide/users.md @@ -2,4 +2,4 @@ redirect_to: 'https://design.gitlab.com/getting-started/personas/' --- -The content of this documented was moved into the [GitLab Design System](https://design.gitlab.com/). +The content of this document was moved into the [GitLab Design System](https://design.gitlab.com/). diff --git a/doc/integration/github.md b/doc/integration/github.md index 7a83b8e4b35..b8156b2b593 100644 --- a/doc/integration/github.md +++ b/doc/integration/github.md @@ -1,28 +1,32 @@ -# Integrate your server with GitHub +# Integrate your GitLab instance with GitHub -Import projects from GitHub and login to your GitLab instance with your GitHub account. +You can integrate your GitLab instance with GitHub.com as well as GitHub Enterprise to enable users to import projects from GitHub and/or to login to your GitLab instance with your GitHub account. -To enable the GitHub OmniAuth provider you must register your application with GitHub. -GitHub will generate an application ID and secret key for you to use. +## Enabling GitHub OAuth -1. Sign in to GitHub. +To enable GitHub OmniAuth provider, you must use GitHub's credentials for your GitLab instance. +To get the credentials (a pair of Client ID and Client Secret), you must register an application as an OAuth App on GitHub. -1. Navigate to your individual user settings or an organization's settings, depending on how you want the application registered. It does not matter if the application is registered as an individual or an organization - that is entirely up to you. +1. Sign in to GitHub. -1. Select "OAuth applications" in the left menu. +1. Navigate to your individual user or organization settings, depending on how you want the application registered. It does not matter if the application is registered as an individual or an organization - that is entirely up to you. -1. If you already have applications listed, switch to the "Developer applications" tab. + - For individual accounts, select **Developer settings** from the left menu, then select **OAuth Apps**. + - For organization accounts, directly select **OAuth Apps** from the left menu. -1. Select "Register new application". +1. Select **Register an application** (if you don't have any OAuth App) or **New OAuth App** (if you already have OAuth Apps). + ![Register OAuth App](img/github_app_entry.png) 1. Provide the required details. - Application name: This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive. - - Homepage URL: The URL to your GitLab installation. 'https://gitlab.company.com' + - Homepage URL: the URL to your GitLab installation. e.g., `https://gitlab.company.com` - Application description: Fill this in if you wish. - - Authorization callback URL is 'http(s)://${YOUR_DOMAIN}'. Please make sure the port is included if your GitLab instance is not configured on default port. -1. Select "Register application". + - Authorization callback URL: `http(s)://${YOUR_DOMAIN}`. Please make sure the port is included if your GitLab instance is not configured on default port. + ![Register OAuth App](img/github_register_app.png) + +1. Select **Register application**. -1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot). +1. You should now see a pair of **Client ID** and **Client Secret** near the top right of the page (see screenshot). Keep this page open as you continue configuration. ![GitHub app](img/github_app.png) @@ -97,9 +101,9 @@ GitHub will generate an application ID and secret key for you to use. __Replace `https://github.example.com/` with your GitHub URL.__ -1. Change 'YOUR_APP_ID' to the client ID from the GitHub application page from step 7. +1. Change `YOUR_APP_ID` to the Client ID from the GitHub application page from step 6. -1. Change 'YOUR_APP_SECRET' to the client secret from the GitHub application page from step 7. +1. Change `YOUR_APP_SECRET` to the Client Secret from the GitHub application page from step 6. 1. Save the configuration file. diff --git a/doc/integration/img/github_app.png b/doc/integration/img/github_app.png Binary files differindex d6c289a1de1..4a1523d41ac 100644 --- a/doc/integration/img/github_app.png +++ b/doc/integration/img/github_app.png diff --git a/doc/integration/img/github_app_entry.png b/doc/integration/img/github_app_entry.png Binary files differnew file mode 100644 index 00000000000..9e151f8cdff --- /dev/null +++ b/doc/integration/img/github_app_entry.png diff --git a/doc/integration/img/github_register_app.png b/doc/integration/img/github_register_app.png Binary files differnew file mode 100644 index 00000000000..edd3f660f4e --- /dev/null +++ b/doc/integration/img/github_register_app.png diff --git a/doc/user/project/img/issue_board.png b/doc/user/project/img/issue_board.png Binary files differindex 925b969eebe..b46b995d8bb 100644 --- a/doc/user/project/img/issue_board.png +++ b/doc/user/project/img/issue_board.png diff --git a/doc/user/project/img/issue_boards_core.png b/doc/user/project/img/issue_boards_core.png Binary files differindex 9e819160861..8bc187482ad 100644 --- a/doc/user/project/img/issue_boards_core.png +++ b/doc/user/project/img/issue_boards_core.png diff --git a/doc/user/project/img/issue_boards_premium.png b/doc/user/project/img/issue_boards_premium.png Binary files differindex bd9164b2961..4e238ea6983 100644 --- a/doc/user/project/img/issue_boards_premium.png +++ b/doc/user/project/img/issue_boards_premium.png diff --git a/doc/user/project/issues/img/issue_board.png b/doc/user/project/issues/img/issue_board.png Binary files differindex df9d6f64985..c75c35a382e 100644 --- a/doc/user/project/issues/img/issue_board.png +++ b/doc/user/project/issues/img/issue_board.png diff --git a/doc/user/project/pages/introduction.md b/doc/user/project/pages/introduction.md index 9f9b64ec20d..ed049e2e648 100644 --- a/doc/user/project/pages/introduction.md +++ b/doc/user/project/pages/introduction.md @@ -446,7 +446,9 @@ See also: [GitLab Pages from A to Z: Part 1 - Static sites and GitLab Pages doma > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/33422) in GitLab 11.5. NOTE: **Note:** -GitLab Pages access control is not activated on GitLab.com. +GitLab Pages access control is not activated on GitLab.com. You can check its +progress on the +[infrastructure issue tracker](https://gitlab.com/gitlab-com/gl-infra/infrastructure/issues/5576). You can enable Pages access control on your project, so that only [members of your project](../../permissions.md#project-members-permissions) diff --git a/lib/api/api.rb b/lib/api/api.rb index 8e259961828..449faf5f8da 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -50,6 +50,10 @@ module API rack_response({ 'message' => '404 Not found' }.to_json, 404) end + rescue_from ::Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError do + rack_response({ 'message' => '409 Conflict: Resource lock' }.to_json, 409) + end + rescue_from UploadedFile::InvalidPathError do |e| rack_response({ 'message' => e.message }.to_json, 400) end diff --git a/lib/gitlab/background_migration/encrypt_columns.rb b/lib/gitlab/background_migration/encrypt_columns.rb index 0d333e47e7b..bd5f12276ab 100644 --- a/lib/gitlab/background_migration/encrypt_columns.rb +++ b/lib/gitlab/background_migration/encrypt_columns.rb @@ -17,6 +17,12 @@ module Gitlab class EncryptColumns def perform(model, attributes, from, to) model = model.constantize if model.is_a?(String) + + # If sidekiq hasn't undergone a restart, its idea of what columns are + # present may be inaccurate, so ensure this is as fresh as possible + model.reset_column_information + model.define_attribute_methods + attributes = expand_attributes(model, Array(attributes).map(&:to_sym)) model.transaction do @@ -41,6 +47,14 @@ module Gitlab raise "Couldn't determine encrypted column for #{klass}##{attribute}" if crypt_column_name.nil? + raise "#{klass} source column: #{attribute} is missing" unless + klass.column_names.include?(attribute.to_s) + + # Running the migration without the destination column being present + # leads to data loss + raise "#{klass} destination column: #{crypt_column_name} is missing" unless + klass.column_names.include?(crypt_column_name.to_s) + [attribute, crypt_column_name] end diff --git a/lib/gitlab/ci/trace.rb b/lib/gitlab/ci/trace.rb index 8eccd262db9..bf5f2a31f0e 100644 --- a/lib/gitlab/ci/trace.rb +++ b/lib/gitlab/ci/trace.rb @@ -3,9 +3,11 @@ module Gitlab module Ci class Trace - include ExclusiveLeaseGuard + include ::Gitlab::ExclusiveLeaseHelpers - LEASE_TIMEOUT = 1.hour + LOCK_TTL = 1.minute + LOCK_RETRIES = 2 + LOCK_SLEEP = 0.001.seconds ArchiveError = Class.new(StandardError) AlreadyArchivedError = Class.new(StandardError) @@ -82,24 +84,10 @@ module Gitlab stream&.close end - def write(mode) - stream = Gitlab::Ci::Trace::Stream.new do - if trace_artifact - raise AlreadyArchivedError, 'Could not write to the archived trace' - elsif current_path - File.open(current_path, mode) - elsif Feature.enabled?('ci_enable_live_trace') - Gitlab::Ci::Trace::ChunkedIO.new(job) - else - File.open(ensure_path, mode) - end + def write(mode, &blk) + in_write_lock do + unsafe_write!(mode, &blk) end - - yield(stream).tap do - job.touch if job.needs_touch? - end - ensure - stream&.close end def erase! @@ -117,13 +105,33 @@ module Gitlab end def archive! - try_obtain_lease do + in_write_lock do unsafe_archive! end end private + def unsafe_write!(mode, &blk) + stream = Gitlab::Ci::Trace::Stream.new do + if trace_artifact + raise AlreadyArchivedError, 'Could not write to the archived trace' + elsif current_path + File.open(current_path, mode) + elsif Feature.enabled?('ci_enable_live_trace') + Gitlab::Ci::Trace::ChunkedIO.new(job) + else + File.open(ensure_path, mode) + end + end + + yield(stream).tap do + job.touch if job.needs_touch? + end + ensure + stream&.close + end + def unsafe_archive! raise AlreadyArchivedError, 'Could not archive again' if trace_artifact raise ArchiveError, 'Job is not finished yet' unless job.complete? @@ -146,6 +154,11 @@ module Gitlab end end + def in_write_lock(&blk) + lock_key = "trace:write:lock:#{job.id}" + in_lock(lock_key, ttl: LOCK_TTL, retries: LOCK_RETRIES, sleep_sec: LOCK_SLEEP, &blk) + end + def archive_stream!(stream) clone_file!(stream, JobArtifactUploader.workhorse_upload_path) do |clone_path| create_build_trace!(job, clone_path) @@ -226,16 +239,6 @@ module Gitlab def trace_artifact job.job_artifacts_trace end - - # For ExclusiveLeaseGuard concern - def lease_key - @lease_key ||= "trace:archive:#{job.id}" - end - - # For ExclusiveLeaseGuard concern - def lease_timeout - LEASE_TIMEOUT - end end end end diff --git a/lib/gitlab/ci/trace/stream.rb b/lib/gitlab/ci/trace/stream.rb index bd40fdf59b1..0f23b95ba15 100644 --- a/lib/gitlab/ci/trace/stream.rb +++ b/lib/gitlab/ci/trace/stream.rb @@ -43,19 +43,14 @@ module Gitlab def append(data, offset) data = data.force_encoding(Encoding::BINARY) - stream.truncate(offset) - stream.seek(0, IO::SEEK_END) + stream.seek(offset, IO::SEEK_SET) stream.write(data) + stream.truncate(offset + data.bytesize) stream.flush() end def set(data) - data = data.force_encoding(Encoding::BINARY) - - stream.seek(0, IO::SEEK_SET) - stream.write(data) - stream.truncate(data.bytesize) - stream.flush() + append(data, 0) end def raw(last_lines: nil) diff --git a/lib/gitlab/exclusive_lease_helpers.rb b/lib/gitlab/exclusive_lease_helpers.rb index 4aaf2474763..7961d4bbd6e 100644 --- a/lib/gitlab/exclusive_lease_helpers.rb +++ b/lib/gitlab/exclusive_lease_helpers.rb @@ -12,6 +12,8 @@ module Gitlab # because it holds the connection until all `retries` is consumed. # This could potentially eat up all connection pools. def in_lock(key, ttl: 1.minute, retries: 10, sleep_sec: 0.01.seconds) + raise ArgumentError, 'Key needs to be specified' unless key + lease = Gitlab::ExclusiveLease.new(key, timeout: ttl) until uuid = lease.try_obtain diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 9b48031b2d3..0a541031884 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -723,11 +723,11 @@ module Gitlab delete_refs(tmp_ref) end - def write_ref(ref_path, ref, old_ref: nil, shell: true) + def write_ref(ref_path, ref, old_ref: nil) ref_path = "#{Gitlab::Git::BRANCH_REF_PREFIX}#{ref_path}" unless ref_path.start_with?("refs/") || ref_path == "HEAD" wrapped_gitaly_errors do - gitaly_repository_client.write_ref(ref_path, ref, old_ref, shell) + gitaly_repository_client.write_ref(ref_path, ref, old_ref) end end diff --git a/lib/gitlab/git_ref_validator.rb b/lib/gitlab/git_ref_validator.rb index a90b69ff42b..3f13ebeb9d0 100644 --- a/lib/gitlab/git_ref_validator.rb +++ b/lib/gitlab/git_ref_validator.rb @@ -13,7 +13,11 @@ module Gitlab return false if ref_name.start_with?(*not_allowed_prefixes) return false if ref_name == 'HEAD' - Rugged::Reference.valid_name? "refs/heads/#{ref_name}" + begin + Rugged::Reference.valid_name?("refs/heads/#{ref_name}") + rescue ArgumentError + return false + end end end end diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb index 12a0ee16649..8a1abfbf874 100644 --- a/lib/gitlab/gitaly_client/repository_service.rb +++ b/lib/gitlab/gitaly_client/repository_service.rb @@ -251,20 +251,15 @@ module Gitlab ) end - def write_ref(ref_path, ref, old_ref, shell) + def write_ref(ref_path, ref, old_ref) request = Gitaly::WriteRefRequest.new( repository: @gitaly_repo, ref: ref_path.b, - revision: ref.b, - shell: shell + revision: ref.b ) request.old_revision = old_ref.b unless old_ref.nil? - response = GitalyClient.call(@storage, :repository_service, :write_ref, request, timeout: GitalyClient.fast_timeout) - - raise Gitlab::Git::CommandError, encode!(response.error) if response.error.present? - - true + GitalyClient.call(@storage, :repository_service, :write_ref, request, timeout: GitalyClient.fast_timeout) end def set_config(entries) diff --git a/lib/gitlab/graphql/loaders/batch_model_loader.rb b/lib/gitlab/graphql/loaders/batch_model_loader.rb new file mode 100644 index 00000000000..5a0099dc6b1 --- /dev/null +++ b/lib/gitlab/graphql/loaders/batch_model_loader.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Gitlab + module Graphql + module Loaders + class BatchModelLoader + attr_reader :model_class, :model_id + + def initialize(model_class, model_id) + @model_class, @model_id = model_class, model_id + end + + # rubocop: disable CodeReuse/ActiveRecord + def find + BatchLoader.for({ model: model_class, id: model_id }).batch do |loader_info, loader| + per_model = loader_info.group_by { |info| info[:model] } + per_model.each do |model, info| + ids = info.map { |i| i[:id] } + results = model.where(id: ids) + + results.each { |record| loader.call({ model: model, id: record.id }, record) } + end + end + end + # rubocop: enable CodeReuse/ActiveRecord + end + end + end +end diff --git a/lib/gitlab/kubernetes/helm/api.rb b/lib/gitlab/kubernetes/helm/api.rb index fd3d187cbc3..b9903e37f40 100644 --- a/lib/gitlab/kubernetes/helm/api.rb +++ b/lib/gitlab/kubernetes/helm/api.rb @@ -16,12 +16,16 @@ module Gitlab create_cluster_role_binding(command) create_config_map(command) + delete_pod!(command.pod_name) kubeclient.create_pod(command.pod_resource) end def update(command) namespace.ensure_exists! + update_config_map(command) + + delete_pod!(command.pod_name) kubeclient.create_pod(command.pod_resource) end @@ -42,6 +46,8 @@ module Gitlab def delete_pod!(pod_name) kubeclient.delete_pod(pod_name, namespace.name) + rescue ::Kubeclient::ResourceNotFoundError + # no-op end def get_config_map(config_map_name) diff --git a/lib/gitlab/kubernetes/logger.rb b/lib/gitlab/kubernetes/logger.rb new file mode 100644 index 00000000000..5e59482419b --- /dev/null +++ b/lib/gitlab/kubernetes/logger.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Gitlab + module Kubernetes + class Logger < ::Gitlab::JsonLogger + def self.file_name_noext + 'kubernetes' + end + end + end +end diff --git a/lib/tasks/gitlab/site_statistics.rake b/lib/tasks/gitlab/site_statistics.rake deleted file mode 100644 index d97f11b2ed5..00000000000 --- a/lib/tasks/gitlab/site_statistics.rake +++ /dev/null @@ -1,15 +0,0 @@ -namespace :gitlab do - desc "GitLab | Refresh Site Statistics counters" - task refresh_site_statistics: :environment do - puts 'Updating Site Statistics counters: ' - - print '* Repositories... ' - SiteStatistic.transaction do - # see https://gitlab.com/gitlab-org/gitlab-ce/issues/48967 - ActiveRecord::Base.connection.execute('SET LOCAL statement_timeout TO 0') if Gitlab::Database.postgresql? - SiteStatistic.update_all('repositories_count = (SELECT COUNT(*) FROM projects)') - end - puts 'OK!'.color(:green) - puts - end -end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 71374388a7d..172a0dc5e91 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1379,13 +1379,13 @@ msgstr "" msgid "Close" msgstr "" -msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster" +msgid "Closed" msgstr "" -msgid "ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which may incur additional costs depending on the hosting provider your Kubernetes cluster is installed on. If you are using Google Kubernetes Engine, you can %{pricingLink}." +msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster" msgstr "" -msgid "ClusterIntegration|A Knative build extends Kubernetes and utilizes existing Kubernetes primitives to provide you with the ability to run on-cluster container builds from source. For example, you can write a build that uses Kubernetes-native resources to obtain your source code from a repository, build it into container a image, and then run that image." +msgid "ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which may incur additional costs depending on the hosting provider your Kubernetes cluster is installed on. If you are using Google Kubernetes Engine, you can %{pricingLink}." msgstr "" msgid "ClusterIntegration|API URL" @@ -1454,6 +1454,9 @@ msgstr "" msgid "ClusterIntegration|Copy Jupyter Hostname to clipboard" msgstr "" +msgid "ClusterIntegration|Copy Knative IP Address to clipboard" +msgstr "" + msgid "ClusterIntegration|Copy Kubernetes cluster name" msgstr "" @@ -1556,9 +1559,15 @@ msgstr "" msgid "ClusterIntegration|Knative" msgstr "" +msgid "ClusterIntegration|Knative (pronounced kay-nay-tiv) extends Kubernetes to provide a set of middleware components that are essential to build modern, source-centric, and container-based applications that can run anywhere: on premises, in the cloud, or even in a third-party data center." +msgstr "" + msgid "ClusterIntegration|Knative Domain Name:" msgstr "" +msgid "ClusterIntegration|Knative IP Address:" +msgstr "" + msgid "ClusterIntegration|Kubernetes cluster" msgstr "" @@ -4467,6 +4476,9 @@ msgstr "" msgid "Open source software to collaborate on code" msgstr "" +msgid "Opened" +msgstr "" + msgid "OpenedNDaysAgo|Opened" msgstr "" @@ -5856,6 +5868,9 @@ msgstr "" msgid "Sign-up restrictions" msgstr "" +msgid "Similar issues" +msgstr "" + msgid "Size and domain settings for static websites" msgstr "" @@ -6392,6 +6407,9 @@ msgstr "" msgid "There was an error when unsubscribing from this label." msgstr "" +msgid "These existing issues have a similar title. It might be better to comment there instead of creating another similar issue." +msgstr "" + msgid "They can be managed using the %{link}." msgstr "" @@ -7840,6 +7858,9 @@ msgstr "" msgid "this document" msgstr "" +msgid "updated" +msgstr "" + msgid "username" msgstr "" diff --git a/package.json b/package.json index 64df2532977..ad571698ec9 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,8 @@ "@babel/preset-env": "^7.1.0", "@gitlab/svgs": "^1.38.0", "@gitlab/ui": "^1.11.0", + "apollo-boost": "^0.1.20", + "apollo-client": "^2.4.5", "autosize": "^4.0.0", "axios": "^0.17.1", "babel-loader": "^8.0.4", @@ -40,7 +42,7 @@ "core-js": "^2.4.1", "cropper": "^2.3.0", "css-loader": "^1.0.0", - "d3": "4.12.2", + "d3": "^4.13.0", "d3-array": "^1.2.1", "d3-axis": "^1.0.8", "d3-brush": "^1.0.4", @@ -60,6 +62,7 @@ "formdata-polyfill": "^3.0.11", "fuzzaldrin-plus": "^0.5.0", "glob": "^7.1.2", + "graphql": "^14.0.2", "imports-loader": "^0.8.0", "jed": "^1.1.1", "jquery": "^3.2.1", @@ -97,6 +100,7 @@ "url-loader": "^1.1.1", "visibilityjs": "^1.2.4", "vue": "^2.5.17", + "vue-apollo": "^3.0.0-beta.25", "vue-loader": "^15.4.2", "vue-resource": "^1.5.0", "vue-router": "^3.0.1", @@ -127,6 +131,7 @@ "eslint-plugin-jasmine": "^2.10.1", "gettext-extractor": "^3.3.2", "gettext-extractor-vue": "^4.0.1", + "graphql-tag": "^2.10.0", "istanbul": "^0.4.5", "jasmine-core": "^2.9.0", "jasmine-diff": "^0.1.3", @@ -141,6 +146,6 @@ "karma-webpack": "^4.0.0-beta.0", "nodemon": "^1.18.4", "prettier": "1.15.2", - "webpack-dev-server": "^3.1.8" + "webpack-dev-server": "^3.1.10" } } diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb index 7fc3d16e864..fe56ac5b71d 100644 --- a/spec/factories/clusters/applications/helm.rb +++ b/spec/factories/clusters/applications/helm.rb @@ -59,6 +59,7 @@ FactoryBot.define do end factory :clusters_applications_runner, class: Clusters::Applications::Runner do + runner factory: %i(ci_runner) cluster factory: %i(cluster with_installed_helm provided_by_gcp) end diff --git a/spec/factories/site_statistics.rb b/spec/factories/site_statistics.rb deleted file mode 100644 index 2533d0eecc2..00000000000 --- a/spec/factories/site_statistics.rb +++ /dev/null @@ -1,6 +0,0 @@ -FactoryBot.define do - factory :site_statistics, class: 'SiteStatistic' do - id 1 - repositories_count 999 - end -end diff --git a/spec/features/explore/new_menu_spec.rb b/spec/features/explore/new_menu_spec.rb deleted file mode 100644 index 259f22139ef..00000000000 --- a/spec/features/explore/new_menu_spec.rb +++ /dev/null @@ -1,167 +0,0 @@ -require 'spec_helper' - -describe 'Top Plus Menu', :js do - let(:user) { create(:user) } - let(:group) { create(:group) } - let(:project) { create(:project, :repository, creator: user, namespace: user.namespace) } - let(:public_project) { create(:project, :public) } - - before do - group.add_owner(user) - end - - context 'used by full user' do - before do - sign_in(user) - end - - it 'click on New project shows new project page' do - visit root_dashboard_path - - click_topmenuitem("New project") - - expect(page).to have_content('Project URL') - expect(page).to have_content('Project name') - end - - it 'click on New group shows new group page' do - visit root_dashboard_path - - click_topmenuitem("New group") - - expect(page).to have_content('Group URL') - expect(page).to have_content('Group name') - end - - it 'click on New snippet shows new snippet page' do - visit root_dashboard_path - - click_topmenuitem("New snippet") - - expect(page).to have_content('New Snippet') - expect(page).to have_content('Title') - end - - it 'click on New issue shows new issue page' do - visit project_path(project) - - click_topmenuitem("New issue") - - expect(page).to have_content('New Issue') - expect(page).to have_content('Title') - end - - it 'click on New merge request shows new merge request page' do - visit project_path(project) - - click_topmenuitem("New merge request") - - expect(page).to have_content('New Merge Request') - expect(page).to have_content('Source branch') - expect(page).to have_content('Target branch') - end - - it 'click on New project snippet shows new snippet page' do - visit project_path(project) - - page.within '.header-content' do - find('.header-new-dropdown-toggle').click - expect(page).to have_selector('.header-new.dropdown.show', count: 1) - find('.header-new-project-snippet a').click - end - - expect(page).to have_content('New Snippet') - expect(page).to have_content('Title') - end - - it 'Click on New subgroup shows new group page', :nested_groups do - visit group_path(group) - - click_topmenuitem("New subgroup") - - expect(page).to have_content('Group URL') - expect(page).to have_content('Group name') - end - - it 'Click on New project in group shows new project page' do - visit group_path(group) - - page.within '.header-content' do - find('.header-new-dropdown-toggle').click - expect(page).to have_selector('.header-new.dropdown.show', count: 1) - find('.header-new-group-project a').click - end - - expect(page).to have_content('Project URL') - expect(page).to have_content('Project name') - end - end - - context 'used by guest user' do - let(:guest_user) { create(:user) } - - before do - group.add_guest(guest_user) - project.add_guest(guest_user) - - sign_in(guest_user) - end - - it 'click on New issue shows new issue page' do - visit project_path(project) - - click_topmenuitem("New issue") - - expect(page).to have_content('New Issue') - expect(page).to have_content('Title') - end - - it 'has no New merge request menu item' do - visit project_path(project) - - hasnot_topmenuitem("New merge request") - end - - it 'has no New project snippet menu item' do - visit project_path(project) - - expect(find('.header-new.dropdown')).not_to have_selector('.header-new-project-snippet') - end - - it 'public project has no New merge request menu item' do - visit project_path(public_project) - - hasnot_topmenuitem("New merge request") - end - - it 'public project has no New project snippet menu item' do - visit project_path(public_project) - - expect(find('.header-new.dropdown')).not_to have_selector('.header-new-project-snippet') - end - - it 'has no New subgroup menu item' do - visit group_path(group) - - hasnot_topmenuitem("New subgroup") - end - - it 'has no New project for group menu item' do - visit group_path(group) - - expect(find('.header-new.dropdown')).not_to have_selector('.header-new-group-project') - end - end - - def click_topmenuitem(item_name) - page.within '.header-content' do - find('.header-new-dropdown-toggle').click - expect(page).to have_selector('.header-new.dropdown.show', count: 1) - click_link item_name - end - end - - def hasnot_topmenuitem(item_name) - expect(find('.header-new.dropdown')).not_to have_content(item_name) - end -end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 5c1ffb76351..406e80e91aa 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -682,6 +682,18 @@ describe 'Issues' do expect(find('.js-issuable-selector .dropdown-toggle-text')).to have_content('bug') end end + + context 'suggestions', :js do + it 'displays list of related issues' do + create(:issue, project: project, title: 'test issue') + + visit new_project_issue_path(project) + + fill_in 'issue_title', with: issue.title + + expect(page).to have_selector('.suggestion-item', count: 1) + end + end end describe 'new issue by email' do diff --git a/spec/features/signed_commits_spec.rb b/spec/features/signed_commits_spec.rb index ef0e55a1468..e2b3444272e 100644 --- a/spec/features/signed_commits_spec.rb +++ b/spec/features/signed_commits_spec.rb @@ -1,25 +1,22 @@ +# frozen_string_literal: true + require 'spec_helper' -describe 'GPG signed commits', :js do - set(:ref) { :'2d1096e3a0ecf1d2baf6dee036cc80775d4940ba' } - let(:project) { create(:project, :repository) } +describe 'GPG signed commits' do + let(:project) { create(:project, :public, :repository) } it 'changes from unverified to verified when the user changes his email to match the gpg key' do - user = create :user, email: 'unrelated.user@example.org' - project.add_maintainer(user) + ref = GpgHelpers::SIGNED_AND_AUTHORED_SHA + user = create(:user, email: 'unrelated.user@example.org') perform_enqueued_jobs do create :gpg_key, key: GpgHelpers::User1.public_key, user: user end - sign_in(user) - - visit project_commits_path(project, ref) + visit project_commit_path(project, ref) - within '#commits-list' do - expect(page).to have_content 'Unverified' - expect(page).not_to have_content 'Verified' - end + expect(page).to have_link 'Unverified' + expect(page).not_to have_link 'Verified' # user changes his email which makes the gpg key verified perform_enqueued_jobs do @@ -27,41 +24,33 @@ describe 'GPG signed commits', :js do user.update!(email: GpgHelpers::User1.emails.first) end - visit project_commits_path(project, ref) + visit project_commit_path(project, ref) - within '#commits-list' do - expect(page).to have_content 'Unverified' - expect(page).to have_content 'Verified' - end + expect(page).not_to have_link 'Unverified' + expect(page).to have_link 'Verified' end it 'changes from unverified to verified when the user adds the missing gpg key' do - user = create :user, email: GpgHelpers::User1.emails.first - project.add_maintainer(user) + ref = GpgHelpers::SIGNED_AND_AUTHORED_SHA + user = create(:user, email: GpgHelpers::User1.emails.first) - sign_in(user) + visit project_commit_path(project, ref) - visit project_commits_path(project, ref) - - within '#commits-list' do - expect(page).to have_content 'Unverified' - expect(page).not_to have_content 'Verified' - end + expect(page).to have_link 'Unverified' + expect(page).not_to have_link 'Verified' # user adds the gpg key which makes the signature valid perform_enqueued_jobs do create :gpg_key, key: GpgHelpers::User1.public_key, user: user end - visit project_commits_path(project, ref) + visit project_commit_path(project, ref) - within '#commits-list' do - expect(page).to have_content 'Unverified' - expect(page).to have_content 'Verified' - end + expect(page).not_to have_link 'Unverified' + expect(page).to have_link 'Verified' end - context 'shows popover badges' do + context 'shows popover badges', :js do let(:user_1) do create :user, email: GpgHelpers::User1.emails.first, username: 'nannie.bernhard', name: 'Nannie Bernhard' end @@ -85,19 +74,10 @@ describe 'GPG signed commits', :js do end end - before do - user = create :user - project.add_maintainer(user) - - sign_in(user) - end - it 'unverified signature' do - visit project_commits_path(project, ref) + visit project_commit_path(project, GpgHelpers::SIGNED_COMMIT_SHA) - within(find('.commit', text: 'signed commit by bette cartwright')) do - click_on 'Unverified' - end + click_on 'Unverified' within '.popover' do expect(page).to have_content 'This commit was signed with an unverified signature.' @@ -108,11 +88,9 @@ describe 'GPG signed commits', :js do it 'unverified signature: user email does not match the committer email, but is the same user' do user_2_key - visit project_commits_path(project, ref) + visit project_commit_path(project, GpgHelpers::DIFFERING_EMAIL_SHA) - within(find('.commit', text: 'signed and authored commit by bette cartwright, different email')) do - click_on 'Unverified' - end + click_on 'Unverified' within '.popover' do expect(page).to have_content 'This commit was signed with a verified signature, but the committer email is not verified to belong to the same user.' @@ -125,11 +103,9 @@ describe 'GPG signed commits', :js do it 'unverified signature: user email does not match the committer email' do user_2_key - visit project_commits_path(project, ref) + visit project_commit_path(project, GpgHelpers::SIGNED_COMMIT_SHA) - within(find('.commit', text: 'signed commit by bette cartwright')) do - click_on 'Unverified' - end + click_on 'Unverified' within '.popover' do expect(page).to have_content "This commit was signed with a different user's verified signature." @@ -142,11 +118,9 @@ describe 'GPG signed commits', :js do it 'verified and the gpg user has a gitlab profile' do user_1_key - visit project_commits_path(project, ref) + visit project_commit_path(project, GpgHelpers::SIGNED_AND_AUTHORED_SHA) - within(find('.commit', text: 'signed and authored commit by nannie bernhard')) do - click_on 'Verified' - end + click_on 'Verified' within '.popover' do expect(page).to have_content 'This commit was signed with a verified signature and the committer email is verified to belong to the same user.' @@ -159,20 +133,16 @@ describe 'GPG signed commits', :js do it "verified and the gpg user's profile doesn't exist anymore" do user_1_key - visit project_commits_path(project, ref) + visit project_commit_path(project, GpgHelpers::SIGNED_AND_AUTHORED_SHA) # wait for the signature to get generated - within(find('.commit', text: 'signed and authored commit by nannie bernhard')) do - expect(page).to have_content 'Verified' - end + expect(page).to have_link 'Verified' user_1.destroy! refresh - within(find('.commit', text: 'signed and authored commit by nannie bernhard')) do - click_on 'Verified' - end + click_on 'Verified' within '.popover' do expect(page).to have_content 'This commit was signed with a verified signature and the committer email is verified to belong to the same user.' diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb new file mode 100644 index 00000000000..ca90673521c --- /dev/null +++ b/spec/graphql/resolvers/issues_resolver_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe Resolvers::IssuesResolver do + include GraphqlHelpers + + let(:current_user) { create(:user) } + set(:project) { create(:project) } + set(:issue) { create(:issue, project: project) } + set(:issue2) { create(:issue, project: project, title: 'foo') } + + before do + project.add_developer(current_user) + end + + describe '#resolve' do + it 'finds all issues' do + expect(resolve_issues).to contain_exactly(issue, issue2) + end + + it 'searches issues' do + expect(resolve_issues(search: 'foo')).to contain_exactly(issue2) + end + + it 'sort issues' do + expect(resolve_issues(sort: 'created_desc')).to eq [issue2, issue] + end + + it 'returns issues user can see' do + project.add_guest(current_user) + + create(:issue, confidential: true) + + expect(resolve_issues).to contain_exactly(issue, issue2) + end + end + + def resolve_issues(args = {}, context = { current_user: current_user }) + resolve(described_class, obj: project, args: args, ctx: context) + end +end diff --git a/spec/graphql/types/issue_type_spec.rb b/spec/graphql/types/issue_type_spec.rb new file mode 100644 index 00000000000..63a07647a60 --- /dev/null +++ b/spec/graphql/types/issue_type_spec.rb @@ -0,0 +1,7 @@ +require 'spec_helper' + +describe GitlabSchema.types['Issue'] do + it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Issue) } + + it { expect(described_class.graphql_name).to eq('Issue') } +end diff --git a/spec/graphql/types/permission_types/issue_spec.rb b/spec/graphql/types/permission_types/issue_spec.rb new file mode 100644 index 00000000000..c3f84629aa2 --- /dev/null +++ b/spec/graphql/types/permission_types/issue_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe Types::PermissionTypes::Issue do + it do + expected_permissions = [ + :read_issue, :admin_issue, :update_issue, + :create_note, :reopen_issue + ] + + expect(described_class).to have_graphql_fields(expected_permissions) + end +end diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb index 49606c397b9..61d4c42665a 100644 --- a/spec/graphql/types/project_type_spec.rb +++ b/spec/graphql/types/project_type_spec.rb @@ -14,5 +14,9 @@ describe GitlabSchema.types['Project'] do end end + describe 'nested issues' do + it { expect(described_class).to have_graphql_field(:issues) } + end + it { is_expected.to have_graphql_field(:pipelines) } end diff --git a/spec/initializers/attr_encrypted_no_db_connection_spec.rb b/spec/initializers/attr_encrypted_no_db_connection_spec.rb new file mode 100644 index 00000000000..2da9f1cbd96 --- /dev/null +++ b/spec/initializers/attr_encrypted_no_db_connection_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe 'GitLab monkey-patches to AttrEncrypted' do + describe '#attribute_instance_methods_as_symbols_available?' do + it 'returns false' do + expect(ActiveRecord::Base.__send__(:attribute_instance_methods_as_symbols_available?)).to be_falsy + end + + it 'does not define virtual attributes' do + klass = Class.new(ActiveRecord::Base) do + # We need some sort of table to work on + self.table_name = 'projects' + + attr_encrypted :foo + end + + instance = klass.new + + aggregate_failures do + %w[ + encrypted_foo encrypted_foo= + encrypted_foo_iv encrypted_foo_iv= + encrypted_foo_salt encrypted_foo_salt= + ].each do |method_name| + expect(instance).not_to respond_to(method_name) + end + end + end + end +end diff --git a/spec/javascripts/blob_edit/blob_bundle_spec.js b/spec/javascripts/blob_edit/blob_bundle_spec.js new file mode 100644 index 00000000000..759d170af77 --- /dev/null +++ b/spec/javascripts/blob_edit/blob_bundle_spec.js @@ -0,0 +1,30 @@ +import blobBundle from '~/blob_edit/blob_bundle'; +import $ from 'jquery'; + +window.ace = { + config: { + set: () => {}, + loadModule: () => {}, + }, + edit: () => ({ focus: () => {} }), +}; + +describe('EditBlob', () => { + beforeEach(() => { + setFixtures(` + <div class="js-edit-blob-form"> + <button class="js-commit-button"></button> + </div>`); + blobBundle(); + }); + + it('sets the window beforeunload listener to a function returning a string', () => { + expect(window.onbeforeunload()).toBe(''); + }); + + it('removes beforeunload listener if commit button is clicked', () => { + $('.js-commit-button').click(); + + expect(window.onbeforeunload).toBeNull(); + }); +}); diff --git a/spec/javascripts/clusters/stores/clusters_store_spec.js b/spec/javascripts/clusters/stores/clusters_store_spec.js index 6a08d08f33e..7ea0878ad45 100644 --- a/spec/javascripts/clusters/stores/clusters_store_spec.js +++ b/spec/javascripts/clusters/stores/clusters_store_spec.js @@ -107,6 +107,7 @@ describe('Clusters Store', () => { requestStatus: null, requestReason: null, hostname: null, + externalIp: null, }, cert_manager: { title: 'Cert-Manager', diff --git a/spec/javascripts/issuable_suggestions/components/app_spec.js b/spec/javascripts/issuable_suggestions/components/app_spec.js new file mode 100644 index 00000000000..7bb8e26b81a --- /dev/null +++ b/spec/javascripts/issuable_suggestions/components/app_spec.js @@ -0,0 +1,96 @@ +import { shallowMount } from '@vue/test-utils'; +import App from '~/issuable_suggestions/components/app.vue'; +import Suggestion from '~/issuable_suggestions/components/item.vue'; + +describe('Issuable suggestions app component', () => { + let vm; + + function createComponent(search = 'search') { + vm = shallowMount(App, { + propsData: { + search, + projectPath: 'project', + }, + }); + } + + afterEach(() => { + vm.destroy(); + }); + + it('does not render with empty search', () => { + createComponent(''); + + expect(vm.isVisible()).toBe(false); + }); + + describe('with data', () => { + let data; + + beforeEach(() => { + data = { issues: [{ id: 1 }, { id: 2 }] }; + }); + + it('renders component', () => { + createComponent(); + vm.setData(data); + + expect(vm.isEmpty()).toBe(false); + }); + + it('does not render with empty search', () => { + createComponent(''); + vm.setData(data); + + expect(vm.isVisible()).toBe(false); + }); + + it('does not render when loading', () => { + createComponent(); + vm.setData({ + ...data, + loading: 1, + }); + + expect(vm.isVisible()).toBe(false); + }); + + it('does not render with empty issues data', () => { + createComponent(); + vm.setData({ issues: [] }); + + expect(vm.isVisible()).toBe(false); + }); + + it('renders list of issues', () => { + createComponent(); + vm.setData(data); + + expect(vm.findAll(Suggestion).length).toBe(2); + }); + + it('adds margin class to first item', () => { + createComponent(); + vm.setData(data); + + expect( + vm + .findAll('li') + .at(0) + .is('.append-bottom-default'), + ).toBe(true); + }); + + it('does not add margin class to last item', () => { + createComponent(); + vm.setData(data); + + expect( + vm + .findAll('li') + .at(1) + .is('.append-bottom-default'), + ).toBe(false); + }); + }); +}); diff --git a/spec/javascripts/issuable_suggestions/components/item_spec.js b/spec/javascripts/issuable_suggestions/components/item_spec.js new file mode 100644 index 00000000000..7bd1fe678f4 --- /dev/null +++ b/spec/javascripts/issuable_suggestions/components/item_spec.js @@ -0,0 +1,139 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlTooltip, GlLink } from '@gitlab/ui'; +import Icon from '~/vue_shared/components/icon.vue'; +import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; +import Suggestion from '~/issuable_suggestions/components/item.vue'; +import mockData from '../mock_data'; + +describe('Issuable suggestions suggestion component', () => { + let vm; + + function createComponent(suggestion = {}) { + vm = shallowMount(Suggestion, { + propsData: { + suggestion: { + ...mockData(), + ...suggestion, + }, + }, + }); + } + + afterEach(() => { + vm.destroy(); + }); + + it('renders title', () => { + createComponent(); + + expect(vm.text()).toContain('Test issue'); + }); + + it('renders issue link', () => { + createComponent(); + + const link = vm.find(GlLink); + + expect(link.attributes('href')).toBe(`${gl.TEST_HOST}/test/issue/1`); + }); + + it('renders IID', () => { + createComponent(); + + expect(vm.text()).toContain('#1'); + }); + + describe('opened state', () => { + it('renders icon', () => { + createComponent(); + + const icon = vm.find(Icon); + + expect(icon.props('name')).toBe('issue-open-m'); + }); + + it('renders created timeago', () => { + createComponent({ + closedAt: '', + }); + + const tooltip = vm.find(GlTooltip); + + expect(tooltip.find('.d-block').text()).toContain('Opened'); + expect(tooltip.text()).toContain('3 days ago'); + }); + }); + + describe('closed state', () => { + it('renders icon', () => { + createComponent({ + state: 'closed', + }); + + const icon = vm.find(Icon); + + expect(icon.props('name')).toBe('issue-close'); + }); + + it('renders closed timeago', () => { + createComponent(); + + const tooltip = vm.find(GlTooltip); + + expect(tooltip.find('.d-block').text()).toContain('Opened'); + expect(tooltip.text()).toContain('1 day ago'); + }); + }); + + describe('author', () => { + it('renders author info', () => { + createComponent(); + + const link = vm.findAll(GlLink).at(1); + + expect(link.text()).toContain('Author Name'); + expect(link.text()).toContain('@author.username'); + }); + + it('renders author image', () => { + createComponent(); + + const image = vm.find(UserAvatarImage); + + expect(image.props('imgSrc')).toBe(`${gl.TEST_HOST}/avatar`); + }); + }); + + describe('counts', () => { + it('renders upvotes count', () => { + createComponent(); + + const count = vm.findAll('.suggestion-counts span').at(0); + + expect(count.text()).toContain('1'); + expect(count.find(Icon).props('name')).toBe('thumb-up'); + }); + + it('renders notes count', () => { + createComponent(); + + const count = vm.findAll('.suggestion-counts span').at(1); + + expect(count.text()).toContain('2'); + expect(count.find(Icon).props('name')).toBe('comment'); + }); + }); + + describe('confidential', () => { + it('renders confidential icon', () => { + createComponent({ + confidential: true, + }); + + const icon = vm.find(Icon); + + expect(icon.props('name')).toBe('eye-slash'); + expect(icon.attributes('data-original-title')).toBe('Confidential'); + }); + }); +}); diff --git a/spec/javascripts/issuable_suggestions/mock_data.js b/spec/javascripts/issuable_suggestions/mock_data.js new file mode 100644 index 00000000000..4f0f9ef8d62 --- /dev/null +++ b/spec/javascripts/issuable_suggestions/mock_data.js @@ -0,0 +1,26 @@ +function getDate(daysMinus) { + const today = new Date(); + today.setDate(today.getDate() - daysMinus); + + return today.toISOString(); +} + +export default () => ({ + id: 1, + iid: 1, + state: 'opened', + upvotes: 1, + userNotesCount: 2, + closedAt: getDate(1), + createdAt: getDate(3), + updatedAt: getDate(2), + confidential: false, + webUrl: `${gl.TEST_HOST}/test/issue/1`, + title: 'Test issue', + author: { + avatarUrl: `${gl.TEST_HOST}/avatar`, + name: 'Author Name', + username: 'author.username', + webUrl: `${gl.TEST_HOST}/author`, + }, +}); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 1c7691f865a..1ec1e8a8dd9 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -346,6 +346,24 @@ describe('common_utils', () => { }); }); + describe('parseBoolean', () => { + it('returns true for "true"', () => { + expect(commonUtils.parseBoolean('true')).toEqual(true); + }); + + it('returns false for "false"', () => { + expect(commonUtils.parseBoolean('false')).toEqual(false); + }); + + it('returns false for "something"', () => { + expect(commonUtils.parseBoolean('something')).toEqual(false); + }); + + it('returns false for null', () => { + expect(commonUtils.parseBoolean(null)).toEqual(false); + }); + }); + describe('convertPermissionToBoolean', () => { it('should convert a boolean in a string to a boolean', () => { expect(commonUtils.convertPermissionToBoolean('true')).toEqual(true); diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 7714197c821..c8df05eccf5 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -239,4 +239,38 @@ describe('MergeRequestTabs', function() { expect($('.content-wrapper')).toContainElement('.container-limited'); }); }); + + describe('tabShown', function() { + const mainContent = document.createElement('div'); + const tabContent = document.createElement('div'); + + beforeEach(function() { + spyOn(mainContent, 'getBoundingClientRect').and.returnValue({ top: 10 }); + spyOn(tabContent, 'getBoundingClientRect').and.returnValue({ top: 100 }); + spyOn(document, 'querySelector').and.callFake(function(selector) { + return selector === '.content-wrapper' ? mainContent : tabContent; + }); + this.class.currentAction = 'commits'; + }); + + it('calls window scrollTo with options if document has scrollBehavior', function() { + document.documentElement.style.scrollBehavior = ''; + + spyOn(window, 'scrollTo'); + + this.class.tabShown('commits', 'foobar'); + + expect(window.scrollTo.calls.first().args[0]).toEqual({ top: 39, behavior: 'smooth' }); + }); + + it('calls window scrollTo with two args if document does not have scrollBehavior', function() { + spyOnProperty(document.documentElement, 'style', 'get').and.returnValue({}); + + spyOn(window, 'scrollTo'); + + this.class.tabShown('commits', 'foobar'); + + expect(window.scrollTo.calls.first().args).toEqual([0, 39]); + }); + }); }); diff --git a/spec/lib/gitlab/background_migration/encrypt_columns_spec.rb b/spec/lib/gitlab/background_migration/encrypt_columns_spec.rb index 2a869446753..1d9bac79dcd 100644 --- a/spec/lib/gitlab/background_migration/encrypt_columns_spec.rb +++ b/spec/lib/gitlab/background_migration/encrypt_columns_spec.rb @@ -65,5 +65,30 @@ describe Gitlab::BackgroundMigration::EncryptColumns, :migration, schema: 201809 expect(hook).to have_attributes(values) end + + it 'reloads the model column information' do + expect(model).to receive(:reset_column_information).and_call_original + expect(model).to receive(:define_attribute_methods).and_call_original + + subject.perform(model, [:token, :url], 1, 1) + end + + it 'fails if a source column is not present' do + columns = model.columns.reject { |c| c.name == 'url' } + allow(model).to receive(:columns) { columns } + + expect do + subject.perform(model, [:token, :url], 1, 1) + end.to raise_error(/source column: url is missing/) + end + + it 'fails if a destination column is not present' do + columns = model.columns.reject { |c| c.name == 'encrypted_url' } + allow(model).to receive(:columns) { columns } + + expect do + subject.perform(model, [:token, :url], 1, 1) + end.to raise_error(/destination column: encrypted_url is missing/) + end end end diff --git a/spec/lib/gitlab/ci/trace/stream_spec.rb b/spec/lib/gitlab/ci/trace/stream_spec.rb index 4f49958dd33..38626f728d7 100644 --- a/spec/lib/gitlab/ci/trace/stream_spec.rb +++ b/spec/lib/gitlab/ci/trace/stream_spec.rb @@ -257,7 +257,8 @@ describe Gitlab::Ci::Trace::Stream, :clean_gitlab_redis_cache do let!(:last_result) { stream.html_with_state } before do - stream.append("5678", 4) + data_stream.seek(4, IO::SEEK_SET) + data_stream.write("5678") stream.seek(0) end @@ -271,25 +272,29 @@ describe Gitlab::Ci::Trace::Stream, :clean_gitlab_redis_cache do end context 'when stream is StringIO' do + let(:data_stream) do + StringIO.new("1234") + end + let(:stream) do - described_class.new do - StringIO.new("1234") - end + described_class.new { data_stream } end it_behaves_like 'html_with_states' end context 'when stream is ChunkedIO' do - let(:stream) do - described_class.new do - Gitlab::Ci::Trace::ChunkedIO.new(build).tap do |chunked_io| - chunked_io.write("1234") - chunked_io.seek(0, IO::SEEK_SET) - end + let(:data_stream) do + Gitlab::Ci::Trace::ChunkedIO.new(build).tap do |chunked_io| + chunked_io.write("1234") + chunked_io.seek(0, IO::SEEK_SET) end end + let(:stream) do + described_class.new { data_stream } + end + it_behaves_like 'html_with_states' end end diff --git a/spec/lib/gitlab/exclusive_lease_helpers_spec.rb b/spec/lib/gitlab/exclusive_lease_helpers_spec.rb index 2e3656b52fb..5107e1efbbd 100644 --- a/spec/lib/gitlab/exclusive_lease_helpers_spec.rb +++ b/spec/lib/gitlab/exclusive_lease_helpers_spec.rb @@ -11,6 +11,14 @@ describe Gitlab::ExclusiveLeaseHelpers, :clean_gitlab_redis_shared_state do let(:options) { {} } + context 'when unique key is not set' do + let(:unique_key) { } + + it 'raises an error' do + expect { subject }.to raise_error ArgumentError + end + end + context 'when the lease is not obtained yet' do before do stub_exclusive_lease(unique_key, 'uuid') diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 1fe73c12fc0..852ee9c96af 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1469,6 +1469,19 @@ describe Gitlab::Git::Repository, :seed_helper do end end end + + it 'writes the HEAD' do + repository.write_ref('HEAD', 'refs/heads/feature') + + expect(repository.commit('HEAD')).to eq(repository.commit('feature')) + expect(repository.root_ref).to eq('feature') + end + + it 'writes other refs' do + repository.write_ref('refs/heads/feature', SeedRepo::Commit::ID) + + expect(repository.commit('feature').sha).to eq(SeedRepo::Commit::ID) + end end describe '#write_config' do diff --git a/spec/lib/gitlab/git_ref_validator_spec.rb b/spec/lib/gitlab/git_ref_validator_spec.rb index ba7fb168a3b..3ab04a1c46d 100644 --- a/spec/lib/gitlab/git_ref_validator_spec.rb +++ b/spec/lib/gitlab/git_ref_validator_spec.rb @@ -27,4 +27,5 @@ describe Gitlab::GitRefValidator do it { expect(described_class.validate('-branch')).to be_falsey } it { expect(described_class.validate('.tag')).to be_falsey } it { expect(described_class.validate('my branch')).to be_falsey } + it { expect(described_class.validate("\xA0\u0000\xB0")).to be_falsey } end diff --git a/spec/lib/gitlab/graphql/loaders/batch_model_loader_spec.rb b/spec/lib/gitlab/graphql/loaders/batch_model_loader_spec.rb new file mode 100644 index 00000000000..4609593ef6a --- /dev/null +++ b/spec/lib/gitlab/graphql/loaders/batch_model_loader_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Gitlab::Graphql::Loaders::BatchModelLoader do + describe '#find' do + let(:issue) { create(:issue) } + let(:user) { create(:user) } + + it 'finds a model by id' do + issue_result = described_class.new(Issue, issue.id).find + user_result = described_class.new(User, user.id).find + + expect(issue_result.__sync).to eq(issue) + expect(user_result.__sync).to eq(user) + end + + it 'only queries once per model' do + other_user = create(:user) + user + issue + + expect do + [described_class.new(User, other_user.id).find, + described_class.new(User, user.id).find, + described_class.new(Issue, issue.id).find].map(&:__sync) + end.not_to exceed_query_limit(2) + end + end +end diff --git a/spec/lib/gitlab/kubernetes/helm/api_spec.rb b/spec/lib/gitlab/kubernetes/helm/api_spec.rb index 8bce7a4cdf5..c7f92cbb143 100644 --- a/spec/lib/gitlab/kubernetes/helm/api_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/api_spec.rb @@ -40,6 +40,7 @@ describe Gitlab::Kubernetes::Helm::Api do allow(client).to receive(:create_config_map).and_return(nil) allow(client).to receive(:create_service_account).and_return(nil) allow(client).to receive(:create_cluster_role_binding).and_return(nil) + allow(client).to receive(:delete_pod).and_return(nil) allow(namespace).to receive(:ensure_exists!).once end @@ -50,6 +51,13 @@ describe Gitlab::Kubernetes::Helm::Api do subject.install(command) end + it 'removes an existing pod before installing' do + expect(client).to receive(:delete_pod).with('install-app-name', 'gitlab-managed-apps').once.ordered + expect(client).to receive(:create_pod).once.ordered + + subject.install(command) + end + context 'with a ConfigMap' do let(:resource) { Gitlab::Kubernetes::ConfigMap.new(application_name, files).generate } @@ -180,6 +188,7 @@ describe Gitlab::Kubernetes::Helm::Api do allow(client).to receive(:update_config_map).and_return(nil) allow(client).to receive(:create_pod).and_return(nil) + allow(client).to receive(:delete_pod).and_return(nil) end it 'ensures the namespace exists before creating the pod' do @@ -189,6 +198,13 @@ describe Gitlab::Kubernetes::Helm::Api do subject.update(command) end + it 'removes an existing pod before updating' do + expect(client).to receive(:delete_pod).with('upgrade-app-name', 'gitlab-managed-apps').once.ordered + expect(client).to receive(:create_pod).once.ordered + + subject.update(command) + end + it 'updates the config map on kubeclient when one exists' do resource = Gitlab::Kubernetes::ConfigMap.new( application_name, files @@ -224,9 +240,18 @@ describe Gitlab::Kubernetes::Helm::Api do describe '#delete_pod!' do it 'deletes the POD from kubernetes cluster' do - expect(client).to receive(:delete_pod).with(command.pod_name, gitlab_namespace).once + expect(client).to receive(:delete_pod).with('install-app-name', 'gitlab-managed-apps').once - subject.delete_pod!(command.pod_name) + subject.delete_pod!('install-app-name') + end + + context 'when the resource being deleted does not exist' do + it 'catches the error' do + expect(client).to receive(:delete_pod).with('install-app-name', 'gitlab-managed-apps') + .and_raise(Kubeclient::ResourceNotFoundError.new(404, 'Not found', nil)) + + subject.delete_pod!('install-app-name') + end end end diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb index 6b0b23eeab3..cfe0e216c78 100644 --- a/spec/models/clusters/applications/ingress_spec.rb +++ b/spec/models/clusters/applications/ingress_spec.rb @@ -5,7 +5,7 @@ describe Clusters::Applications::Ingress do include_examples 'cluster application core specs', :clusters_applications_ingress include_examples 'cluster application status specs', :clusters_applications_ingress - include_examples 'cluster application helm specs', :clusters_applications_knative + include_examples 'cluster application helm specs', :clusters_applications_ingress before do allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in) diff --git a/spec/models/clusters/applications/jupyter_spec.rb b/spec/models/clusters/applications/jupyter_spec.rb index faaabafddb7..a40edbf267b 100644 --- a/spec/models/clusters/applications/jupyter_spec.rb +++ b/spec/models/clusters/applications/jupyter_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' describe Clusters::Applications::Jupyter do include_examples 'cluster application core specs', :clusters_applications_jupyter - include_examples 'cluster application helm specs', :clusters_applications_knative + include_examples 'cluster application helm specs', :clusters_applications_jupyter it { is_expected.to belong_to(:oauth_application) } diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb index be2a91d566b..d43d88c2924 100644 --- a/spec/models/clusters/applications/knative_spec.rb +++ b/spec/models/clusters/applications/knative_spec.rb @@ -7,6 +7,11 @@ describe Clusters::Applications::Knative do include_examples 'cluster application status specs', :clusters_applications_knative include_examples 'cluster application helm specs', :clusters_applications_knative + before do + allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in) + allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async) + end + describe '.installed' do subject { described_class.installed } @@ -45,6 +50,48 @@ describe Clusters::Applications::Knative do it { is_expected.to contain_exactly(cluster) } end + describe 'make_installed with external_ip' do + before do + application.make_installed! + end + + let(:application) { create(:clusters_applications_knative, :installing) } + + it 'schedules a ClusterWaitForIngressIpAddressWorker' do + expect(ClusterWaitForIngressIpAddressWorker).to have_received(:perform_in) + .with(Clusters::Applications::Knative::FETCH_IP_ADDRESS_DELAY, 'knative', application.id) + end + end + + describe '#schedule_status_update with external_ip' do + let(:application) { create(:clusters_applications_knative, :installed) } + + before do + application.schedule_status_update + end + + it 'schedules a ClusterWaitForIngressIpAddressWorker' do + expect(ClusterWaitForIngressIpAddressWorker).to have_received(:perform_async) + .with('knative', application.id) + end + + context 'when the application is not installed' do + let(:application) { create(:clusters_applications_knative, :installing) } + + it 'does not schedule a ClusterWaitForIngressIpAddressWorker' do + expect(ClusterWaitForIngressIpAddressWorker).not_to have_received(:perform_async) + end + end + + context 'when there is already an external_ip' do + let(:application) { create(:clusters_applications_knative, :installed, external_ip: '111.222.222.111') } + + it 'does not schedule a ClusterWaitForIngressIpAddressWorker' do + expect(ClusterWaitForIngressIpAddressWorker).not_to have_received(:perform_in) + end + end + end + describe '#install_command' do subject { knative.install_command } diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb index b5aa1dcece5..893ed3e3f64 100644 --- a/spec/models/clusters/applications/prometheus_spec.rb +++ b/spec/models/clusters/applications/prometheus_spec.rb @@ -5,7 +5,7 @@ describe Clusters::Applications::Prometheus do include_examples 'cluster application core specs', :clusters_applications_prometheus include_examples 'cluster application status specs', :clusters_applications_prometheus - include_examples 'cluster application helm specs', :clusters_applications_knative + include_examples 'cluster application helm specs', :clusters_applications_prometheus describe '.installed' do subject { described_class.installed } diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb index a8b28a335d6..97e50809647 100644 --- a/spec/models/clusters/applications/runner_spec.rb +++ b/spec/models/clusters/applications/runner_spec.rb @@ -5,7 +5,7 @@ describe Clusters::Applications::Runner do include_examples 'cluster application core specs', :clusters_applications_runner include_examples 'cluster application status specs', :clusters_applications_runner - include_examples 'cluster application helm specs', :clusters_applications_knative + include_examples 'cluster application helm specs', :clusters_applications_runner it { is_expected.to belong_to(:runner) } @@ -90,7 +90,7 @@ describe Clusters::Applications::Runner do context 'without a runner' do let(:project) { create(:project) } let(:cluster) { create(:cluster, :with_installed_helm, projects: [project]) } - let(:application) { create(:clusters_applications_runner, cluster: cluster) } + let(:application) { create(:clusters_applications_runner, runner: nil, cluster: cluster) } it 'creates a runner' do expect do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 51278836604..47c331fbc7a 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -109,22 +109,6 @@ describe Project do end end - context 'Site Statistics' do - context 'when creating a new project' do - it 'tracks project in SiteStatistic' do - expect { create(:project) }.to change { SiteStatistic.fetch.repositories_count }.by(1) - end - end - - context 'when deleting a project' do - it 'untracks project in SiteStatistic' do - project = create(:project) - - expect { project.destroy }.to change { SiteStatistic.fetch.repositories_count }.by(-1) - end - end - end - context 'updating cd_cd_settings' do it 'does not raise an error' do project = create(:project) @@ -2203,12 +2187,6 @@ describe Project do project.change_head(project.default_branch) end - it 'creates the new reference with rugged' do - expect(project.repository.raw_repository).to receive(:write_ref).with('HEAD', "refs/heads/#{project.default_branch}", shell: false) - - project.change_head(project.default_branch) - end - it 'copies the gitattributes' do expect(project.repository).to receive(:copy_gitattributes).with(project.default_branch) project.change_head(project.default_branch) diff --git a/spec/models/site_statistic_spec.rb b/spec/models/site_statistic_spec.rb deleted file mode 100644 index 0e739900065..00000000000 --- a/spec/models/site_statistic_spec.rb +++ /dev/null @@ -1,81 +0,0 @@ -require 'spec_helper' - -describe SiteStatistic do - describe '.fetch' do - context 'existing record' do - it 'returns existing SiteStatistic model' do - statistics = create(:site_statistics) - - expect(described_class.fetch).to be_a(described_class) - expect(described_class.fetch).to eq(statistics) - end - end - - context 'non existing record' do - it 'creates a new SiteStatistic model' do - expect(described_class.first).to be_nil - expect(described_class.fetch).to be_a(described_class) - end - end - end - - describe '.track' do - context 'with allowed attributes' do - let(:statistics) { create(:site_statistics) } - - it 'increases the attribute counter' do - expect { described_class.track('repositories_count') }.to change { statistics.reload.repositories_count }.by(1) - end - - it 'doesnt increase the attribute counter when an exception happens during transaction' do - expect do - begin - described_class.transaction do - described_class.track('repositories_count') - - raise StandardError - end - rescue StandardError - # no-op - end - end.not_to change { statistics.reload.repositories_count } - end - end - - context 'with not allowed attributes' do - it 'returns error' do - expect { described_class.track('something_else') }.to raise_error(ArgumentError).with_message(/Invalid attribute: \'something_else\' to \'track\' method/) - end - end - end - - describe '.untrack' do - context 'with allowed attributes' do - let(:statistics) { create(:site_statistics) } - - it 'decreases the attribute counter' do - expect { described_class.untrack('repositories_count') }.to change { statistics.reload.repositories_count }.by(-1) - end - - it 'doesnt decrease the attribute counter when an exception happens during transaction' do - expect do - begin - described_class.transaction do - described_class.track('repositories_count') - - raise StandardError - end - rescue StandardError - # no-op - end - end.not_to change { described_class.fetch.repositories_count } - end - end - - context 'with not allowed attributes' do - it 'returns error' do - expect { described_class.untrack('something_else') }.to raise_error(ArgumentError).with_message(/Invalid attribute: \'something_else\' to \'untrack\' method/) - end - end - end -end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 733c1c49f08..7bd6dccd0ad 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1137,12 +1137,38 @@ describe User do expect(described_class.find_by_any_email(user.email.upcase, confirmed: true)).to eq user end - it 'finds by secondary email' do - email = create(:email, email: 'foo@example.com') - user = email.user + context 'finds by secondary email' do + let(:user) { email.user } - expect(described_class.find_by_any_email(email.email)).to eq user - expect(described_class.find_by_any_email(email.email, confirmed: true)).to eq user + context 'primary email confirmed' do + context 'secondary email confirmed' do + let!(:email) { create(:email, :confirmed, email: 'foo@example.com') } + + it 'finds user respecting the confirmed flag' do + expect(described_class.find_by_any_email(email.email)).to eq user + expect(described_class.find_by_any_email(email.email, confirmed: true)).to eq user + end + end + + context 'secondary email not confirmed' do + let!(:email) { create(:email, email: 'foo@example.com') } + + it 'finds user respecting the confirmed flag' do + expect(described_class.find_by_any_email(email.email)).to eq user + expect(described_class.find_by_any_email(email.email, confirmed: true)).to be_nil + end + end + end + + context 'primary email not confirmed' do + let(:user) { create(:user, confirmed_at: nil) } + let!(:email) { create(:email, :confirmed, user: user, email: 'foo@example.com') } + + it 'finds user respecting the confirmed flag' do + expect(described_class.find_by_any_email(email.email)).to eq user + expect(described_class.find_by_any_email(email.email, confirmed: true)).to be_nil + end + end end it 'returns nil when nothing found' do diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb new file mode 100644 index 00000000000..355336ad7e2 --- /dev/null +++ b/spec/requests/api/graphql/project/issues_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe 'getting an issue list for a project' do + include GraphqlHelpers + + let(:project) { create(:project, :repository, :public) } + let(:current_user) { create(:user) } + let(:issues_data) { graphql_data['project']['issues']['edges'] } + let!(:issues) do + create(:issue, project: project, discussion_locked: true) + create(:issue, project: project) + end + let(:fields) do + <<~QUERY + edges { + node { + #{all_graphql_fields_for('issues'.classify)} + } + } + QUERY + end + + let(:query) do + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + query_graphql_field('issues', {}, fields) + ) + end + + it_behaves_like 'a working graphql query' do + before do + post_graphql(query, current_user: current_user) + end + end + + it 'includes a web_url' do + post_graphql(query, current_user: current_user) + + expect(issues_data[0]['node']['webUrl']).to be_present + end + + it 'includes discussion locked' do + post_graphql(query, current_user: current_user) + + expect(issues_data[0]['node']['discussionLocked']).to eq false + expect(issues_data[1]['node']['discussionLocked']).to eq true + end + + context 'when the user does not have access to the issue' do + it 'returns nil' do + project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE) + + post_graphql(query) + + expect(issues_data).to eq [] + end + end +end diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index 909703a8d47..b36087b86a7 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -830,6 +830,18 @@ describe API::Runner, :clean_gitlab_redis_shared_state do expect(job.trace.raw).to eq 'BUILD TRACE UPDATED' expect(job.job_artifacts_trace.open.read).to eq 'BUILD TRACE UPDATED' end + + context 'when concurrent update of trace is happening' do + before do + job.trace.write('wb') do + update_job(state: 'success', trace: 'BUILD TRACE UPDATED') + end + end + + it 'returns that operation conflicts' do + expect(response.status).to eq(409) + end + end end context 'when no trace is given' do @@ -1022,6 +1034,18 @@ describe API::Runner, :clean_gitlab_redis_shared_state do end end + context 'when concurrent update of trace is happening' do + before do + job.trace.write('wb') do + patch_the_trace + end + end + + it 'returns that operation conflicts' do + expect(response.status).to eq(409) + end + end + context 'when the job is canceled' do before do job.cancel diff --git a/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb b/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb index eb0bdb61ee3..f3036fbcb0e 100644 --- a/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb +++ b/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb @@ -28,41 +28,7 @@ describe Clusters::Applications::CheckIngressIpAddressService do allow(application.cluster).to receive(:kubeclient).and_return(kubeclient) end - describe '#execute' do - context 'when the ingress ip address is available' do - it 'updates the external_ip for the app' do - subject + include_examples 'check ingress ip executions', :clusters_applications_ingress - expect(application.external_ip).to eq('111.222.111.222') - end - end - - context 'when the ingress ip address is not available' do - let(:ingress) { nil } - - it 'does not error' do - subject - end - end - - context 'when the exclusive lease cannot be obtained' do - it 'does not call kubeclient' do - stub_exclusive_lease_taken(lease_key, timeout: 15.seconds.to_i) - - subject - - expect(kubeclient).not_to have_received(:get_service) - end - end - - context 'when there is already an external_ip' do - let(:application) { create(:clusters_applications_ingress, :installed, external_ip: '001.111.002.111') } - - it 'does not call kubeclient' do - subject - - expect(kubeclient).not_to have_received(:get_service) - end - end - end + include_examples 'check ingress ip executions', :clusters_applications_knative end diff --git a/spec/services/clusters/applications/check_installation_progress_service_spec.rb b/spec/services/clusters/applications/check_installation_progress_service_spec.rb index ea17f2bb423..45b8ce94815 100644 --- a/spec/services/clusters/applications/check_installation_progress_service_spec.rb +++ b/spec/services/clusters/applications/check_installation_progress_service_spec.rb @@ -8,14 +8,6 @@ describe Clusters::Applications::CheckInstallationProgressService do let(:phase) { Gitlab::Kubernetes::Pod::UNKNOWN } let(:errors) { nil } - shared_examples 'a terminated installation' do - it 'removes the installation POD' do - expect(service).to receive(:remove_installation_pod).once - - service.execute - end - end - shared_examples 'a not yet terminated installation' do |a_phase| let(:phase) { a_phase } @@ -39,15 +31,13 @@ describe Clusters::Applications::CheckInstallationProgressService do context 'when timeouted' do let(:application) { create(:clusters_applications_helm, :timeouted) } - it_behaves_like 'a terminated installation' - it 'make the application errored' do expect(ClusterWaitForAppInstallationWorker).not_to receive(:perform_in) service.execute expect(application).to be_errored - expect(application.status_reason).to match(/\btimed out\b/) + expect(application.status_reason).to eq("Installation timed out. Check pod logs for install-helm for more details.") end end end @@ -66,7 +56,11 @@ describe Clusters::Applications::CheckInstallationProgressService do expect(service).to receive(:installation_phase).once.and_return(phase) end - it_behaves_like 'a terminated installation' + it 'removes the installation POD' do + expect(service).to receive(:remove_installation_pod).once + + service.execute + end it 'make the application installed' do expect(ClusterWaitForAppInstallationWorker).not_to receive(:perform_in) @@ -86,13 +80,11 @@ describe Clusters::Applications::CheckInstallationProgressService do expect(service).to receive(:installation_phase).once.and_return(phase) end - it_behaves_like 'a terminated installation' - it 'make the application errored' do service.execute expect(application).to be_errored - expect(application.status_reason).to eq("Installation failed") + expect(application.status_reason).to eq("Installation failed. Check pod logs for install-helm for more details.") end end @@ -113,6 +105,12 @@ describe Clusters::Applications::CheckInstallationProgressService do expect(application).to be_errored expect(application.status_reason).to eq('Kubernetes error: 401') end + + it 'should log error' do + expect(service.send(:logger)).to receive(:error) + + service.execute + end end end end diff --git a/spec/services/clusters/applications/install_service_spec.rb b/spec/services/clusters/applications/install_service_spec.rb index 2f801d019fe..018d9822d3e 100644 --- a/spec/services/clusters/applications/install_service_spec.rb +++ b/spec/services/clusters/applications/install_service_spec.rb @@ -33,8 +33,9 @@ describe Clusters::Applications::InstallService do end context 'when k8s cluster communication fails' do + let(:error) { Kubeclient::HttpError.new(500, 'system failure', nil) } + before do - error = Kubeclient::HttpError.new(500, 'system failure', nil) expect(helm_client).to receive(:install).with(install_command).and_raise(error) end @@ -44,18 +45,81 @@ describe Clusters::Applications::InstallService do expect(application).to be_errored expect(application.status_reason).to match('Kubernetes error: 500') end + + it 'logs errors' do + expect(service.send(:logger)).to receive(:error).with( + { + exception: 'Kubeclient::HttpError', + message: 'system failure', + service: 'Clusters::Applications::InstallService', + app_id: application.id, + project_ids: application.cluster.project_ids, + group_ids: [], + error_code: 500 + } + ) + + expect(Gitlab::Sentry).to receive(:track_acceptable_exception).with( + error, + extra: { + exception: 'Kubeclient::HttpError', + message: 'system failure', + service: 'Clusters::Applications::InstallService', + app_id: application.id, + project_ids: application.cluster.project_ids, + group_ids: [], + error_code: 500 + } + ) + + service.execute + end end - context 'when application cannot be persisted' do + context 'a non kubernetes error happens' do let(:application) { create(:clusters_applications_helm, :scheduled) } + let(:error) { StandardError.new("something bad happened") } + + before do + expect(application).to receive(:make_installing!).once.and_raise(error) + end it 'make the application errored' do - expect(application).to receive(:make_installing!).once.and_raise(ActiveRecord::RecordInvalid) expect(helm_client).not_to receive(:install) service.execute expect(application).to be_errored + expect(application.status_reason).to eq("Can't start installation process.") + end + + it 'logs errors' do + expect(service.send(:logger)).to receive(:error).with( + { + exception: 'StandardError', + error_code: nil, + message: 'something bad happened', + service: 'Clusters::Applications::InstallService', + app_id: application.id, + project_ids: application.cluster.projects.pluck(:id), + group_ids: [] + } + ) + + expect(Gitlab::Sentry).to receive(:track_acceptable_exception).with( + error, + extra: { + exception: 'StandardError', + error_code: nil, + message: 'something bad happened', + service: 'Clusters::Applications::InstallService', + app_id: application.id, + project_ids: application.cluster.projects.pluck(:id), + group_ids: [] + } + ) + + service.execute end end end diff --git a/spec/support/helpers/gpg_helpers.rb b/spec/support/helpers/gpg_helpers.rb index 3f7279a50e0..8d1637228d0 100644 --- a/spec/support/helpers/gpg_helpers.rb +++ b/spec/support/helpers/gpg_helpers.rb @@ -1,5 +1,9 @@ +# frozen_string_literal: true + module GpgHelpers - SIGNED_COMMIT_SHA = '8a852d50dda17cc8fd1408d2fd0c5b0f24c76ca4'.freeze + SIGNED_COMMIT_SHA = '8a852d50dda17cc8fd1408d2fd0c5b0f24c76ca4' + SIGNED_AND_AUTHORED_SHA = '3c1d9a0266cb0c62d926f4a6c649beed561846f5' + DIFFERING_EMAIL_SHA = 'a17a9f66543673edf0a3d1c6b93bdda3fe600f32' module User1 extend self diff --git a/spec/support/shared_examples/ci_trace_shared_examples.rb b/spec/support/shared_examples/ci_trace_shared_examples.rb index 94e82b8ce90..377bd82b67e 100644 --- a/spec/support/shared_examples/ci_trace_shared_examples.rb +++ b/spec/support/shared_examples/ci_trace_shared_examples.rb @@ -272,16 +272,11 @@ shared_examples_for 'common trace features' do include ExclusiveLeaseHelpers before do - stub_exclusive_lease_taken("trace:archive:#{trace.job.id}", timeout: 1.hour) + stub_exclusive_lease_taken("trace:write:lock:#{trace.job.id}", timeout: 1.minute) end it 'blocks concurrent archiving' do - expect(Rails.logger).to receive(:error).with('Cannot obtain an exclusive lease. There must be another instance already in execution.') - - subject - - build.reload - expect(build.job_artifacts_trace).to be_nil + expect { subject }.to raise_error(::Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError) end end end diff --git a/spec/support/shared_examples/services/check_ingress_ip_address_service_shared_examples.rb b/spec/support/shared_examples/services/check_ingress_ip_address_service_shared_examples.rb new file mode 100644 index 00000000000..14638a574a5 --- /dev/null +++ b/spec/support/shared_examples/services/check_ingress_ip_address_service_shared_examples.rb @@ -0,0 +1,33 @@ +shared_examples 'check ingress ip executions' do |app_name| + describe '#execute' do + let(:application) { create(app_name, :installed) } + let(:service) { described_class.new(application) } + let(:kubeclient) { double(::Kubeclient::Client, get_service: kube_service) } + + context 'when the ingress ip address is available' do + it 'updates the external_ip for the app' do + subject + + expect(application.external_ip).to eq('111.222.111.222') + end + end + + context 'when the ingress ip address is not available' do + let(:ingress) { nil } + + it 'does not error' do + subject + end + end + + context 'when the exclusive lease cannot be obtained' do + it 'does not call kubeclient' do + stub_exclusive_lease_taken(lease_key, timeout: 15.seconds.to_i) + + subject + + expect(kubeclient).not_to have_received(:get_service) + end + end + end +end diff --git a/spec/tasks/gitlab/site_statistics_rake_spec.rb b/spec/tasks/gitlab/site_statistics_rake_spec.rb deleted file mode 100644 index c43ce25a540..00000000000 --- a/spec/tasks/gitlab/site_statistics_rake_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true -require 'rake_helper' - -describe 'rake gitlab:refresh_site_statistics' do - before do - Rake.application.rake_require 'tasks/gitlab/site_statistics' - - create(:project) - SiteStatistic.fetch.update(repositories_count: 0) - end - - let(:task) { 'gitlab:refresh_site_statistics' } - - it 'recalculates existing counters' do - run_rake_task(task) - - expect(SiteStatistic.fetch.repositories_count).to eq(1) - end - - it 'displays message listing counters' do - expect { run_rake_task(task) }.to output(/Updating Site Statistics counters:.* Repositories\.\.\. OK!/m).to_stdout - end -end diff --git a/spec/views/layouts/header/_new_dropdown.haml_spec.rb b/spec/views/layouts/header/_new_dropdown.haml_spec.rb new file mode 100644 index 00000000000..2e19d0cec26 --- /dev/null +++ b/spec/views/layouts/header/_new_dropdown.haml_spec.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'layouts/header/_new_dropdown' do + let(:user) { create(:user) } + + context 'group-specific links' do + let(:group) { create(:group) } + + before do + stub_current_user(user) + + assign(:group, group) + end + + context 'as a Group owner' do + before do + group.add_owner(user) + end + + it 'has a "New project" link' do + render + + expect(rendered).to have_link( + 'New project', + href: new_project_path(namespace_id: group.id) + ) + end + + it 'has a "New subgroup" link', :nested_groups do + render + + expect(rendered).to have_link( + 'New subgroup', + href: new_group_path(parent_id: group.id) + ) + end + end + end + + context 'project-specific links' do + let(:project) { create(:project, creator: user, namespace: user.namespace) } + + before do + assign(:project, project) + end + + context 'as a Project owner' do + before do + stub_current_user(user) + end + + it 'has a "New issue" link' do + render + + expect(rendered).to have_link( + 'New issue', + href: new_project_issue_path(project) + ) + end + + it 'has a "New merge request" link' do + render + + expect(rendered).to have_link( + 'New merge request', + href: project_new_merge_request_path(project) + ) + end + + it 'has a "New snippet" link' do + render + + expect(rendered).to have_link( + 'New snippet', + href: new_project_snippet_path(project) + ) + end + end + + context 'as a Project guest' do + let(:guest) { create(:user) } + + before do + stub_current_user(guest) + project.add_guest(guest) + end + + it 'has no "New merge request" link' do + render + + expect(rendered).not_to have_link('New merge request') + end + + it 'has no "New snippet" link' do + render + + expect(rendered).not_to have_link( + 'New snippet', + href: new_project_snippet_path(project) + ) + end + end + end + + context 'global links' do + before do + stub_current_user(user) + end + + it 'has a "New project" link' do + render + + expect(rendered).to have_link('New project', href: new_project_path) + end + + it 'has a "New group" link' do + render + + expect(rendered).to have_link('New group', href: new_group_path) + end + + it 'has a "New snippet" link' do + render + + expect(rendered).to have_link('New snippet', href: new_snippet_path) + end + end + + def stub_current_user(current_user) + allow(view).to receive(:current_user).and_return(current_user) + end +end diff --git a/spec/workers/stuck_ci_jobs_worker_spec.rb b/spec/workers/stuck_ci_jobs_worker_spec.rb index 557934346c9..e09b8e5b964 100644 --- a/spec/workers/stuck_ci_jobs_worker_spec.rb +++ b/spec/workers/stuck_ci_jobs_worker_spec.rb @@ -5,7 +5,7 @@ describe StuckCiJobsWorker do let!(:runner) { create :ci_runner } let!(:job) { create :ci_build, runner: runner } - let(:trace_lease_key) { "trace:archive:#{job.id}" } + let(:trace_lease_key) { "trace:write:lock:#{job.id}" } let(:trace_lease_uuid) { SecureRandom.uuid } let(:worker_lease_key) { StuckCiJobsWorker::EXCLUSIVE_LEASE_KEY } let(:worker_lease_uuid) { SecureRandom.uuid } diff --git a/yarn.lock b/yarn.lock index 63a8913fa3d..aa4d3eb1bc7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -654,6 +654,11 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow== +"@types/async@2.0.50": + version "2.0.50" + resolved "https://registry.yarnpkg.com/@types/async/-/async-2.0.50.tgz#117540e026d64e1846093abbd5adc7e27fda7bcb" + integrity sha512-VMhZMMQgV1zsR+lX/0IBfAk+8Eb7dPVMWiQGFAt3qjo5x7Ml6b77jUo0e1C3ToD+XRDXqtrfw+6AB0uUsPEr3Q== + "@types/events@*": version "1.2.0" resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86" @@ -678,12 +683,7 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== -"@types/node@*": - version "10.5.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.5.2.tgz#f19f05314d5421fe37e74153254201a7bf00a707" - integrity sha512-m9zXmifkZsMHZBOyxZWilMwmTlpC8x5Ty360JKTiXvlXZfBWYpsg9ZZvP/Ye+iZUh+Q+MxDLjItVTWIsfwz+8Q== - -"@types/node@^10.11.7": +"@types/node@*", "@types/node@^10.11.7": version "10.12.9" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.9.tgz#a07bfa74331471e1dc22a47eb72026843f7b95c8" integrity sha512-eajkMXG812/w3w4a1OcBlaTwsFPO5F7fJ/amy+tieQxEMWBlbV1JGSjkFM+zkHNf81Cad+dfIRA+IBkvmvdAeA== @@ -698,6 +698,11 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45" integrity sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ== +"@types/zen-observable@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d" + integrity sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg== + "@vue/component-compiler-utils@^2.0.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-2.2.0.tgz#bbbb7ed38a9a8a7c93abe7ef2e54a90a04b631b4" @@ -882,7 +887,7 @@ abbrev@1, abbrev@1.0.x: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" integrity sha1-kbR5JYinc4wl813W9jdSovh3YTU= -accepts@~1.3.3, accepts@~1.3.4, accepts@~1.3.5: +accepts@~1.3.4, accepts@~1.3.5: version "1.3.5" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" integrity sha1-63d99gEXI6OxTopywIBcjoZ0a9I= @@ -996,6 +1001,103 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" +apollo-boost@^0.1.20: + version "0.1.20" + resolved "https://registry.yarnpkg.com/apollo-boost/-/apollo-boost-0.1.20.tgz#cc3e418ebd2bea857656685d32a7a20443493363" + integrity sha512-n2MiEY5IGpD/cy0RH+pM9vbmobM/JZ5qz38XQAUA41FxxMPlLFQxf0IUMm0tijLOJvJJBub3pDt+Of4TVPBCqA== + dependencies: + apollo-cache "^1.1.20" + apollo-cache-inmemory "^1.3.9" + apollo-client "^2.4.5" + apollo-link "^1.0.6" + apollo-link-error "^1.0.3" + apollo-link-http "^1.3.1" + apollo-link-state "^0.4.0" + graphql-tag "^2.4.2" + +apollo-cache-inmemory@^1.3.9: + version "1.3.9" + resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.3.9.tgz#10738ba6a04faaeeb0da21bbcc1f7c0b5902910c" + integrity sha512-Q2k84p/OqIuMUyeWGc6XbVXXZu0erYOO+wTx9p+CnQUspnNvf7zmvFNgFnmudXzfuG1m1CSzePk6fC/M1ehOqQ== + dependencies: + apollo-cache "^1.1.20" + apollo-utilities "^1.0.25" + optimism "^0.6.6" + +apollo-cache@1.1.20, apollo-cache@^1.1.20: + version "1.1.20" + resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.1.20.tgz#6152cc4baf6a63e376efee79f75de4f5c84bf90e" + integrity sha512-+Du0/4kUSuf5PjPx0+pvgMGV12ezbHA8/hubYuqRQoy/4AWb4faa61CgJNI6cKz2mhDd9m94VTNKTX11NntwkQ== + dependencies: + apollo-utilities "^1.0.25" + +apollo-client@^2.4.5: + version "2.4.5" + resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.4.5.tgz#545beda1ef60814943b5622f0feabc9f29ee9822" + integrity sha512-nUm06EGa4TP/IY68OzmC3lTD32TqkjLOQdb69uYo+lHl8NnwebtrAw3qFtsQtTEz6ueBp/Z/HasNZng4jwafVQ== + dependencies: + "@types/zen-observable" "^0.8.0" + apollo-cache "1.1.20" + apollo-link "^1.0.0" + apollo-link-dedup "^1.0.0" + apollo-utilities "1.0.25" + symbol-observable "^1.0.2" + zen-observable "^0.8.0" + optionalDependencies: + "@types/async" "2.0.50" + +apollo-link-dedup@^1.0.0: + version "1.0.10" + resolved "https://registry.yarnpkg.com/apollo-link-dedup/-/apollo-link-dedup-1.0.10.tgz#7b94589fe7f969777efd18a129043c78430800ae" + integrity sha512-tpUI9lMZsidxdNygSY1FxflXEkUZnvKRkMUsXXuQUNoSLeNtEvUX7QtKRAl4k9ubLl8JKKc9X3L3onAFeGTK8w== + dependencies: + apollo-link "^1.2.3" + +apollo-link-error@^1.0.3: + version "1.1.1" + resolved "https://registry.yarnpkg.com/apollo-link-error/-/apollo-link-error-1.1.1.tgz#69d7124d4dc11ce60f505c940f05d4f1aa0945fb" + integrity sha512-/yPcaQWcBdB94vpJ4FsiCJt1dAGGRm+6Tsj3wKwP+72taBH+UsGRQQZk7U/1cpZwl1yqhHZn+ZNhVOebpPcIlA== + dependencies: + apollo-link "^1.2.3" + +apollo-link-http-common@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.5.tgz#d094beb7971523203359bf830bfbfa7b4e7c30ed" + integrity sha512-6FV1wr5AqAyJ64Em1dq5hhGgiyxZE383VJQmhIoDVc3MyNcFL92TkhxREOs4rnH2a9X2iJMko7nodHSGLC6d8w== + dependencies: + apollo-link "^1.2.3" + +apollo-link-http@^1.3.1: + version "1.5.5" + resolved "https://registry.yarnpkg.com/apollo-link-http/-/apollo-link-http-1.5.5.tgz#7dbe851821771ad67fa29e3900c57f38cbd80da8" + integrity sha512-C5N6N/mRwmepvtzO27dgMEU3MMtRKSqcljBkYNZmWwH11BxkUQ5imBLPM3V4QJXNE7NFuAQAB5PeUd4ligivTQ== + dependencies: + apollo-link "^1.2.3" + apollo-link-http-common "^0.2.5" + +apollo-link-state@^0.4.0: + version "0.4.2" + resolved "https://registry.yarnpkg.com/apollo-link-state/-/apollo-link-state-0.4.2.tgz#ac00e9be9b0ca89eae0be6ba31fe904b80bbe2e8" + integrity sha512-xMPcAfuiPVYXaLwC6oJFIZrKgV3GmdO31Ag2eufRoXpvT0AfJZjdaPB4450Nu9TslHRePN9A3quxNueILlQxlw== + dependencies: + apollo-utilities "^1.0.8" + graphql-anywhere "^4.1.0-alpha.0" + +apollo-link@^1.0.0, apollo-link@^1.0.6, apollo-link@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.3.tgz#9bd8d5fe1d88d31dc91dae9ecc22474d451fb70d" + integrity sha512-iL9yS2OfxYhigme5bpTbmRyC+Htt6tyo2fRMHT3K1XRL/C5IQDDz37OjpPy4ndx7WInSvfSZaaOTKFja9VWqSw== + dependencies: + apollo-utilities "^1.0.0" + zen-observable-ts "^0.8.10" + +apollo-utilities@1.0.25, apollo-utilities@^1.0.0, apollo-utilities@^1.0.25, apollo-utilities@^1.0.8: + version "1.0.25" + resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.0.25.tgz#899b00f5f990fb451675adf84cb3de82eb6372ea" + integrity sha512-AXvqkhni3Ir1ffm4SA1QzXn8k8I5BBl4PVKEyak734i4jFdp+xgfUyi2VCqF64TJlFTA/B73TRDUvO2D+tKtZg== + dependencies: + fast-json-stable-stringify "^2.0.0" + append-transform@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-1.0.0.tgz#046a52ae582a228bd72f58acfbe2967c678759ab" @@ -1038,11 +1140,6 @@ arr-union@^3.1.0: resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= -array-find-index@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" - integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= - array-find@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/array-find/-/array-find-1.0.0.tgz#6c8e286d11ed768327f8e62ecee87353ca3e78b8" @@ -1139,9 +1236,9 @@ async@^2.0.0, async@^2.5.0, async@^2.6.1: lodash "^4.17.10" atob@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/atob/-/atob-2.0.3.tgz#19c7a760473774468f20b2d2d03372ad7d4cbf5d" - integrity sha1-GcenYEc3dEaPILLS0DNyrX1Mv10= + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== autosize@^4.0.0: version "4.0.0" @@ -1565,11 +1662,6 @@ builtin-status-codes@^3.0.0: resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= -bytes@2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.5.0.tgz#4c9423ea2d252c270c41b2bdefeff9bb6b62c06a" - integrity sha1-TJQj6i0lLCcMQbK97+/5u2tiwGo= - bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" @@ -1892,12 +1984,7 @@ combine-lists@^1.0.0: dependencies: lodash "^4.5.0" -commander@2, commander@^2.18.0: - version "2.18.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.18.0.tgz#2bf063ddee7c7891176981a2cc798e5754bc6970" - integrity sha512-6CYPa+JP2ftfRU2qkDK+UTVeQYosOg/2GbcjIcKPHfinyOLPVGXu/ovN86RP49Re5ndJK1N0kuiidFFuepc4ZQ== - -commander@^2.19.0: +commander@2, commander@^2.18.0, commander@^2.19.0: version "2.19.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== @@ -1937,12 +2024,12 @@ component-inherit@0.0.3: resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143" integrity sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM= -compressible@~2.0.10: - version "2.0.11" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.11.tgz#16718a75de283ed8e604041625a2064586797d8a" - integrity sha1-FnGKdd4oPtjmBAQWJaIGRYZ5fYo= +compressible@~2.0.14: + version "2.0.15" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.15.tgz#857a9ab0a7e5a07d8d837ed43fe2defff64fe212" + integrity sha512-4aE67DL33dSW9gw4CI2H/yTxqHLNcxp0yS6jB+4h+wr3e43+1z7vm0HU9qXOH8j+qjKuL8+UtkOxYQSMq60Ylw== dependencies: - mime-db ">= 1.29.0 < 2" + mime-db ">= 1.36.0 < 2" compression-webpack-plugin@^2.0.0: version "2.0.0" @@ -1957,17 +2044,17 @@ compression-webpack-plugin@^2.0.0: webpack-sources "^1.0.1" compression@^1.5.2: - version "1.7.0" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.0.tgz#030c9f198f1643a057d776a738e922da4373012d" - integrity sha1-AwyfGY8WQ6BX13anOOki2kNzAS0= + version "1.7.3" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.3.tgz#27e0e176aaf260f7f2c2813c3e440adb9f1993db" + integrity sha512-HSjyBG5N1Nnz7tF2+O7A9XUhyjru71/fwgNb7oIsEVHR0WShfs2tIS/EySLgiTe98aOK18YDlMXpzjCXY/n9mg== dependencies: - accepts "~1.3.3" - bytes "2.5.0" - compressible "~2.0.10" - debug "2.6.8" + accepts "~1.3.5" + bytes "3.0.0" + compressible "~2.0.14" + debug "2.6.9" on-headers "~1.0.1" - safe-buffer "5.1.1" - vary "~1.1.1" + safe-buffer "5.1.2" + vary "~1.1.2" concat-map@0.0.1: version "0.0.1" @@ -2242,13 +2329,6 @@ cssesc@^0.1.0: resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-0.1.0.tgz#c814903e45623371a0477b40109aaafbeeaddbb4" integrity sha1-yBSQPkViM3GgR3tAEJqq++6t27Q= -currently-unhandled@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" - integrity sha1-mI3zP+qxke95mmE2nddsF635V+o= - dependencies: - array-find-index "^1.0.1" - custom-event@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" @@ -2335,12 +2415,7 @@ d3-force@1.1.0: d3-quadtree "1" d3-timer "1" -d3-format@1, d3-format@1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.2.1.tgz#4e19ecdb081a341dafaf5f555ee956bcfdbf167f" - integrity sha512-U4zRVLDXW61bmqoo+OJ/V687e1T5nVd3TAKAJKgtpZ/P1JsMgyod0y9br+mlQOryTAACdiXI3wCjuERHFNp91w== - -d3-format@1.2.2: +d3-format@1, d3-format@1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.2.2.tgz#1a39c479c8a57fe5051b2e67a3bee27061a74e7a" integrity sha512-zH9CfF/3C8zUI47nsiKfD0+AGDEuM8LwBIP7pBVpyR4l/sKkZqITmMtxRp04rwBrlshIZ17XeFAaovN3++wzkw== @@ -2412,12 +2487,7 @@ d3-scale@1.0.7, d3-scale@^1.0.7: d3-time "1" d3-time-format "2" -d3-selection@1, d3-selection@1.2.0, d3-selection@^1.1.0, d3-selection@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.2.0.tgz#1b8ec1c7cedadfb691f2ba20a4a3cfbeb71bbc88" - integrity sha512-xW2Pfcdzh1gOaoI+LGpPsLR2VpBQxuFoxvrvguK8ZmrJbPIVvfNG6pU6GNfK41D6Qz15sj61sbW/AFYuukwaLQ== - -d3-selection@1.3.0: +d3-selection@1, d3-selection@1.3.0, d3-selection@^1.1.0, d3-selection@^1.2.0: version "1.3.0" resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.3.0.tgz#d53772382d3dc4f7507bfb28bcd2d6aed2a0ad6d" integrity sha512-qgpUOg9tl5CirdqESUAu0t9MU/t3O9klYfGfyKsXEmhyxyzLpzpeh08gaxBUTQw1uXIOkr/30Ut2YRjSSxlmHA== @@ -2474,42 +2544,6 @@ d3-zoom@1.7.1: d3-selection "1" d3-transition "1" -d3@4.12.2: - version "4.12.2" - resolved "https://registry.yarnpkg.com/d3/-/d3-4.12.2.tgz#12f775564c6a9de229f63db03446e2cb7bb56c8f" - integrity sha512-aKAlpgTmpuGeEpezB+GvPpX1x+gCMs/PHpuse6sCpkgw4Un3ZeqUobIc87eIy9adcl+wxPAnEyKyO5oulH3MOw== - dependencies: - d3-array "1.2.1" - d3-axis "1.0.8" - d3-brush "1.0.4" - d3-chord "1.0.4" - d3-collection "1.0.4" - d3-color "1.0.3" - d3-dispatch "1.0.3" - d3-drag "1.2.1" - d3-dsv "1.0.8" - d3-ease "1.0.3" - d3-force "1.1.0" - d3-format "1.2.1" - d3-geo "1.9.1" - d3-hierarchy "1.1.5" - d3-interpolate "1.1.6" - d3-path "1.0.5" - d3-polygon "1.0.3" - d3-quadtree "1.0.3" - d3-queue "3.0.7" - d3-random "1.1.0" - d3-request "1.0.6" - d3-scale "1.0.7" - d3-selection "1.2.0" - d3-shape "1.2.0" - d3-time "1.0.8" - d3-time-format "2.1.1" - d3-timer "1.0.7" - d3-transition "1.1.1" - d3-voronoi "1.1.2" - d3-zoom "1.7.1" - d3@^4.13.0: version "4.13.0" resolved "https://registry.yarnpkg.com/d3/-/d3-4.13.0.tgz#ab236ff8cf0cfc27a81e69bf2fb7518bc9b4f33d" @@ -2582,24 +2616,17 @@ de-indent@^1.0.2: resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" integrity sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0= -debug@2.6.8: - version "2.6.8" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" - integrity sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw= - dependencies: - ms "2.0.0" - -debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.6, debug@^2.6.8, debug@^2.6.9: +debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== dependencies: ms "2.0.0" -debug@^3.1.0: - version "3.2.5" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.5.tgz#c2418fbfd7a29f4d4f70ff4cea604d4b64c46407" - integrity sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg== +debug@^3.1.0, debug@^3.2.5: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== dependencies: ms "^2.1.1" @@ -2639,10 +2666,10 @@ deep-equal@^1.0.1: resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU= -deep-extend@~0.4.0: - version "0.4.2" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" - integrity sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8= +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== deep-is@~0.1.3: version "0.1.3" @@ -3361,12 +3388,12 @@ events@^1.0.0: resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= -eventsource@0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-0.1.6.tgz#0acede849ed7dd1ccc32c811bb11b944d4f29232" - integrity sha1-Cs7ehJ7X3RzMMsgRuxG5RNTykjI= +eventsource@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.0.7.tgz#8fbc72c93fcd34088090bc0a4e64f4b5cee6d8d0" + integrity sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ== dependencies: - original ">=0.0.5" + original "^1.0.0" evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: version "1.0.3" @@ -3492,9 +3519,9 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: is-extendable "^1.0.1" extend@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" - integrity sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ= + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== external-editor@^2.0.1: version "2.2.0" @@ -3555,7 +3582,7 @@ faye-websocket@^0.10.0: dependencies: websocket-driver ">=0.5.1" -faye-websocket@~0.11.0: +faye-websocket@~0.11.1: version "0.11.1" resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.1.tgz#f0efe18c4f56e4f40afc7e06c719fd5ee6188f38" integrity sha1-8O/hjE9W5PQK/H4Gxxn9XuYYjzg= @@ -3992,6 +4019,25 @@ graphlibrary@^2.2.0: dependencies: lodash "^4.17.5" +graphql-anywhere@^4.1.0-alpha.0: + version "4.1.22" + resolved "https://registry.yarnpkg.com/graphql-anywhere/-/graphql-anywhere-4.1.22.tgz#1c831ba3c9e5664a0dd24d10d23a9e9512d92056" + integrity sha512-qm2/1cKM8nfotxDhm4J0r1znVlK0Yge/yEKt26EVVBgpIhvxjXYFALCGbr7cvfDlvzal1iSPpaYa+8YTtjsxQA== + dependencies: + apollo-utilities "^1.0.25" + +graphql-tag@^2.10.0, graphql-tag@^2.4.2: + version "2.10.0" + resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.0.tgz#87da024be863e357551b2b8700e496ee2d4353ae" + integrity sha512-9FD6cw976TLLf9WYIUPCaaTpniawIjHWZSwIRZSjrfufJamcXbVVYfN2TWvJYbw0Xf2JjYbl1/f2+wDnBVw3/w== + +graphql@^14.0.2: + version "14.0.2" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.0.2.tgz#7dded337a4c3fd2d075692323384034b357f5650" + integrity sha512-gUC4YYsaiSJT1h40krG3J+USGlwhzNTXSb4IOZljn9ag5Tj+RkoXrWp+Kh7WyE3t1NCfab5kzCuxBIvOMERMXw== + dependencies: + iterall "^1.2.2" + gzip-size@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-5.0.0.tgz#a55ecd99222f4c48fd8c01c625ce3b349d0a0e80" @@ -4199,7 +4245,7 @@ http-deceiver@^1.2.7: resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" integrity sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc= -http-errors@1.6.2, http-errors@~1.6.1, http-errors@~1.6.2: +http-errors@1.6.2, http-errors@~1.6.2: version "1.6.2" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" integrity sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY= @@ -4288,6 +4334,11 @@ immediate@~3.0.5: resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= +immutable-tuple@^0.4.9: + version "0.4.9" + resolved "https://registry.yarnpkg.com/immutable-tuple/-/immutable-tuple-0.4.9.tgz#473ebdd6c169c461913a454bf87ef8f601a20ff0" + integrity sha512-LWbJPZnidF8eczu7XmcnLBsumuyRBkpwIRPCZxlojouhBo5jEBO4toj6n7hMy6IxHU/c+MqDSWkvaTpPlMQcyA== + import-lazy@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" @@ -4835,6 +4886,11 @@ isurl@^1.0.0-alpha5: has-to-string-tag-x "^1.2.0" is-object "^1.0.1" +iterall@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7" + integrity sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA== + jasmine-core@^2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.9.0.tgz#bfbb56defcd30789adec5a3fbba8504233289c72" @@ -5276,14 +5332,6 @@ loose-envify@^1.0.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" -loud-rejection@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" - integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8= - dependencies: - currently-unhandled "^0.4.1" - signal-exit "^3.0.0" - lowercase-keys@1.0.0, lowercase-keys@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" @@ -5446,17 +5494,17 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -"mime-db@>= 1.29.0 < 2", mime-db@~1.33.0: - version "1.33.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" - integrity sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ== +"mime-db@>= 1.36.0 < 2", mime-db@~1.37.0: + version "1.37.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.37.0.tgz#0b6a0ce6fdbe9576e25f1f2d2fde8830dc0ad0d8" + integrity sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg== -mime-types@~2.1.15, mime-types@~2.1.18: - version "2.1.18" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" - integrity sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ== +mime-types@~2.1.17, mime-types@~2.1.18: + version "2.1.21" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.21.tgz#28995aa1ecb770742fe6ae7e58f9181c744b3f96" + integrity sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg== dependencies: - mime-db "~1.33.0" + mime-db "~1.37.0" mime@1.4.1: version "1.4.1" @@ -5660,10 +5708,10 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -needle@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.1.tgz#b5e325bd3aae8c2678902fa296f729455d1d3a7d" - integrity sha512-t/ZswCM9JTWjAdXS9VpvqhI2Ct2sL2MdY4fUXqGJaGBk13ge99ObqRksRTbBE56K+wxUXwwfZYOuZHifFW9q+Q== +needle@^2.2.1: + version "2.2.4" + resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.4.tgz#51931bff82533b1928b7d1d69e01f1b00ffd2a4e" + integrity sha512-HyoqEb4wr/rsoaIDfTH2aVL9nWtQqba2/HvMv+++m8u0dz808MaagKILxtfeSN7QU7nvbQ79zk3vYOJp9zsNEA== dependencies: debug "^2.1.2" iconv-lite "^0.4.4" @@ -5727,17 +5775,17 @@ node-forge@0.6.33: vm-browserify "0.0.4" node-pre-gyp@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.0.tgz#6e4ef5bb5c5203c6552448828c852c40111aac46" - integrity sha512-G7kEonQLRbcA/mOoFoxvlMrw6Q6dPf92+t/l0DFSMuSlDoWaI9JWIyPwK0jyE1bph//CUEL65/Fz1m2vJbmjQQ== + version "0.10.3" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc" + integrity sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A== dependencies: detect-libc "^1.0.2" mkdirp "^0.5.1" - needle "^2.2.0" + needle "^2.2.1" nopt "^4.0.1" npm-packlist "^1.1.6" npmlog "^4.0.2" - rc "^1.1.7" + rc "^1.2.7" rimraf "^2.6.1" semver "^5.3.0" tar "^4" @@ -5982,6 +6030,13 @@ opn@^5.1.0: dependencies: is-wsl "^1.1.0" +optimism@^0.6.6: + version "0.6.8" + resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.6.8.tgz#0780b546da8cd0a72e5207e0c3706c990c8673a6" + integrity sha512-bN5n1KCxSqwBDnmgDnzMtQTHdL+uea2HYFx1smvtE+w2AMl0Uy31g0aXnP/Nt85OINnMJPRpJyfRQLTCqn5Weg== + dependencies: + immutable-tuple "^0.4.9" + optimist@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" @@ -6002,12 +6057,12 @@ optionator@^0.8.1, optionator@^0.8.2: type-check "~0.3.2" wordwrap "~1.0.0" -original@>=0.0.5: - version "1.0.0" - resolved "https://registry.yarnpkg.com/original/-/original-1.0.0.tgz#9147f93fa1696d04be61e01bd50baeaca656bd3b" - integrity sha1-kUf5P6FpbQS+YeAb1QuurKZWvTs= +original@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f" + integrity sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg== dependencies: - url-parse "1.0.x" + url-parse "^1.4.3" os-browserify@^0.3.0: version "0.3.0" @@ -6180,7 +6235,7 @@ parseuri@0.0.5: dependencies: better-assert "~1.0.0" -parseurl@~1.3.1, parseurl@~1.3.2: +parseurl@~1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" integrity sha1-/CidTtiZMRlGDBViUyYs3I3mW/M= @@ -6576,15 +6631,10 @@ querystring@0.2.0: resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= -querystringify@0.0.x: - version "0.0.4" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-0.0.4.tgz#0cf7f84f9463ff0ae51c4c4b142d95be37724d9c" - integrity sha1-DPf4T5Rj/wrlHExLFC2VvjdyTZw= - -querystringify@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-1.0.0.tgz#6286242112c5b712fa654e526652bf6a13ff05cb" - integrity sha1-YoYkIRLFtxL6ZU5SZlK/ahP/Bcs= +querystringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.0.tgz#7ded8dfbf7879dcc60d0a644ac6754b283ad17ef" + integrity sha512-sluvZZ1YiTLD5jsqZcDmFyV2EwToyXZBfpoVOmktMmW+VEnhgakFHnasVph65fOjGPTWN0Nw3+XQaSeMayr0kg== randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: version "2.0.6" @@ -6633,12 +6683,12 @@ raw-loader@^0.5.1: resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-0.5.1.tgz#0c3d0beaed8a01c966d9787bf778281252a979aa" integrity sha1-DD0L6u2KAclm2Xh793goElKpeao= -rc@^1.0.1, rc@^1.1.6, rc@^1.1.7: - version "1.2.5" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.5.tgz#275cd687f6e3b36cc756baa26dfee80a790301fd" - integrity sha1-J1zWh/bjs2zHVrqibf7oCnkDAf0= +rc@^1.0.1, rc@^1.1.6, rc@^1.2.7: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== dependencies: - deep-extend "~0.4.0" + deep-extend "^0.6.0" ini "~1.3.0" minimist "^1.2.0" strip-json-comments "~2.0.1" @@ -6852,7 +6902,7 @@ require-uncached@^1.0.3: caller-path "^0.1.0" resolve-from "^1.0.0" -requires-port@1.0.x, requires-port@1.x.x: +requires-port@1.x.x, requires-port@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= @@ -6967,7 +7017,7 @@ safe-buffer@5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" integrity sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg== -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@5.1.2, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== @@ -7057,12 +7107,7 @@ semver-diff@^2.0.0: dependencies: semver "^5.0.3" -"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1: - version "5.5.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477" - integrity sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw== - -semver@^5.6.0: +"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0: version "5.6.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg== @@ -7092,17 +7137,17 @@ serialize-javascript@^1.4.0: integrity sha1-fJWFFNtqwkQ6irwGLcn3iGp/YAU= serve-index@^1.7.2: - version "1.9.0" - resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.0.tgz#d2b280fc560d616ee81b48bf0fa82abed2485ce7" - integrity sha1-0rKA/FYNYW7oG0i/D6gqvtJIXOc= + version "1.9.1" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" + integrity sha1-03aNabHn2C5c4FD/9bRTvqEqkjk= dependencies: - accepts "~1.3.3" + accepts "~1.3.4" batch "0.6.1" - debug "2.6.8" + debug "2.6.9" escape-html "~1.0.3" - http-errors "~1.6.1" - mime-types "~2.1.15" - parseurl "~1.3.1" + http-errors "~1.6.2" + mime-types "~2.1.17" + parseurl "~1.3.2" serve-static@1.13.2: version "1.13.2" @@ -7301,17 +7346,17 @@ socket.io@2.1.1: socket.io-client "2.1.1" socket.io-parser "~3.2.0" -sockjs-client@1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.1.5.tgz#1bb7c0f7222c40f42adf14f4442cbd1269771a83" - integrity sha1-G7fA9yIsQPQq3xT0RCy9Eml3GoM= +sockjs-client@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.3.0.tgz#12fc9d6cb663da5739d3dc5fb6e8687da95cb177" + integrity sha512-R9jxEzhnnrdxLCNln0xg5uGHqMnkhPSTzUZH2eXcR03S/On9Yvoq2wyUZILRUhZCNVu2PmwWVoyuiPz8th8zbg== dependencies: - debug "^2.6.6" - eventsource "0.1.6" - faye-websocket "~0.11.0" - inherits "^2.0.1" + debug "^3.2.5" + eventsource "^1.0.7" + faye-websocket "~0.11.1" + inherits "^2.0.3" json3 "^3.3.2" - url-parse "^1.1.8" + url-parse "^1.4.3" sockjs@0.3.19: version "0.3.19" @@ -7638,6 +7683,11 @@ svg4everybody@2.1.9: resolved "https://registry.yarnpkg.com/svg4everybody/-/svg4everybody-2.1.9.tgz#5bd9f6defc133859a044646d4743fabc28db7e2d" integrity sha1-W9n23vwTOFmgRGRtR0P6vCjbfi0= +symbol-observable@^1.0.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" + integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== + table@^4.0.3: version "4.0.3" resolved "http://registry.npmjs.org/table/-/table-4.0.3.tgz#00b5e2b602f1794b9acaf9ca908a76386a7813bc" @@ -7715,6 +7765,11 @@ three@^0.84.0: resolved "https://registry.yarnpkg.com/three/-/three-0.84.0.tgz#95be85a55a0fa002aa625ed559130957dcffd918" integrity sha1-lb6FpVoPoAKqYl7VWRMJV9z/2Rg= +throttle-debounce@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.0.1.tgz#7307ddd6cd9acadb349132fbf6c18d78c88a5e62" + integrity sha512-Sr6jZBlWShsAaSXKyNXyNicOrJW/KtkDqIEwHt4wYwWA2wa/q67Luhqoujg48V8hTk60wB56tYrJJn6jc2R7VA== + through2@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be" @@ -8022,11 +8077,6 @@ urix@^0.1.0: resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= -url-join@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.0.tgz#4d3340e807d3773bda9991f8305acdcc2a665d2a" - integrity sha1-TTNA6AfTdzvamZH4MFrNzCpmXSo= - url-loader@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-1.1.1.tgz#4d1f3b4f90dde89f02c008e662d604d7511167c1" @@ -8050,21 +8100,13 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" -url-parse@1.0.x: - version "1.0.5" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.0.5.tgz#0854860422afdcfefeb6c965c662d4800169927b" - integrity sha1-CFSGBCKv3P7+tsllxmLUgAFpkns= - dependencies: - querystringify "0.0.x" - requires-port "1.0.x" - -url-parse@^1.1.8: - version "1.1.9" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.1.9.tgz#c67f1d775d51f0a18911dd7b3ffad27bb9e5bd19" - integrity sha1-xn8dd11R8KGJEd17P/rSe7nlvRk= +url-parse@^1.4.3: + version "1.4.4" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.4.tgz#cac1556e95faa0303691fec5cf9d5a1bc34648f8" + integrity sha512-/92DTTorg4JjktLNLe6GPS2/RvAd/RGr6LuktmWSMLEOa6rjnlrFXNgSbSmkNvCoL2T028A0a1JaJLzRMlFoHg== dependencies: - querystringify "~1.0.0" - requires-port "1.0.x" + querystringify "^2.0.0" + requires-port "^1.0.0" url-search-params-polyfill@^5.0.0: version "5.0.0" @@ -8144,7 +8186,7 @@ validate-npm-package-license@^3.0.1: spdx-correct "~1.0.0" spdx-expression-parse "~1.0.0" -vary@~1.1.1, vary@~1.1.2: +vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= @@ -8166,6 +8208,14 @@ void-elements@^2.0.0: resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w= +vue-apollo@^3.0.0-beta.25: + version "3.0.0-beta.25" + resolved "https://registry.yarnpkg.com/vue-apollo/-/vue-apollo-3.0.0-beta.25.tgz#05a9a699b2ba6103639e9bd6c3bb88ca04c4b637" + integrity sha512-M7/l3h0NlFvaZ/s/wrtRiOt3xXMbaNNuteGaCY+U5D0ABrQqvCgy5mayIZHurQxbloluNkbCt18wRKAgJTAuKA== + dependencies: + chalk "^2.4.1" + throttle-debounce "^2.0.0" + vue-eslint-parser@^3.2.1: version "3.2.2" resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-3.2.2.tgz#47c971ee4c39b0ee7d7f5e154cb621beb22f7a34" @@ -8298,23 +8348,20 @@ webpack-cli@^3.1.0: v8-compile-cache "^2.0.0" yargs "^12.0.1" -webpack-dev-middleware@3.2.0, webpack-dev-middleware@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.2.0.tgz#a20ceef194873710052da678f3c6ee0aeed92552" - integrity sha512-YJLMF/96TpKXaEQwaLEo+Z4NDK8aV133ROF6xp9pe3gQoS7sxfpXh4Rv9eC+8vCvWfmDjRQaMSlRPbO+9G6jgA== +webpack-dev-middleware@3.4.0, webpack-dev-middleware@^3.2.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.4.0.tgz#1132fecc9026fd90f0ecedac5cbff75d1fb45890" + integrity sha512-Q9Iyc0X9dP9bAsYskAVJ/hmIZZQwf/3Sy4xCAZgL5cUkjZmUZLt4l5HpbST/Pdgjn3u6pE7u5OdGd1apgzRujA== dependencies: - loud-rejection "^1.6.0" memory-fs "~0.4.1" mime "^2.3.1" - path-is-absolute "^1.0.0" range-parser "^1.0.3" - url-join "^4.0.0" webpack-log "^2.0.0" -webpack-dev-server@^3.1.8: - version "3.1.8" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.1.8.tgz#eb7a95945d1108170f902604fb3b939533d9daeb" - integrity sha512-c+tcJtDqnPdxCAzEEZKdIPmg3i5i7cAHe+B+0xFNK0BlCc2HF/unYccbU7xTgfGc5xxhCztCQzFmsqim+KhI+A== +webpack-dev-server@^3.1.10: + version "3.1.10" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.1.10.tgz#507411bee727ee8d2fdffdc621b66a64ab3dea2b" + integrity sha512-RqOAVjfqZJtQcB0LmrzJ5y4Jp78lv9CK0MZ1YJDTaTmedMZ9PU9FLMQNrMCfVu8hHzaVLVOJKBlGEHMN10z+ww== dependencies: ansi-html "0.0.7" bonjour "^3.5.0" @@ -8337,11 +8384,11 @@ webpack-dev-server@^3.1.8: selfsigned "^1.9.1" serve-index "^1.7.2" sockjs "0.3.19" - sockjs-client "1.1.5" + sockjs-client "1.3.0" spdy "^3.4.1" strip-ansi "^3.0.0" supports-color "^5.1.0" - webpack-dev-middleware "3.2.0" + webpack-dev-middleware "3.4.0" webpack-log "^2.0.0" yargs "12.0.2" @@ -8584,3 +8631,15 @@ yeast@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk= + +zen-observable-ts@^0.8.10: + version "0.8.10" + resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.10.tgz#18e2ce1c89fe026e9621fd83cc05168228fce829" + integrity sha512-5vqMtRggU/2GhePC9OU4sYEWOdvmayp2k3gjPf4F0mXwB3CSbbNznfDUvDJx9O2ZTa1EIXdJhPchQveFKwNXPQ== + dependencies: + zen-observable "^0.8.0" + +zen-observable@^0.8.0: + version "0.8.11" + resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.11.tgz#d3415885eeeb42ee5abb9821c95bb518fcd6d199" + integrity sha512-N3xXQVr4L61rZvGMpWe8XoCGX8vhU35dPyQ4fm5CY/KDlG0F75un14hjbckPXTDuKUY6V0dqR2giT6xN8Y4GEQ== |