summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.rubocop_todo/rspec/any_instance_of.yml1
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor.vue14
-rw-r--r--app/assets/javascripts/content_editor/components/formatting_toolbar.vue9
-rw-r--r--app/assets/javascripts/pages/projects/commit/show/index.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/editor_mode_dropdown.vue58
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue38
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue21
-rw-r--r--db/post_migrate/20221214105307_add_token_encrypted_partition_id_index_to_ci_build.rb23
-rw-r--r--db/schema_migrations/202212141053071
-rw-r--r--doc/development/rake_tasks.md44
-rw-r--r--doc/user/project/remote_development/index.md15
-rw-r--r--lib/gitlab/database/async_indexes/index_creator.rb2
-rw-r--r--lib/gitlab/seeders/ci/runner/runner_fleet_pipeline_seeder.rb162
-rw-r--r--lib/gitlab/seeders/ci/runner/runner_fleet_seeder.rb235
-rw-r--r--lib/tasks/gitlab/seed/runner_fleet.rake40
-rw-r--r--locale/gitlab.pot15
-rw-r--r--package.json2
-rw-r--r--qa/qa/page/component/wiki_page_form.rb10
-rw-r--r--spec/features/admin/admin_users_spec.rb6
-rw-r--r--spec/features/issues/user_edits_issue_spec.rb7
-rw-r--r--spec/frontend/vue_shared/components/markdown/editor_mode_dropdown_spec.js58
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_spec.js18
-rw-r--r--spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js75
-rw-r--r--spec/frontend/vue_shared/components/markdown/toolbar_spec.js15
-rw-r--r--spec/lib/gitlab/database/async_indexes/index_creator_spec.rb2
-rw-r--r--spec/lib/gitlab/seeders/ci/runner/runner_fleet_pipeline_seeder_spec.rb53
-rw-r--r--spec/lib/gitlab/seeders/ci/runner/runner_fleet_seeder_spec.rb70
-rw-r--r--spec/support/shared_examples/features/content_editor_shared_examples.rb3
-rw-r--r--spec/tasks/gitlab/seed/runner_fleet_rake_spec.rb42
-rw-r--r--spec/views/admin/application_settings/general.html.haml_spec.rb6
-rw-r--r--spec/views/groups/edit.html.haml_spec.rb8
-rw-r--r--spec/views/projects/edit.html.haml_spec.rb6
-rw-r--r--yarn.lock8
35 files changed, 948 insertions, 130 deletions
diff --git a/.rubocop_todo/rspec/any_instance_of.yml b/.rubocop_todo/rspec/any_instance_of.yml
index 3a01725168a..b107938dd17 100644
--- a/.rubocop_todo/rspec/any_instance_of.yml
+++ b/.rubocop_todo/rspec/any_instance_of.yml
@@ -2,7 +2,6 @@
# Cop supports --autocorrect.
RSpec/AnyInstanceOf:
Exclude:
- - 'ee/spec/features/ci_shared_runner_warnings_spec.rb'
- 'ee/spec/features/issues/form_spec.rb'
- 'ee/spec/features/projects/new_project_spec.rb'
- 'ee/spec/features/registrations/welcome_spec.rb'
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index a6de580ddba..7c0f335267c 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-c5b4c5311b1c093be5826711bdbe239b0ef49aec
+50c8cb6e62396936c9e568e859dd717595281788
diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue
index 53a37fc0c51..237808983ee 100644
--- a/app/assets/javascripts/content_editor/components/content_editor.vue
+++ b/app/assets/javascripts/content_editor/components/content_editor.vue
@@ -168,7 +168,12 @@ export default {
class="md-area"
:class="{ 'is-focused': focused }"
>
- <formatting-toolbar v-if="!useBottomToolbar" ref="toolbar" class="gl-border-b" />
+ <formatting-toolbar
+ v-if="!useBottomToolbar"
+ ref="toolbar"
+ class="gl-border-b"
+ @enableMarkdownEditor="$emit('enableMarkdownEditor')"
+ />
<div class="gl-relative gl-mt-4">
<formatting-bubble-menu />
<code-block-bubble-menu />
@@ -181,7 +186,12 @@ export default {
/>
<loading-indicator v-if="isLoading" />
</div>
- <formatting-toolbar v-if="useBottomToolbar" ref="toolbar" class="gl-border-t" />
+ <formatting-toolbar
+ v-if="useBottomToolbar"
+ ref="toolbar"
+ class="gl-border-t"
+ @enableMarkdownEditor="$emit('enableMarkdownEditor')"
+ />
</div>
</div>
</content-editor-provider>
diff --git a/app/assets/javascripts/content_editor/components/formatting_toolbar.vue b/app/assets/javascripts/content_editor/components/formatting_toolbar.vue
index 8a25ad3fd96..36ca3b8cfb6 100644
--- a/app/assets/javascripts/content_editor/components/formatting_toolbar.vue
+++ b/app/assets/javascripts/content_editor/components/formatting_toolbar.vue
@@ -1,4 +1,5 @@
<script>
+import EditorModeDropdown from '~/vue_shared/components/markdown/editor_mode_dropdown.vue';
import trackUIControl from '../services/track_ui_control';
import ToolbarButton from './toolbar_button.vue';
import ToolbarImageButton from './toolbar_image_button.vue';
@@ -9,6 +10,7 @@ import ToolbarMoreDropdown from './toolbar_more_dropdown.vue';
export default {
components: {
+ EditorModeDropdown,
ToolbarButton,
ToolbarTextStyleDropdown,
ToolbarLinkButton,
@@ -20,6 +22,11 @@ export default {
trackToolbarControlExecution({ contentType, value }) {
trackUIControl({ property: contentType, value });
},
+ handleEditorModeChanged(mode) {
+ if (mode === 'markdown') {
+ this.$emit('enableMarkdownEditor');
+ }
+ },
},
};
</script>
@@ -101,6 +108,8 @@ export default {
/>
<toolbar-table-button data-testid="table" @execute="trackToolbarControlExecution" />
<toolbar-more-dropdown data-testid="more" @execute="trackToolbarControlExecution" />
+
+ <editor-mode-dropdown class="gl-ml-auto" value="richText" @input="handleEditorModeChanged" />
</div>
</template>
<style>
diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js
index af0097b415c..46704d96552 100644
--- a/app/assets/javascripts/pages/projects/commit/show/index.js
+++ b/app/assets/javascripts/pages/projects/commit/show/index.js
@@ -78,3 +78,5 @@ if (filesContainer.length) {
loadAwardsHandler();
initCommitActions();
+
+syntaxHighlight([document.querySelector('.files')]);
diff --git a/app/assets/javascripts/vue_shared/components/markdown/editor_mode_dropdown.vue b/app/assets/javascripts/vue_shared/components/markdown/editor_mode_dropdown.vue
new file mode 100644
index 00000000000..6702a81e747
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown/editor_mode_dropdown.vue
@@ -0,0 +1,58 @@
+<script>
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ },
+ props: {
+ size: {
+ type: String,
+ required: false,
+ default: 'medium',
+ },
+ value: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ markdownEditorSelected() {
+ return this.value === 'markdown';
+ },
+ text() {
+ return this.markdownEditorSelected ? __('View rich text') : __('View markdown');
+ },
+ },
+};
+</script>
+<template>
+ <gl-dropdown
+ category="tertiary"
+ data-qa-selector="editing_mode_switcher"
+ :size="size"
+ :text="text"
+ right
+ >
+ <gl-dropdown-item
+ is-check-item
+ :is-checked="!markdownEditorSelected"
+ @click="$emit('input', 'richText')"
+ ><div class="gl-font-weight-bold">{{ __('Rich text') }}</div>
+ <div class="gl-text-secondary">
+ {{ __('View the formatted output in real-time as you edit.') }}
+ </div>
+ </gl-dropdown-item>
+ <gl-dropdown-item
+ is-check-item
+ :is-checked="markdownEditorSelected"
+ @click="$emit('input', 'markdown')"
+ ><div class="gl-font-weight-bold">{{ __('Markdown') }}</div>
+ <div class="gl-text-secondary">
+ {{ __('View and edit markdown, with the option to preview the formatted output.') }}
+ </div></gl-dropdown-item
+ >
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index b5f2602af5e..7b76fc3fc6d 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -122,6 +122,11 @@ export default {
required: false,
default: () => [],
},
+ showContentEditorSwitcher: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -364,6 +369,8 @@ export default {
:quick-actions-docs-path="quickActionsDocsPath"
:can-attach-file="canAttachFile"
:show-comment-tool-bar="showCommentToolBar"
+ :show-content-editor-switcher="showContentEditorSwitcher"
+ @enableContentEditor="$emit('enableContentEditor')"
/>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
index d01eae0308f..c53118b9f62 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
@@ -1,16 +1,13 @@
<script>
-import { GlSegmentedControl } from '@gitlab/ui';
-import { __ } from '~/locale';
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import axios from '~/lib/utils/axios_utils';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { EDITING_MODE_MARKDOWN_FIELD, EDITING_MODE_CONTENT_EDITOR } from '../../constants';
import MarkdownField from './field.vue';
export default {
components: {
- MarkdownField,
LocalStorageSync,
- GlSegmentedControl,
+ MarkdownField,
ContentEditor: () =>
import(
/* webpackChunkName: 'content_editor' */ '~/content_editor/components/content_editor.vue'
@@ -91,7 +88,6 @@ export default {
data() {
return {
editingMode: EDITING_MODE_MARKDOWN_FIELD,
- switchEditingControlEnabled: true,
autofocused: false,
};
},
@@ -114,19 +110,16 @@ export default {
updateMarkdownFromMarkdownField({ target }) {
this.$emit('input', target.value);
},
- enableSwitchEditingControl() {
- this.switchEditingControlEnabled = true;
- },
- disableSwitchEditingControl() {
- this.switchEditingControlEnabled = false;
- },
renderMarkdown(markdown) {
return axios.post(this.renderMarkdownPath, { text: markdown }).then(({ data }) => data.body);
},
onEditingModeChange(editingMode) {
+ this.editingMode = editingMode;
this.notifyEditingModeChange(editingMode);
},
onEditingModeRestored(editingMode) {
+ this.editingMode = editingMode;
+ this.$emit(editingMode);
this.notifyEditingModeChange(editingMode);
},
notifyEditingModeChange(editingMode) {
@@ -142,25 +135,10 @@ export default {
this.autofocused = true;
},
},
- switchEditingControlOptions: [
- { text: __('Source'), value: EDITING_MODE_MARKDOWN_FIELD },
- { text: __('Rich text'), value: EDITING_MODE_CONTENT_EDITOR },
- ],
};
</script>
<template>
<div>
- <div class="gl-display-flex gl-justify-content-start gl-mb-3">
- <gl-segmented-control
- v-model="editingMode"
- data-testid="toggle-editing-mode-button"
- data-qa-selector="editing_mode_button"
- class="gl-display-flex"
- :options="$options.switchEditingControlOptions"
- :disabled="!enableContentEditor || !switchEditingControlEnabled"
- @change="onEditingModeChange"
- />
- </div>
<local-storage-sync
v-model="editingMode"
storage-key="gl-wiki-content-editor-enabled"
@@ -176,7 +154,9 @@ export default {
:quick-actions-docs-path="quickActionsDocsPath"
:uploads-path="uploadsPath"
:enable-preview="enablePreview"
+ show-content-editor-switcher
class="bordered-box"
+ @enableContentEditor="onEditingModeChange('contentEditor')"
>
<template #textarea>
<textarea
@@ -205,10 +185,8 @@ export default {
:use-bottom-toolbar="useBottomToolbar"
@initialized="setEditorAsAutofocused"
@change="updateMarkdownFromContentEditor"
- @loading="disableSwitchEditingControl"
- @loadingSuccess="enableSwitchEditingControl"
- @loadingError="enableSwitchEditingControl"
@keydown="$emit('keydown', $event)"
+ @enableMarkdownEditor="onEditingModeChange('markdownField')"
/>
<input
:id="formFieldId"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
index b5640e12541..e8be242f660 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -1,5 +1,6 @@
<script>
import { GlButton, GlLink, GlLoadingIcon, GlSprintf, GlIcon } from '@gitlab/ui';
+import EditorModeDropdown from './editor_mode_dropdown.vue';
export default {
components: {
@@ -8,6 +9,7 @@ export default {
GlLoadingIcon,
GlSprintf,
GlIcon,
+ EditorModeDropdown,
},
props: {
markdownDocsPath: {
@@ -29,12 +31,24 @@ export default {
required: false,
default: true,
},
+ showContentEditorSwitcher: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
hasQuickActionsDocsPath() {
return this.quickActionsDocsPath !== '';
},
},
+ methods: {
+ handleEditorModeChanged(mode) {
+ if (mode === 'richText') {
+ this.$emit('enableContentEditor');
+ }
+ },
+ },
};
</script>
@@ -121,5 +135,12 @@ export default {
{{ __('Cancel') }}
</gl-button>
</span>
+ <editor-mode-dropdown
+ v-if="showContentEditorSwitcher"
+ size="small"
+ class="gl-float-right gl-line-height-28 gl-display-block"
+ value="markdown"
+ @input="handleEditorModeChanged"
+ />
</div>
</template>
diff --git a/db/post_migrate/20221214105307_add_token_encrypted_partition_id_index_to_ci_build.rb b/db/post_migrate/20221214105307_add_token_encrypted_partition_id_index_to_ci_build.rb
new file mode 100644
index 00000000000..4c64e9b0e70
--- /dev/null
+++ b/db/post_migrate/20221214105307_add_token_encrypted_partition_id_index_to_ci_build.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class AddTokenEncryptedPartitionIdIndexToCiBuild < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ TABLE_NAME = :ci_builds
+ INDEX_NAME = :unique_ci_builds_token_encrypted_and_partition_id
+ COLUMNS = %i[token_encrypted partition_id].freeze
+
+ def up
+ prepare_async_index(
+ TABLE_NAME,
+ COLUMNS,
+ where: 'token_encrypted IS NOT NULL',
+ unique: true,
+ name: INDEX_NAME
+ )
+ end
+
+ def down
+ unprepare_async_index(TABLE_NAME, COLUMNS, name: INDEX_NAME)
+ end
+end
diff --git a/db/schema_migrations/20221214105307 b/db/schema_migrations/20221214105307
new file mode 100644
index 00000000000..d8e1f90901b
--- /dev/null
+++ b/db/schema_migrations/20221214105307
@@ -0,0 +1 @@
+731ff12680cd8f21b245fcb5b0772567a7534cfe17248a741dc12d4b5e2e951d \ No newline at end of file
diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md
index 14fbe0e875b..2e20e66e12c 100644
--- a/doc/development/rake_tasks.md
+++ b/doc/development/rake_tasks.md
@@ -75,6 +75,50 @@ bin/rake "gitlab:seed:group_seed[subgroup_depth, username]"
Group are additionally seeded with epics if GitLab instance has epics feature available.
+#### Seeding a runner fleet test environment
+
+Use the `gitlab:seed:runner_fleet` task to seed a full runner fleet, specifically groups with subgroups and projects that contain runners and pipelines:
+
+```shell
+bin/rake "gitlab:seed:runner_fleet[username, registration_prefix, runner_count, job_count]"
+```
+
+By default, the Rake task uses the `root` username to create 40 runners and 400 jobs.
+
+```mermaid
+graph TD
+ G1[Top level group 1] --> G11
+ G2[Top level group 2] --> G21
+ G11[Group 1.1] --> G111
+ G11[Group 1.1] --> G112
+ G111[Group 1.1.1] --> P1111
+ G112[Group 1.1.2] --> P1121
+ G21[Group 2.1] --> P211
+
+ P1111[Project 1.1.1.1<br><i>70% of jobs, sent to first 5 runners</i>]
+ P1121[Project 1.1.2.1<br><i>15% of jobs, sent to first 5 runners</i>]
+ P211[Project 2.1.1<br><i>15% of jobs, sent to first 5 runners</i>]
+
+ P1111R1[Shared runner]
+ P1111R[Project 1.1.1.1 runners<br>20% total runners]
+ P1121R[Project 1.1.2.1 runners<br>49% total runners]
+ G111R[Group 1.1.1 runners<br>30% total runners<br><i>remaining jobs</i>]
+ G21R[Group 2.1 runners<br>1% total runners]
+
+ P1111 --> P1111R1
+ P1111 --> G111R
+ P1111 --> P1111R
+ P1121 --> P1111R1
+ P1121 --> P1121R
+ P211 --> P1111R1
+ P211 --> G21R
+
+ classDef groups fill:#09f6,color:#000000,stroke:#333,stroke-width:3px;
+ classDef projects fill:#f96a,color:#000000,stroke:#333,stroke-width:2px;
+ class G1,G2,G11,G111,G112,G21 groups
+ class P1111,P1121,P211 projects
+```
+
#### Seeding custom metrics for the monitoring dashboard
A lot of different types of metrics are supported in the monitoring dashboard.
diff --git a/doc/user/project/remote_development/index.md b/doc/user/project/remote_development/index.md
index 62220dd2fde..119ca930f92 100644
--- a/doc/user/project/remote_development/index.md
+++ b/doc/user/project/remote_development/index.md
@@ -81,7 +81,7 @@ To connect a development environment to the Web IDE:
1. [Create a development environment](#manage-a-development-environment).
1. [Fetch a token](#fetch-a-token).
-1. [Connect to the Web IDE](#connect-to-the-web-ide).
+1. [Configure a remote connection](#configure-a-remote-connection).
#### Manage a development environment
@@ -134,9 +134,18 @@ To remove a development environment:
docker exec my-environment cat TOKEN
```
-#### Connect to the Web IDE
+#### Configure a remote connection
-To connect to the Web IDE:
+To configure a remote connection from the Web IDE:
+
+1. Open the Web IDE.
+1. In the Menu Bar, select **View > Terminal** or press <kbd>Control</kbd>+<kbd>`</kbd>.
+1. In the terminal panel, select **Configure a remote connection**.
+1. Enter the URL for the remote host including the port (for example, `yourdomain.com:3443`).
+1. Enter the project path.
+1. Enter the [token you fetched](#fetch-a-token).
+
+Alternatively, you can pass the parameters from a URL and connect directly to the Web IDE:
1. Run the following command:
diff --git a/lib/gitlab/database/async_indexes/index_creator.rb b/lib/gitlab/database/async_indexes/index_creator.rb
index 2fb4cc8f675..d432745d03c 100644
--- a/lib/gitlab/database/async_indexes/index_creator.rb
+++ b/lib/gitlab/database/async_indexes/index_creator.rb
@@ -7,7 +7,7 @@ module Gitlab
include ExclusiveLeaseGuard
TIMEOUT_PER_ACTION = 1.day
- STATEMENT_TIMEOUT = 9.hours
+ STATEMENT_TIMEOUT = 20.hours
def initialize(async_index)
@async_index = async_index
diff --git a/lib/gitlab/seeders/ci/runner/runner_fleet_pipeline_seeder.rb b/lib/gitlab/seeders/ci/runner/runner_fleet_pipeline_seeder.rb
new file mode 100644
index 00000000000..a0abe8bf9e4
--- /dev/null
+++ b/lib/gitlab/seeders/ci/runner/runner_fleet_pipeline_seeder.rb
@@ -0,0 +1,162 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Seeders
+ module Ci
+ module Runner
+ class RunnerFleetPipelineSeeder
+ DEFAULT_JOB_COUNT = 400
+
+ MAX_QUEUE_TIME_IN_SECONDS = 5 * 60
+ PIPELINE_CREATION_RANGE_MIN_IN_MINUTES = 120
+ PIPELINE_CREATION_RANGE_MAX_IN_MINUTES = 30 * 24 * 60
+ PIPELINE_START_RANGE_MAX_IN_MINUTES = 60 * 60
+ PIPELINE_FINISH_RANGE_MAX_IN_MINUTES = 60
+
+ PROJECT_JOB_DISTRIBUTION = [
+ { allocation: 70, job_count_default: 10 },
+ { allocation: 15, job_count_default: 10 },
+ { allocation: 15, job_count_default: 100 }
+ # remaining jobs on 4th project
+ ].freeze
+
+ attr_reader :logger
+
+ # Initializes the class
+ #
+ # @param [Gitlab::Logger] logger
+ # @param [Integer] job_count the number of jobs to create across the runners
+ # @param [Array<Hash>] projects_to_runners list of project IDs to respective runner IDs
+ def initialize(logger = Gitlab::AppLogger, projects_to_runners:, job_count:)
+ @logger = logger
+ @projects_to_runners = projects_to_runners
+ @job_count = job_count || DEFAULT_JOB_COUNT
+ end
+
+ def seed
+ logger.info(message: 'Starting seed of runner fleet pipelines', job_count: @job_count)
+
+ remaining_job_count = @job_count
+ PROJECT_JOB_DISTRIBUTION.each_with_index do |d, index|
+ remaining_job_count = create_pipelines_and_distribute_jobs(remaining_job_count, project_index: index, **d)
+ end
+
+ if remaining_job_count > 0
+ create_pipeline(
+ job_count: remaining_job_count,
+ **@projects_to_runners[PROJECT_JOB_DISTRIBUTION.length],
+ status: Random.rand(1..100) < 40 ? 'failed' : 'success'
+ )
+ remaining_job_count = 0
+ end
+
+ logger.info(
+ message: 'Completed seeding of runner fleet',
+ job_count: @job_count - remaining_job_count
+ )
+
+ nil
+ end
+
+ private
+
+ def create_pipelines_and_distribute_jobs(remaining_job_count, project_index:, allocation:, job_count_default:)
+ max_jobs_per_pipeline = [1, @job_count / 3].max
+
+ create_pipelines(
+ remaining_job_count,
+ **@projects_to_runners[project_index],
+ total_jobs: @job_count * allocation / 100,
+ pipeline_job_count: job_count_default.clamp(1, max_jobs_per_pipeline)
+ )
+ end
+
+ def create_pipelines(remaining_job_count, project_id:, runner_ids:, total_jobs:, pipeline_job_count:)
+ pipeline_job_count = remaining_job_count if pipeline_job_count > remaining_job_count
+ return 0 if pipeline_job_count == 0
+
+ pipeline_count = [1, total_jobs / pipeline_job_count].max
+
+ (1..pipeline_count).each do
+ create_pipeline(
+ job_count: pipeline_job_count,
+ project_id: project_id,
+ runner_ids: runner_ids,
+ status: Random.rand(1..100) < 70 ? 'failed' : 'success'
+ )
+ remaining_job_count -= pipeline_job_count
+ end
+
+ remaining_job_count
+ end
+
+ def create_pipeline(job_count:, runner_ids:, project_id:, status: 'success', **attrs)
+ logger.info(message: 'Creating pipeline with builds on project',
+ status: status, job_count: job_count, project_id: project_id, **attrs)
+
+ raise ArgumentError('runner_ids') unless runner_ids
+ raise ArgumentError('project_id') unless project_id
+
+ sha = '00000000'
+ created_at = Random.rand(PIPELINE_CREATION_RANGE_MIN_IN_MINUTES..PIPELINE_CREATION_RANGE_MAX_IN_MINUTES)
+ .minutes.ago
+ started_at = created_at + Random.rand(1..PIPELINE_START_RANGE_MAX_IN_MINUTES).seconds
+ finished_at = started_at + Random.rand(1..PIPELINE_FINISH_RANGE_MAX_IN_MINUTES).minutes
+
+ pipeline = ::Ci::Pipeline.new(
+ project_id: project_id,
+ ref: 'main',
+ sha: sha,
+ source: 'api',
+ status: status,
+ created_at: created_at,
+ started_at: started_at,
+ finished_at: finished_at,
+ **attrs
+ )
+ pipeline.ensure_project_iid! # allocate an internal_id outside of pipeline creation transaction
+ pipeline.save!
+
+ (1..job_count).each do |index|
+ create_build(pipeline, runner_ids.sample, job_status(pipeline.status, index, job_count), index)
+ end
+
+ pipeline
+ end
+
+ def create_build(pipeline, runner_id, job_status, index)
+ started_at = pipeline.started_at
+ finished_at = pipeline.finished_at
+ max_job_duration = [MAX_QUEUE_TIME_IN_SECONDS, finished_at - started_at].min
+ job_started_at = started_at + Random.rand(1..max_job_duration).seconds
+ job_finished_at = Random.rand(job_started_at..finished_at)
+
+ build_attrs = {
+ name: "Fake job #{index}",
+ scheduling_type: 'dag',
+ ref: 'main',
+ status: job_status,
+ pipeline_id: pipeline.id,
+ runner_id: runner_id,
+ project_id: pipeline.project_id,
+ created_at: started_at,
+ queued_at: started_at,
+ started_at: job_started_at,
+ finished_at: job_finished_at
+ }
+ logger.info(message: 'Creating build', **build_attrs)
+
+ ::Ci::Build.new(importing: true, **build_attrs).tap(&:save!)
+ end
+
+ def job_status(pipeline_status, job_index, job_count)
+ return 'success' if pipeline_status == 'success'
+ return 'failed' if job_index == job_count # Ensure that a failed pipeline has at least 1 failed job
+
+ Random.rand(0..1) == 0 ? 'failed' : 'success'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/seeders/ci/runner/runner_fleet_seeder.rb b/lib/gitlab/seeders/ci/runner/runner_fleet_seeder.rb
new file mode 100644
index 00000000000..7cc796dfd6f
--- /dev/null
+++ b/lib/gitlab/seeders/ci/runner/runner_fleet_seeder.rb
@@ -0,0 +1,235 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Seeders
+ module Ci
+ module Runner
+ class RunnerFleetSeeder
+ DEFAULT_USERNAME = 'root'
+ DEFAULT_PREFIX = 'rf-'
+ DEFAULT_RUNNER_COUNT = 40
+ DEFAULT_JOB_COUNT = DEFAULT_RUNNER_COUNT * 10
+
+ TAG_LIST = %w[gitlab-org docker ruby 2gb mysql linux shared shell deploy hhvm windows build postgres ios stage android stz front back review-apps pc java scraper test kubernetes staging no-priority osx php nodejs production nvm x86_64 gcc nginx dev unity odoo node sbt amazon xamarin debian gcloud e2e clang composer npm energiency dind flake8 cordova x64 private aws solution ruby2.2 python xcode kube compute mongo runner docker-compose phpunit t-matix docker-machine win server docker-in-docker redis go dotnet win7 area51-1 testing chefdk light osx_10-11 ubuntu gulp jertis gitlab-runner frontendv2 capifony centos7 mac gradle golang docker-builder runrepeat maven centos6 msvc14 amd64 xcode_8-2 macos VS2015 mono osx_10-12 azure-contend-docker msbuild git deployer local development python2.7 eezeeit release ios_9-3 fastlane selenium integration tests review cabinet-dev vs2015 ios_10-2 latex odoo_test quantum-ci prod sqlite heavy icc html-test labs feature alugha ps appivo-server fast web ios_9-2 c# python3 home js xcode_7-3 drupal 7 arm headless php70 gce x86 msvc builder Windows bower mssql pagetest wpf ssh inmobiliabeta.com xcode_7-2 repo laravel testonly gcp online-auth powershell ila-preprod ios_10-1 lossless sharesies backbone javascript fusonic-review autoscale ci ubuntu1604 rails windows10 xcode_8-1 php56 drupal embedded readyselect xamarin.ios XCode-8.1 iOS-10.1 macOS-10.12.1 develop taggun koumoul-internal docker-build iOS angular2 deployment xcode8 lcov test-cluster priv api bundler freebsd x86-64 BOB xcode_8 nuget vinome-backend cq_check fusonic-perf django php7 dy-manager-shell DEV mongodb neadev meteor ANSIBLE ftp master exerica-build server01 exerica-test mother-of-god nodejs-app ansible Golang mpi exploragen shootr Android macos_10-12 win64 ngsrunner @docker images script-maven ayk makepkg Linux ecolint wix xcode_8-0 coverage dreamhost multi ubuntu1404 eyeka jow3an-site repository politibot qt haskellstack arch priviti backend Sisyphus gm-dev dotNet internal support rpi .net buildbot-01 quay.io BOB2 codebnb vs2013 no-reset live 192.168.100.209 failfast-ci ios_10 crm_master_builds Qt packer selenium hub ci-shell rust dyscount-ci-manager-shell kubespray vagrant deployAutomobileBuild 1md k8s behat vinome-frontend development-nanlabs build-backend libvirt build-frontend contend-server windows-x64 chimpAPI ec2-runner kubectl linux-x64 epitech portals kvm ucaya-docker scala desktop buildmacbinaries ghc buildwinbinaries sonarqube deploySteelDistributorsBuild macOS r cpran rubocop binarylane r-packages alpha SIGAC tester area51-2 customer Build qa acegames_central mTaxNativeShell c++ cloveapp-ios smallville portal root lemmy nightly buildlinuxbinaries rundeck taxonic ios_10-0 n0004 data fedora rr-test seedai_master_builds geofence_master_builds].freeze # rubocop:disable Layout/LineLength
+
+ attr_reader :logger
+
+ # Initializes the class
+ #
+ # @param [Gitlab::Logger] logger
+ # @param [Hash] options
+ # @option options [String] :username username of the user that will create the fleet
+ # @option options [String] :registration_prefix string to use as prefix in group, project, and runner names
+ # @option options [Integer] :runner_count number of runners to create across the groups and projects
+ # @return [Array<Hash>] list of project IDs to respective runner IDs
+ def initialize(logger = Gitlab::AppLogger, **options)
+ username = options.fetch(:username, DEFAULT_USERNAME)
+
+ @logger = logger
+ @user = User.find_by_username(username)
+ @registration_prefix = options[:registration_prefix] || DEFAULT_PREFIX
+ @runner_count = options[:runner_count] || DEFAULT_RUNNER_COUNT
+ @groups = {}
+ @projects = {}
+ end
+
+ # seed returns an array of hashes of projects to its assigned runners
+ def seed
+ return unless within_plan_limits?
+
+ logger.info(
+ message: 'Starting seed of runner fleet',
+ user_id: @user.id,
+ registration_prefix: @registration_prefix,
+ runner_count: @runner_count
+ )
+
+ groups_and_projects = create_groups_and_projects
+ runner_ids = create_runners(groups_and_projects)
+
+ logger.info(
+ message: 'Completed seeding of runner fleet',
+ registration_prefix: @registration_prefix,
+ groups: @groups.count,
+ projects: @projects.count,
+ runner_count: @runner_count
+ )
+
+ %i[project_1_1_1_1 project_1_1_2_1 project_2_1_1].map do |project_key|
+ { project_id: groups_and_projects[project_key].id, runner_ids: runner_ids[project_key] }
+ end
+ end
+
+ private
+
+ def within_plan_limits?
+ plan_limits = Plan.default.actual_limits
+
+ if plan_limits.ci_registered_group_runners < @runner_count
+ logger.error('The plan limits for group runners is set to ' \
+ "#{plan_limits.ci_registered_group_runners} runners. " \
+ 'You should raise the plan limits to avoid errors during runner creation')
+ return false
+ elsif plan_limits.ci_registered_project_runners < @runner_count
+ logger.error('The plan limits for project runners is set to ' \
+ "#{plan_limits.ci_registered_project_runners} runners. " \
+ 'You should raise the plan limits to avoid errors during runner creation')
+ return false
+ end
+
+ true
+ end
+
+ def create_groups_and_projects
+ root_group_1 = ensure_group(name: 'top-level group 1')
+ root_group_2 = ensure_group(name: 'top-level group 2')
+ group_1_1 = ensure_group(name: 'group 1.1', parent_id: root_group_1.id)
+ group_1_1_1 = ensure_group(name: 'group 1.1.1', parent_id: group_1_1.id)
+ group_1_1_2 = ensure_group(name: 'group 1.1.2', parent_id: group_1_1.id)
+ group_2_1 = ensure_group(name: 'group 2.1', parent_id: root_group_2.id)
+
+ {
+ root_group_1: root_group_1,
+ root_group_2: root_group_2,
+ group_1_1: group_1_1,
+ group_1_1_1: group_1_1_1,
+ group_1_1_2: group_1_1_2,
+ project_1_1_1_1: ensure_project(name: 'project 1.1.1.1', namespace_id: group_1_1_1.id),
+ project_1_1_2_1: ensure_project(name: 'project 1.1.2.1', namespace_id: group_1_1_2.id),
+ group_2_1: group_2_1,
+ project_2_1_1: ensure_project(name: 'project 2.1.1', namespace_id: group_2_1.id)
+ }
+ end
+
+ def create_runners(gp)
+ group_1_1_1_runners = []
+ group_2_1_runners = []
+ project_1_1_1_1_runners = []
+ project_1_1_2_1_runners = []
+ project_2_1_1_runners = []
+ project_1_1_1_1_shared_runner_1 =
+ create_runner(name: 'project 1.1.1.1 shared runner 1', scope: gp[:project_1_1_1_1])
+ project_1_1_1_1_runners << project_1_1_1_1_shared_runner_1
+ project_1_1_2_1_runners << assign_runner(project_1_1_1_1_shared_runner_1, gp[:project_1_1_2_1])
+ project_2_1_1_runners << assign_runner(project_1_1_1_1_shared_runner_1, gp[:project_2_1_1])
+
+ (2..@runner_count).each do
+ case Random.rand(0..100)
+ when 0..30
+ runner_name = "group 1.1.1 runner #{1 + group_1_1_1_runners.count}"
+ group_1_1_1_runners << create_runner(name: runner_name, scope: gp[:group_1_1_1])
+ when 31..50
+ runner_name = "project 1.1.1.1 runner #{1 + project_1_1_1_1_runners.count}"
+ project_1_1_1_1_runners << create_runner(name: runner_name, scope: gp[:project_1_1_1_1])
+ when 51..99
+ runner_name = "project 1.1.2.1 runner #{1 + project_1_1_2_1_runners.count}"
+ project_1_1_2_1_runners << create_runner(name: runner_name, scope: gp[:project_1_1_2_1])
+ else
+ runner_name = "group 2.1 runner #{1 + group_2_1_runners.count}"
+ group_2_1_runners << create_runner(name: runner_name, scope: gp[:group_2_1])
+ end
+ end
+
+ { # use only the first 5 runners to assign CI jobs
+ project_1_1_1_1: (project_1_1_1_1_runners.map(&:id) + group_1_1_1_runners.map(&:id)).first(5),
+ project_1_1_2_1: project_1_1_2_1_runners.map(&:id).first(5),
+ project_2_1_1: (project_2_1_1_runners.map(&:id) + group_2_1_runners.map(&:id)).first(5)
+ }
+ end
+
+ def ensure_group(name:, parent_id: nil, **args)
+ args[:description] ||= "Runner fleet #{name}"
+ name = generate_name(name)
+
+ group = ::Group.by_parent(parent_id).find_by_name(name)
+ group ||= create_group(name: name, path: name.tr(' ', '-'), parent_id: parent_id, **args)
+
+ register_record(group, @groups)
+ end
+
+ def generate_name(name)
+ "#{@registration_prefix}#{name}"
+ end
+
+ def create_group(**args)
+ logger.info(message: 'Creating group', **args)
+
+ ensure_success(::Groups::CreateService.new(@user, **args).execute)
+ end
+
+ def ensure_project(name:, namespace_id:, **args)
+ args[:description] ||= "Runner fleet #{name}"
+ name = generate_name(name)
+
+ project = ::Project.in_namespace(namespace_id).find_by_name(name)
+ project ||= create_project(name: name, namespace_id: namespace_id, **args)
+
+ register_record(project, @projects)
+ end
+
+ def create_project(**args)
+ logger.info(message: 'Creating project', **args)
+
+ ensure_success(::Projects::CreateService.new(@user, **args).execute)
+ end
+
+ def register_record(record, records)
+ return record if record.errors.any?
+
+ records[record.id] = record
+ end
+
+ def ensure_success(record)
+ return record unless record.errors.any?
+
+ logger.error(record.errors.full_messages.to_sentence)
+ raise RuntimeError
+ end
+
+ def create_runner(scope:, name:, **args)
+ name = generate_name(name)
+
+ logger.info(message: 'Creating runner', scope: scope.class.name, name: name)
+
+ executor = ::Ci::Runner::EXECUTOR_NAME_TO_TYPES.keys.sample
+ args.merge!(additional_runner_args(name, executor))
+
+ response = ::Ci::Runners::RegisterRunnerService.new.execute(scope.runners_token, name: name, **args)
+ runner = response.payload[:runner]
+
+ ::Ci::Runners::ProcessRunnerVersionUpdateWorker.new.perform(args[:version])
+
+ if runner && runner.errors.empty? &&
+ Random.rand(0..100) < 70 # % of runners having contacted GitLab instance
+ runner.heartbeat(args.merge(executor: executor))
+ runner.save!
+ end
+
+ ensure_success(runner)
+ end
+
+ def additional_runner_args(name, executor)
+ base_tags = ['runner-fleet', "#{@registration_prefix}runner", executor]
+ tag_limit = ::Ci::Runner::TAG_LIST_MAX_LENGTH - base_tags.length
+
+ {
+ tag_list: base_tags + TAG_LIST.sample(Random.rand(1..tag_limit)),
+ description: "Runner fleet #{name}",
+ run_untagged: false,
+ active: Random.rand(1..3) != 1,
+ version: ::Gitlab::Ci::RunnerReleases.instance.releases.sample.to_s,
+ ip_address: '127.0.0.1'
+ }
+ end
+
+ def assign_runner(runner, project)
+ result = ::Ci::Runners::AssignRunnerService.new(runner, project, @user).execute
+ result.track_and_raise_exception
+
+ runner
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/seed/runner_fleet.rake b/lib/tasks/gitlab/seed/runner_fleet.rake
new file mode 100644
index 00000000000..98f06c692b9
--- /dev/null
+++ b/lib/tasks/gitlab/seed/runner_fleet.rake
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+# Seed database with:
+# 1. 2 root groups, one with 2 sub-groups and another with 1 sub-group
+# 1. 1 project in each of the sub-groups
+# 1. 1 shared project runner, and 1 group runner in a couple of the sub-groups
+# 1. Successful and failed pipelines assigned to the first 5 runners of each group/project
+# 1. 1 pipeline on one group runner with the remaining jobs
+#
+# @param username - user creating subgroups (i.e. GitLab admin)
+# @param registration_prefix - prefix used for the group, project, and runner names
+# @param runner_count - total number of runners to create (default: 40)
+# @param job_count - total number of jobs to create and assign to runners (default: 400)
+#
+# @example
+# bundle exec rake "gitlab:seed:runner_fleet[root, rf-]"
+#
+namespace :gitlab do
+ namespace :seed do
+ desc 'Seed groups with sub-groups/projects/runners/jobs for Runner Fleet testing'
+ task :runner_fleet,
+ [:username, :registration_prefix, :runner_count, :job_count] => :gitlab_environment do |_t, args|
+ timings = Benchmark.measure do
+ projects_to_runners = Gitlab::Seeders::Ci::Runner::RunnerFleetSeeder.new(
+ Gitlab::AppLogger,
+ username: args.username,
+ registration_prefix: args.registration_prefix,
+ runner_count: args.runner_count&.to_i
+ ).seed
+
+ Gitlab::Seeders::Ci::Runner::RunnerFleetPipelineSeeder.new(
+ projects_to_runners: projects_to_runners,
+ job_count: args.job_count&.to_i
+ ).seed
+ end
+
+ puts "Seed finished. Timings: #{timings}"
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index fff1c2c6834..8c78bdbee90 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -25340,6 +25340,9 @@ msgstr ""
msgid "Mark to do as done"
msgstr ""
+msgid "Markdown"
+msgstr ""
+
msgid "Markdown Help"
msgstr ""
@@ -45739,6 +45742,9 @@ msgstr ""
msgid "View all projects"
msgstr ""
+msgid "View and edit markdown, with the option to preview the formatted output."
+msgstr ""
+
msgid "View blame"
msgstr ""
@@ -45833,6 +45839,9 @@ msgstr ""
msgid "View logs"
msgstr ""
+msgid "View markdown"
+msgstr ""
+
msgid "View milestones"
msgstr ""
@@ -45865,6 +45874,9 @@ msgstr[1] ""
msgid "View replaced file @ "
msgstr ""
+msgid "View rich text"
+msgstr ""
+
msgid "View seat usage"
msgstr ""
@@ -45877,6 +45889,9 @@ msgstr ""
msgid "View the documentation"
msgstr ""
+msgid "View the formatted output in real-time as you edit."
+msgstr ""
+
msgid "View the latest successful deployment to this environment"
msgstr ""
diff --git a/package.json b/package.json
index 47e94ce962a..a406a98bbb8 100644
--- a/package.json
+++ b/package.json
@@ -59,7 +59,7 @@
"@gitlab/svgs": "3.14.0",
"@gitlab/ui": "52.6.1",
"@gitlab/visual-review-tools": "1.7.3",
- "@gitlab/web-ide": "0.0.1-dev-20221217175648",
+ "@gitlab/web-ide": "0.0.1-dev-20221221021927",
"@mermaid-js/mermaid-mindmap": "^9.3.0",
"@rails/actioncable": "6.1.4-7",
"@rails/ujs": "6.1.4-7",
diff --git a/qa/qa/page/component/wiki_page_form.rb b/qa/qa/page/component/wiki_page_form.rb
index 7a7329e6110..9143a25d9ab 100644
--- a/qa/qa/page/component/wiki_page_form.rb
+++ b/qa/qa/page/component/wiki_page_form.rb
@@ -17,7 +17,10 @@ module QA
base.view 'app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue' do
element :markdown_editor_form_field
- element :editing_mode_button
+ end
+
+ base.view 'app/assets/javascripts/vue_shared/components/markdown/editor_mode_dropdown.vue' do
+ element :editing_mode_switcher
end
base.view 'app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue' do
@@ -55,8 +58,9 @@ module QA
end
def use_new_editor
- within_element(:editing_mode_button) do
- find('label', text: 'Rich text').click
+ click_element(:editing_mode_switcher)
+ within_element(:editing_mode_switcher) do
+ find('button', text: 'Rich text').click
end
wait_until(reload: false) do
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index 1f40f1f1bce..ca08bc9e577 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -69,13 +69,9 @@ RSpec.describe "Admin::Users", feature_category: :user_management do
expect(page).not_to have_content(message)
end
- context 'with no license and service ping disabled' do
+ context 'with no license and service ping disabled', :without_license do
before do
stub_application_setting(usage_ping_enabled: false)
-
- if Gitlab.ee?
- allow(License).to receive(:current).and_return(nil)
- end
end
it 'renders registration features CTA' do
diff --git a/spec/features/issues/user_edits_issue_spec.rb b/spec/features/issues/user_edits_issue_spec.rb
index 223832a6ede..19b2633969d 100644
--- a/spec/features/issues/user_edits_issue_spec.rb
+++ b/spec/features/issues/user_edits_issue_spec.rb
@@ -107,14 +107,14 @@ RSpec.describe "Issues > User edits issue", :js, feature_category: :team_plannin
end
it 'places focus on the web editor' do
- toggle_editing_mode_selector = '[data-testid="toggle-editing-mode-button"] label'
content_editor_focused_selector = '[data-testid="content-editor"].is-focused'
markdown_field_focused_selector = 'textarea:focus'
click_edit_issue_description
expect(page).to have_selector(markdown_field_focused_selector)
- find(toggle_editing_mode_selector, text: 'Rich text').click
+ click_on _('View rich text')
+ click_on _('Rich text')
expect(page).not_to have_selector(content_editor_focused_selector)
@@ -124,7 +124,8 @@ RSpec.describe "Issues > User edits issue", :js, feature_category: :team_plannin
expect(page).to have_selector(content_editor_focused_selector)
- find(toggle_editing_mode_selector, text: 'Source').click
+ click_on _('View markdown')
+ click_on _('Markdown')
expect(page).not_to have_selector(markdown_field_focused_selector)
end
diff --git a/spec/frontend/vue_shared/components/markdown/editor_mode_dropdown_spec.js b/spec/frontend/vue_shared/components/markdown/editor_mode_dropdown_spec.js
new file mode 100644
index 00000000000..34071775b9c
--- /dev/null
+++ b/spec/frontend/vue_shared/components/markdown/editor_mode_dropdown_spec.js
@@ -0,0 +1,58 @@
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import EditorModeDropdown from '~/vue_shared/components/markdown/editor_mode_dropdown.vue';
+
+describe('vue_shared/component/markdown/editor_mode_dropdown', () => {
+ let wrapper;
+
+ const createComponent = ({ value, size } = {}) => {
+ wrapper = shallowMount(EditorModeDropdown, {
+ propsData: {
+ value,
+ size,
+ },
+ });
+ };
+
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDropdownItem = (text) =>
+ wrapper
+ .findAllComponents(GlDropdownItem)
+ .filter((item) => item.text().startsWith(text))
+ .at(0);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe.each`
+ modeText | value | dropdownText | otherMode
+ ${'Rich text'} | ${'richText'} | ${'View markdown'} | ${'Markdown'}
+ ${'Markdown'} | ${'markdown'} | ${'View rich text'} | ${'Rich text'}
+ `('$modeText', ({ modeText, value, dropdownText, otherMode }) => {
+ beforeEach(() => {
+ createComponent({ value });
+ });
+
+ it('shows correct dropdown label', () => {
+ expect(findDropdown().props('text')).toEqual(dropdownText);
+ });
+
+ it('checks correct checked dropdown item', () => {
+ expect(findDropdownItem(modeText).props().isChecked).toBe(true);
+ expect(findDropdownItem(otherMode).props().isChecked).toBe(false);
+ });
+
+ it('emits event on click', () => {
+ findDropdownItem(modeText).vm.$emit('click');
+
+ expect(wrapper.emitted().input).toEqual([[value]]);
+ });
+ });
+
+ it('passes size to dropdown', () => {
+ createComponent({ size: 'small', value: 'markdown' });
+
+ expect(findDropdown().props('size')).toEqual('small');
+ });
+});
diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js
index 285ea10c813..3b8e78bbadd 100644
--- a/spec/frontend/vue_shared/components/markdown/field_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_spec.js
@@ -37,7 +37,7 @@ describe('Markdown field component', () => {
axiosMock.restore();
});
- function createSubject({ lines = [], enablePreview = true } = {}) {
+ function createSubject({ lines = [], enablePreview = true, showContentEditorSwitcher } = {}) {
// We actually mount a wrapper component so that we can force Vue to rerender classes in order to test a regression
// caused by mixing Vanilla JS and Vue.
subject = mountExtended(
@@ -68,6 +68,7 @@ describe('Markdown field component', () => {
lines,
enablePreview,
restrictedToolBarItems,
+ showContentEditorSwitcher,
},
},
);
@@ -191,6 +192,7 @@ describe('Markdown field component', () => {
markdownDocsPath,
quickActionsDocsPath: '',
showCommentToolBar: true,
+ showContentEditorSwitcher: false,
});
});
});
@@ -342,4 +344,18 @@ describe('Markdown field component', () => {
restrictedToolBarItems,
);
});
+
+ describe('showContentEditorSwitcher', () => {
+ it('defaults to false', () => {
+ createSubject();
+
+ expect(findMarkdownToolbar().props('showContentEditorSwitcher')).toBe(false);
+ });
+
+ it('passes showContentEditorSwitcher', () => {
+ createSubject({ showContentEditorSwitcher: true });
+
+ expect(findMarkdownToolbar().props('showContentEditorSwitcher')).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
index 5f416db2676..64b4b55fce5 100644
--- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
@@ -1,4 +1,3 @@
-import { GlSegmentedControl } from '@gitlab/ui';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
@@ -49,7 +48,6 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
},
});
};
- const findSegmentedControl = () => wrapper.findComponent(GlSegmentedControl);
const findMarkdownField = () => wrapper.findComponent(MarkdownField);
const findTextarea = () => wrapper.find('textarea');
const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
@@ -97,36 +95,26 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
expect(findTextarea().element.value).toBe(value);
});
- it('renders switch segmented control', () => {
+ it(`emits ${EDITING_MODE_CONTENT_EDITOR} event when enableContentEditor emitted from markdown editor`, () => {
buildWrapper();
- expect(findSegmentedControl().props()).toEqual({
- checked: EDITING_MODE_MARKDOWN_FIELD,
- options: [
- {
- text: expect.any(String),
- value: EDITING_MODE_MARKDOWN_FIELD,
- },
- {
- text: expect.any(String),
- value: EDITING_MODE_CONTENT_EDITOR,
- },
- ],
- });
+ findMarkdownField().vm.$emit('enableContentEditor');
+
+ expect(wrapper.emitted(EDITING_MODE_CONTENT_EDITOR)).toHaveLength(1);
});
- describe.each`
- editingMode
- ${EDITING_MODE_CONTENT_EDITOR}
- ${EDITING_MODE_MARKDOWN_FIELD}
- `('when segmented control emits change event with $editingMode value', ({ editingMode }) => {
- it(`emits ${editingMode} event`, () => {
- buildWrapper();
+ it(`emits ${EDITING_MODE_MARKDOWN_FIELD} event when enableMarkdownEditor emitted from content editor`, async () => {
+ buildWrapper({
+ stubs: { ContentEditor: stubComponent(ContentEditor) },
+ });
- findSegmentedControl().vm.$emit('change', editingMode);
+ findMarkdownField().vm.$emit('enableContentEditor');
- expect(wrapper.emitted(editingMode)).toHaveLength(1);
- });
+ await nextTick();
+
+ findContentEditor().vm.$emit('enableMarkdownEditor');
+
+ expect(wrapper.emitted(EDITING_MODE_MARKDOWN_FIELD)).toHaveLength(1);
});
describe(`when editingMode is ${EDITING_MODE_MARKDOWN_FIELD}`, () => {
@@ -159,11 +147,10 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
expect(wrapper.emitted('keydown')).toHaveLength(1);
});
- describe(`when segmented control triggers input event with ${EDITING_MODE_CONTENT_EDITOR} value`, () => {
+ describe(`when markdown field triggers enableContentEditor event`, () => {
beforeEach(() => {
buildWrapper();
- findSegmentedControl().vm.$emit('input', EDITING_MODE_CONTENT_EDITOR);
- findSegmentedControl().vm.$emit('change', EDITING_MODE_CONTENT_EDITOR);
+ findMarkdownField().vm.$emit('enableContentEditor');
});
it('displays the content editor', () => {
@@ -202,7 +189,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
describe(`when editingMode is ${EDITING_MODE_CONTENT_EDITOR}`, () => {
beforeEach(() => {
buildWrapper();
- findSegmentedControl().vm.$emit('input', EDITING_MODE_CONTENT_EDITOR);
+ findMarkdownField().vm.$emit('enableContentEditor');
});
describe('when autofocus is true', () => {
@@ -234,9 +221,9 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
expect(wrapper.emitted('keydown')).toEqual([[event]]);
});
- describe(`when segmented control triggers input event with ${EDITING_MODE_MARKDOWN_FIELD} value`, () => {
+ describe(`when richText editor triggers enableMarkdownEditor event`, () => {
beforeEach(() => {
- findSegmentedControl().vm.$emit('input', EDITING_MODE_MARKDOWN_FIELD);
+ findContentEditor().vm.$emit('enableMarkdownEditor');
});
it('hides the content editor', () => {
@@ -251,29 +238,5 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
expect(findLocalStorageSync().props().value).toBe(EDITING_MODE_MARKDOWN_FIELD);
});
});
-
- describe('when content editor emits loading event', () => {
- beforeEach(() => {
- findContentEditor().vm.$emit('loading');
- });
-
- it('disables switch editing mode control', () => {
- // This is the only way that I found to check the segmented control is disabled
- expect(findSegmentedControl().find('input[disabled]').exists()).toBe(true);
- });
-
- describe.each`
- event
- ${'loadingSuccess'}
- ${'loadingError'}
- `('when content editor emits $event event', ({ event }) => {
- beforeEach(() => {
- findContentEditor().vm.$emit(event);
- });
- it('enables the switch editing mode control', () => {
- expect(findSegmentedControl().find('input[disabled]').exists()).toBe(false);
- });
- });
- });
});
});
diff --git a/spec/frontend/vue_shared/components/markdown/toolbar_spec.js b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
index f698794b951..b1a1dbbeb7a 100644
--- a/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
@@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils';
import Toolbar from '~/vue_shared/components/markdown/toolbar.vue';
+import EditorModeDropdown from '~/vue_shared/components/markdown/editor_mode_dropdown.vue';
describe('toolbar', () => {
let wrapper;
@@ -47,4 +48,18 @@ describe('toolbar', () => {
expect(wrapper.find('.comment-toolbar').exists()).toBe(true);
});
});
+
+ describe('with content editor switcher', () => {
+ beforeEach(() => {
+ createMountedWrapper({
+ showContentEditorSwitcher: true,
+ });
+ });
+
+ it('re-emits event from switcher', () => {
+ wrapper.findComponent(EditorModeDropdown).vm.$emit('input', 'richText');
+
+ expect(wrapper.emitted('enableContentEditor')).toEqual([[]]);
+ });
+ });
});
diff --git a/spec/lib/gitlab/database/async_indexes/index_creator_spec.rb b/spec/lib/gitlab/database/async_indexes/index_creator_spec.rb
index 7ad3eb395a9..be1d331a449 100644
--- a/spec/lib/gitlab/database/async_indexes/index_creator_spec.rb
+++ b/spec/lib/gitlab/database/async_indexes/index_creator_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe Gitlab::Database::AsyncIndexes::IndexCreator do
it 'creates the index while controlling statement timeout' do
allow(connection).to receive(:execute).and_call_original
- expect(connection).to receive(:execute).with("SET statement_timeout TO '32400s'").ordered.and_call_original
+ expect(connection).to receive(:execute).with("SET statement_timeout TO '72000s'").ordered.and_call_original
expect(connection).to receive(:execute).with(async_index.definition).ordered.and_call_original
expect(connection).to receive(:execute).with("RESET statement_timeout").ordered.and_call_original
diff --git a/spec/lib/gitlab/seeders/ci/runner/runner_fleet_pipeline_seeder_spec.rb b/spec/lib/gitlab/seeders/ci/runner/runner_fleet_pipeline_seeder_spec.rb
new file mode 100644
index 00000000000..4660a0c1e5d
--- /dev/null
+++ b/spec/lib/gitlab/seeders/ci/runner/runner_fleet_pipeline_seeder_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+NULL_LOGGER = Gitlab::JsonLogger.new('/dev/null')
+
+RSpec.describe ::Gitlab::Seeders::Ci::Runner::RunnerFleetPipelineSeeder, feature_category: :runner_fleet do
+ subject(:seeder) do
+ described_class.new(NULL_LOGGER, projects_to_runners: projects_to_runners, job_count: job_count)
+ end
+
+ def runner_ids_for_project(runner_count, project)
+ create_list(:ci_runner, runner_count, :project, projects: [project]).map(&:id)
+ end
+
+ let_it_be(:projects) { create_list(:project, 4) }
+ let_it_be(:projects_to_runners) do
+ [
+ { project_id: projects[0].id, runner_ids: runner_ids_for_project(2, projects[0]) },
+ { project_id: projects[1].id, runner_ids: runner_ids_for_project(1, projects[1]) },
+ { project_id: projects[2].id, runner_ids: runner_ids_for_project(2, projects[2]) },
+ { project_id: projects[3].id, runner_ids: runner_ids_for_project(1, projects[3]) }
+ ]
+ end
+
+ describe '#seed' do
+ context 'with job_count specified' do
+ let(:job_count) { 20 }
+
+ it 'creates expected jobs', :aggregate_failures do
+ expect { seeder.seed }.to change { Ci::Build.count }.by(job_count)
+ .and change { Ci::Pipeline.count }.by(4)
+
+ projects_to_runners.first(3).each do |project|
+ expect(Ci::Build.where(runner_id: project[:runner_ids])).not_to be_empty
+ end
+ end
+ end
+
+ context 'with nil job_count' do
+ let(:job_count) { nil }
+
+ before do
+ stub_const('Gitlab::Seeders::Ci::Runner::RunnerFleetPipelineSeeder::DEFAULT_JOB_COUNT', 2)
+ end
+
+ it 'creates expected jobs', :aggregate_failures do
+ expect { seeder.seed }.to change { Ci::Build.count }.by(2)
+ .and change { Ci::Pipeline.count }.by(2)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/seeders/ci/runner/runner_fleet_seeder_spec.rb b/spec/lib/gitlab/seeders/ci/runner/runner_fleet_seeder_spec.rb
new file mode 100644
index 00000000000..d5d623de6e4
--- /dev/null
+++ b/spec/lib/gitlab/seeders/ci/runner/runner_fleet_seeder_spec.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+NULL_LOGGER = Gitlab::JsonLogger.new('/dev/null')
+
+RSpec.describe ::Gitlab::Seeders::Ci::Runner::RunnerFleetSeeder, feature_category: :runner_fleet do
+ let_it_be(:user) { create(:user, :admin, username: 'test-admin') }
+
+ subject(:seeder) do
+ described_class.new(NULL_LOGGER,
+ username: user.username,
+ registration_prefix: registration_prefix,
+ runner_count: runner_count)
+ end
+
+ describe '#seed', :enable_admin_mode do
+ subject(:seed) { seeder.seed }
+
+ let(:runner_count) { 20 }
+ let(:registration_prefix) { 'prefix-' }
+ let(:runner_releases_url) do
+ ::Gitlab::CurrentSettings.current_application_settings.public_runner_releases_url
+ end
+
+ before do
+ WebMock.stub_request(:get, runner_releases_url).to_return(
+ body: '[]',
+ status: 200,
+ headers: { 'Content-Type' => 'application/json' }
+ )
+ end
+
+ it 'creates expected hierarchy', :aggregate_failures do
+ expect { seed }.to change { Ci::Runner.count }.by(runner_count)
+ .and change { Project.count }.by(3)
+ .and change { Group.count }.by(6)
+
+ expect(Group.search(registration_prefix)).to contain_exactly(
+ an_object_having_attributes(name: "#{registration_prefix}top-level group 1"),
+ an_object_having_attributes(name: "#{registration_prefix}top-level group 2"),
+ an_object_having_attributes(name: "#{registration_prefix}group 1.1"),
+ an_object_having_attributes(name: "#{registration_prefix}group 1.1.1"),
+ an_object_having_attributes(name: "#{registration_prefix}group 1.1.2"),
+ an_object_having_attributes(name: "#{registration_prefix}group 2.1")
+ )
+
+ expect(Project.search(registration_prefix)).to contain_exactly(
+ an_object_having_attributes(name: "#{registration_prefix}project 1.1.1.1"),
+ an_object_having_attributes(name: "#{registration_prefix}project 1.1.2.1"),
+ an_object_having_attributes(name: "#{registration_prefix}project 2.1.1")
+ )
+
+ project_1_1_1_1 = Project.find_by_name("#{registration_prefix}project 1.1.1.1")
+ project_1_1_2_1 = Project.find_by_name("#{registration_prefix}project 1.1.2.1")
+ project_2_1_1 = Project.find_by_name("#{registration_prefix}project 2.1.1")
+ expect(seed).to contain_exactly(
+ { project_id: project_1_1_1_1.id, runner_ids: an_instance_of(Array) },
+ { project_id: project_1_1_2_1.id, runner_ids: an_instance_of(Array) },
+ { project_id: project_2_1_1.id, runner_ids: an_instance_of(Array) }
+ )
+ seed.each do |project|
+ expect(project[:runner_ids].length).to be_between(0, 5)
+ expect(Project.find(project[:project_id]).all_available_runners.ids).to include(*project[:runner_ids])
+ expect(::Ci::Pipeline.for_project(project[:runner_ids])).to be_empty
+ expect(::Ci::Build.where(runner_id: project[:runner_ids])).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/content_editor_shared_examples.rb b/spec/support/shared_examples/features/content_editor_shared_examples.rb
index efdf7513b2d..6cd9c4ce1c4 100644
--- a/spec/support/shared_examples/features/content_editor_shared_examples.rb
+++ b/spec/support/shared_examples/features/content_editor_shared_examples.rb
@@ -4,7 +4,8 @@ RSpec.shared_examples 'edits content using the content editor' do
let(:content_editor_testid) { '[data-testid="content-editor"] [contenteditable].ProseMirror' }
def switch_to_content_editor
- find('[data-testid="toggle-editing-mode-button"] label', text: 'Rich text').click
+ click_button _('View rich text')
+ click_button _('Rich text')
end
def type_in_content_editor(keys)
diff --git a/spec/tasks/gitlab/seed/runner_fleet_rake_spec.rb b/spec/tasks/gitlab/seed/runner_fleet_rake_spec.rb
new file mode 100644
index 00000000000..11bfc703861
--- /dev/null
+++ b/spec/tasks/gitlab/seed/runner_fleet_rake_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'rake_helper'
+
+RSpec.describe 'gitlab:seed:runner_fleet rake task', :silence_stdout, feature_category: :runner_fleet do
+ let(:registration_prefix) { 'rf-' }
+ let(:runner_count) { 10 }
+ let(:job_count) { 20 }
+ let(:task_params) { [username, registration_prefix, runner_count, job_count] }
+ let(:runner_releases_url) do
+ ::Gitlab::CurrentSettings.current_application_settings.public_runner_releases_url
+ end
+
+ before do
+ Rake.application.rake_require('tasks/gitlab/seed/runner_fleet')
+
+ WebMock.stub_request(:get, runner_releases_url).to_return(
+ body: '[]',
+ status: 200,
+ headers: { 'Content-Type' => 'application/json' }
+ )
+ end
+
+ subject(:rake_task) { run_rake_task('gitlab:seed:runner_fleet', task_params) }
+
+ context 'with admin username', :enable_admin_mode do
+ let(:username) { 'runner_fleet_seed' }
+ let!(:admin) { create(:user, :admin, username: username) }
+
+ it 'performs runner fleet seed successfully' do
+ expect { rake_task }
+ .to change { Group.count }.by(6)
+ .and change { Project.count }.by(3)
+ .and change { Ci::Runner.count }.by(runner_count)
+ .and change { Ci::Build.count }.by(job_count)
+
+ expect(Group.search(registration_prefix).count).to eq 6
+ expect(Project.search(registration_prefix).count).to eq 3
+ expect(Ci::Runner.search(registration_prefix).count).to eq runner_count
+ end
+ end
+end
diff --git a/spec/views/admin/application_settings/general.html.haml_spec.rb b/spec/views/admin/application_settings/general.html.haml_spec.rb
index f229fd2dcdc..dd49de8f880 100644
--- a/spec/views/admin/application_settings/general.html.haml_spec.rb
+++ b/spec/views/admin/application_settings/general.html.haml_spec.rb
@@ -46,13 +46,9 @@ RSpec.describe 'admin/application_settings/general.html.haml' do
it_behaves_like 'does not render registration features prompt', :application_setting_disabled_repository_size_limit
end
- context 'with no license and service ping disabled' do
+ context 'with no license and service ping disabled', :without_license do
before do
stub_application_setting(usage_ping_enabled: false)
-
- if Gitlab.ee?
- allow(License).to receive(:current).and_return(nil)
- end
end
it_behaves_like 'renders registration features prompt', :application_setting_disabled_repository_size_limit
diff --git a/spec/views/groups/edit.html.haml_spec.rb b/spec/views/groups/edit.html.haml_spec.rb
index ddcfea0ab10..fda93ebab51 100644
--- a/spec/views/groups/edit.html.haml_spec.rb
+++ b/spec/views/groups/edit.html.haml_spec.rb
@@ -127,13 +127,7 @@ RSpec.describe 'groups/edit.html.haml' do
allow(view).to receive(:current_user) { user }
end
- context 'prompt user about registration features' do
- before do
- if Gitlab.ee?
- allow(License).to receive(:current).and_return(nil)
- end
- end
-
+ context 'prompt user about registration features', :without_license do
context 'with service ping disabled' do
before do
stub_application_setting(usage_ping_enabled: false)
diff --git a/spec/views/projects/edit.html.haml_spec.rb b/spec/views/projects/edit.html.haml_spec.rb
index 2935e4395ba..bf154b61609 100644
--- a/spec/views/projects/edit.html.haml_spec.rb
+++ b/spec/views/projects/edit.html.haml_spec.rb
@@ -93,13 +93,9 @@ RSpec.describe 'projects/edit' do
it_behaves_like 'does not render registration features prompt', :project_disabled_repository_size_limit
end
- context 'with no license and service ping disabled' do
+ context 'with no license and service ping disabled', :without_license do
before do
stub_application_setting(usage_ping_enabled: false)
-
- if Gitlab.ee?
- allow(License).to receive(:current).and_return(nil)
- end
end
it_behaves_like 'renders registration features prompt', :project_disabled_repository_size_limit
diff --git a/yarn.lock b/yarn.lock
index 8fc039355fc..ad8898365f0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1155,10 +1155,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/visual-review-tools/-/visual-review-tools-1.7.3.tgz#9ea641146436da388ffbad25d7f2abe0df52c235"
integrity sha512-NMV++7Ew1FSBDN1xiZaauU9tfeSfgDHcOLpn+8bGpP+O5orUPm2Eu66R5eC5gkjBPaXosNAxNWtriee+aFk4+g==
-"@gitlab/web-ide@0.0.1-dev-20221217175648":
- version "0.0.1-dev-20221217175648"
- resolved "https://registry.yarnpkg.com/@gitlab/web-ide/-/web-ide-0.0.1-dev-20221217175648.tgz#1042fa5a4facfef191aa8df8d8ac3b386c8d1334"
- integrity sha512-njFkUVpIxyNJFSTY82RH5RzyndqyUkortLY87xKcfF0DeQttAOOfcD5jyyktp9ddRorj/ksWT2vNZ+qjEKwlIg==
+"@gitlab/web-ide@0.0.1-dev-20221221021927":
+ version "0.0.1-dev-20221221021927"
+ resolved "https://registry.yarnpkg.com/@gitlab/web-ide/-/web-ide-0.0.1-dev-20221221021927.tgz#76dfdda3be1e285b2d9627b3598d6b4bc0b49e58"
+ integrity sha512-oa0utK+FDnWj5N/vocJg+nuiPkdYGKfVSoRqLT77P2yNKETeRzjf6F265T1RlbvPmZgHXpc4ZCE58bJmXt4pJA==
"@graphql-eslint/eslint-plugin@3.12.0":
version "3.12.0"