Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
8288587742
commit
d9f331328a
|
|
@ -360,5 +360,6 @@ rspec foss-impact:
|
|||
expire_in: 7d
|
||||
paths:
|
||||
- tmp/matching_foss_tests.txt
|
||||
- tmp/capybara/
|
||||
# EE: Merge Request pipelines
|
||||
##################################################
|
||||
|
|
|
|||
|
|
@ -25,9 +25,10 @@ function importMermaidModule() {
|
|||
return import(/* webpackChunkName: 'mermaid' */ 'mermaid')
|
||||
.then(mermaid => {
|
||||
let theme = 'neutral';
|
||||
const ideDarkThemes = ['dark', 'solarized-dark'];
|
||||
|
||||
if (
|
||||
window.gon?.user_color_scheme === 'dark' &&
|
||||
ideDarkThemes.includes(window.gon?.user_color_scheme) &&
|
||||
// if on the Web IDE page
|
||||
document.querySelector('.ide')
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -282,12 +282,16 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
installClicked() {
|
||||
if (this.disabled || this.installButtonDisabled) return;
|
||||
|
||||
eventHub.$emit('installApplication', {
|
||||
id: this.id,
|
||||
params: this.installApplicationRequestParams,
|
||||
});
|
||||
},
|
||||
updateConfirmed() {
|
||||
if (this.isUpdating) return;
|
||||
|
||||
eventHub.$emit('updateApplication', {
|
||||
id: this.id,
|
||||
params: this.installApplicationRequestParams,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
export default {
|
||||
computed: {
|
||||
canSeeDescriptionVersion() {},
|
||||
canDeleteDescriptionVersion() {},
|
||||
displayDeleteButton() {},
|
||||
shouldShowDescriptionVersion() {},
|
||||
descriptionVersionToggleIcon() {},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -630,6 +630,10 @@ export const softDeleteDescriptionVersion = (
|
|||
.catch(error => {
|
||||
dispatch('receiveDeleteDescriptionVersionError', error);
|
||||
Flash(__('Something went wrong while deleting description changes. Please try again.'));
|
||||
|
||||
// Throw an error here because a component like SystemNote -
|
||||
// needs to know if the request failed to reset its internal state.
|
||||
throw new Error();
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@
|
|||
import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
|
||||
import PublishToolbar from './publish_toolbar.vue';
|
||||
import EditHeader from './edit_header.vue';
|
||||
import UnsavedChangesConfirmDialog from './unsaved_changes_confirm_dialog.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RichContentEditor,
|
||||
PublishToolbar,
|
||||
EditHeader,
|
||||
UnsavedChangesConfirmDialog,
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
|
|
@ -50,6 +52,7 @@ export default {
|
|||
<div class="d-flex flex-grow-1 flex-column h-100">
|
||||
<edit-header class="py-2" :title="title" />
|
||||
<rich-content-editor v-model="editableContent" class="mb-9 h-100" />
|
||||
<unsaved-changes-confirm-dialog :modified="modified" />
|
||||
<publish-toolbar
|
||||
class="gl-fixed gl-left-0 gl-bottom-0 gl-w-full"
|
||||
:return-url="returnUrl"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
<script>
|
||||
export default {
|
||||
props: {
|
||||
modified: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
created() {
|
||||
window.addEventListener('beforeunload', this.requestConfirmation);
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('beforeunload', this.requestConfirmation);
|
||||
},
|
||||
methods: {
|
||||
requestConfirmation(e) {
|
||||
if (this.modified) {
|
||||
e.preventDefault();
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
e.returnValue = '';
|
||||
}
|
||||
},
|
||||
},
|
||||
render: () => null,
|
||||
};
|
||||
</script>
|
||||
|
|
@ -132,7 +132,7 @@ export default {
|
|||
</pre>
|
||||
<pre v-else class="wrapper mt-2" v-html="descriptionVersion"></pre>
|
||||
<gl-deprecated-button
|
||||
v-if="canDeleteDescriptionVersion"
|
||||
v-if="displayDeleteButton"
|
||||
ref="deleteDescriptionVersionButton"
|
||||
v-gl-tooltip
|
||||
:title="__('Remove description history')"
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
@import './ide_theme_overrides';
|
||||
|
||||
@import './ide_themes/dark';
|
||||
@import './ide_themes/solarized-dark';
|
||||
|
||||
$search-list-icon-width: 18px;
|
||||
$ide-activity-bar-width: 60px;
|
||||
|
|
@ -89,7 +90,7 @@ $ide-commit-header-height: 48px;
|
|||
|
||||
&.active {
|
||||
background-color: var(--ide-highlight-background, $white);
|
||||
border-bottom-color: var(--ide-border-color, $white);
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
&:not(.disabled) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
// -------
|
||||
// Please see `app/assets/stylesheets/page_bundles/ide_themes/README.md` for a guide on contributing new themes
|
||||
// -------
|
||||
.ide.theme-solarized-dark {
|
||||
--ide-border-color: #002c38;
|
||||
--ide-border-color-alt: var(--ide-background);
|
||||
--ide-highlight-accent: #fff;
|
||||
--ide-text-color: #ddd;
|
||||
--ide-text-color-secondary: #ddd;
|
||||
--ide-background: #004152;
|
||||
--ide-background-hover: #003b4d;
|
||||
--ide-highlight-background: #003240;
|
||||
--ide-link-color: #73b9ff;
|
||||
--ide-footer-background: var(--ide-highlight-background);
|
||||
|
||||
--ide-input-border: #d8d8d8;
|
||||
--ide-input-background: transparent;
|
||||
--ide-input-color: #fff;
|
||||
|
||||
--ide-btn-default-background: transparent;
|
||||
--ide-btn-default-border: var(--ide-input-border);
|
||||
--ide-btn-default-hover-border: #d8d8d8;
|
||||
|
||||
--ide-btn-primary-background: #1068bf;
|
||||
--ide-btn-primary-border: #428fdc;
|
||||
--ide-btn-primary-hover-border: #63a6e9;
|
||||
|
||||
--ide-btn-success-background: #217645;
|
||||
--ide-btn-success-border: #108548;
|
||||
--ide-btn-success-hover-border: #2da160;
|
||||
|
||||
--ide-btn-disabled-border: rgba(223, 223, 223, 0.24);
|
||||
--ide-btn-disabled-color: rgba(145, 145, 145, 0.48);
|
||||
|
||||
--ide-btn-hover-border-width: 2px;
|
||||
|
||||
--ide-dropdown-background: #004c61;
|
||||
--ide-dropdown-hover-background: #00617a;
|
||||
|
||||
--ide-dropdown-btn-hover-border: #e9ecef;
|
||||
--ide-dropdown-btn-hover-background: var(--ide-background-hover);
|
||||
|
||||
--ide-file-row-btn-hover-background: #005a73;
|
||||
|
||||
--ide-diff-insert: rgba(155, 185, 85, 0.2);
|
||||
--ide-diff-remove: rgba(255, 0, 0, 0.2);
|
||||
|
||||
--ide-animation-gradient-1: var(--ide-file-row-btn-hover-background);
|
||||
--ide-animation-gradient-2: var(--ide-dropdown-hover-background);
|
||||
}
|
||||
|
|
@ -66,7 +66,7 @@ module Timebox
|
|||
groups = groups.compact if groups.is_a? Array
|
||||
groups = [] if groups.nil?
|
||||
|
||||
if Feature.enabled?(:optimized_timebox_queries)
|
||||
if Feature.enabled?(:optimized_timebox_queries, default_enabled: true)
|
||||
from_union([where(project_id: projects), where(group_id: groups)], remove_duplicates: false)
|
||||
else
|
||||
where(project_id: projects).or(where(group_id: groups))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Display confirmation modal when user exits SSE and there are unsaved changes
|
||||
merge_request: 33103
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add solarized dark for Web IDE
|
||||
merge_request: 33148
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -17,10 +17,6 @@ module Gitlab
|
|||
class Application < Rails::Application
|
||||
require_dependency Rails.root.join('lib/gitlab')
|
||||
require_dependency Rails.root.join('lib/gitlab/utils')
|
||||
require_dependency Rails.root.join('lib/gitlab/redis/wrapper')
|
||||
require_dependency Rails.root.join('lib/gitlab/redis/cache')
|
||||
require_dependency Rails.root.join('lib/gitlab/redis/queues')
|
||||
require_dependency Rails.root.join('lib/gitlab/redis/shared_state')
|
||||
require_dependency Rails.root.join('lib/gitlab/current_settings')
|
||||
require_dependency Rails.root.join('lib/gitlab/middleware/read_only')
|
||||
require_dependency Rails.root.join('lib/gitlab/middleware/basic_health_check')
|
||||
|
|
@ -262,17 +258,6 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
# Use caching across all environments
|
||||
# Full list of options:
|
||||
# https://api.rubyonrails.org/classes/ActiveSupport/Cache/RedisCacheStore.html#method-c-new
|
||||
caching_config_hash = {}
|
||||
caching_config_hash[:redis] = Gitlab::Redis::Cache.pool
|
||||
caching_config_hash[:compress] = Gitlab::Utils.to_boolean(ENV.fetch('ENABLE_REDIS_CACHE_COMPRESSION', '1'))
|
||||
caching_config_hash[:namespace] = Gitlab::Redis::Cache::CACHE_NAMESPACE
|
||||
caching_config_hash[:expires_in] = 2.weeks # Cache should not grow forever
|
||||
|
||||
config.cache_store = :redis_cache_store, caching_config_hash
|
||||
|
||||
config.active_job.queue_adapter = :sidekiq
|
||||
|
||||
# This is needed for gitlab-shell
|
||||
|
|
|
|||
|
|
@ -1070,6 +1070,9 @@ production: &base
|
|||
|
||||
## ActionCable settings
|
||||
action_cable:
|
||||
# Enables handling of ActionCable requests on the Puma web workers
|
||||
# When this is disabled, a standalone ActionCable server must be started
|
||||
in_app: true
|
||||
# Number of threads used to process ActionCable connection callbacks and channel actions
|
||||
# worker_pool_size: 4
|
||||
|
||||
|
|
|
|||
|
|
@ -726,6 +726,7 @@ Settings.webpack.dev_server['port'] ||= 3808
|
|||
# ActionCable settings
|
||||
#
|
||||
Settings['action_cable'] ||= Settingslogic.new({})
|
||||
Settings.action_cable['in_app'] ||= false
|
||||
Settings.action_cable['worker_pool_size'] ||= 4
|
||||
|
||||
#
|
||||
|
|
|
|||
|
|
@ -1,3 +1,14 @@
|
|||
# Use caching across all environments
|
||||
# Full list of options:
|
||||
# https://api.rubyonrails.org/classes/ActiveSupport/Cache/RedisCacheStore.html#method-c-new
|
||||
caching_config_hash = {}
|
||||
caching_config_hash[:redis] = Gitlab::Redis::Cache.pool
|
||||
caching_config_hash[:compress] = Gitlab::Utils.to_boolean(ENV.fetch('ENABLE_REDIS_CACHE_COMPRESSION', '1'))
|
||||
caching_config_hash[:namespace] = Gitlab::Redis::Cache::CACHE_NAMESPACE
|
||||
caching_config_hash[:expires_in] = 2.weeks # Cache should not grow forever
|
||||
|
||||
Gitlab::Application.config.cache_store = :redis_cache_store, caching_config_hash
|
||||
|
||||
# Make sure we initialize a Redis connection pool before multi-threaded
|
||||
# execution starts by
|
||||
# 1. Sidekiq
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
require 'action_cable/subscription_adapter/redis'
|
||||
|
||||
Rails.application.configure do
|
||||
# We only mount the ActionCable engine in tests where we run it in-app
|
||||
# For other environments, we run it on a standalone Puma server
|
||||
config.action_cable.mount_path = Rails.env.test? ? '/-/cable' : nil
|
||||
# Mount the ActionCable engine when in-app mode is enabled
|
||||
config.action_cable.mount_path = Gitlab.config.action_cable.in_app ? '/-/cable' : nil
|
||||
|
||||
config.action_cable.url = Gitlab::Utils.append_path(Gitlab.config.gitlab.relative_url_root, '/-/cable')
|
||||
config.action_cable.worker_pool_size = Gitlab.config.action_cable.worker_pool_size
|
||||
end
|
||||
|
|
|
|||
|
|
@ -141,6 +141,9 @@ that can also be promoted in case of disaster.
|
|||
|
||||
## Configure GitLab to scale
|
||||
|
||||
NOTE: **Note:**
|
||||
From GitLab 13.0, using NFS for Git repositories is deprecated. In GitLab 14.0, support for NFS for Git repositories is scheduled to be removed. Upgrade to [Gitaly Cluster](../gitaly/praefect.md) as soon as possible.
|
||||
|
||||
The following components are the ones you need to configure in order to scale
|
||||
GitLab. They are listed in the order you'll typically configure them if they are
|
||||
required by your [reference architecture](#reference-architectures) of choice.
|
||||
|
|
@ -163,7 +166,7 @@ column.
|
|||
| Repmgr | PostgreSQL cluster management and failover | [PostgreSQL and Repmgr configuration](../high_availability/database.md) | Yes |
|
||||
| [Redis](../../development/architecture.md#redis) ([3](#footnotes)) | Key/value store for fast data lookup and caching | [Redis configuration](../high_availability/redis.md) | Yes |
|
||||
| Redis Sentinel | Redis | [Redis Sentinel configuration](../high_availability/redis.md) | Yes |
|
||||
| [Gitaly](../../development/architecture.md#gitaly) ([2](#footnotes)) ([7](#footnotes)) ([9](#footnotes)) | Provides access to Git repositories | [Gitaly configuration](../gitaly/index.md#running-gitaly-on-its-own-server) | Yes |
|
||||
| [Gitaly](../../development/architecture.md#gitaly) ([2](#footnotes)) ([7](#footnotes)) | Provides access to Git repositories | [Gitaly configuration](../gitaly/index.md#running-gitaly-on-its-own-server) | Yes |
|
||||
| [Sidekiq](../../development/architecture.md#sidekiq) | Asynchronous/background jobs | [Sidekiq configuration](../high_availability/sidekiq.md) | Yes |
|
||||
| [GitLab application services](../../development/architecture.md#unicorn)([1](#footnotes)) | Puma/Unicorn, Workhorse, GitLab Shell - serves front-end requests (UI, API, Git over HTTP/SSH) | [GitLab app scaling configuration](../high_availability/gitlab.md) | Yes |
|
||||
| [Prometheus](../../development/architecture.md#prometheus) and [Grafana](../../development/architecture.md#grafana) | GitLab environment monitoring | [Monitoring node for scaling](../high_availability/monitoring_node.md) | Yes |
|
||||
|
|
@ -177,9 +180,7 @@ column.
|
|||
on workload.
|
||||
|
||||
1. Gitaly node requirements are dependent on customer data, specifically the number of
|
||||
projects and their sizes. We recommend two nodes as an absolute minimum,
|
||||
and at least four nodes should be used when supporting 50,000 or more users.
|
||||
We also recommend that each Gitaly node should store no more than 5TB of data
|
||||
projects and their sizes. We recommend that each Gitaly node should store no more than 5TB of data
|
||||
and have the number of [`gitaly-ruby` workers](../gitaly/index.md#gitaly-ruby)
|
||||
set to 20% of available CPUs. Additional nodes should be considered in conjunction
|
||||
with a review of expected data size and spread based on the recommendations above.
|
||||
|
|
@ -216,7 +217,3 @@ column.
|
|||
or higher, are required for your CPU or Node counts accordingly. For more information, a
|
||||
[Sysbench](https://github.com/akopytov/sysbench) benchmark of the CPU can be found
|
||||
[here](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
|
||||
|
||||
1. From GitLab 13.0, using NFS for Git repositories is deprecated. In GitLab
|
||||
14.0, support for NFS for Git repositories is scheduled to be removed.
|
||||
Upgrade to [Gitaly Cluster](../gitaly/praefect.md) as soon as possible.
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def puma?
|
||||
!!defined?(::Puma) && !defined?(ACTION_CABLE_SERVER)
|
||||
!!defined?(::Puma)
|
||||
end
|
||||
|
||||
# For unicorn, we need to check for actual server instances to avoid false positives.
|
||||
|
|
@ -70,11 +70,11 @@ module Gitlab
|
|||
end
|
||||
|
||||
def web_server?
|
||||
puma? || unicorn? || action_cable?
|
||||
puma? || unicorn?
|
||||
end
|
||||
|
||||
def action_cable?
|
||||
!!defined?(ACTION_CABLE_SERVER)
|
||||
web_server? && (!!defined?(ACTION_CABLE_SERVER) || Gitlab.config.action_cable.in_app)
|
||||
end
|
||||
|
||||
def multi_threaded?
|
||||
|
|
@ -82,19 +82,21 @@ module Gitlab
|
|||
end
|
||||
|
||||
def max_threads
|
||||
main_thread = 1
|
||||
threads = 1 # main thread
|
||||
|
||||
if action_cable?
|
||||
Gitlab::Application.config.action_cable.worker_pool_size
|
||||
elsif puma?
|
||||
Puma.cli_config.options[:max_threads]
|
||||
if puma?
|
||||
threads += Puma.cli_config.options[:max_threads]
|
||||
elsif sidekiq?
|
||||
# An extra thread for the poller in Sidekiq Cron:
|
||||
# https://github.com/ondrejbartas/sidekiq-cron#under-the-hood
|
||||
Sidekiq.options[:concurrency] + 1
|
||||
else
|
||||
0
|
||||
end + main_thread
|
||||
threads += Sidekiq.options[:concurrency] + 1
|
||||
end
|
||||
|
||||
if action_cable?
|
||||
threads += Gitlab.config.action_cable.worker_pool_size
|
||||
end
|
||||
|
||||
threads
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ module QA
|
|||
raise ResourceUpdateFailedError, "Could not change repository storage to #{new_storage}. Request returned (#{response.code}): `#{response}`."
|
||||
end
|
||||
|
||||
wait_until(sleep_interval: 1) { Runtime::API::RepositoryStorageMoves.has_status?(self, 'finished') }
|
||||
wait_until(sleep_interval: 1) { Runtime::API::RepositoryStorageMoves.has_status?(self, 'finished', new_storage) }
|
||||
rescue Support::Repeater::RepeaterConditionExceededError
|
||||
raise Runtime::API::RepositoryStorageMoves::RepositoryStorageMovesError, 'Timed out while waiting for the repository storage move to finish'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,11 +9,11 @@ module QA
|
|||
|
||||
RepositoryStorageMovesError = Class.new(RuntimeError)
|
||||
|
||||
def has_status?(project, status)
|
||||
def has_status?(project, status, destination_storage = Env.additional_repository_storage)
|
||||
all.any? do |move|
|
||||
move[:project][:path_with_namespace] == project.path_with_namespace &&
|
||||
move[:state] == status &&
|
||||
move[:destination_storage_name] == Env.additional_repository_storage
|
||||
move[:destination_storage_name] == destination_storage
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
context 'Create' do
|
||||
describe 'Changing Gitaly repository storage', :orchestrated, :requires_admin do
|
||||
shared_examples 'repository storage move' do
|
||||
it 'confirms a `finished` status after moving project repository storage' do
|
||||
expect(project).to have_file('README.md')
|
||||
|
||||
project.change_repository_storage(destination_storage)
|
||||
|
||||
expect(Runtime::API::RepositoryStorageMoves).to have_status(project, 'finished', destination_storage)
|
||||
|
||||
Resource::Repository::ProjectPush.fabricate! do |push|
|
||||
push.project = project
|
||||
push.file_name = 'new_file'
|
||||
push.file_content = '# This is a new file'
|
||||
push.commit_message = 'Add new file'
|
||||
push.new_branch = false
|
||||
end
|
||||
|
||||
expect(project).to have_file('README.md')
|
||||
expect(project).to have_file('new_file')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when moving from one Gitaly storage to another', :repository_storage do
|
||||
let(:project) do
|
||||
Resource::Project.fabricate_via_api! do |project|
|
||||
project.name = 'repo-storage-move-status'
|
||||
project.initialize_with_readme = true
|
||||
end
|
||||
end
|
||||
let(:destination_storage) { QA::Runtime::Env.additional_repository_storage }
|
||||
|
||||
it_behaves_like 'repository storage move'
|
||||
end
|
||||
|
||||
context 'when moving from Gitaly to Gitaly Cluster', :requires_praefect do
|
||||
let(:project) do
|
||||
Resource::Project.fabricate_via_api! do |project|
|
||||
project.name = 'repo-storage-move'
|
||||
project.initialize_with_readme = true
|
||||
project.repository_storage = 'gitaly'
|
||||
end
|
||||
end
|
||||
let(:destination_storage) { QA::Runtime::Env.praefect_repository_storage }
|
||||
|
||||
it_behaves_like 'repository storage move'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
context 'Create' do
|
||||
describe 'Gitaly repository storage', :orchestrated, :repository_storage, :requires_admin do
|
||||
let(:project) do
|
||||
Resource::Project.fabricate_via_api! do |project|
|
||||
project.name = 'repo-storage-status'
|
||||
project.initialize_with_readme = true
|
||||
end
|
||||
end
|
||||
|
||||
it 'confirms a `finished` status after moving project repository storage' do
|
||||
expect(project).to have_file('README.md')
|
||||
|
||||
project.change_repository_storage(QA::Runtime::Env.additional_repository_storage)
|
||||
|
||||
expect(Runtime::API::RepositoryStorageMoves).to have_status(project, 'finished')
|
||||
|
||||
Resource::Repository::ProjectPush.fabricate! do |push|
|
||||
push.project = project
|
||||
push.file_name = 'new_file'
|
||||
push.file_content = '# This is a new file'
|
||||
push.commit_message = 'Add new file'
|
||||
push.new_branch = false
|
||||
end
|
||||
|
||||
expect(project).to have_file('README.md')
|
||||
expect(project).to have_file('new_file')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
context 'Create' do
|
||||
context 'Create', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/220189', type: :investigating } do
|
||||
describe 'Merge request creation from fork' do
|
||||
it 'user forks a project, submits a merge request and maintainer merges it' do
|
||||
Flow::Login.sign_in
|
||||
|
|
|
|||
|
|
@ -1,243 +1,192 @@
|
|||
import Vue from 'vue';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import mountComponent from 'helpers/vue_mount_component_helper';
|
||||
import eventHub from '~/clusters/event_hub';
|
||||
import { APPLICATION_STATUS, ELASTIC_STACK } from '~/clusters/constants';
|
||||
import applicationRow from '~/clusters/components/application_row.vue';
|
||||
import ApplicationRow from '~/clusters/components/application_row.vue';
|
||||
import UninstallApplicationConfirmationModal from '~/clusters/components/uninstall_application_confirmation_modal.vue';
|
||||
import UpdateApplicationConfirmationModal from '~/clusters/components/update_application_confirmation_modal.vue';
|
||||
|
||||
import { DEFAULT_APPLICATION_STATE } from '../services/mock_data';
|
||||
|
||||
describe('Application Row', () => {
|
||||
let vm;
|
||||
let ApplicationRow;
|
||||
|
||||
beforeEach(() => {
|
||||
ApplicationRow = Vue.extend(applicationRow);
|
||||
});
|
||||
let wrapper;
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
const mountComponent = data => {
|
||||
wrapper = shallowMount(ApplicationRow, {
|
||||
propsData: {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
...data,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
describe('Title', () => {
|
||||
it('shows title', () => {
|
||||
vm = mountComponent(ApplicationRow, {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
titleLink: null,
|
||||
});
|
||||
const title = vm.$el.querySelector('.js-cluster-application-title');
|
||||
mountComponent({ titleLink: null });
|
||||
|
||||
expect(title.tagName).toEqual('SPAN');
|
||||
expect(title.textContent.trim()).toEqual(DEFAULT_APPLICATION_STATE.title);
|
||||
const title = wrapper.find('.js-cluster-application-title');
|
||||
|
||||
expect(title.element).toBeInstanceOf(HTMLSpanElement);
|
||||
expect(title.text()).toEqual(DEFAULT_APPLICATION_STATE.title);
|
||||
});
|
||||
|
||||
it('shows title link', () => {
|
||||
expect(DEFAULT_APPLICATION_STATE.titleLink).toBeDefined();
|
||||
mountComponent();
|
||||
const title = wrapper.find('.js-cluster-application-title');
|
||||
|
||||
vm = mountComponent(ApplicationRow, {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
});
|
||||
const title = vm.$el.querySelector('.js-cluster-application-title');
|
||||
|
||||
expect(title.tagName).toEqual('A');
|
||||
expect(title.textContent.trim()).toEqual(DEFAULT_APPLICATION_STATE.title);
|
||||
expect(title.element).toBeInstanceOf(HTMLAnchorElement);
|
||||
expect(title.text()).toEqual(DEFAULT_APPLICATION_STATE.title);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Install button', () => {
|
||||
it('has indeterminate state on page load', () => {
|
||||
vm = mountComponent(ApplicationRow, {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
status: null,
|
||||
});
|
||||
const button = () => wrapper.find('.js-cluster-application-install-button');
|
||||
const checkButtonState = (label, loading, disabled) => {
|
||||
expect(button().props('label')).toEqual(label);
|
||||
expect(button().props('loading')).toEqual(loading);
|
||||
expect(button().props('disabled')).toEqual(disabled);
|
||||
};
|
||||
|
||||
expect(vm.installButtonLabel).toBeUndefined();
|
||||
it('has indeterminate state on page load', () => {
|
||||
mountComponent({ status: null });
|
||||
|
||||
expect(button().props('label')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('has install button', () => {
|
||||
const installationBtn = vm.$el.querySelector('.js-cluster-application-install-button');
|
||||
mountComponent();
|
||||
|
||||
expect(installationBtn).not.toBe(null);
|
||||
expect(button().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('has disabled "Install" when APPLICATION_STATUS.NOT_INSTALLABLE', () => {
|
||||
vm = mountComponent(ApplicationRow, {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
status: APPLICATION_STATUS.NOT_INSTALLABLE,
|
||||
});
|
||||
mountComponent({ status: APPLICATION_STATUS.NOT_INSTALLABLE });
|
||||
|
||||
expect(vm.installButtonLabel).toEqual('Install');
|
||||
expect(vm.installButtonLoading).toEqual(false);
|
||||
expect(vm.installButtonDisabled).toEqual(true);
|
||||
checkButtonState('Install', false, true);
|
||||
});
|
||||
|
||||
it('has enabled "Install" when APPLICATION_STATUS.INSTALLABLE', () => {
|
||||
vm = mountComponent(ApplicationRow, {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
status: APPLICATION_STATUS.INSTALLABLE,
|
||||
});
|
||||
mountComponent({ status: APPLICATION_STATUS.INSTALLABLE });
|
||||
|
||||
expect(vm.installButtonLabel).toEqual('Install');
|
||||
expect(vm.installButtonLoading).toEqual(false);
|
||||
expect(vm.installButtonDisabled).toEqual(false);
|
||||
checkButtonState('Install', false, false);
|
||||
});
|
||||
|
||||
it('has loading "Installing" when APPLICATION_STATUS.INSTALLING', () => {
|
||||
vm = mountComponent(ApplicationRow, {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
status: APPLICATION_STATUS.INSTALLING,
|
||||
});
|
||||
mountComponent({ status: APPLICATION_STATUS.INSTALLING });
|
||||
|
||||
expect(vm.installButtonLabel).toEqual('Installing');
|
||||
expect(vm.installButtonLoading).toEqual(true);
|
||||
expect(vm.installButtonDisabled).toEqual(true);
|
||||
checkButtonState('Installing', true, true);
|
||||
});
|
||||
|
||||
it('has disabled "Installed" when application is installed and not uninstallable', () => {
|
||||
vm = mountComponent(ApplicationRow, {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
mountComponent({
|
||||
status: APPLICATION_STATUS.INSTALLED,
|
||||
installed: true,
|
||||
uninstallable: false,
|
||||
});
|
||||
|
||||
expect(vm.installButtonLabel).toEqual('Installed');
|
||||
expect(vm.installButtonLoading).toEqual(false);
|
||||
expect(vm.installButtonDisabled).toEqual(true);
|
||||
checkButtonState('Installed', false, true);
|
||||
});
|
||||
|
||||
it('hides when application is installed and uninstallable', () => {
|
||||
vm = mountComponent(ApplicationRow, {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
mountComponent({
|
||||
status: APPLICATION_STATUS.INSTALLED,
|
||||
installed: true,
|
||||
uninstallable: true,
|
||||
});
|
||||
const installBtn = vm.$el.querySelector('.js-cluster-application-install-button');
|
||||
|
||||
expect(installBtn).toBe(null);
|
||||
expect(button().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('has enabled "Install" when install fails', () => {
|
||||
vm = mountComponent(ApplicationRow, {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
mountComponent({
|
||||
status: APPLICATION_STATUS.INSTALLABLE,
|
||||
installFailed: true,
|
||||
});
|
||||
|
||||
expect(vm.installButtonLabel).toEqual('Install');
|
||||
expect(vm.installButtonLoading).toEqual(false);
|
||||
expect(vm.installButtonDisabled).toEqual(false);
|
||||
checkButtonState('Install', false, false);
|
||||
});
|
||||
|
||||
it('has enabled "Install" when REQUEST_FAILURE (so you can try installing again)', () => {
|
||||
vm = mountComponent(ApplicationRow, {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
status: APPLICATION_STATUS.INSTALLABLE,
|
||||
});
|
||||
mountComponent({ status: APPLICATION_STATUS.INSTALLABLE });
|
||||
|
||||
expect(vm.installButtonLabel).toEqual('Install');
|
||||
expect(vm.installButtonLoading).toEqual(false);
|
||||
expect(vm.installButtonDisabled).toEqual(false);
|
||||
checkButtonState('Install', false, false);
|
||||
});
|
||||
|
||||
it('clicking install button emits event', () => {
|
||||
jest.spyOn(eventHub, '$emit');
|
||||
vm = mountComponent(ApplicationRow, {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
status: APPLICATION_STATUS.INSTALLABLE,
|
||||
});
|
||||
const installButton = vm.$el.querySelector('.js-cluster-application-install-button');
|
||||
const spy = jest.spyOn(eventHub, '$emit');
|
||||
mountComponent({ status: APPLICATION_STATUS.INSTALLABLE });
|
||||
|
||||
installButton.click();
|
||||
button().vm.$emit('click');
|
||||
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('installApplication', {
|
||||
expect(spy).toHaveBeenCalledWith('installApplication', {
|
||||
id: DEFAULT_APPLICATION_STATE.id,
|
||||
params: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('clicking install button when installApplicationRequestParams are provided emits event', () => {
|
||||
jest.spyOn(eventHub, '$emit');
|
||||
vm = mountComponent(ApplicationRow, {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
const spy = jest.spyOn(eventHub, '$emit');
|
||||
mountComponent({
|
||||
status: APPLICATION_STATUS.INSTALLABLE,
|
||||
installApplicationRequestParams: { hostname: 'jupyter' },
|
||||
});
|
||||
const installButton = vm.$el.querySelector('.js-cluster-application-install-button');
|
||||
|
||||
installButton.click();
|
||||
button().vm.$emit('click');
|
||||
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('installApplication', {
|
||||
expect(spy).toHaveBeenCalledWith('installApplication', {
|
||||
id: DEFAULT_APPLICATION_STATE.id,
|
||||
params: { hostname: 'jupyter' },
|
||||
});
|
||||
});
|
||||
|
||||
it('clicking disabled install button emits nothing', () => {
|
||||
jest.spyOn(eventHub, '$emit');
|
||||
vm = mountComponent(ApplicationRow, {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
status: APPLICATION_STATUS.INSTALLING,
|
||||
});
|
||||
const installButton = vm.$el.querySelector('.js-cluster-application-install-button');
|
||||
const spy = jest.spyOn(eventHub, '$emit');
|
||||
mountComponent({ status: APPLICATION_STATUS.INSTALLING });
|
||||
|
||||
expect(vm.installButtonDisabled).toEqual(true);
|
||||
expect(button().props('disabled')).toEqual(true);
|
||||
|
||||
installButton.click();
|
||||
button().vm.$emit('click');
|
||||
|
||||
expect(eventHub.$emit).not.toHaveBeenCalled();
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Uninstall button', () => {
|
||||
it('displays button when app is installed and uninstallable', () => {
|
||||
vm = mountComponent(ApplicationRow, {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
mountComponent({
|
||||
installed: true,
|
||||
uninstallable: true,
|
||||
status: APPLICATION_STATUS.NOT_INSTALLABLE,
|
||||
});
|
||||
const uninstallButton = vm.$el.querySelector('.js-cluster-application-uninstall-button');
|
||||
const uninstallButton = wrapper.find('.js-cluster-application-uninstall-button');
|
||||
|
||||
expect(uninstallButton).toBeTruthy();
|
||||
expect(uninstallButton.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('displays a success toast message if application uninstall was successful', () => {
|
||||
vm = mountComponent(ApplicationRow, {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
it('displays a success toast message if application uninstall was successful', async () => {
|
||||
mountComponent({
|
||||
title: 'GitLab Runner',
|
||||
uninstallSuccessful: false,
|
||||
});
|
||||
|
||||
vm.$toast = { show: jest.fn() };
|
||||
vm.uninstallSuccessful = true;
|
||||
wrapper.vm.$toast = { show: jest.fn() };
|
||||
wrapper.setProps({ uninstallSuccessful: true });
|
||||
|
||||
return vm.$nextTick(() => {
|
||||
expect(vm.$toast.show).toHaveBeenCalledWith('GitLab Runner uninstalled successfully.');
|
||||
});
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(
|
||||
'GitLab Runner uninstalled successfully.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when confirmation modal triggers confirm event', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(ApplicationRow, {
|
||||
propsData: {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('triggers uninstallApplication event', () => {
|
||||
jest.spyOn(eventHub, '$emit');
|
||||
mountComponent();
|
||||
wrapper.find(UninstallApplicationConfirmationModal).vm.$emit('confirm');
|
||||
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('uninstallApplication', {
|
||||
|
|
@ -247,126 +196,91 @@ describe('Application Row', () => {
|
|||
});
|
||||
|
||||
describe('Update button', () => {
|
||||
it('has indeterminate state on page load', () => {
|
||||
vm = mountComponent(ApplicationRow, {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
status: null,
|
||||
});
|
||||
const updateBtn = vm.$el.querySelector('.js-cluster-application-update-button');
|
||||
const button = () => wrapper.find('.js-cluster-application-update-button');
|
||||
|
||||
expect(updateBtn).toBe(null);
|
||||
it('has indeterminate state on page load', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(button().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('has enabled "Update" when "updateAvailable" is true', () => {
|
||||
vm = mountComponent(ApplicationRow, {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
updateAvailable: true,
|
||||
});
|
||||
const updateBtn = vm.$el.querySelector('.js-cluster-application-update-button');
|
||||
mountComponent({ updateAvailable: true });
|
||||
|
||||
expect(updateBtn).not.toBe(null);
|
||||
expect(updateBtn.innerHTML).toContain('Update');
|
||||
expect(button().exists()).toBe(true);
|
||||
expect(button().props('label')).toContain('Update');
|
||||
});
|
||||
|
||||
it('has enabled "Retry update" when update process fails', () => {
|
||||
vm = mountComponent(ApplicationRow, {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
mountComponent({
|
||||
status: APPLICATION_STATUS.INSTALLED,
|
||||
updateFailed: true,
|
||||
});
|
||||
const updateBtn = vm.$el.querySelector('.js-cluster-application-update-button');
|
||||
|
||||
expect(updateBtn).not.toBe(null);
|
||||
expect(updateBtn.innerHTML).toContain('Retry update');
|
||||
expect(button().exists()).toBe(true);
|
||||
expect(button().props('label')).toContain('Retry update');
|
||||
});
|
||||
|
||||
it('has disabled "Updating" when APPLICATION_STATUS.UPDATING', () => {
|
||||
vm = mountComponent(ApplicationRow, {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
status: APPLICATION_STATUS.UPDATING,
|
||||
});
|
||||
const updateBtn = vm.$el.querySelector('.js-cluster-application-update-button');
|
||||
mountComponent({ status: APPLICATION_STATUS.UPDATING });
|
||||
|
||||
expect(updateBtn).not.toBe(null);
|
||||
expect(vm.isUpdating).toBe(true);
|
||||
expect(updateBtn.innerHTML).toContain('Updating');
|
||||
expect(button().exists()).toBe(true);
|
||||
expect(button().props('label')).toContain('Updating');
|
||||
});
|
||||
|
||||
it('clicking update button emits event', () => {
|
||||
jest.spyOn(eventHub, '$emit');
|
||||
vm = mountComponent(ApplicationRow, {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
const spy = jest.spyOn(eventHub, '$emit');
|
||||
mountComponent({
|
||||
status: APPLICATION_STATUS.INSTALLED,
|
||||
updateAvailable: true,
|
||||
});
|
||||
const updateBtn = vm.$el.querySelector('.js-cluster-application-update-button');
|
||||
|
||||
updateBtn.click();
|
||||
button().vm.$emit('click');
|
||||
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('updateApplication', {
|
||||
expect(spy).toHaveBeenCalledWith('updateApplication', {
|
||||
id: DEFAULT_APPLICATION_STATE.id,
|
||||
params: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('clicking disabled update button emits nothing', () => {
|
||||
jest.spyOn(eventHub, '$emit');
|
||||
vm = mountComponent(ApplicationRow, {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
status: APPLICATION_STATUS.UPDATING,
|
||||
});
|
||||
const updateBtn = vm.$el.querySelector('.js-cluster-application-update-button');
|
||||
const spy = jest.spyOn(eventHub, '$emit');
|
||||
mountComponent({ status: APPLICATION_STATUS.UPDATING });
|
||||
|
||||
updateBtn.click();
|
||||
button().vm.$emit('click');
|
||||
|
||||
expect(eventHub.$emit).not.toHaveBeenCalled();
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('displays an error message if application update failed', () => {
|
||||
vm = mountComponent(ApplicationRow, {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
mountComponent({
|
||||
title: 'GitLab Runner',
|
||||
status: APPLICATION_STATUS.INSTALLED,
|
||||
updateFailed: true,
|
||||
});
|
||||
const failureMessage = vm.$el.querySelector('.js-cluster-application-update-details');
|
||||
const failureMessage = wrapper.find('.js-cluster-application-update-details');
|
||||
|
||||
expect(failureMessage).not.toBe(null);
|
||||
expect(failureMessage.innerHTML).toContain(
|
||||
expect(failureMessage.exists()).toBe(true);
|
||||
expect(failureMessage.text()).toContain(
|
||||
'Update failed. Please check the logs and try again.',
|
||||
);
|
||||
});
|
||||
|
||||
it('displays a success toast message if application update was successful', () => {
|
||||
vm = mountComponent(ApplicationRow, {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
it('displays a success toast message if application update was successful', async () => {
|
||||
mountComponent({
|
||||
title: 'GitLab Runner',
|
||||
updateSuccessful: false,
|
||||
});
|
||||
|
||||
vm.$toast = { show: jest.fn() };
|
||||
vm.updateSuccessful = true;
|
||||
wrapper.vm.$toast = { show: jest.fn() };
|
||||
wrapper.setProps({ updateSuccessful: true });
|
||||
|
||||
return vm.$nextTick(() => {
|
||||
expect(vm.$toast.show).toHaveBeenCalledWith('GitLab Runner updated successfully.');
|
||||
});
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('GitLab Runner updated successfully.');
|
||||
});
|
||||
|
||||
describe('when updating does not require confirmation', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(ApplicationRow, {
|
||||
propsData: {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
updateAvailable: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
beforeEach(() => mountComponent({ updateAvailable: true }));
|
||||
|
||||
it('the modal is not rendered', () => {
|
||||
expect(wrapper.contains(UpdateApplicationConfirmationModal)).toBe(false);
|
||||
|
|
@ -378,23 +292,14 @@ describe('Application Row', () => {
|
|||
});
|
||||
|
||||
describe('when updating requires confirmation', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(ApplicationRow, {
|
||||
propsData: {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
updateAvailable: true,
|
||||
id: ELASTIC_STACK,
|
||||
version: '1.1.2',
|
||||
},
|
||||
mountComponent({
|
||||
updateAvailable: true,
|
||||
id: ELASTIC_STACK,
|
||||
version: '1.1.2',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('displays a modal', () => {
|
||||
expect(wrapper.contains(UpdateApplicationConfirmationModal)).toBe(true);
|
||||
});
|
||||
|
|
@ -415,60 +320,37 @@ describe('Application Row', () => {
|
|||
});
|
||||
|
||||
describe('updating Elastic Stack special case', () => {
|
||||
let wrapper;
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('needs confirmation if version is lower than 3.0.0', () => {
|
||||
wrapper = shallowMount(ApplicationRow, {
|
||||
propsData: {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
updateAvailable: true,
|
||||
id: ELASTIC_STACK,
|
||||
version: '1.1.2',
|
||||
},
|
||||
mountComponent({
|
||||
updateAvailable: true,
|
||||
id: ELASTIC_STACK,
|
||||
version: '1.1.2',
|
||||
});
|
||||
|
||||
wrapper.vm.$nextTick(() => {
|
||||
expect(wrapper.contains("[data-qa-selector='update_button_with_confirmation']")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(wrapper.contains(UpdateApplicationConfirmationModal)).toBe(true);
|
||||
});
|
||||
expect(wrapper.contains("[data-qa-selector='update_button_with_confirmation']")).toBe(true);
|
||||
expect(wrapper.contains(UpdateApplicationConfirmationModal)).toBe(true);
|
||||
});
|
||||
|
||||
it('does not need confirmation is version is 3.0.0', () => {
|
||||
wrapper = shallowMount(ApplicationRow, {
|
||||
propsData: {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
updateAvailable: true,
|
||||
id: ELASTIC_STACK,
|
||||
version: '3.0.0',
|
||||
},
|
||||
mountComponent({
|
||||
updateAvailable: true,
|
||||
id: ELASTIC_STACK,
|
||||
version: '3.0.0',
|
||||
});
|
||||
|
||||
wrapper.vm.$nextTick(() => {
|
||||
expect(wrapper.contains("[data-qa-selector='update_button']")).toBe(true);
|
||||
expect(wrapper.contains(UpdateApplicationConfirmationModal)).toBe(false);
|
||||
});
|
||||
expect(wrapper.contains("[data-qa-selector='update_button']")).toBe(true);
|
||||
expect(wrapper.contains(UpdateApplicationConfirmationModal)).toBe(false);
|
||||
});
|
||||
|
||||
it('does not need confirmation if version is higher than 3.0.0', () => {
|
||||
wrapper = shallowMount(ApplicationRow, {
|
||||
propsData: {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
updateAvailable: true,
|
||||
id: ELASTIC_STACK,
|
||||
version: '5.2.1',
|
||||
},
|
||||
mountComponent({
|
||||
updateAvailable: true,
|
||||
id: ELASTIC_STACK,
|
||||
version: '5.2.1',
|
||||
});
|
||||
|
||||
wrapper.vm.$nextTick(() => {
|
||||
expect(wrapper.contains("[data-qa-selector='update_button']")).toBe(true);
|
||||
expect(wrapper.contains(UpdateApplicationConfirmationModal)).toBe(false);
|
||||
});
|
||||
expect(wrapper.contains("[data-qa-selector='update_button']")).toBe(true);
|
||||
expect(wrapper.contains(UpdateApplicationConfirmationModal)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -476,63 +358,57 @@ describe('Application Row', () => {
|
|||
describe('Version', () => {
|
||||
it('displays a version number if application has been updated', () => {
|
||||
const version = '0.1.45';
|
||||
vm = mountComponent(ApplicationRow, {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
mountComponent({
|
||||
status: APPLICATION_STATUS.INSTALLED,
|
||||
updateSuccessful: true,
|
||||
version,
|
||||
});
|
||||
const updateDetails = vm.$el.querySelector('.js-cluster-application-update-details');
|
||||
const versionEl = vm.$el.querySelector('.js-cluster-application-update-version');
|
||||
const updateDetails = wrapper.find('.js-cluster-application-update-details');
|
||||
const versionEl = wrapper.find('.js-cluster-application-update-version');
|
||||
|
||||
expect(updateDetails.innerHTML).toContain('Updated');
|
||||
expect(versionEl).not.toBe(null);
|
||||
expect(versionEl.innerHTML).toContain(version);
|
||||
expect(updateDetails.text()).toContain('Updated');
|
||||
expect(versionEl.exists()).toBe(true);
|
||||
expect(versionEl.text()).toContain(version);
|
||||
});
|
||||
|
||||
it('contains a link to the chart repo if application has been updated', () => {
|
||||
const version = '0.1.45';
|
||||
const chartRepo = 'https://gitlab.com/gitlab-org/charts/gitlab-runner';
|
||||
vm = mountComponent(ApplicationRow, {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
mountComponent({
|
||||
status: APPLICATION_STATUS.INSTALLED,
|
||||
updateSuccessful: true,
|
||||
chartRepo,
|
||||
version,
|
||||
});
|
||||
const versionEl = vm.$el.querySelector('.js-cluster-application-update-version');
|
||||
const versionEl = wrapper.find('.js-cluster-application-update-version');
|
||||
|
||||
expect(versionEl.href).toEqual(chartRepo);
|
||||
expect(versionEl.target).toEqual('_blank');
|
||||
expect(versionEl.attributes('href')).toEqual(chartRepo);
|
||||
expect(versionEl.props('target')).toEqual('_blank');
|
||||
});
|
||||
|
||||
it('does not display a version number if application update failed', () => {
|
||||
const version = '0.1.45';
|
||||
vm = mountComponent(ApplicationRow, {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
mountComponent({
|
||||
status: APPLICATION_STATUS.INSTALLED,
|
||||
updateFailed: true,
|
||||
version,
|
||||
});
|
||||
const updateDetails = vm.$el.querySelector('.js-cluster-application-update-details');
|
||||
const versionEl = vm.$el.querySelector('.js-cluster-application-update-version');
|
||||
const updateDetails = wrapper.find('.js-cluster-application-update-details');
|
||||
const versionEl = wrapper.find('.js-cluster-application-update-version');
|
||||
|
||||
expect(updateDetails.innerHTML).toContain('failed');
|
||||
expect(versionEl).toBe(null);
|
||||
expect(updateDetails.text()).toContain('failed');
|
||||
expect(versionEl.exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error block', () => {
|
||||
const generalErrorMessage = () => wrapper.find('.js-cluster-application-general-error-message');
|
||||
|
||||
describe('when nothing fails', () => {
|
||||
it('does not show error block', () => {
|
||||
vm = mountComponent(ApplicationRow, {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
});
|
||||
const generalErrorMessage = vm.$el.querySelector(
|
||||
'.js-cluster-application-general-error-message',
|
||||
);
|
||||
mountComponent();
|
||||
|
||||
expect(generalErrorMessage).toBeNull();
|
||||
expect(generalErrorMessage().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -541,8 +417,7 @@ describe('Application Row', () => {
|
|||
const requestReason = 'We broke the request 0.0';
|
||||
|
||||
beforeEach(() => {
|
||||
vm = mountComponent(ApplicationRow, {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
mountComponent({
|
||||
status: APPLICATION_STATUS.ERROR,
|
||||
statusReason,
|
||||
requestReason,
|
||||
|
|
@ -551,37 +426,28 @@ describe('Application Row', () => {
|
|||
});
|
||||
|
||||
it('shows status reason if it is available', () => {
|
||||
const statusErrorMessage = vm.$el.querySelector(
|
||||
'.js-cluster-application-status-error-message',
|
||||
);
|
||||
const statusErrorMessage = wrapper.find('.js-cluster-application-status-error-message');
|
||||
|
||||
expect(statusErrorMessage.textContent.trim()).toEqual(statusReason);
|
||||
expect(statusErrorMessage.text()).toEqual(statusReason);
|
||||
});
|
||||
|
||||
it('shows request reason if it is available', () => {
|
||||
const requestErrorMessage = vm.$el.querySelector(
|
||||
'.js-cluster-application-request-error-message',
|
||||
);
|
||||
const requestErrorMessage = wrapper.find('.js-cluster-application-request-error-message');
|
||||
|
||||
expect(requestErrorMessage.textContent.trim()).toEqual(requestReason);
|
||||
expect(requestErrorMessage.text()).toEqual(requestReason);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when install fails', () => {
|
||||
beforeEach(() => {
|
||||
vm = mountComponent(ApplicationRow, {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
mountComponent({
|
||||
status: APPLICATION_STATUS.ERROR,
|
||||
installFailed: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('shows a general message indicating the installation failed', () => {
|
||||
const generalErrorMessage = vm.$el.querySelector(
|
||||
'.js-cluster-application-general-error-message',
|
||||
);
|
||||
|
||||
expect(generalErrorMessage.textContent.trim()).toEqual(
|
||||
expect(generalErrorMessage().text()).toEqual(
|
||||
`Something went wrong while installing ${DEFAULT_APPLICATION_STATE.title}`,
|
||||
);
|
||||
});
|
||||
|
|
@ -589,19 +455,14 @@ describe('Application Row', () => {
|
|||
|
||||
describe('when uninstall fails', () => {
|
||||
beforeEach(() => {
|
||||
vm = mountComponent(ApplicationRow, {
|
||||
...DEFAULT_APPLICATION_STATE,
|
||||
mountComponent({
|
||||
status: APPLICATION_STATUS.ERROR,
|
||||
uninstallFailed: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('shows a general message indicating the uninstalling failed', () => {
|
||||
const generalErrorMessage = vm.$el.querySelector(
|
||||
'.js-cluster-application-general-error-message',
|
||||
);
|
||||
|
||||
expect(generalErrorMessage.textContent.trim()).toEqual(
|
||||
expect(generalErrorMessage().text()).toEqual(
|
||||
`Something went wrong while uninstalling ${DEFAULT_APPLICATION_STATE.title}`,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1083,4 +1083,62 @@ describe('Actions Notes Store', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('softDeleteDescriptionVersion', () => {
|
||||
const endpoint = '/path/to/diff/1';
|
||||
const payload = {
|
||||
endpoint,
|
||||
startingVersion: undefined,
|
||||
versionId: 1,
|
||||
};
|
||||
|
||||
describe('if response contains no errors', () => {
|
||||
it('dispatches requestDeleteDescriptionVersion', done => {
|
||||
axiosMock.onDelete(endpoint).replyOnce(200);
|
||||
testAction(
|
||||
actions.softDeleteDescriptionVersion,
|
||||
payload,
|
||||
{},
|
||||
[],
|
||||
[
|
||||
{
|
||||
type: 'requestDeleteDescriptionVersion',
|
||||
},
|
||||
{
|
||||
type: 'receiveDeleteDescriptionVersion',
|
||||
payload: payload.versionId,
|
||||
},
|
||||
],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('if response contains errors', () => {
|
||||
const errorMessage = 'Request failed with status code 503';
|
||||
it('dispatches receiveDeleteDescriptionVersionError and throws an error', done => {
|
||||
axiosMock.onDelete(endpoint).replyOnce(503);
|
||||
testAction(
|
||||
actions.softDeleteDescriptionVersion,
|
||||
payload,
|
||||
{},
|
||||
[],
|
||||
[
|
||||
{
|
||||
type: 'requestDeleteDescriptionVersion',
|
||||
},
|
||||
{
|
||||
type: 'receiveDeleteDescriptionVersionError',
|
||||
payload: new Error(errorMessage),
|
||||
},
|
||||
],
|
||||
)
|
||||
.then(() => done.fail('Expected error to be thrown'))
|
||||
.catch(() => {
|
||||
expect(Flash).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_
|
|||
import EditArea from '~/static_site_editor/components/edit_area.vue';
|
||||
import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue';
|
||||
import EditHeader from '~/static_site_editor/components/edit_header.vue';
|
||||
import UnsavedChangesConfirmDialog from '~/static_site_editor/components/unsaved_changes_confirm_dialog.vue';
|
||||
|
||||
import { sourceContentTitle as title, sourceContent as content, returnUrl } from '../mock_data';
|
||||
|
||||
|
|
@ -28,6 +29,7 @@ describe('~/static_site_editor/components/edit_area.vue', () => {
|
|||
const findEditHeader = () => wrapper.find(EditHeader);
|
||||
const findRichContentEditor = () => wrapper.find(RichContentEditor);
|
||||
const findPublishToolbar = () => wrapper.find(PublishToolbar);
|
||||
const findUnsavedChangesConfirmDialog = () => wrapper.find(UnsavedChangesConfirmDialog);
|
||||
|
||||
beforeEach(() => {
|
||||
buildWrapper();
|
||||
|
|
@ -49,9 +51,16 @@ describe('~/static_site_editor/components/edit_area.vue', () => {
|
|||
|
||||
it('renders publish toolbar', () => {
|
||||
expect(findPublishToolbar().exists()).toBe(true);
|
||||
expect(findPublishToolbar().props('returnUrl')).toBe(returnUrl);
|
||||
expect(findPublishToolbar().props('savingChanges')).toBe(savingChanges);
|
||||
expect(findPublishToolbar().props('saveable')).toBe(false);
|
||||
expect(findPublishToolbar().props()).toMatchObject({
|
||||
returnUrl,
|
||||
savingChanges,
|
||||
saveable: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders unsaved changes confirm dialog', () => {
|
||||
expect(findUnsavedChangesConfirmDialog().exists()).toBe(true);
|
||||
expect(findUnsavedChangesConfirmDialog().props('modified')).toBe(false);
|
||||
});
|
||||
|
||||
describe('when content changes', () => {
|
||||
|
|
@ -61,10 +70,14 @@ describe('~/static_site_editor/components/edit_area.vue', () => {
|
|||
return wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
it('sets publish toolbar as saveable when content changes', () => {
|
||||
it('sets publish toolbar as saveable', () => {
|
||||
expect(findPublishToolbar().props('saveable')).toBe(true);
|
||||
});
|
||||
|
||||
it('sets unsaved changes confirm dialog as modified', () => {
|
||||
expect(findUnsavedChangesConfirmDialog().props('modified')).toBe(true);
|
||||
});
|
||||
|
||||
it('sets publish toolbar as not saveable when content changes are rollback', () => {
|
||||
findRichContentEditor().vm.$emit('input', content);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
|
||||
import UnsavedChangesConfirmDialog from '~/static_site_editor/components/unsaved_changes_confirm_dialog.vue';
|
||||
|
||||
describe('static_site_editor/components/unsaved_changes_confirm_dialog', () => {
|
||||
let wrapper;
|
||||
let event;
|
||||
let returnValueSetter;
|
||||
|
||||
const buildWrapper = (propsData = {}) => {
|
||||
wrapper = shallowMount(UnsavedChangesConfirmDialog, {
|
||||
propsData,
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
event = new Event('beforeunload');
|
||||
|
||||
jest.spyOn(event, 'preventDefault');
|
||||
returnValueSetter = jest.spyOn(event, 'returnValue', 'set');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
event.preventDefault.mockRestore();
|
||||
returnValueSetter.mockRestore();
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('displays confirmation dialog when modified = true', () => {
|
||||
buildWrapper({ modified: true });
|
||||
window.dispatchEvent(event);
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalled();
|
||||
expect(returnValueSetter).toHaveBeenCalledWith('');
|
||||
});
|
||||
|
||||
it('does not display confirmation dialog when modified = false', () => {
|
||||
buildWrapper();
|
||||
window.dispatchEvent(event);
|
||||
|
||||
expect(event.preventDefault).not.toHaveBeenCalled();
|
||||
expect(returnValueSetter).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -48,18 +48,45 @@ describe Gitlab::Runtime do
|
|||
before do
|
||||
stub_const('::Puma', puma_type)
|
||||
allow(puma_type).to receive_message_chain(:cli_config, :options).and_return(max_threads: 2)
|
||||
stub_config(action_cable: { in_app: false })
|
||||
end
|
||||
|
||||
it_behaves_like "valid runtime", :puma, 3
|
||||
|
||||
context "when ActionCable in-app mode is enabled" do
|
||||
before do
|
||||
stub_config(action_cable: { in_app: true, worker_pool_size: 3 })
|
||||
end
|
||||
|
||||
it_behaves_like "valid runtime", :puma, 6
|
||||
end
|
||||
|
||||
context "when ActionCable standalone is run" do
|
||||
before do
|
||||
stub_const('ACTION_CABLE_SERVER', true)
|
||||
stub_config(action_cable: { worker_pool_size: 8 })
|
||||
end
|
||||
|
||||
it_behaves_like "valid runtime", :puma, 11
|
||||
end
|
||||
end
|
||||
|
||||
context "unicorn" do
|
||||
before do
|
||||
stub_const('::Unicorn', Module.new)
|
||||
stub_const('::Unicorn::HttpServer', Class.new)
|
||||
stub_config(action_cable: { in_app: false })
|
||||
end
|
||||
|
||||
it_behaves_like "valid runtime", :unicorn, 1
|
||||
|
||||
context "when ActionCable in-app mode is enabled" do
|
||||
before do
|
||||
stub_config(action_cable: { in_app: true, worker_pool_size: 3 })
|
||||
end
|
||||
|
||||
it_behaves_like "valid runtime", :unicorn, 4
|
||||
end
|
||||
end
|
||||
|
||||
context "sidekiq" do
|
||||
|
|
@ -105,17 +132,4 @@ describe Gitlab::Runtime do
|
|||
|
||||
it_behaves_like "valid runtime", :rails_runner, 1
|
||||
end
|
||||
|
||||
context "action_cable" do
|
||||
before do
|
||||
stub_const('ACTION_CABLE_SERVER', true)
|
||||
stub_const('::Puma', Module.new)
|
||||
|
||||
allow(Gitlab::Application).to receive_message_chain(:config, :action_cable, :worker_pool_size).and_return(8)
|
||||
end
|
||||
|
||||
it "reports its maximum concurrency based on ActionCable's worker pool size" do
|
||||
expect(subject.max_threads).to eq(9)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue