Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-10-17 21:09:13 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-10-17 21:09:13 +0300
commit5150ecc452f4cf1c899f79d35d52af978ff2d43f (patch)
treeed36b7982b574d6b4ec5b4e3f68a61a0f7e762d1
parent3884d9d7160e80a70ad327813ada6cab03cded65 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_todo/metrics/abc_size.yml1
-rw-r--r--app/assets/javascripts/editor/schema/ci.json7
-rw-r--r--app/assets/javascripts/header_search/components/app.vue19
-rw-r--r--app/assets/javascripts/listbox/index.js70
-rw-r--r--app/assets/javascripts/nav/components/top_nav_app.vue10
-rw-r--r--app/assets/javascripts/pdf/index.vue17
-rw-r--r--app/assets/javascripts/runner/admin_runners/admin_runners_app.vue18
-rw-r--r--app/assets/javascripts/runner/components/runner_bulk_delete_checkbox.vue11
-rw-r--r--app/assets/javascripts/runner/components/runner_details.vue7
-rw-r--r--app/assets/javascripts/runner/components/runner_list.vue24
-rw-r--r--app/assets/javascripts/runner/graphql/list/local_state.js18
-rw-r--r--app/assets/javascripts/runner/group_runners/group_runners_app.vue9
-rw-r--r--app/assets/javascripts/runner/group_runners/index.js6
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue63
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/index.js10
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue8
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue30
-rw-r--r--app/assets/javascripts/work_items/constants.js6
-rw-r--r--app/assets/javascripts/work_items/graphql/get_issue_details.query.graphql9
-rw-r--r--app/assets/javascripts/work_items/index.js10
-rw-r--r--app/assets/stylesheets/page_bundles/work_items.scss20
-rw-r--r--app/controllers/admin/runners_controller.rb4
-rw-r--r--app/controllers/groups/runners_controller.rb4
-rw-r--r--app/models/ci/pipeline.rb9
-rw-r--r--app/models/ci/runner.rb6
-rw-r--r--app/models/hooks/web_hook.rb2
-rw-r--r--app/views/admin/application_settings/_account_and_limit.html.haml6
-rw-r--r--app/views/admin/application_settings/_ci_cd.html.haml3
-rw-r--r--app/views/admin/application_settings/_performance_bar.html.haml2
-rw-r--r--app/views/ci/variables/_url_query_variable_row.html.haml4
-rw-r--r--app/views/ci/variables/_variable_row.html.haml6
-rw-r--r--app/views/clusters/clusters/_health.html.haml2
-rw-r--r--app/views/clusters/clusters/user/_form.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml32
-rw-r--r--app/views/layouts/terms.html.haml2
-rw-r--r--app/views/projects/blob/_template_selectors.html.haml12
-rw-r--r--app/views/projects/buttons/_clone.html.haml8
-rw-r--r--app/views/projects/issues/_work_item_links.html.haml2
-rw-r--r--app/views/shared/_clone_panel.html.haml4
-rw-r--r--app/views/shared/web_hooks/_hook_errors.html.haml12
-rwxr-xr-xbin/profile-url22
-rwxr-xr-xbin/rubocop-profile39
-rw-r--r--config/feature_flags/development/enforce_runner_token_expires_at.yml8
-rw-r--r--config/feature_flags/development/graphql_keyset_pagination_without_next_page_query.yml2
-rw-r--r--config/feature_flags/development/pipeline_name.yml (renamed from config/feature_flags/development/gl_listbox_for_sort_dropdowns.yml)8
-rw-r--r--config/webpack.config.js6
-rw-r--r--config/webpack.vendor.config.js2
-rw-r--r--db/migrate/20221006131506_add_free_user_cap_over_limit_notified_at_to_namespace_details.rb20
-rw-r--r--db/schema_migrations/202210061315061
-rw-r--r--db/structure.sql3
-rw-r--r--doc/ci/runners/configure_runners.md8
-rw-r--r--doc/ci/yaml/index.md24
-rw-r--r--doc/development/profiling.md57
-rw-r--r--doc/topics/awesome_co.md143
-rw-r--r--doc/user/clusters/integrations.md6
-rw-r--r--doc/user/crm/index.md2
-rw-r--r--doc/user/group/epics/index.md16
-rw-r--r--doc/user/project/issues/managing_issues.md1
-rw-r--r--doc/user/project/wiki/index.md5
-rw-r--r--doc/user/tasks.md18
-rw-r--r--lib/api/merge_requests.rb1
-rw-r--r--lib/api/search.rb10
-rw-r--r--lib/gitlab/ci/config.rb4
-rw-r--r--lib/gitlab/ci/config/entry/workflow.rb7
-rw-r--r--lib/gitlab/ci/pipeline/chain/populate.rb11
-rw-r--r--lib/gitlab/ci/yaml_processor/result.rb4
-rw-r--r--lib/gitlab/gon_helper.rb1
-rw-r--r--lib/gitlab/profiler.rb31
-rw-r--r--locale/gitlab.pot23
-rw-r--r--package.json2
-rw-r--r--qa/qa/fixtures/package_managers/maven/project/request_forwarding/gitlab_ci.yaml.erb8
-rw-r--r--qa/qa/fixtures/package_managers/maven/project/request_forwarding/settings.xml.erb23
-rw-r--r--qa/qa/mobile/page/main/menu.rb4
-rw-r--r--qa/qa/page/admin/menu.rb69
-rw-r--r--qa/qa/page/component/clone_panel.rb12
-rw-r--r--qa/qa/page/component/legacy_clone_panel.rb8
-rw-r--r--qa/qa/page/main/menu.rb8
-rw-r--r--qa/qa/page/main/terms.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_project_level_spec.rb104
-rw-r--r--rubocop/ext/path_util.rb26
-rw-r--r--rubocop/ext/variable_force.rb17
-rw-r--r--rubocop/rubocop.rb5
-rw-r--r--spec/features/dashboard/user_filters_projects_spec.rb9
-rw-r--r--spec/features/groups/labels/sort_labels_spec.rb6
-rw-r--r--spec/features/groups/milestones_sorting_spec.rb5
-rw-r--r--spec/features/projects/labels/sort_labels_spec.rb6
-rw-r--r--spec/features/projects/milestones/milestones_sorting_spec.rb5
-rw-r--r--spec/features/projects/user_sorts_projects_spec.rb2
-rw-r--r--spec/features/projects/wikis_spec.rb4
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/rules.yml2
-rw-r--r--spec/frontend/header_search/components/app_spec.js26
-rw-r--r--spec/frontend/listbox/index_spec.js154
-rw-r--r--spec/frontend/nav/components/top_nav_app_spec.js28
-rw-r--r--spec/frontend/pdf/page_spec.js2
-rw-r--r--spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js4
-rw-r--r--spec/frontend/runner/admin_runners/admin_runners_app_spec.js41
-rw-r--r--spec/frontend/runner/components/runner_bulk_delete_checkbox_spec.js99
-rw-r--r--spec/frontend/runner/components/runner_details_spec.js28
-rw-r--r--spec/frontend/runner/components/runner_list_spec.js43
-rw-r--r--spec/frontend/runner/graphql/local_state_spec.js51
-rw-r--r--spec/frontend/runner/group_runner_show/group_runner_show_app_spec.js5
-rw-r--r--spec/frontend/runner/group_runners/group_runners_app_spec.js13
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js13
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js2
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_spec.js31
-rw-r--r--spec/frontend/work_items/mock_data.js115
-rw-r--r--spec/lib/gitlab/ci/config/entry/workflow_spec.rb48
-rw-r--r--spec/lib/gitlab/ci/config_spec.rb27
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb43
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb29
-rw-r--r--spec/lib/gitlab/profiler_spec.rb84
-rw-r--r--spec/models/ci/pipeline_spec.rb18
-rw-r--r--spec/models/concerns/token_authenticatable_spec.rb100
-rw-r--r--spec/models/hooks/web_hook_spec.rb4
-rw-r--r--spec/requests/api/merge_requests_spec.rb8
-rw-r--r--spec/views/projects/hooks/edit.html.haml_spec.rb2
-rw-r--r--yarn.lock40
118 files changed, 1556 insertions, 809 deletions
diff --git a/.rubocop_todo/metrics/abc_size.yml b/.rubocop_todo/metrics/abc_size.yml
index 747ef3823f5..f5646151592 100644
--- a/.rubocop_todo/metrics/abc_size.yml
+++ b/.rubocop_todo/metrics/abc_size.yml
@@ -12,3 +12,4 @@ Metrics/AbcSize:
- 'lib/gitlab/analytics/cycle_analytics/request_params.rb'
- 'lib/gitlab/sidekiq_middleware/server_metrics.rb'
- 'qa/qa/resource/repository/push.rb'
+ - 'ee/db/seeds/awesome_co/**/*.rb'
diff --git a/app/assets/javascripts/editor/schema/ci.json b/app/assets/javascripts/editor/schema/ci.json
index cd99e6ef64f..e56932a9a31 100644
--- a/app/assets/javascripts/editor/schema/ci.json
+++ b/app/assets/javascripts/editor/schema/ci.json
@@ -103,6 +103,7 @@
"workflow": {
"type": "object",
"properties": {
+ "name": { "$ref": "#/definitions/workflowName" },
"rules": {
"type": "array",
"items": {
@@ -714,6 +715,12 @@
]
}
},
+ "workflowName": {
+ "type": "string",
+ "markdownDescription": "Defines the pipeline name. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#workflowname).",
+ "minLength": 1,
+ "maxLength": 255
+ },
"globalVariables": {
"markdownDescription": "Defines default variables for all jobs. Job level property overrides global variables. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#variables).",
"type": "object",
diff --git a/app/assets/javascripts/header_search/components/app.vue b/app/assets/javascripts/header_search/components/app.vue
index f4b939fb20f..8fc0ce48e61 100644
--- a/app/assets/javascripts/header_search/components/app.vue
+++ b/app/assets/javascripts/header_search/components/app.vue
@@ -14,6 +14,7 @@ import { visitUrl } from '~/lib/utils/url_utility';
import { truncate } from '~/lib/utils/text_utility';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { s__, sprintf } from '~/locale';
+import Tracking from '~/tracking';
import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue';
import {
FIRST_DROPDOWN_INDEX,
@@ -163,8 +164,17 @@ export default {
...mapActions(['setSearch', 'fetchAutocompleteOptions', 'clearAutocomplete']),
openDropdown() {
this.showDropdown = true;
- this.isFocused = true;
- this.$emit('expandSearchBar', true);
+
+ // check isFocused state to avoid firing duplicate events
+ if (!this.isFocused) {
+ this.isFocused = true;
+ this.$emit('expandSearchBar', true);
+
+ Tracking.event(undefined, 'focus_input', {
+ label: 'global_search',
+ property: 'top_navigation',
+ });
+ }
},
closeDropdown() {
this.showDropdown = false;
@@ -178,6 +188,11 @@ export default {
this.showDropdown = false;
this.isFocused = false;
this.$emit('collapseSearchBar');
+
+ Tracking.event(undefined, 'blur_input', {
+ label: 'global_search',
+ property: 'top_navigation',
+ });
}, 200);
},
submitSearch() {
diff --git a/app/assets/javascripts/listbox/index.js b/app/assets/javascripts/listbox/index.js
index 2eeb0a77032..7eacbf7fcdd 100644
--- a/app/assets/javascripts/listbox/index.js
+++ b/app/assets/javascripts/listbox/index.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlDropdownItem, GlListbox } from '@gitlab/ui';
+import { GlListbox } from '@gitlab/ui';
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
@@ -31,59 +31,25 @@ export function initListbox(el, { onChange } = {}) {
},
},
render(h) {
- if (gon.features?.glListboxForSortDropdowns) {
- return h(GlListbox, {
- props: {
- items,
- right,
- selected: this.selected,
- toggleText: this.text,
- },
- class: className,
- on: {
- select: (selectedValue) => {
- this.selected = selectedValue;
- const selectedItem = items.find(({ value }) => value === selectedValue);
-
- if (typeof onChange === 'function') {
- onChange(selectedItem);
- }
- },
- },
- });
- }
-
- return h(
- GlDropdown,
- {
- props: {
- text: this.text,
- right,
+ return h(GlListbox, {
+ props: {
+ items,
+ right,
+ selected: this.selected,
+ toggleText: this.text,
+ },
+ class: className,
+ on: {
+ select: (selectedValue) => {
+ this.selected = selectedValue;
+ const selectedItem = items.find(({ value }) => value === selectedValue);
+
+ if (typeof onChange === 'function') {
+ onChange(selectedItem);
+ }
},
- class: className,
},
- items.map((item) =>
- h(
- GlDropdownItem,
- {
- props: {
- isCheckItem: true,
- isChecked: this.selected === item.value,
- },
- on: {
- click: () => {
- this.selected = item.value;
-
- if (typeof onChange === 'function') {
- onChange(item);
- }
- },
- },
- },
- item.text,
- ),
- ),
- );
+ });
},
});
}
diff --git a/app/assets/javascripts/nav/components/top_nav_app.vue b/app/assets/javascripts/nav/components/top_nav_app.vue
index ca6e6567f74..e55bf25a60c 100644
--- a/app/assets/javascripts/nav/components/top_nav_app.vue
+++ b/app/assets/javascripts/nav/components/top_nav_app.vue
@@ -1,5 +1,6 @@
<script>
import { GlNav, GlIcon, GlNavItemDropdown, GlDropdownForm, GlTooltipDirective } from '@gitlab/ui';
+import Tracking from '~/tracking';
import TopNavDropdownMenu from './top_nav_dropdown_menu.vue';
export default {
@@ -19,6 +20,14 @@ export default {
required: true,
},
},
+ methods: {
+ trackToggleEvent() {
+ Tracking.event(undefined, 'click_nav', {
+ label: 'hamburger_menu',
+ property: 'top_navigation',
+ });
+ },
+ },
};
</script>
@@ -32,6 +41,7 @@ export default {
toggle-class="top-nav-toggle js-top-nav-dropdown-toggle gl-px-3!"
no-flip
no-caret
+ @toggle="trackToggleEvent"
>
<template #button-content>
<gl-icon name="hamburger" />
diff --git a/app/assets/javascripts/pdf/index.vue b/app/assets/javascripts/pdf/index.vue
index ddc880db227..f35f9341fa1 100644
--- a/app/assets/javascripts/pdf/index.vue
+++ b/app/assets/javascripts/pdf/index.vue
@@ -1,9 +1,10 @@
<script>
-import pdfjsLib from 'pdfjs-dist/build/pdf';
-import workerSrc from 'pdfjs-dist/build/pdf.worker.min';
+import { getDocument, GlobalWorkerOptions } from 'pdfjs-dist/legacy/build/pdf';
import Page from './page/index.vue';
+GlobalWorkerOptions.workerSrc = '/assets/webpack/pdfjs/pdf.worker.min.js';
+
export default {
components: { Page },
props: {
@@ -30,18 +31,16 @@ export default {
},
watch: { pdf: 'load' },
mounted() {
- pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc;
if (this.hasPDF) this.load();
},
methods: {
load() {
this.pages = [];
- return pdfjsLib
- .getDocument({
- url: this.document,
- cMapUrl: '/assets/webpack/cmaps/',
- cMapPacked: true,
- })
+ return getDocument({
+ url: this.document,
+ cMapUrl: '/assets/webpack/pdfjs/cmaps/',
+ cMapPacked: true,
+ })
.promise.then(this.renderPages)
.then((pages) => {
this.pages = pages;
diff --git a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
index 34c44321a9b..dbaabb35cde 100644
--- a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
+++ b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
@@ -17,8 +17,6 @@ import allRunnersCountQuery from 'ee_else_ce/runner/graphql/list/all_runners_cou
import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
import RunnerStackedLayoutBanner from '../components/runner_stacked_layout_banner.vue';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
-import RunnerBulkDelete from '../components/runner_bulk_delete.vue';
-import RunnerBulkDeleteCheckbox from '../components/runner_bulk_delete_checkbox.vue';
import RunnerList from '../components/runner_list.vue';
import RunnerListEmptyState from '../components/runner_list_empty_state.vue';
import RunnerName from '../components/runner_name.vue';
@@ -45,8 +43,6 @@ export default {
RegistrationDropdown,
RunnerStackedLayoutBanner,
RunnerFilteredSearchBar,
- RunnerBulkDelete,
- RunnerBulkDeleteCheckbox,
RunnerList,
RunnerListEmptyState,
RunnerName,
@@ -56,7 +52,7 @@ export default {
RunnerActionsCell,
},
mixins: [glFeatureFlagMixin()],
- inject: ['emptyStateSvgPath', 'emptyStateFilteredSvgPath', 'localMutations'],
+ inject: ['emptyStateSvgPath', 'emptyStateFilteredSvgPath'],
props: {
registrationToken: {
type: String,
@@ -155,12 +151,6 @@ export default {
reportToSentry(error) {
captureException({ error, component: this.$options.name });
},
- onChecked({ runner, isChecked }) {
- this.localMutations.setRunnerChecked({
- runner,
- isChecked,
- });
- },
onPaginationInput(value) {
this.search.pagination = value;
},
@@ -211,16 +201,12 @@ export default {
:filtered-svg-path="emptyStateFilteredSvgPath"
/>
<template v-else>
- <runner-bulk-delete :runners="runners.items" @deleted="onDeleted" />
<runner-list
:runners="runners.items"
:loading="runnersLoading"
:checkable="true"
- @checked="onChecked"
+ @deleted="onDeleted"
>
- <template #head-checkbox>
- <runner-bulk-delete-checkbox :runners="runners.items" />
- </template>
<template #runner-name="{ runner }">
<gl-link :href="runner.adminUrl">
<runner-name :runner="runner" />
diff --git a/app/assets/javascripts/runner/components/runner_bulk_delete_checkbox.vue b/app/assets/javascripts/runner/components/runner_bulk_delete_checkbox.vue
index c4e7cad9da9..75afb7a00bc 100644
--- a/app/assets/javascripts/runner/components/runner_bulk_delete_checkbox.vue
+++ b/app/assets/javascripts/runner/components/runner_bulk_delete_checkbox.vue
@@ -26,14 +26,17 @@ export default {
},
},
computed: {
+ deletableRunners() {
+ return this.runners.filter((runner) => runner.userPermissions?.deleteRunner);
+ },
disabled() {
- return !this.runners.length;
+ return !this.deletableRunners.length;
},
checked() {
- return Boolean(this.runners.length) && this.runners.every(this.isChecked);
+ return Boolean(this.deletableRunners.length) && this.deletableRunners.every(this.isChecked);
},
indeterminate() {
- return !this.checked && this.runners.some(this.isChecked);
+ return !this.checked && this.deletableRunners.some(this.isChecked);
},
label() {
return this.checked ? s__('Runners|Unselect all') : s__('Runners|Select all');
@@ -45,7 +48,7 @@ export default {
},
onChange($event) {
this.localMutations.setRunnersChecked({
- runners: this.runners,
+ runners: this.deletableRunners,
isChecked: $event,
});
},
diff --git a/app/assets/javascripts/runner/components/runner_details.vue b/app/assets/javascripts/runner/components/runner_details.vue
index 79f934764c6..3d72abcd393 100644
--- a/app/assets/javascripts/runner/components/runner_details.vue
+++ b/app/assets/javascripts/runner/components/runner_details.vue
@@ -4,7 +4,6 @@ import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import { ACCESS_LEVEL_REF_PROTECTED, GROUP_TYPE, PROJECT_TYPE } from '../constants';
import RunnerDetail from './runner_detail.vue';
@@ -29,7 +28,6 @@ export default {
RunnerTags,
TimeAgo,
},
- mixins: [glFeatureFlagMixin()],
props: {
runner: {
type: Object,
@@ -117,10 +115,7 @@ export default {
</template>
</runner-detail>
<runner-detail :label="s__('Runners|Maximum job timeout')" :value="maximumTimeout" />
- <runner-detail
- v-if="glFeatures.enforceRunnerTokenExpiresAt"
- :empty-value="s__('Runners|Never expires')"
- >
+ <runner-detail :empty-value="s__('Runners|Never expires')">
<template #label>
{{ s__('Runners|Token expiry') }}
<help-popover :options="tokenExpirationHelpPopoverOptions">
diff --git a/app/assets/javascripts/runner/components/runner_list.vue b/app/assets/javascripts/runner/components/runner_list.vue
index d4a3311ff9f..e895537dcdc 100644
--- a/app/assets/javascripts/runner/components/runner_list.vue
+++ b/app/assets/javascripts/runner/components/runner_list.vue
@@ -5,6 +5,8 @@ import { s__ } from '~/locale';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import checkedRunnerIdsQuery from '../graphql/list/checked_runner_ids.query.graphql';
import { formatJobCount, tableField } from '../utils';
+import RunnerBulkDelete from './runner_bulk_delete.vue';
+import RunnerBulkDeleteCheckbox from './runner_bulk_delete_checkbox.vue';
import RunnerStackedSummaryCell from './cells/runner_stacked_summary_cell.vue';
import RunnerStatusPopover from './runner_status_popover.vue';
import RunnerStatusCell from './cells/runner_status_cell.vue';
@@ -23,6 +25,8 @@ export default {
GlTableLite,
GlSkeletonLoader,
HelpPopover,
+ RunnerBulkDelete,
+ RunnerBulkDeleteCheckbox,
RunnerStatusPopover,
RunnerStackedSummaryCell,
RunnerStatusCell,
@@ -39,6 +43,7 @@ export default {
},
},
},
+ inject: ['localMutations'],
props: {
checkable: {
type: Boolean,
@@ -55,7 +60,7 @@ export default {
required: true,
},
},
- emits: ['checked'],
+ emits: ['deleted'],
data() {
return { checkedRunnerIds: [] };
},
@@ -84,6 +89,12 @@ export default {
},
},
methods: {
+ canDelete(runner) {
+ return runner.userPermissions?.deleteRunner;
+ },
+ onDeleted(event) {
+ this.$emit('deleted', event);
+ },
formatJobCount(jobCount) {
return formatJobCount(jobCount);
},
@@ -96,7 +107,7 @@ export default {
return {};
},
onCheckboxChange(runner, isChecked) {
- this.$emit('checked', {
+ this.localMutations.setRunnerChecked({
runner,
isChecked,
});
@@ -109,6 +120,7 @@ export default {
</script>
<template>
<div>
+ <runner-bulk-delete v-if="checkable" :runners="runners" @deleted="onDeleted" />
<gl-table-lite
:aria-busy="loading"
:class="tableClass"
@@ -121,11 +133,15 @@ export default {
fixed
>
<template #head(checkbox)>
- <slot name="head-checkbox"></slot>
+ <runner-bulk-delete-checkbox :runners="runners" />
</template>
<template #cell(checkbox)="{ item }">
- <gl-form-checkbox :checked="isChecked(item)" @change="onCheckboxChange(item, $event)" />
+ <gl-form-checkbox
+ v-if="canDelete(item)"
+ :checked="isChecked(item)"
+ @change="onCheckboxChange(item, $event)"
+ />
</template>
<template #head(status)="{ label }">
diff --git a/app/assets/javascripts/runner/graphql/list/local_state.js b/app/assets/javascripts/runner/graphql/list/local_state.js
index 4e1625cb1ac..e0477c660b4 100644
--- a/app/assets/javascripts/runner/graphql/list/local_state.js
+++ b/app/assets/javascripts/runner/graphql/list/local_state.js
@@ -48,16 +48,18 @@ export const createLocalState = () => {
const localMutations = {
setRunnerChecked({ runner, isChecked }) {
- checkedRunnerIdsVar({
- ...checkedRunnerIdsVar(),
- [runner.id]: isChecked,
- });
+ const { id, userPermissions } = runner;
+ if (userPermissions?.deleteRunner) {
+ checkedRunnerIdsVar({
+ ...checkedRunnerIdsVar(),
+ [id]: isChecked,
+ });
+ }
},
setRunnersChecked({ runners, isChecked }) {
- const newVal = runners.reduce(
- (acc, { id }) => ({ ...acc, [id]: isChecked }),
- checkedRunnerIdsVar(),
- );
+ const newVal = runners
+ .filter(({ userPermissions }) => userPermissions?.deleteRunner)
+ .reduce((acc, { id }) => ({ ...acc, [id]: isChecked }), checkedRunnerIdsVar());
checkedRunnerIdsVar(newVal);
},
clearChecked() {
diff --git a/app/assets/javascripts/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/runner/group_runners/group_runners_app.vue
index bc943c03a62..7f56d895682 100644
--- a/app/assets/javascripts/runner/group_runners/group_runners_app.vue
+++ b/app/assets/javascripts/runner/group_runners/group_runners_app.vue
@@ -12,6 +12,7 @@ import {
} from 'ee_else_ce/runner/runner_search_utils';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import groupRunnersQuery from 'ee_else_ce/runner/graphql/list/group_runners.query.graphql';
+import groupRunnersCountQuery from 'ee_else_ce/runner/graphql/list/group_runners_count.query.graphql';
import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
import RunnerStackedLayoutBanner from '../components/runner_stacked_layout_banner.vue';
@@ -173,13 +174,17 @@ export default {
editUrl(runner) {
return this.runners.urlsById[runner.id]?.edit;
},
+ refetchCounts() {
+ this.$apollo.getClient().refetchQueries({ include: [groupRunnersCountQuery] });
+ },
onToggledPaused() {
// When a runner becomes Paused, the tab count can
// become stale, refetch outdated counts.
- this.$refs['runner-type-tabs'].refetch();
+ this.refetchCounts();
},
onDeleted({ message }) {
this.$root.$toast?.show(message);
+ this.refetchCounts();
},
reportToSentry(error) {
captureException({ error, component: this.$options.name });
@@ -245,7 +250,7 @@ export default {
:filtered-svg-path="emptyStateFilteredSvgPath"
/>
<template v-else>
- <runner-list :runners="runners.items" :loading="runnersLoading">
+ <runner-list :runners="runners.items" :loading="runnersLoading" @deleted="onDeleted">
<template #runner-name="{ runner }">
<gl-link :href="webUrl(runner)">
<runner-name :runner="runner" />
diff --git a/app/assets/javascripts/runner/group_runners/index.js b/app/assets/javascripts/runner/group_runners/index.js
index feed6b0ceb7..0e7efd2b8a1 100644
--- a/app/assets/javascripts/runner/group_runners/index.js
+++ b/app/assets/javascripts/runner/group_runners/index.js
@@ -2,6 +2,7 @@ import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
+import { createLocalState } from '../graphql/list/local_state';
import GroupRunnersApp from './group_runners_app.vue';
Vue.use(GlToast);
@@ -26,8 +27,10 @@ export const initGroupRunners = (selector = '#js-group-runners') => {
emptyStateFilteredSvgPath,
} = el.dataset;
+ const { cacheConfig, typeDefs, localMutations } = createLocalState();
+
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(),
+ defaultClient: createDefaultClient({}, { cacheConfig, typeDefs }),
});
return new Vue({
@@ -35,6 +38,7 @@ export const initGroupRunners = (selector = '#js-group-runners') => {
apolloProvider,
provide: {
runnerInstallHelpPage,
+ localMutations,
groupId,
onlineContactTimeoutSecs: parseInt(onlineContactTimeoutSecs, 10),
staleTimeoutSecs: parseInt(staleTimeoutSecs, 10),
diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue
index 3255c7e5e4d..ccb58706fe0 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -23,6 +23,7 @@ import {
WIDGET_TYPE_WEIGHT,
WIDGET_TYPE_HIERARCHY,
WORK_ITEM_VIEWED_STORAGE_KEY,
+ WIDGET_TYPE_ITERATION,
} from '../constants';
import workItemQuery from '../graphql/work_item.query.graphql';
@@ -65,6 +66,7 @@ export default {
WorkItemInformation,
LocalStorageSync,
WorkItemTypeIcon,
+ WorkItemIteration: () => import('ee_component/work_items/components/work_item_iteration.vue'),
},
mixins: [glFeatureFlagMixin()],
props: {
@@ -134,7 +136,7 @@ export default {
};
},
skip() {
- return !this.workItemDueDate;
+ return !this.isWidgetPresent(WIDGET_TYPE_START_AND_DUE_DATE);
},
},
{
@@ -145,7 +147,7 @@ export default {
};
},
skip() {
- return !this.workItemAssignees;
+ return !this.isWidgetPresent(WIDGET_TYPE_ASSIGNEES);
},
},
],
@@ -170,28 +172,8 @@ export default {
workItemsMvc2Enabled() {
return this.glFeatures.workItemsMvc2;
},
- hasDescriptionWidget() {
- return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_DESCRIPTION);
- },
- workItemAssignees() {
- return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_ASSIGNEES);
- },
- workItemLabels() {
- return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_LABELS);
- },
- workItemDueDate() {
- return this.workItem?.widgets?.find(
- (widget) => widget.type === WIDGET_TYPE_START_AND_DUE_DATE,
- );
- },
- workItemWeight() {
- return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_WEIGHT);
- },
- workItemHierarchy() {
- return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY);
- },
parentWorkItem() {
- return this.workItemHierarchy?.parent;
+ return this.isWidgetPresent(WIDGET_TYPE_HIERARCHY)?.parent;
},
parentWorkItemConfidentiality() {
return this.parentWorkItem?.confidential;
@@ -205,6 +187,27 @@ export default {
noAccessSvgPath() {
return `data:image/svg+xml;utf8,${encodeURIComponent(noAccessSvg)}`;
},
+ hasDescriptionWidget() {
+ return this.isWidgetPresent(WIDGET_TYPE_DESCRIPTION);
+ },
+ workItemAssignees() {
+ return this.isWidgetPresent(WIDGET_TYPE_ASSIGNEES);
+ },
+ workItemLabels() {
+ return this.isWidgetPresent(WIDGET_TYPE_LABELS);
+ },
+ workItemDueDate() {
+ return this.isWidgetPresent(WIDGET_TYPE_START_AND_DUE_DATE);
+ },
+ workItemWeight() {
+ return this.isWidgetPresent(WIDGET_TYPE_WEIGHT);
+ },
+ workItemHierarchy() {
+ return this.isWidgetPresent(WIDGET_TYPE_HIERARCHY);
+ },
+ workItemIteration() {
+ return this.isWidgetPresent(WIDGET_TYPE_ITERATION);
+ },
},
beforeDestroy() {
/** make sure that if the user has not even dismissed the alert ,
@@ -212,6 +215,9 @@ export default {
this.dismissBanner();
},
methods: {
+ isWidgetPresent(type) {
+ return this.workItem?.widgets?.find((widget) => widget.type === type);
+ },
dismissBanner() {
this.showInfoBanner = false;
},
@@ -416,6 +422,17 @@ export default {
:work-item-type="workItemType"
@error="updateError = $event"
/>
+ <template v-if="workItemsMvc2Enabled">
+ <work-item-iteration
+ v-if="workItemIteration"
+ class="gl-mb-5"
+ :iteration="workItemIteration.iteration"
+ :can-update="canUpdate"
+ :work-item-id="workItem.id"
+ :work-item-type="workItemType"
+ @error="updateError = $event"
+ />
+ </template>
<work-item-description
v-if="hasDescriptionWidget"
:work-item-id="workItem.id"
diff --git a/app/assets/javascripts/work_items/components/work_item_links/index.js b/app/assets/javascripts/work_items/components/work_item_links/index.js
index 8f31b07b6a3..37aa48be6e5 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/index.js
+++ b/app/assets/javascripts/work_items/components/work_item_links/index.js
@@ -16,7 +16,13 @@ export default function initWorkItemLinks() {
return;
}
- const { projectPath, wiHasIssueWeightsFeature, iid } = workItemLinksRoot.dataset;
+ const {
+ projectPath,
+ wiHasIssueWeightsFeature,
+ iid,
+ wiHasIterationsFeature,
+ projectNamespace,
+ } = workItemLinksRoot.dataset;
// eslint-disable-next-line no-new
new Vue({
@@ -31,6 +37,8 @@ export default function initWorkItemLinks() {
iid,
fullPath: projectPath,
hasIssueWeightsFeature: wiHasIssueWeightsFeature,
+ hasIterationsFeature: wiHasIterationsFeature,
+ projectNamespace,
},
render: (createElement) =>
createElement('work-item-links', {
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
index 827ec64f98a..0d3e951de7e 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
@@ -5,7 +5,7 @@ import { s__ } from '~/locale';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
-import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql';
+import getIssueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
import { isMetaKey } from '~/lib/utils/common_utils';
import { setUrlParams, updateHistory } from '~/lib/utils/url_utility';
@@ -59,7 +59,7 @@ export default {
},
},
parentIssue: {
- query: issueConfidentialQuery,
+ query: getIssueDetailsQuery,
variables() {
return {
fullPath: this.projectPath,
@@ -86,6 +86,9 @@ export default {
confidential() {
return this.parentIssue?.confidential || this.workItem?.confidential || false;
},
+ issuableIteration() {
+ return this.parentIssue?.iteration;
+ },
children() {
return (
this.workItem?.widgets.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY)?.children
@@ -305,6 +308,7 @@ export default {
:issuable-gid="issuableGid"
:children-ids="childrenIds"
:parent-confidential="confidential"
+ :parent-iteration="issuableIteration"
@cancel="hideAddForm"
@addWorkItemChild="addChild"
/>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
index eafd9ee88dd..a01f4616cab 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
@@ -16,7 +16,7 @@ export default {
GlFormGroup,
GlFormInput,
},
- inject: ['projectPath'],
+ inject: ['projectPath', 'hasIterationsFeature'],
props: {
issuableGid: {
type: String,
@@ -33,6 +33,11 @@ export default {
required: false,
default: false,
},
+ parentIteration: {
+ type: Object,
+ required: false,
+ default: () => {},
+ },
},
apollo: {
workItemTypes: {
@@ -77,6 +82,9 @@ export default {
taskWorkItemType() {
return this.workItemTypes.find((type) => type.name === TASK_TYPE_NAME)?.id;
},
+ parentIterationId() {
+ return this.parentIteration?.id;
+ },
},
methods: {
getIdFromGraphQLId,
@@ -133,6 +141,13 @@ export default {
} else {
this.unsetError();
this.$emit('addWorkItemChild', data.workItemCreate.workItem);
+ /**
+ * call update mutation only when there is an iteration associated with the issue
+ */
+ // TODO: setting the iteration should be moved to the creation mutation once the backend is done
+ if (this.parentIterationId && this.hasIterationsFeature) {
+ this.addIterationToWorkItem(data.workItemCreate.workItem.id);
+ }
}
})
.catch(() => {
@@ -143,6 +158,19 @@ export default {
this.childToCreateTitle = null;
});
},
+ async addIterationToWorkItem(workItemId) {
+ await this.$apollo.mutate({
+ mutation: updateWorkItemMutation,
+ variables: {
+ input: {
+ id: workItemId,
+ iterationWidget: {
+ iterationId: this.parentIterationId,
+ },
+ },
+ },
+ });
+ },
},
i18n: {
inputLabel: __('Title'),
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index 97c445de711..0d426299408 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -17,6 +17,8 @@ export const WIDGET_TYPE_LABELS = 'LABELS';
export const WIDGET_TYPE_START_AND_DUE_DATE = 'START_AND_DUE_DATE';
export const WIDGET_TYPE_WEIGHT = 'WEIGHT';
export const WIDGET_TYPE_HIERARCHY = 'HIERARCHY';
+export const WIDGET_TYPE_ITERATION = 'ITERATION';
+
export const WORK_ITEM_VIEWED_STORAGE_KEY = 'gl-show-work-item-banner';
export const WORK_ITEM_TYPE_ENUM_INCIDENT = 'INCIDENT';
@@ -54,6 +56,10 @@ export const I18N_WORK_ITEM_ARE_YOU_SURE_DELETE = s__(
);
export const I18N_WORK_ITEM_DELETED = s__('WorkItem|%{workItemType} deleted');
+export const I18N_WORK_ITEM_FETCH_ITERATIONS_ERROR = s__(
+ 'WorkItem|Something went wrong when fetching iterations. Please try again.',
+);
+
export const sprintfWorkItem = (msg, workItemTypeArg) => {
const workItemType = workItemTypeArg || s__('WorkItem|Work item');
return capitalizeFirstCharacter(
diff --git a/app/assets/javascripts/work_items/graphql/get_issue_details.query.graphql b/app/assets/javascripts/work_items/graphql/get_issue_details.query.graphql
new file mode 100644
index 00000000000..6edb6c89f16
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/get_issue_details.query.graphql
@@ -0,0 +1,9 @@
+query issuableDetails($fullPath: ID!, $iid: String) {
+ workspace: project(fullPath: $fullPath) {
+ id
+ issuable: issue(iid: $iid) {
+ id
+ confidential
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/index.js b/app/assets/javascripts/work_items/index.js
index bb4c7052238..f872d8c6b12 100644
--- a/app/assets/javascripts/work_items/index.js
+++ b/app/assets/javascripts/work_items/index.js
@@ -6,7 +6,13 @@ import { createRouter } from './router';
export const initWorkItemsRoot = () => {
const el = document.querySelector('#js-work-items');
- const { fullPath, hasIssueWeightsFeature, issuesListPath } = el.dataset;
+ const {
+ fullPath,
+ hasIssueWeightsFeature,
+ issuesListPath,
+ projectNamespace,
+ hasIterationsFeature,
+ } = el.dataset;
return new Vue({
el,
@@ -17,6 +23,8 @@ export const initWorkItemsRoot = () => {
fullPath,
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
issuesListPath,
+ projectNamespace,
+ hasIterationsFeature: parseBoolean(hasIterationsFeature),
},
render(createElement) {
return createElement(App);
diff --git a/app/assets/stylesheets/page_bundles/work_items.scss b/app/assets/stylesheets/page_bundles/work_items.scss
index d0fc011dde7..7a5cc72ceb8 100644
--- a/app/assets/stylesheets/page_bundles/work_items.scss
+++ b/app/assets/stylesheets/page_bundles/work_items.scss
@@ -63,3 +63,23 @@
display: none;
}
}
+
+.work-item-iteration {
+ .gl-dropdown-toggle {
+ background: none !important;
+
+ &:hover,
+ &:focus {
+ box-shadow: inset 0 0 0 $gl-border-size-1 var(--gray-darkest, $gray-darkest) !important;
+ }
+
+ &.is-not-focused:not(:hover, :focus) {
+ box-shadow: none;
+
+ .gl-button-icon {
+ display: none;
+ }
+ }
+ }
+}
+
diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb
index 2e0807cdab4..96fe0c9331d 100644
--- a/app/controllers/admin/runners_controller.rb
+++ b/app/controllers/admin/runners_controller.rb
@@ -5,10 +5,6 @@ class Admin::RunnersController < Admin::ApplicationController
before_action :runner, except: [:index, :tag_list, :runner_setup_scripts]
- before_action only: [:show] do
- push_frontend_feature_flag(:enforce_runner_token_expires_at)
- end
-
feature_category :runner
urgency :low
diff --git a/app/controllers/groups/runners_controller.rb b/app/controllers/groups/runners_controller.rb
index 01c1529e831..18b055b3f05 100644
--- a/app/controllers/groups/runners_controller.rb
+++ b/app/controllers/groups/runners_controller.rb
@@ -5,10 +5,6 @@ class Groups::RunnersController < Groups::ApplicationController
before_action :authorize_update_runner!, only: [:edit, :update, :destroy, :pause, :resume]
before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
- before_action only: [:show] do
- push_frontend_feature_flag(:enforce_runner_token_expires_at)
- end
-
feature_category :runner
urgency :low
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 853437b2d27..4287c0b7884 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -617,6 +617,15 @@ module Ci
# auto_canceled_by_pipeline_id - store the pipeline_id of the pipeline that triggered cancellation
# execute_async - if true cancel the children asyncronously
def cancel_running(retries: 1, cascade_to_children: true, auto_canceled_by_pipeline_id: nil, execute_async: true)
+ Gitlab::AppJsonLogger.info(
+ event: 'pipeline_cancel_running',
+ pipeline_id: id,
+ auto_canceled_by_pipeline_id: auto_canceled_by_pipeline_id,
+ cascade_to_children: cascade_to_children,
+ execute_async: execute_async,
+ **Gitlab::ApplicationContext.current
+ )
+
update(auto_canceled_by_id: auto_canceled_by_pipeline_id) if auto_canceled_by_pipeline_id
cancel_jobs(cancelable_statuses, retries: retries, auto_canceled_by_pipeline_id: auto_canceled_by_pipeline_id)
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 2bd26e15953..3be627989b1 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -14,7 +14,7 @@ module Ci
include Presentable
include EachBatch
- add_authentication_token_field :token, encrypted: :optional, expires_at: :compute_token_expiration, expiration_enforced?: :token_expiration_enforced?
+ add_authentication_token_field :token, encrypted: :optional, expires_at: :compute_token_expiration
enum access_level: {
not_protected: 0,
@@ -480,10 +480,6 @@ module Ci
end
end
- def self.token_expiration_enforced?
- Feature.enabled?(:enforce_runner_token_expires_at)
- end
-
private
scope :with_upgrade_status, ->(upgrade_status) do
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index f7f29b3e82e..ed04b0c3d1f 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -7,7 +7,7 @@ class WebHook < ApplicationRecord
MAX_FAILURES = 100
FAILURE_THRESHOLD = 3 # three strikes
- INITIAL_BACKOFF = 10.minutes
+ INITIAL_BACKOFF = 1.minute
MAX_BACKOFF = 1.day
BACKOFF_GROWTH_FACTOR = 2.0
diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml
index e676dae37b3..b08a549148d 100644
--- a/app/views/admin/application_settings/_account_and_limit.html.haml
+++ b/app/views/admin/application_settings/_account_and_limit.html.haml
@@ -17,14 +17,14 @@
.form-group
= f.label :receive_max_input_size, _('Maximum push size (MB)'), class: 'label-light'
- = f.number_field :receive_max_input_size, class: 'form-control gl-form-input qa-receive-max-input-size-field', title: _('Maximum size limit for a single commit.'), data: { toggle: 'tooltip', container: 'body' }
+ = f.number_field :receive_max_input_size, class: 'form-control gl-form-input', title: _('Maximum size limit for a single commit.'), data: { toggle: 'tooltip', container: 'body', qa_selector: 'receive_max_input_size_field' }
.form-group
= f.label :max_export_size, _('Maximum export size (MB)'), class: 'label-light'
= f.number_field :max_export_size, class: 'form-control gl-form-input', title: _('Maximum size of export files.'), data: { toggle: 'tooltip', container: 'body' }
%span.form-text.text-muted= _('Set to 0 for no size limit.')
.form-group
= f.label :max_import_size, _('Maximum import size (MB)'), class: 'label-light'
- = f.number_field :max_import_size, class: 'form-control gl-form-input qa-receive-max-import-size-field', title: _('Maximum size of import files.'), data: { toggle: 'tooltip', container: 'body' }
+ = f.number_field :max_import_size, class: 'form-control gl-form-input', title: _('Maximum size of import files.'), data: { toggle: 'tooltip', container: 'body' }
%span.form-text.text-muted= _('Only effective when remote storage is enabled. Set to 0 for no size limit.')
.form-group
= f.label :session_expire_delay, _('Session duration (minutes)'), class: 'label-light'
@@ -69,4 +69,4 @@
= render 'admin/application_settings/invitation_flow_enforcement', form: f
= render 'admin/application_settings/user_restrictions', form: f
= render_if_exists 'admin/application_settings/availability_on_namespace_setting', form: f
- = f.submit _('Save changes'), class: 'qa-save-changes-button', pajamas_button: true
+ = f.submit _('Save changes'), pajamas_button: true, data: { qa_selector: 'save_changes_button' }
diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml
index 05aea2b343d..f6635ad17ef 100644
--- a/app/views/admin/application_settings/_ci_cd.html.haml
+++ b/app/views/admin/application_settings/_ci_cd.html.haml
@@ -53,8 +53,7 @@
= link_to sprite_icon('question-o'), help_page_path('ci/pipelines/settings', anchor: 'specify-a-custom-cicd-configuration-file'), target: '_blank', rel: 'noopener noreferrer'
.form-group
= f.gitlab_ui_checkbox_component :suggest_pipeline_enabled, s_('AdminSettings|Enable pipeline suggestion banner'), help_text: s_('AdminSettings|Display a banner on merge requests in projects with no pipelines to initiate steps to add a .gitlab-ci.yml file.')
- - if Feature.enabled?(:enforce_runner_token_expires_at)
- #js-runner-token-expiration-intervals{ data: runner_token_expiration_interval_attributes }
+ #js-runner-token-expiration-intervals{ data: runner_token_expiration_interval_attributes }
= f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_performance_bar.html.haml b/app/views/admin/application_settings/_performance_bar.html.haml
index 823bc0380af..d4f6d84ea74 100644
--- a/app/views/admin/application_settings/_performance_bar.html.haml
+++ b/app/views/admin/application_settings/_performance_bar.html.haml
@@ -10,4 +10,4 @@
= f.label :performance_bar_allowed_group_path, _('Allow access to members of the following group'), class: 'label-bold'
= f.text_field :performance_bar_allowed_group_path, class: 'form-control gl-form-input', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path
- = f.submit _('Save changes'), class: 'qa-save-changes-button', pajamas_button: true
+ = f.submit _('Save changes'), pajamas_button: true, data: { qa_selector: 'save_changes_button' }
diff --git a/app/views/ci/variables/_url_query_variable_row.html.haml b/app/views/ci/variables/_url_query_variable_row.html.haml
index 9c34daf88bd..77bcacdb94b 100644
--- a/app/views/ci/variables/_url_query_variable_row.html.haml
+++ b/app/views/ci/variables/_url_query_variable_row.html.haml
@@ -15,12 +15,12 @@
%input.js-ci-variable-input-destroy{ type: "hidden", name: destroy_input_name }
%select.js-ci-variable-input-variable-type.ci-variable-body-item.form-control.select-control.custom-select.table-section.section-15{ name: variable_type_input_name }
= options_for_select(ci_variable_type_options, variable_type)
- %input.js-ci-variable-input-key.ci-variable-body-item.qa-ci-variable-input-key.form-control.table-section.section-15{ type: "text",
+ %input.js-ci-variable-input-key.ci-variable-body-item.form-control.table-section.section-15{ type: "text",
name: key_input_name,
value: key,
placeholder: s_('CiVariables|Input variable key') }
.ci-variable-body-item.gl-show-field-errors.table-section.section-15.border-top-0.p-0
- %textarea.js-ci-variable-input-value.js-secret-value.qa-ci-variable-input-value.form-control{ rows: 1,
+ %textarea.js-ci-variable-input-value.js-secret-value.form-control{ rows: 1,
name: value_input_name,
placeholder: s_('CiVariables|Input variable value') }
= value
diff --git a/app/views/ci/variables/_variable_row.html.haml b/app/views/ci/variables/_variable_row.html.haml
index 483c767d029..c5e518d8526 100644
--- a/app/views/ci/variables/_variable_row.html.haml
+++ b/app/views/ci/variables/_variable_row.html.haml
@@ -18,14 +18,14 @@
%input.js-ci-variable-input-destroy{ type: "hidden", name: destroy_input_name }
%select.js-ci-variable-input-variable-type.ci-variable-body-item.form-control.select-control.custom-select.table-section.section-15{ name: variable_type_input_name }
= options_for_select(ci_variable_type_options, variable_type)
- %input.js-ci-variable-input-key.ci-variable-body-item.qa-ci-variable-input-key.form-control.gl-form-input.table-section.section-15{ type: "text",
+ %input.js-ci-variable-input-key.ci-variable-body-item.form-control.gl-form-input.table-section.section-15{ type: "text",
name: key_input_name,
value: key,
placeholder: s_('CiVariables|Input variable key') }
.ci-variable-body-item.gl-show-field-errors.table-section.section-15.border-top-0.p-0
- .form-control.js-secret-value-placeholder.qa-ci-variable-input-value.overflow-hidden{ class: ('hide' unless id) }
+ .form-control.js-secret-value-placeholder.overflow-hidden{ class: ('hide' unless id) }
= '*' * 17
- %textarea.js-ci-variable-input-value.js-secret-value.qa-ci-variable-input-value.form-control.gl-form-input{ class: ('hide' if id),
+ %textarea.js-ci-variable-input-value.js-secret-value.form-control.gl-form-input{ class: ('hide' if id),
rows: 1,
name: value_input_name,
placeholder: s_('CiVariables|Input variable value') }
diff --git a/app/views/clusters/clusters/_health.html.haml b/app/views/clusters/clusters/_health.html.haml
index 50facdf91a2..9e7820d3136 100644
--- a/app/views/clusters/clusters/_health.html.haml
+++ b/app/views/clusters/clusters/_health.html.haml
@@ -1,6 +1,6 @@
- add_page_specific_style 'page_bundles/prometheus'
-%section.settings.no-animate.expanded.cluster-health-graphs.qa-cluster-health-section#cluster-health
+%section.settings.no-animate.expanded.cluster-health-graphs#cluster-health
- if @cluster&.integration_prometheus_available?
#prometheus-graphs{ data: @cluster.health_data(clusterable) }
diff --git a/app/views/clusters/clusters/user/_form.html.haml b/app/views/clusters/clusters/user/_form.html.haml
index bf7b24181c1..557c95f8478 100644
--- a/app/views/clusters/clusters/user/_form.html.haml
+++ b/app/views/clusters/clusters/user/_form.html.haml
@@ -36,7 +36,7 @@
= platform_kubernetes_field.form_group :authorization_type,
{ help: '%{help_text} %{help_link}'.html_safe % { help_text: rbac_help_text, help_link: rbac_help_link } } do
= platform_kubernetes_field.check_box :authorization_type,
- { class: 'qa-rbac-checkbox', label: s_('ClusterIntegration|RBAC-enabled cluster'),
+ { data: { qa_selector: 'rbac_checkbox'}, label: s_('ClusterIntegration|RBAC-enabled cluster'),
label_class: 'label-bold', inline: true }, 'rbac', 'abac'
.form-group
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 800dcef5b72..b74dfd4d3a1 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -132,7 +132,7 @@
- if header_link?(:user_dropdown)
%li.nav-item.header-user.js-nav-user-dropdown.dropdown{ data: { track_label: "profile_dropdown", track_action: "click_dropdown", track_value: "", qa_selector: 'user_menu', testid: 'user-menu' }, class: ('mr-0' if has_impersonation_link) }
= link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
- = render Pajamas::AvatarComponent.new(current_user, size: 24, class: 'header-user-avatar qa-user-avatar')
+ = render Pajamas::AvatarComponent.new(current_user, size: 24, class: 'header-user-avatar', avatar_options: { data: { qa_selector: 'user_avatar_content' } })
= render_if_exists 'layouts/header/user_notification_dot', project: project, namespace: group
= sprite_icon('chevron-down', css_class: 'caret-down')
.dropdown-menu.dropdown-menu-right
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index 56f333664df..8815dec5a6b 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -1,4 +1,4 @@
-%aside.nav-sidebar.qa-admin-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?), 'aria-label': _('Admin navigation') }
+%aside.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?), 'aria-label': _('Admin navigation'), data: { qa_selector: 'admin_sidebar_content' } }
.nav-sidebar-inner-scroll
.context-header
= link_to admin_root_path, title: _('Admin Overview'), class: 'has-tooltip', data: { container: 'body', placement: 'right' } do
@@ -6,7 +6,7 @@
= sprite_icon('admin', size: 18)
%span.sidebar-context-title
= _('Admin Area')
- %ul.sidebar-top-level-items{ data: { qa_selector: 'admin_sidebar_overview_submenu_content' } }
+ %ul.sidebar-top-level-items{ data: { qa_selector: 'admin_overview_submenu_content' } }
= nav_link(controller: %w[dashboard admin admin/projects users groups admin/topics jobs runners gitaly_servers cohorts], html_options: {class: 'home'}) do
= link_to admin_root_path, class: 'has-sub-items' do
.nav-icon-container
@@ -28,15 +28,15 @@
%span
= _('Projects')
= nav_link(controller: %w[users cohorts]) do
- = link_to admin_users_path, title: _('Users'), data: { qa_selector: 'users_overview_link' } do
+ = link_to admin_users_path, title: _('Users'), data: { qa_selector: 'admin_overview_users_link' } do
%span
= _('Users')
= nav_link(controller: :groups) do
- = link_to admin_groups_path, title: _('Groups'), data: { qa_selector: 'groups_overview_link' } do
+ = link_to admin_groups_path, title: _('Groups'), data: { qa_selector: 'admin_overview_groups_link' } do
%span
= _('Groups')
= nav_link(controller: [:admin, 'admin/topics']) do
- = link_to admin_topics_path, title: _('Topics'), data: { qa_selector: 'topics_overview_link' } do
+ = link_to admin_topics_path, title: _('Topics') do
%span
= _('Topics')
= nav_link path: 'jobs#index' do
@@ -75,13 +75,13 @@
= _('Usage Trends')
= nav_link(controller: admin_monitoring_nav_links) do
- = link_to admin_system_info_path, data: { qa_selector: 'admin_monitoring_link' }, class: 'has-sub-items' do
+ = link_to admin_system_info_path, data: { qa_selector: 'admin_monitoring_menu_link' }, class: 'has-sub-items' do
.nav-icon-container
= sprite_icon('monitor')
%span.nav-item-name
= _('Monitoring')
- %ul.sidebar-sub-level-items{ data: { qa_selector: 'admin_sidebar_monitoring_submenu_content' } }
+ %ul.sidebar-sub-level-items{ data: { qa_selector: 'admin_monitoring_submenu_content' } }
= nav_link(controller: admin_monitoring_nav_links, html_options: { class: "fly-out-top-item" } ) do
= link_to admin_system_info_path do
%strong.fly-out-top-item-name
@@ -222,10 +222,10 @@
= link_to general_admin_application_settings_path, class: 'has-sub-items' do
.nav-icon-container
= sprite_icon('settings')
- %span.nav-item-name.qa-admin-settings-item
+ %span.nav-item-name{ data: { qa_selector: 'admin_settings_menu_link' } }
= _('Settings')
- %ul.sidebar-sub-level-items{ data: { qa_selector: 'admin_sidebar_settings_submenu_content' } }
+ %ul.sidebar-sub-level-items{ data: { qa_selector: 'admin_settings_submenu_content' } }
-# This active_nav_link check is also used in `app/views/layouts/admin.html.haml`
= nav_link(controller: [:application_settings, :integrations, :appearances], html_options: { class: "fly-out-top-item" } ) do
= link_to general_admin_application_settings_path do
@@ -233,24 +233,24 @@
= _('Settings')
%li.divider.fly-out-top-item
= nav_link(path: 'application_settings#general') do
- = link_to general_admin_application_settings_path, title: _('General'), class: 'qa-admin-settings-general-item' do
+ = link_to general_admin_application_settings_path, title: _('General'), data: { qa_selector: 'admin_settings_general_link' } do
%span
= _('General')
- = render_if_exists 'layouts/nav/sidebar/advanced_search', class: 'qa-admin-settings-advanced-search'
+ = render_if_exists 'layouts/nav/sidebar/advanced_search', data: { qa_selector: 'admin_settings_advanced_search_link' }
- if instance_level_integrations?
= nav_link(path: ['application_settings#integrations', 'integrations#edit']) do
- = link_to integrations_admin_application_settings_path, title: _('Integrations'), data: { qa_selector: 'integration_settings_link' } do
+ = link_to integrations_admin_application_settings_path, title: _('Integrations'), data: { qa_selector: 'admin_settings_integrations_link' } do
%span
= _('Integrations')
= nav_link(path: 'application_settings#repository') do
- = link_to repository_admin_application_settings_path, title: _('Repository'), class: 'qa-admin-settings-repository-item' do
+ = link_to repository_admin_application_settings_path, title: _('Repository'), data: { qa_selector: 'admin_settings_repository_link' } do
%span
= _('Repository')
- if Gitlab.ee? && License.feature_available?(:custom_file_templates)
= nav_link(path: 'application_settings#templates') do
- = link_to templates_admin_application_settings_path, title: _('Templates'), class: 'qa-admin-settings-template-item' do
+ = link_to templates_admin_application_settings_path, title: _('Templates'), data: { qa_selector: 'admin_settings_templates_link' } do
%span
= _('Templates')
= nav_link(path: 'application_settings#ci_cd') do
@@ -262,7 +262,7 @@
%span
= _('Reporting')
= nav_link(path: 'application_settings#metrics_and_profiling') do
- = link_to metrics_and_profiling_admin_application_settings_path, title: _('Metrics and profiling'), class: 'qa-admin-settings-metrics-and-profiling-item' do
+ = link_to metrics_and_profiling_admin_application_settings_path, title: _('Metrics and profiling'), data: { qa_selector: 'admin_settings_metrics_and_profiling_link' } do
%span
= _('Metrics and profiling')
= nav_link(path: ['application_settings#service_usage_data']) do
@@ -270,7 +270,7 @@
%span
= _('Service usage data')
= nav_link(path: 'application_settings#network') do
- = link_to network_admin_application_settings_path, title: _('Network'), data: { qa_selector: 'admin_settings_network_item' } do
+ = link_to network_admin_application_settings_path, title: _('Network'), data: { qa_selector: 'admin_settings_network_link' } do
%span
= _('Network')
= nav_link(controller: :appearances ) do
diff --git a/app/views/layouts/terms.html.haml b/app/views/layouts/terms.html.haml
index 91848b4f54b..032be73f70c 100644
--- a/app/views/layouts/terms.html.haml
+++ b/app/views/layouts/terms.html.haml
@@ -25,7 +25,7 @@
%ul.nav.navbar-nav
%li.header-user.dropdown
= link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
- = render Pajamas::AvatarComponent.new(current_user, size: 24, class: 'gl-mr-3', avatar_options: { data: { qa_selector: 'user_avatar' } })
+ = render Pajamas::AvatarComponent.new(current_user, size: 24, class: 'gl-mr-3', avatar_options: { data: { qa_selector: 'user_avatar_content' } })
= sprite_icon('chevron-down')
.dropdown-menu.dropdown-menu-right
= render 'layouts/header/current_user_dropdown'
diff --git a/app/views/projects/blob/_template_selectors.html.haml b/app/views/projects/blob/_template_selectors.html.haml
index a76e61bc3dd..249c474587c 100644
--- a/app/views/projects/blob/_template_selectors.html.haml
+++ b/app/views/projects/blob/_template_selectors.html.haml
@@ -2,14 +2,14 @@
.template-selector-dropdowns-wrap
.template-type-selector.js-template-type-selector-wrap.hidden
- toggle_text = should_suggest_gitlab_ci_yml? ? '.gitlab-ci.yml' : 'Select a template type'
- = dropdown_tag(_(toggle_text), options: { toggle_class: 'js-template-type-selector qa-template-type-dropdown', dropdown_class: 'dropdown-menu-selectable' })
+ = dropdown_tag(_(toggle_text), options: { toggle_class: 'js-template-type-selector', dropdown_class: 'dropdown-menu-selectable', data: { qa_selector: 'template_type_dropdown' } })
.license-selector.js-license-selector-wrap.js-template-selector-wrap.hidden
- = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-license-selector qa-license-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: licenses_for_select(@project), project: @project.name, fullname: @project.namespace.human_name } } )
+ = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-license-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: licenses_for_select(@project), project: @project.name, fullname: @project.namespace.human_name, qa_selector: 'license_dropdown' } } )
.gitignore-selector.js-gitignore-selector-wrap.js-template-selector-wrap.hidden
- = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitignore-selector qa-gitignore-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitignore_names(@project) } } )
+ = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitignore-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitignore_names(@project), qa_selector: 'gitignore_dropdown' } } )
.metrics-dashboard-selector.js-metrics-dashboard-selector-wrap.js-template-selector-wrap.hidden
- = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-metrics-dashboard-selector qa-metrics-dashboard-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: metrics_dashboard_ymls(@project) } } )
+ = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-metrics-dashboard-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: metrics_dashboard_ymls(@project), qa_selector: 'metrics_dashboard_dropdown' } } )
#gitlab-ci-yml-selector.gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.js-template-selector-wrap.hidden
- = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitlab-ci-yml-selector qa-gitlab-ci-yml-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls(@project), selected: params[:template] } } )
+ = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitlab-ci-yml-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls(@project), selected: params[:template], qa_selector: 'gitlab_ci_yml_dropdown' } } )
.dockerfile-selector.js-dockerfile-selector-wrap.js-template-selector-wrap.hidden
- = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-dockerfile-selector qa-dockerfile-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: dockerfile_names(@project) } } )
+ = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-dockerfile-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: dockerfile_names(@project), qa_selector: 'dockerfile_dropdown' } } )
diff --git a/app/views/projects/buttons/_clone.html.haml b/app/views/projects/buttons/_clone.html.haml
index 10a6bc6b524..34aecd31c57 100644
--- a/app/views/projects/buttons/_clone.html.haml
+++ b/app/views/projects/buttons/_clone.html.haml
@@ -2,17 +2,17 @@
- dropdown_class = local_assigns.fetch(:dropdown_class, '')
.git-clone-holder.js-git-clone-holder
- %a#clone-dropdown.gl-button.btn.btn-confirm.clone-dropdown-btn.qa-clone-dropdown{ href: '#', data: { toggle: 'dropdown' } }
+ %a#clone-dropdown.gl-button.btn.btn-confirm.clone-dropdown-btn{ href: '#', data: { toggle: 'dropdown', qa_selector: 'clone_dropdown' } }
%span.gl-mr-2.js-clone-dropdown-label
= _('Clone')
= sprite_icon("chevron-down", css_class: "icon")
- %ul.dropdown-menu.dropdown-menu-large.dropdown-menu-selectable.clone-options-dropdown.qa-clone-options{ class: dropdown_class }
+ %ul.dropdown-menu.dropdown-menu-large.dropdown-menu-selectable.clone-options-dropdown{ class: dropdown_class, data: { qa_selector: 'clone_dropdown_content' } }
- if ssh_enabled?
%li{ class: 'gl-px-4!' }
%label.label-bold
= _('Clone with SSH')
.input-group.btn-group
- = text_field_tag :ssh_project_clone, project.ssh_url_to_repo, class: "js-select-on-focus form-control qa-ssh-clone-url", readonly: true, aria: { label: _('Repository clone URL') }
+ = text_field_tag :ssh_project_clone, project.ssh_url_to_repo, class: "js-select-on-focus form-control", readonly: true, aria: { label: _('Repository clone URL') }, data: { qa_selector: 'ssh_clone_url_content' }
.input-group-append
= clipboard_button(target: '#ssh_project_clone', title: _("Copy URL"), class: "input-group-text gl-button btn btn-icon btn-default")
= render_if_exists 'projects/buttons/geo'
@@ -21,7 +21,7 @@
%label.label-bold
= _('Clone with %{http_label}') % { http_label: gitlab_config.protocol.upcase }
.input-group.btn-group
- = text_field_tag :http_project_clone, project.http_url_to_repo, class: "js-select-on-focus form-control qa-http-clone-url", readonly: true, aria: { label: _('Repository clone URL') }
+ = text_field_tag :http_project_clone, project.http_url_to_repo, class: "js-select-on-focus form-control", readonly: true, aria: { label: _('Repository clone URL') }, data: { qa_selector: 'http_clone_url_content' }
.input-group-append
= clipboard_button(target: '#http_project_clone', title: _("Copy URL"), class: "input-group-text gl-button btn btn-icon btn-default")
= render_if_exists 'projects/buttons/geo'
diff --git a/app/views/projects/issues/_work_item_links.html.haml b/app/views/projects/issues/_work_item_links.html.haml
index bc2136b89fb..c0de711136a 100644
--- a/app/views/projects/issues/_work_item_links.html.haml
+++ b/app/views/projects/issues/_work_item_links.html.haml
@@ -1,2 +1,2 @@
- if Feature.enabled?(:work_items_hierarchy, @project)
- .js-work-item-links-root{ data: { issuable_id: @issue.id, iid: @issue.iid, project_path: @project.full_path, wi: work_items_index_data(@project) } }
+ .js-work-item-links-root{ data: { issuable_id: @issue.id, iid: @issue.iid, project_namespace: @project.namespace.path, project_path: @project.full_path, wi: work_items_index_data(@project) } }
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index 6b502ee928e..abaf250fa69 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -9,14 +9,14 @@
%span.js-clone-dropdown-label
= default_clone_protocol.upcase
= sprite_icon('chevron-down', css_class: 'gl-icon')
- %ul.dropdown-menu.dropdown-menu-selectable.clone-options-dropdown
+ %ul.dropdown-menu.dropdown-menu-selectable{ data: { qa_selector: 'clone_dropdown_content' } }
%li
= ssh_clone_button(container)
%li
= http_clone_button(container)
= render_if_exists 'shared/kerberos_clone_button', container: container
- = text_field_tag :clone_url, default_url_to_repo(container), class: "js-select-on-focus btn gl-button", readonly: true, aria: { label: _('Repository clone URL') }
+ = text_field_tag :clone_url, default_url_to_repo(container), class: "js-select-on-focus btn gl-button", readonly: true, aria: { label: _('Repository clone URL') }, data: { qa_selector: 'clone_url_content' }
.input-group-append
= clipboard_button(target: '#clone_url', title: _("Copy URL"), class: "input-group-text gl-button btn-default btn-clipboard")
diff --git a/app/views/shared/web_hooks/_hook_errors.html.haml b/app/views/shared/web_hooks/_hook_errors.html.haml
index d95efe83e15..098cc19c435 100644
--- a/app/views/shared/web_hooks/_hook_errors.html.haml
+++ b/app/views/shared/web_hooks/_hook_errors.html.haml
@@ -4,16 +4,12 @@
- link_end = '</a>'.html_safe
- if hook.rate_limited?
- - support_path = 'https://support.gitlab.com/hc/en-us/requests/new'
- - placeholders = { strong_start: strong_start,
- strong_end: strong_end,
- limit: hook.rate_limit,
- support_link_start: link_start % { url: support_path },
- support_link_end: link_end }
- = render Pajamas::AlertComponent.new(title: s_('Webhooks|Webhook was automatically disabled'),
+ - placeholders = { limit: number_with_delimiter(hook.rate_limit),
+ root_namespace: hook.parent.root_namespace.path }
+ = render Pajamas::AlertComponent.new(title: s_('Webhooks|Webhook rate limit has been reached'),
variant: :danger) do |c|
= c.body do
- = s_('Webhooks|The webhook was triggered more than %{limit} times per minute and is now disabled. To re-enable this webhook, fix the problems shown in %{strong_start}Recent events%{strong_end}, then re-test your settings. %{support_link_start}Contact Support%{support_link_end} if you need help re-enabling your webhook.').html_safe % placeholders
+ = s_("Webhooks|Webhooks for %{root_namespace} are now disabled because they've been triggered more than %{limit} times per minute. They'll be automatically re-enabled in the next minute.").html_safe % placeholders
- elsif hook.permanently_disabled?
= render Pajamas::AlertComponent.new(title: s_('Webhooks|Webhook failed to connect'),
variant: :danger) do |c|
diff --git a/bin/profile-url b/bin/profile-url
index 9e8585aabba..6047cb70b8d 100755
--- a/bin/profile-url
+++ b/bin/profile-url
@@ -8,15 +8,15 @@ opt_parser = OptionParser.new do |opt|
Profile a URL on this GitLab instance.
Usage:
- #{__FILE__} url --output=<profile-html> --sql=<sql-log> [--user=<user>] [--post=<post-data>]
+ #{__FILE__} url --output=<profile-dump> --sql=<sql-log> [--user=<user>] [--post=<post-data>]
Example:
- #{__FILE__} /dashboard/issues --output=dashboard-profile.html --sql=dashboard.log --user=root
+ #{__FILE__} /dashboard/issues --output=dashboard-profile.dump --sql=dashboard.log --user=root
DOCSTRING
opt.separator ''
opt.separator 'Options:'
- opt.on('-o', '--output=/tmp/profile.html', 'profile output filename') do |output|
+ opt.on('-o', '--output=/tmp/profile.dump', 'profile output filename') do |output|
options[:profile_output] = output
end
@@ -45,13 +45,9 @@ end
require File.expand_path('../config/environment', File.dirname(__FILE__))
-result = Gitlab::Profiler.profile(options[:url],
- logger: Logger.new(options[:sql_output]),
- post_data: options[:post_data],
- user: UserFinder.new(options[:username]).find_by_username,
- private_token: ENV['PRIVATE_TOKEN'])
-
-printer = RubyProf::CallStackPrinter.new(result)
-file = File.open(options[:profile_output], 'w')
-printer.print(file)
-file.close
+Gitlab::Profiler.profile(options[:url],
+ logger: Logger.new(options[:sql_output]),
+ post_data: options[:post_data],
+ user: UserFinder.new(options[:username]).find_by_username,
+ private_token: ENV['PRIVATE_TOKEN'],
+ profiler_options: { out: options[:profile_output] })
diff --git a/bin/rubocop-profile b/bin/rubocop-profile
new file mode 100755
index 00000000000..d1e31edbeed
--- /dev/null
+++ b/bin/rubocop-profile
@@ -0,0 +1,39 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+# Profile bundled RuboCop version.
+#
+# See https://github.com/rubocop/rubocop/blob/master/bin/rubocop-profile
+
+if ARGV.include?('-h') || ARGV.include?('--help')
+ puts "Usage: same as main `rubocop` command but gathers profiling info"
+ puts "Additional option: `--memory` to print memory usage"
+ exit(0)
+end
+with_mem = ARGV.delete('--memory')
+ARGV.unshift '--cache', 'false' unless ARGV.include?('--cache')
+
+require 'stackprof'
+if with_mem
+ require 'memory_profiler'
+ MemoryProfiler.start
+end
+StackProf.start
+start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+begin
+ require 'rubocop'
+
+ exit RuboCop::CLI.new.run
+ensure
+ delta = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
+ puts "Finished in #{delta.round(1)} seconds"
+ StackProf.stop
+ if with_mem
+ puts "Building memory report..."
+ report = MemoryProfiler.stop
+ end
+ Dir.mkdir('tmp') unless File.exist?('tmp')
+ StackProf.results('tmp/stackprof.dump')
+ report&.pretty_print(scale_bytes: true)
+ puts "StackProf written to `tmp/stackprof.dump`."
+end
diff --git a/config/feature_flags/development/enforce_runner_token_expires_at.yml b/config/feature_flags/development/enforce_runner_token_expires_at.yml
deleted file mode 100644
index a1cb3bdcfdd..00000000000
--- a/config/feature_flags/development/enforce_runner_token_expires_at.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: enforce_runner_token_expires_at
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78557
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/352008
-milestone: '14.8'
-type: development
-group: group::runner
-default_enabled: false
diff --git a/config/feature_flags/development/graphql_keyset_pagination_without_next_page_query.yml b/config/feature_flags/development/graphql_keyset_pagination_without_next_page_query.yml
index e289ad9af50..7b4c884a82f 100644
--- a/config/feature_flags/development/graphql_keyset_pagination_without_next_page_query.yml
+++ b/config/feature_flags/development/graphql_keyset_pagination_without_next_page_query.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/373792
milestone: '15.4'
type: development
group: group::optimize
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/gl_listbox_for_sort_dropdowns.yml b/config/feature_flags/development/pipeline_name.yml
index 03d930012ab..40557a7d01e 100644
--- a/config/feature_flags/development/gl_listbox_for_sort_dropdowns.yml
+++ b/config/feature_flags/development/pipeline_name.yml
@@ -1,8 +1,8 @@
---
-name: gl_listbox_for_sort_dropdowns
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/98363
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/364715
+name: pipeline_name
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/97502
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/376095
milestone: '15.5'
type: development
-group: group::foundations
+group: group::delivery
default_enabled: false
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 65b38265b52..05523952769 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -646,7 +646,11 @@ module.exports = {
patterns: [
{
from: path.join(ROOT_PATH, 'node_modules/pdfjs-dist/cmaps/'),
- to: path.join(WEBPACK_OUTPUT_PATH, 'cmaps/'),
+ to: path.join(WEBPACK_OUTPUT_PATH, 'pdfjs/cmaps/'),
+ },
+ {
+ from: path.join(ROOT_PATH, 'node_modules/pdfjs-dist/legacy/build/pdf.worker.min.js'),
+ to: path.join(WEBPACK_OUTPUT_PATH, 'pdfjs/'),
},
{
from: path.join(ROOT_PATH, 'node_modules', SOURCEGRAPH_PACKAGE, '/'),
diff --git a/config/webpack.vendor.config.js b/config/webpack.vendor.config.js
index 248a34d9c2e..1300bf16e56 100644
--- a/config/webpack.vendor.config.js
+++ b/config/webpack.vendor.config.js
@@ -26,8 +26,6 @@ module.exports = {
entry: {
vendor: [
'jquery/dist/jquery.slim.js',
- 'pdfjs-dist/build/pdf',
- 'pdfjs-dist/build/pdf.worker.min',
'core-js',
'echarts',
'lodash',
diff --git a/db/migrate/20221006131506_add_free_user_cap_over_limit_notified_at_to_namespace_details.rb b/db/migrate/20221006131506_add_free_user_cap_over_limit_notified_at_to_namespace_details.rb
new file mode 100644
index 00000000000..7237eee18f0
--- /dev/null
+++ b/db/migrate/20221006131506_add_free_user_cap_over_limit_notified_at_to_namespace_details.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class AddFreeUserCapOverLimitNotifiedAtToNamespaceDetails < Gitlab::Database::Migration[2.0]
+ disable_ddl_transaction!
+
+ TABLE_NAME = 'namespace_details'
+ COLUMN_NAME = 'free_user_cap_over_limit_notified_at'
+
+ def up
+ with_lock_retries do
+ add_column(TABLE_NAME, COLUMN_NAME, :datetime_with_timezone)
+ end
+ end
+
+ def down
+ with_lock_retries do
+ remove_column TABLE_NAME, COLUMN_NAME
+ end
+ end
+end
diff --git a/db/schema_migrations/20221006131506 b/db/schema_migrations/20221006131506
new file mode 100644
index 00000000000..cafb6518fdd
--- /dev/null
+++ b/db/schema_migrations/20221006131506
@@ -0,0 +1 @@
+2652f733d5998b4dacc89a7c43af45e6d411235efcdc120be02bbf04eb1c55d6 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index b8779dd9416..15af16ada65 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -17866,7 +17866,8 @@ CREATE TABLE namespace_details (
cached_markdown_version integer,
description text,
description_html text,
- free_user_cap_over_limt_notified_at timestamp with time zone
+ free_user_cap_over_limt_notified_at timestamp with time zone,
+ free_user_cap_over_limit_notified_at timestamp with time zone
);
CREATE TABLE namespace_limits (
diff --git a/doc/ci/runners/configure_runners.md b/doc/ci/runners/configure_runners.md
index 7f28ef77814..28481a7e7ab 100644
--- a/doc/ci/runners/configure_runners.md
+++ b/doc/ci/runners/configure_runners.md
@@ -915,12 +915,8 @@ To determine which runners need to be upgraded:
## Authentication token security
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30942) in GitLab 15.3 [with a flag](../../administration/feature_flags.md) named `enforce_runner_token_expires_at`. Disabled by default.
-
-FLAG:
-On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to
-[enable the feature flag](../../administration/feature_flags.md) named `enforce_runner_token_expires_at`.
-On GitLab.com, this feature is not available.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30942) in GitLab 15.3 [with a flag](../../administration/feature_flags.md) named `enforce_runner_token_expires_at`. Disabled by default.
+> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/377902) in GitLab 15.5. Feature flag `enforce_runner_token_expires_at` removed.
Each runner has an [authentication token](../../api/runners.md#registration-and-authentication-tokens)
to connect with the GitLab instance.
diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md
index aff4dfbb67b..87333f59589 100644
--- a/doc/ci/yaml/index.md
+++ b/doc/ci/yaml/index.md
@@ -398,6 +398,30 @@ Use [`workflow`](workflow.md) to control pipeline behavior.
- [`workflow: rules` examples](workflow.md#workflow-rules-examples)
- [Switch between branch pipelines and merge request pipelines](workflow.md#switch-between-branch-pipelines-and-merge-request-pipelines)
+#### `workflow:name`
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/372538) in GitLab 15.5 [with a flag](../../administration/feature_flags.md) named `pipeline_name`. Disabled by default.
+
+FLAG:
+On self-managed GitLab, by default this feature is not available. To make it available,
+ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `pipeline_name`.
+The feature is not ready for production use.
+
+You can use `name` in `workflow:` to define a name for pipelines.
+
+All pipelines are assigned the defined name. Any leading or trailing spaces in the name are removed.
+
+**Possible inputs**:
+
+- A string.
+
+**Example of `workflow:name`**:
+
+```yaml
+workflow:
+ name: 'Pipeline name'
+```
+
#### `workflow:rules`
The `rules` keyword in `workflow` is similar to [`rules` defined in jobs](#rules),
diff --git a/doc/development/profiling.md b/doc/development/profiling.md
index 01db208f1cf..3eb2c7c9144 100644
--- a/doc/development/profiling.md
+++ b/doc/development/profiling.md
@@ -20,6 +20,9 @@ The first argument to the profiler is either a full URL
(including the instance hostname) or an absolute path, including the
leading slash.
+By default the report dump will be stored in a temporary file, which can be
+interacted with using the [stackprof API](#reading-a-gitlabprofiler-report).
+
When using the script, command-line documentation is available by passing no
arguments.
@@ -31,10 +34,11 @@ For example:
```ruby
Gitlab::Profiler.profile('/my-user')
-# Returns a RubyProf::Profile for the regular operation of this request
+# Returns the location of the temp file where the report dump is stored
class UsersController; def show; sleep 100; end; end
Gitlab::Profiler.profile('/my-user')
-# Returns a RubyProf::Profile where 100 seconds is spent in UsersController#show
+# Returns the location of the temp file where the report dump is stored
+# where 100 seconds is spent in UsersController#show
```
For routes that require authorization you must provide a user to
@@ -52,57 +56,14 @@ documented with the method source.
Gitlab::Profiler.profile('/gitlab-org/gitlab-test', user: User.first, logger: Logger.new($stdout))
```
-There is also a RubyProf printer available:
-`Gitlab::Profiler::TotalTimeFlatPrinter`. This acts like
-`RubyProf::FlatPrinter`, but its `min_percent` option works on the method's
-total time, not its self time. (This is because we often spend most of our time
-in library code, but this comes from calls in our application.) It also offers a
-`max_percent` option to help filter out outer calls that aren't useful (like
-`ActionDispatch::Integration::Session#process`).
-
-There is a convenience method for using this,
-`Gitlab::Profiler.print_by_total_time`:
-
-```ruby
-result = Gitlab::Profiler.profile('/my-user')
-Gitlab::Profiler.print_by_total_time(result, max_percent: 60, min_percent: 2)
-# Measure Mode: wall_time
-# Thread ID: 70005223698240
-# Fiber ID: 70004894952580
-# Total: 1.768912
-# Sort by: total_time
-#
-# %self total self wait child calls name
-# 0.00 1.017 0.000 0.000 1.017 14 *ActionView::Helpers::RenderingHelper#render
-# 0.00 1.017 0.000 0.000 1.017 14 *ActionView::Renderer#render_partial
-# 0.00 1.017 0.000 0.000 1.017 14 *ActionView::PartialRenderer#render
-# 0.00 1.007 0.000 0.000 1.007 14 *ActionView::PartialRenderer#render_partial
-# 0.00 0.930 0.000 0.000 0.930 14 Hamlit::TemplateHandler#call
-# 0.00 0.928 0.000 0.000 0.928 14 Temple::Engine#call
-# 0.02 0.865 0.000 0.000 0.864 638 *Enumerable#inject
-```
-
-To print the profile in HTML format, use the following example:
-
-```ruby
-result = Gitlab::Profiler.profile('/my-user')
-
-printer = RubyProf::CallStackPrinter.new(result)
-printer.print(File.open('/tmp/profile.html', 'w'))
-```
-
-### Stackprof support
-
-By default, `Gitlab::Profiler.profile` uses a tracing profiler called [`ruby-prof`](https://ruby-prof.github.io/). However, sampling profilers
-[run faster and use less memory](https://jvns.ca/blog/2017/12/17/how-do-ruby---python-profilers-work-/), so they might be preferred.
-
-You can switch to [Stackprof](https://github.com/tmm1/stackprof) (a sampling profiler) to generate a profile by passing `sampling_mode: true`.
Pass in a `profiler_options` hash to configure the output file (`out`) of the sampling data. For example:
```ruby
-Gitlab::Profiler.profile('/gitlab-org/gitlab-test', user: User.first, sampling_mode: true, profiler_options: { out: 'tmp/profile.dump' })
+Gitlab::Profiler.profile('/gitlab-org/gitlab-test', user: User.first, profiler_options: { out: 'tmp/profile.dump' })
```
+## Reading a GitLab::Profiler report
+
You can get a summary of where time was spent by running Stackprof against the sampling data. For example:
```shell
diff --git a/doc/topics/awesome_co.md b/doc/topics/awesome_co.md
new file mode 100644
index 00000000000..0d725f64f3a
--- /dev/null
+++ b/doc/topics/awesome_co.md
@@ -0,0 +1,143 @@
+---
+stage: Ecosystem
+group: Foundations
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
+description: AwesomeCo test data harness created by the Test Data Working Group https://about.gitlab.com/company/team/structure/working-groups/demo-test-data/
+comments: false
+---
+
+# AwesomeCo
+
+AwesomeCo is a test data seeding harness, that can seed test data into a user or group namespace.
+
+AwesomeCo uses FactoryBot in the backend which makes maintenance extremely easy. When a Model is changed,
+FactoryBot will already be reflected to account for the change.
+
+## Docker Setup
+
+See [AwesomeCo Docker Demo](https://gitlab.com/-/snippets/2390362)
+
+## GDK Setup
+
+```shell
+$ gdk start db
+ok: run: services/postgresql: (pid n) 0s, normally down
+ok: run: services/redis: (pid n) 74s, normally down
+$ bundle install
+Bundle complete!
+$ bundle exec rake db:migrate
+main: migrated
+ci: migrated
+```
+
+### Run
+
+The `gitlab:seed:awesome_co` Rake task takes two arguments. `:name` and `:namespace_id`.
+
+```shell
+$ bundle exec rake "gitlab:seed:awesome_co[awesome_co,1]"
+Seeding AwesomeCo for Administrator
+```
+
+#### `:name`
+
+Where `:name` is the name of the AwesomeCo. (This will reflect .rb files located in db/seeds/awesome_co/*.rb)
+
+#### `:namespace_id`
+
+Where `:namespace_id` is the ID of the User or Group Namespace
+
+## List of Awesome Companies
+
+Each company (i.e. test data template) is represented as a Ruby file (.rb) in `db/seeds/awesome_co`.
+
+### AwesomeCo (db/seeds/awesome_co/awesome_co.rb)
+
+```shell
+$ bundle exec rake "gitlab:seed:awesome_co[awesome_co,:namespace_id]"
+Seeding AwesomeCo for :namespace_id
+```
+
+AwesomeCo is an automated seeding of [this demo repository](https://gitlab.com/tech-marketing/demos/gitlab-agile-demo/awesome-co).
+
+## Develop
+
+AwesomeCo seeding uses FactoryBot definitions from `spec/factories` which ...
+
+1. Saves time on development
+1. Are easy-to-read
+1. Are easy to maintain
+1. Do not rely on an API that may change in the future
+1. Are always up-to-date
+1. Execute on the lowest-level (`ActiveRecord`) possible to create data as quickly as possible
+
+> _from the [FactoryBot README](https://github.com/thoughtbot/factory_bot#readme_) : factory_bot is a fixtures replacement with a straightforward definition syntax, support for multiple build
+> strategies (saved instances, unsaved instances, attribute hashes, and stubbed objects), and support for multiple factories for the same class, including factory
+> inheritance
+
+Factories reside in `spec/factories/*` and are fixtures for Rails models found in `app/models/*`. For example, For a model named `app/models/issue.rb`, the factory will
+be named `spec/factories/issues.rb`. For a model named `app/models/project.rb`, the factory will be named `app/models/projects.rb`.
+
+### Taxonomy of a Factory
+
+Factories consist of three main parts - the **Name** of the factory, the **Traits** and the **Attributes**.
+
+Given: `create(:iteration, :with_title, :current, title: 'My Iteration')`
+
+|||
+|:-|:-|
+| **:iteration** | This is the **Name** of the factory. The file name will be the plural form of this **Name** and reside under either `spec/factories/iterations.rb` or `ee/spec/factories/iterations.rb`. |
+| **:with_title** | This is a **Trait** of the factory. [See how it's defined](https://gitlab.com/gitlab-org/gitlab/-/blob/9c2a1f98483921dd006d70fdaed316e21fc5652f/ee/spec/factories/iterations.rb#L21-23). |
+| **:current** | This is a **Trait** of the factory. [See how it's defined](https://gitlab.com/gitlab-org/gitlab/-/blob/9c2a1f98483921dd006d70fdaed316e21fc5652f/ee/spec/factories/iterations.rb#L29-31). |
+| **title: 'My Iteration'** | This is an **Attribute** of the factory that will be passed to the Model for creation. |
+
+### Examples
+
+In these examples, you will see an instance variable `@owner`. This is the `root` user (`User.first`).
+
+#### Create a Group
+
+```ruby
+my_group = create(:group, name: 'My Group', path: 'my-group-path')
+```
+
+#### Create a Project
+
+```ruby
+# create a Project belonging to a Group
+my_project = create(:project, :public, name: 'My Project', namespace: my_group, creator: @owner)
+```
+
+#### Create an Issue
+
+```ruby
+# create an Issue belonging to a Project
+my_issue = create(:issue, title: 'My Issue', project: my_project, weight: 2)
+```
+
+#### Create an Iteration
+
+```ruby
+# create an Iteration under a Group
+my_iteration = create(:iteration, :with_title, :current, title: 'My Iteration', group: my_group)
+```
+
+### Frequently encountered issues
+
+#### ActiveRecord::RecordInvalid: Validation failed: Email has already been taken, Username has already been taken
+
+This is because, by default, our factories are written to backfill any data that is missing. For instance, when a project
+is created, the project must have somebody that created it. If the owner is not specified, the factory attempts to create it.
+
+**How to fix**
+
+Check the respective Factory to find out what key is required. Usually `:author` or `:owner`.
+
+```ruby
+# This throws ActiveRecord::RecordInvalid
+create(:project, name: 'Throws Error', namespace: create(:group, name: 'Some Group'))
+
+# Specify the user where @owner is a [User] record
+create(:project, name: 'No longer throws error', owner: @owner, namespace: create(:group, name: 'Some Group'))
+create(:epic, group: create(:group), author: @owner)
+```
diff --git a/doc/user/clusters/integrations.md b/doc/user/clusters/integrations.md
index a7854176e81..c5e56fcd3a7 100644
--- a/doc/user/clusters/integrations.md
+++ b/doc/user/clusters/integrations.md
@@ -88,11 +88,11 @@ Prometheus as long as you meet the requirements above.
To enable the Prometheus integration for your cluster:
1. Go to the cluster's page:
- - For a [project-level cluster](../project/clusters/index.md), navigate to your project's
+ - For a [project-level cluster](../project/clusters/index.md), go to your project's
**Infrastructure > Kubernetes clusters**.
- - For a [group-level cluster](../group/clusters/index.md), navigate to your group's
+ - For a [group-level cluster](../group/clusters/index.md), go to your group's
**Kubernetes** page.
- - For an [instance-level cluster](../instance/clusters/index.md), navigate to your instance's
+ - For an [instance-level cluster](../instance/clusters/index.md), go to your instance's
**Kubernetes** page.
1. Select the **Integrations** tab.
1. Check the **Enable Prometheus integration** checkbox.
diff --git a/doc/user/crm/index.md b/doc/user/crm/index.md
index 43d2d67c53b..d7ab2195e2d 100644
--- a/doc/user/crm/index.md
+++ b/doc/user/crm/index.md
@@ -186,7 +186,7 @@ When you use the `/add_contacts` or `/remove_contacts` quick actions, follow the
The root group is the topmost group in the group hierarchy.
-When you move an issue, project, or group **within the same group hierarchy**,
+When you move an issue, project, or group **in the same group hierarchy**,
issues retain their contacts.
When you move an issue or project and the **root group changes**,
diff --git a/doc/user/group/epics/index.md b/doc/user/group/epics/index.md
index 565174d69d5..da6e675f0eb 100644
--- a/doc/user/group/epics/index.md
+++ b/doc/user/group/epics/index.md
@@ -35,6 +35,22 @@ graph TD
Also, read more about possible [planning hierarchies](../planning_hierarchy/index.md).
+### Child issues from different group hierarchies
+
+<!-- When feature flag is removed, integrate this info as a sentence in
+https://docs.gitlab.com/ee/user/group/epics/manage_epics.html#add-an-existing-issue-to-an-epic -->
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/371081) in GitLab 15.5 [with a flag](../../../administration/feature_flags.md) named `epic_issues_from_different_hierarchies`. Disabled by default.
+> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/373304) in GitLab 15.5.
+
+FLAG:
+On self-managed GitLab, by default this feature is unavailable. To make it available, ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `epic_issues_from_different_hierarchies`.
+On GitLab.com, this feature is available.
+
+You can add issues from a different group hierarchy to an epic.
+To do it, paste the issue URL when
+[adding an existing issue](manage_epics.md#add-an-existing-issue-to-an-epic).
+
## Roadmap in epics **(ULTIMATE)**
If your epic contains one or more [child epics](manage_epics.md#multi-level-child-epics) that
diff --git a/doc/user/project/issues/managing_issues.md b/doc/user/project/issues/managing_issues.md
index 8241fb882d8..213c615326f 100644
--- a/doc/user/project/issues/managing_issues.md
+++ b/doc/user/project/issues/managing_issues.md
@@ -577,6 +577,7 @@ Or:
> - Filtering by type was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/322755) in GitLab 13.10 [with a flag](../../../administration/feature_flags.md) named `vue_issues_list`. Disabled by default.
> - Filtering by type was [enabled on self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/322755) in GitLab 14.10.
> - Filtering by type is generally available in GitLab 15.1. [Feature flag `vue_issues_list`](https://gitlab.com/gitlab-org/gitlab/-/issues/359966) removed.
+> - Filtering by health status [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218711) in GitLab 15.5.
To filter the list of issues:
diff --git a/doc/user/project/wiki/index.md b/doc/user/project/wiki/index.md
index 69f6e055240..eedc44be3f9 100644
--- a/doc/user/project/wiki/index.md
+++ b/doc/user/project/wiki/index.md
@@ -390,6 +390,11 @@ line of your Apache configuration to ensure your page slugs render correctly.
WARNING:
This operation deletes all data in the wiki.
+WARNING:
+Any command that changes data directly could be damaging if not run correctly, or under the
+right conditions. We highly recommend running them in a test environment with a backup of the
+instance ready to be restored, just in case.
+
To clear all data from a project wiki and recreate it in a blank state:
1. [Start a Rails console session](../../../administration/operations/rails_console.md#starting-a-rails-console-session).
diff --git a/doc/user/tasks.md b/doc/user/tasks.md
index f2d9f777849..ae24460c9f9 100644
--- a/doc/user/tasks.md
+++ b/doc/user/tasks.md
@@ -185,3 +185,21 @@ To set issue weight of a task:
The task window opens.
1. Next to **Weight**, enter a whole, positive number.
1. Select the close icon (**{close}**).
+
+## Add a task to an iteration **(PREMIUM)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/362550) in GitLab 15.5.
+
+You can add a task to an [iteration](group/iterations/index.md).
+You can see the iteration title and period only when you view a task.
+
+Prerequisites:
+
+- You must have at least the Reporter role for the project.
+
+To add a task to an iteration:
+
+1. In the issue description, in the **Tasks** section, select the title of the task you want to edit.
+ The task window opens.
+1. Next to **Iteration**, select **Add to iteration**.
+1. From the dropdown list, select the iteration to be associated with the task.
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 800954a4311..a0e7d0b10cd 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -218,6 +218,7 @@ module API
[
current_user&.cache_key,
mr.merge_status,
+ mr.labels.map(&:cache_key),
mr.merge_request_assignees.map(&:cache_key),
mr.merge_request_reviewers.map(&:cache_key)
].join(":")
diff --git a/lib/api/search.rb b/lib/api/search.rb
index 44bb4228786..ff17696ed3e 100644
--- a/lib/api/search.rb
+++ b/lib/api/search.rb
@@ -63,7 +63,7 @@ module API
@results = search_service(additional_params).search_objects(preload_method)
end
- set_global_search_log_information
+ set_global_search_log_information(additional_params)
Gitlab::Metrics::GlobalSearchSlis.record_apdex(
elapsed: @search_duration_s,
@@ -105,7 +105,7 @@ module API
# EE, without having to modify this file directly.
end
- def search_type
+ def search_type(additional_params = {})
'basic'
end
@@ -113,10 +113,10 @@ module API
params[:scope]
end
- def set_global_search_log_information
+ def set_global_search_log_information(additional_params)
Gitlab::Instrumentation::GlobalSearchApi.set_information(
- type: search_type,
- level: search_service.level,
+ type: search_type(additional_params),
+ level: search_service(additional_params).level,
scope: search_scope,
search_duration_s: @search_duration_s
)
diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb
index 438fa1cb3b2..661c6fb87e3 100644
--- a/lib/gitlab/ci/config.rb
+++ b/lib/gitlab/ci/config.rb
@@ -85,6 +85,10 @@ module Gitlab
root.workflow_entry.rules_value
end
+ def workflow_name
+ root.workflow_entry.name
+ end
+
def normalized_jobs
@normalized_jobs ||= Ci::Config::Normalizer.new(jobs).normalize_jobs
end
diff --git a/lib/gitlab/ci/config/entry/workflow.rb b/lib/gitlab/ci/config/entry/workflow.rb
index 5bc992a38a0..691d9e2d48b 100644
--- a/lib/gitlab/ci/config/entry/workflow.rb
+++ b/lib/gitlab/ci/config/entry/workflow.rb
@@ -6,12 +6,17 @@ module Gitlab
module Entry
class Workflow < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Configurable
+ include ::Gitlab::Config::Entry::Validatable
+ include ::Gitlab::Config::Entry::Attributable
- ALLOWED_KEYS = %i[rules].freeze
+ ALLOWED_KEYS = %i[rules name].freeze
+
+ attributes :name
validations do
validates :config, type: Hash
validates :config, allowed_keys: ALLOWED_KEYS
+ validates :name, allow_nil: true, length: { minimum: 1, maximum: 255 }
end
entry :rules, Entry::Rules,
diff --git a/lib/gitlab/ci/pipeline/chain/populate.rb b/lib/gitlab/ci/pipeline/chain/populate.rb
index 654e24be8e1..4bec8355732 100644
--- a/lib/gitlab/ci/pipeline/chain/populate.rb
+++ b/lib/gitlab/ci/pipeline/chain/populate.rb
@@ -25,6 +25,8 @@ module Gitlab
return error('Failed to build the pipeline!')
end
+ set_pipeline_name
+
raise Populate::PopulateError if pipeline.persisted?
end
@@ -34,6 +36,15 @@ module Gitlab
private
+ def set_pipeline_name
+ return if Feature.disabled?(:pipeline_name, pipeline.project) ||
+ @command.yaml_processor_result.workflow_name.blank?
+
+ name = @command.yaml_processor_result.workflow_name
+
+ pipeline.build_pipeline_metadata(project: pipeline.project, title: name)
+ end
+
def stage_names
# We filter out `.pre/.post` stages, as they alone are not considered
# a complete pipeline:
diff --git a/lib/gitlab/ci/yaml_processor/result.rb b/lib/gitlab/ci/yaml_processor/result.rb
index c0097cd84de..5c3864362da 100644
--- a/lib/gitlab/ci/yaml_processor/result.rb
+++ b/lib/gitlab/ci/yaml_processor/result.rb
@@ -36,6 +36,10 @@ module Gitlab
@workflow_rules ||= @ci_config.workflow_rules
end
+ def workflow_name
+ @workflow_name ||= @ci_config.workflow_name&.strip
+ end
+
def root_variables
@root_variables ||= transform_to_array(@ci_config.variables)
end
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 29ef8d2fa74..814040d29e1 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -56,7 +56,6 @@ module Gitlab
push_frontend_feature_flag(:security_auto_fix)
push_frontend_feature_flag(:new_header_search)
push_frontend_feature_flag(:source_editor_toolbar)
- push_frontend_feature_flag(:gl_listbox_for_sort_dropdowns)
push_frontend_feature_flag(:integration_slack_app_notifications)
end
diff --git a/lib/gitlab/profiler.rb b/lib/gitlab/profiler.rb
index fd9f73d18c1..f8a85f693bc 100644
--- a/lib/gitlab/profiler.rb
+++ b/lib/gitlab/profiler.rb
@@ -43,12 +43,9 @@ module Gitlab
# - private_token: instead of providing a user instance, the token can be
# given as a string. Takes precedence over the user option.
#
- # - sampling_mode: When true, uses a sampling profiler (StackProf) instead of a tracing profiler (RubyProf).
- #
- # - profiler_options: A keyword Hash of arguments passed to the profiler. Defaults by profiler type:
- # RubyProf - {}
- # StackProf - { mode: :wall, out: <some temporary file>, interval: 1000, raw: true }
- def self.profile(url, logger: nil, post_data: nil, user: nil, private_token: nil, sampling_mode: false, profiler_options: {})
+ # - profiler_options: A keyword Hash of arguments passed to the profiler. Defaults:
+ # { mode: :wall, out: <some temporary file>, interval: 1000, raw: true }
+ def self.profile(url, logger: nil, post_data: nil, user: nil, private_token: nil, profiler_options: {})
app = ActionDispatch::Integration::Session.new(Rails.application)
verb = :get
headers = {}
@@ -80,7 +77,7 @@ module Gitlab
with_custom_logger(logger) do
with_user(user) do
- with_profiler(sampling_mode, profiler_options) do
+ with_profiler(profiler_options) do
app.public_send(verb, url, params: post_data, headers: headers) # rubocop:disable GitlabSecurity/PublicSend
end
end
@@ -174,21 +171,11 @@ module Gitlab
end
# rubocop: enable CodeReuse/ActiveRecord
- def self.print_by_total_time(result, options = {})
- default_options = { sort_method: :total_time, filter_by: :total_time }
-
- RubyProf::FlatPrinter.new(result).print($stdout, default_options.merge(options))
- end
-
- def self.with_profiler(sampling_mode, profiler_options)
- if sampling_mode
- require 'stackprof'
- args = { mode: :wall, interval: 1000, raw: true }.merge!(profiler_options)
- args[:out] ||= ::Tempfile.new(["profile-#{Time.now.to_i}-", ".dump"]).path
- ::StackProf.run(**args) { yield }
- else
- RubyProf.profile(**profiler_options) { yield }
- end
+ def self.with_profiler(profiler_options)
+ require 'stackprof'
+ args = { mode: :wall, interval: 1000, raw: true }.merge!(profiler_options)
+ args[:out] ||= ::Tempfile.new(["profile-#{Time.now.to_i}-", ".dump"]).path
+ ::StackProf.run(**args) { yield }
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index e40527d7301..6e155e9bc7b 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -45295,9 +45295,6 @@ msgstr ""
msgid "Webhooks|The webhook failed to connect, and is disabled. To re-enable it, check %{strong_start}Recent events%{strong_end} for error details, then test your settings below."
msgstr ""
-msgid "Webhooks|The webhook was triggered more than %{limit} times per minute and is now disabled. To re-enable this webhook, fix the problems shown in %{strong_start}Recent events%{strong_end}, then re-test your settings. %{support_link_start}Contact Support%{support_link_end} if you need help re-enabling your webhook."
-msgstr ""
-
msgid "Webhooks|Trigger"
msgstr ""
@@ -45322,7 +45319,10 @@ msgstr ""
msgid "Webhooks|Webhook fails to connect"
msgstr ""
-msgid "Webhooks|Webhook was automatically disabled"
+msgid "Webhooks|Webhook rate limit has been reached"
+msgstr ""
+
+msgid "Webhooks|Webhooks for %{root_namespace} are now disabled because they've been triggered more than %{limit} times per minute. They'll be automatically re-enabled in the next minute."
msgstr ""
msgid "Webhooks|Wiki page events"
@@ -45731,6 +45731,9 @@ msgstr ""
msgid "WorkItem|Add task"
msgstr ""
+msgid "WorkItem|Add to iteration"
+msgstr ""
+
msgid "WorkItem|Are you sure you want to cancel editing?"
msgstr ""
@@ -45781,9 +45784,18 @@ msgstr ""
msgid "WorkItem|Issue"
msgstr ""
+msgid "WorkItem|Iteration"
+msgstr ""
+
msgid "WorkItem|Learn about tasks."
msgstr ""
+msgid "WorkItem|No iteration"
+msgstr ""
+
+msgid "WorkItem|No matching results"
+msgstr ""
+
msgid "WorkItem|No tasks are currently assigned. Use tasks to break down this issue into smaller parts."
msgstr ""
@@ -45814,6 +45826,9 @@ msgstr ""
msgid "WorkItem|Something went wrong when deleting the task. Please try again."
msgstr ""
+msgid "WorkItem|Something went wrong when fetching iterations. Please try again."
+msgstr ""
+
msgid "WorkItem|Something went wrong when fetching labels. Please try again."
msgstr ""
diff --git a/package.json b/package.json
index 6f4a7c9eaf8..bfeb065cfd1 100644
--- a/package.json
+++ b/package.json
@@ -151,7 +151,7 @@
"mousetrap": "1.6.5",
"papaparse": "^5.3.1",
"patch-package": "^6.4.7",
- "pdfjs-dist": "^2.0.943",
+ "pdfjs-dist": "^2.16.105",
"pikaday": "^1.8.0",
"popper.js": "^1.16.1",
"portal-vue": "^2.1.7",
diff --git a/qa/qa/fixtures/package_managers/maven/project/request_forwarding/gitlab_ci.yaml.erb b/qa/qa/fixtures/package_managers/maven/project/request_forwarding/gitlab_ci.yaml.erb
new file mode 100644
index 00000000000..a41bdc4d650
--- /dev/null
+++ b/qa/qa/fixtures/package_managers/maven/project/request_forwarding/gitlab_ci.yaml.erb
@@ -0,0 +1,8 @@
+install:
+ image: maven:3.6-jdk-11
+ script:
+ - 'mvn install -U -s settings.xml'
+ only:
+ - "<%= imported_project.default_branch %>"
+ tags:
+ - "runner-for-<%= imported_project.name %>" \ No newline at end of file
diff --git a/qa/qa/fixtures/package_managers/maven/project/request_forwarding/settings.xml.erb b/qa/qa/fixtures/package_managers/maven/project/request_forwarding/settings.xml.erb
new file mode 100644
index 00000000000..caf1fc9b761
--- /dev/null
+++ b/qa/qa/fixtures/package_managers/maven/project/request_forwarding/settings.xml.erb
@@ -0,0 +1,23 @@
+<settings>
+ <servers>
+ <server>
+ <id>central-proxy</id>
+ <configuration>
+ <httpHeaders>
+ <property>
+ <name>Private-Token</name>
+ <value><%= personal_access_token %></value>
+ </property>
+ </httpHeaders>
+ </configuration>
+ </server>
+ </servers>
+ <mirrors>
+ <mirror>
+ <id>central-proxy</id>
+ <name>GitLab proxy of central repo</name>
+ <url><%= gitlab_address_with_port %>/api/v4/projects/<%= imported_project.id %>/packages/maven</url>
+ <mirrorOf>central</mirrorOf>
+ </mirror>
+ </mirrors>
+</settings>
diff --git a/qa/qa/mobile/page/main/menu.rb b/qa/qa/mobile/page/main/menu.rb
index 40bb421b383..73d3b9f7982 100644
--- a/qa/qa/mobile/page/main/menu.rb
+++ b/qa/qa/mobile/page/main/menu.rb
@@ -22,10 +22,10 @@ module QA
end
def open_mobile_menu
- if has_no_element?(:user_avatar)
+ if has_no_element?(:user_avatar_content)
Support::Retrier.retry_until do
click_element(:mobile_navbar_button)
- has_element?(:user_avatar)
+ has_element?(:user_avatar_content)
end
end
end
diff --git a/qa/qa/page/admin/menu.rb b/qa/qa/page/admin/menu.rb
index e55e156fb8a..3164676f8e4 100644
--- a/qa/qa/page/admin/menu.rb
+++ b/qa/qa/page/admin/menu.rb
@@ -5,78 +5,79 @@ module QA
module Admin
class Menu < Page::Base
view 'app/views/layouts/nav/sidebar/_admin.html.haml' do
- element :admin_sidebar
- element :admin_sidebar_settings_submenu_content
- element :admin_settings_item
- element :admin_settings_repository_item
- element :admin_settings_general_item
- element :admin_settings_metrics_and_profiling_item
+ element :admin_sidebar_content
+ element :admin_monitoring_menu_link
+ element :admin_monitoring_submenu_content
+ element :admin_overview_submenu_content
+ element :admin_overview_users_link
+ element :admin_overview_groups_link
+ element :admin_settings_menu_link
+ element :admin_settings_submenu_content
+ element :admin_settings_general_link
+ element :admin_settings_integrations_link
+ element :admin_settings_metrics_and_profiling_link
+ element :admin_settings_network_link
element :admin_settings_preferences_link
- element :admin_monitoring_link
- element :admin_sidebar_monitoring_submenu_content
- element :admin_sidebar_overview_submenu_content
- element :users_overview_link
- element :groups_overview_link
- element :integration_settings_link
+ element :admin_settings_repository_link
end
def go_to_preferences_settings
- hover_element(:admin_settings_item) do
- within_submenu(:admin_sidebar_settings_submenu_content) do
+ hover_element(:admin_settings_menu_link) do
+ within_submenu(:admin_settings_submenu_content) do
click_element :admin_settings_preferences_link
end
end
end
def go_to_repository_settings
- hover_element(:admin_settings_item) do
- within_submenu(:admin_sidebar_settings_submenu_content) do
- click_element :admin_settings_repository_item
+ hover_element(:admin_settings_menu_link) do
+ within_submenu(:admin_settings_submenu_content) do
+ click_element :admin_settings_repository_link
end
end
end
def go_to_integration_settings
- hover_element(:admin_settings_item) do
- within_submenu(:admin_sidebar_settings_submenu_content) do
- click_element :integration_settings_link
+ hover_element(:admin_settings_menu_link) do
+ within_submenu(:admin_settings_submenu_content) do
+ click_element :admin_settings_integrations_link
end
end
end
def go_to_general_settings
- hover_element(:admin_settings_item) do
- within_submenu(:admin_sidebar_settings_submenu_content) do
- click_element :admin_settings_general_item
+ hover_element(:admin_settings_menu_link) do
+ within_submenu(:admin_settings_submenu_content) do
+ click_element :admin_settings_general_link
end
end
end
def go_to_metrics_and_profiling_settings
- hover_element(:admin_settings_item) do
- within_submenu(:admin_sidebar_settings_submenu_content) do
- click_element :admin_settings_metrics_and_profiling_item
+ hover_element(:admin_settings_menu_link) do
+ within_submenu(:admin_settings_submenu_content) do
+ click_element :admin_settings_metrics_and_profiling_link
end
end
end
def go_to_network_settings
- hover_element(:admin_settings_item) do
- within_submenu(:admin_sidebar_settings_submenu_content) do
- click_element :admin_settings_network_item
+ hover_element(:admin_settings_menu_link) do
+ within_submenu(:admin_settings_submenu_content) do
+ click_element :admin_settings_network_link
end
end
end
def go_to_users_overview
- within_submenu(:admin_sidebar_overview_submenu_content) do
- click_element :users_overview_link
+ within_submenu(:admin_overview_submenu_content) do
+ click_element :admin_overview_users_link
end
end
def go_to_groups_overview
- within_submenu(:admin_sidebar_overview_submenu_content) do
- click_element :groups_overview_link
+ within_submenu(:admin_overview_submenu_content) do
+ click_element :admin_overview_groups_link
end
end
@@ -92,7 +93,7 @@ module QA
end
def within_sidebar
- within_element(:admin_sidebar) do
+ within_element(:admin_sidebar_content) do
yield
end
end
diff --git a/qa/qa/page/component/clone_panel.rb b/qa/qa/page/component/clone_panel.rb
index a0aea6fe44d..3ea29ff63da 100644
--- a/qa/qa/page/component/clone_panel.rb
+++ b/qa/qa/page/component/clone_panel.rb
@@ -11,18 +11,18 @@ module QA
base.view 'app/views/projects/buttons/_clone.html.haml' do
element :clone_dropdown
- element :clone_options
- element :ssh_clone_url
- element :http_clone_url
+ element :clone_dropdown_content
+ element :ssh_clone_url_content
+ element :http_clone_url_content
end
end
def repository_clone_http_location
- repository_clone_location(:http_clone_url)
+ repository_clone_location(:http_clone_url_content)
end
def repository_clone_ssh_location
- repository_clone_location(:ssh_clone_url)
+ repository_clone_location(:ssh_clone_url_content)
end
private
@@ -31,7 +31,7 @@ module QA
wait_until(reload: false) do
click_element :clone_dropdown
- within_element :clone_options do
+ within_element :clone_dropdown_content do
Git::Location.new(find_element(kind).value)
end
end
diff --git a/qa/qa/page/component/legacy_clone_panel.rb b/qa/qa/page/component/legacy_clone_panel.rb
index f15d159a712..ee372a3f9aa 100644
--- a/qa/qa/page/component/legacy_clone_panel.rb
+++ b/qa/qa/page/component/legacy_clone_panel.rb
@@ -11,8 +11,8 @@ module QA
base.view 'app/views/shared/_clone_panel.html.haml' do
element :clone_dropdown
- element :clone_options_dropdown, '.clone-options-dropdown' # rubocop:disable QA/ElementWithPattern
- element :clone_url, 'text_field_tag :clone_url' # rubocop:disable QA/ElementWithPattern
+ element :clone_dropdown_content
+ element :clone_url_content
end
end
@@ -28,7 +28,7 @@ module QA
end
def repository_location
- Git::Location.new(find('#clone_url').value)
+ Git::Location.new(find_element(:clone_url_content).text)
end
private
@@ -37,7 +37,7 @@ module QA
wait_until(reload: false) do
click_element :clone_dropdown
- page.within('.clone-options-dropdown') do
+ within_element(:clone_dropdown_content) do
click_link(kind)
end
diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb
index aaf10e12e82..2f618224a73 100644
--- a/qa/qa/page/main/menu.rb
+++ b/qa/qa/page/main/menu.rb
@@ -14,7 +14,7 @@ module QA
view 'app/views/layouts/header/_default.html.haml' do
element :navbar, required: true
element :canary_badge_link
- element :user_avatar, required: !QA::Runtime::Env.mobile_layout?
+ element :user_avatar_content, required: !QA::Runtime::Env.mobile_layout?
element :user_menu, required: !QA::Runtime::Env.mobile_layout?
element :stop_impersonation_link
element :issues_shortcut_button, required: !QA::Runtime::Env.mobile_layout?
@@ -184,11 +184,11 @@ module QA
end
def has_personal_area?(wait: Capybara.default_max_wait_time)
- has_element?(:user_avatar, wait: wait)
+ has_element?(:user_avatar_content, wait: wait)
end
def has_no_personal_area?(wait: Capybara.default_max_wait_time)
- has_no_element?(:user_avatar, wait: wait)
+ has_no_element?(:user_avatar_content, wait: wait)
end
def has_admin_area_link?(wait: Capybara.default_max_wait_time)
@@ -227,7 +227,7 @@ module QA
def within_user_menu(&block)
within_top_menu do
- click_element :user_avatar unless has_element?(:user_profile_link, wait: 1)
+ click_element :user_avatar_content unless has_element?(:user_profile_link, wait: 1)
within_element(:user_menu, &block)
end
diff --git a/qa/qa/page/main/terms.rb b/qa/qa/page/main/terms.rb
index 024510c33cf..24f6b03549b 100644
--- a/qa/qa/page/main/terms.rb
+++ b/qa/qa/page/main/terms.rb
@@ -5,7 +5,7 @@ module QA
module Main
class Terms < Page::Base
view 'app/views/layouts/terms.html.haml' do
- element :user_avatar, required: true
+ element :user_avatar_content, required: true
end
view 'app/assets/javascripts/terms/components/app.vue' do
diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_project_level_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_project_level_spec.rb
index a67c6addc4d..c3ddfb25357 100644
--- a/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_project_level_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_project_level_spec.rb
@@ -1,7 +1,11 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Package', :orchestrated, :packages, :object_storage, :reliable do
+ RSpec.describe 'Package', :orchestrated, :packages, :object_storage, :reliable,
+ feature_flag: {
+ name: 'maven_central_request_forwarding',
+ scope: :global
+ } do
describe 'Maven project level endpoint' do
include Runtime::Fixtures
@@ -143,5 +147,103 @@ module QA
end
end
end
+
+ describe 'Maven request forwarding' do
+ include Runtime::Fixtures
+
+ let(:group_id) { 'com.gitlab.qa' }
+ let(:artifact_id) { "maven-#{SecureRandom.hex(8)}" }
+ let(:package_name) { "#{group_id}/#{artifact_id}".tr('.', '/') }
+ let(:package_version) { '1.3.7' }
+ let(:package_type) { 'maven' }
+ let(:personal_access_token) { Runtime::Env.personal_access_token }
+ let(:group) { Resource::Group.fabricate_via_api! }
+
+ let(:gitlab_address_with_port) do
+ uri = URI.parse(Runtime::Scenario.gitlab_address)
+ "#{uri.scheme}://#{uri.host}:#{uri.port}"
+ end
+
+ let(:package) do
+ Resource::Package.init do |package|
+ package.name = package_name
+ package.project = imported_project
+ end
+ end
+
+ let(:runner) do
+ Resource::Runner.fabricate! do |runner|
+ runner.name = "qa-runner-#{Time.now.to_i}"
+ runner.tags = ["runner-for-#{imported_project.name}"]
+ runner.executor = :docker
+ runner.token = group.reload!.runners_token
+ end
+ end
+
+ let(:imported_project) do
+ Resource::ProjectImportedFromURL.fabricate_via_browser_ui! do |project|
+ project.name = "#{package_type}_imported_project"
+ project.group = group
+ project.gitlab_repository_path = 'https://gitlab.com/gitlab-org/quality/imported-projects/maven.git'
+ end
+ end
+
+ before do
+ Runtime::Feature.enable(:maven_central_request_forwarding)
+ Flow::Login.sign_in_unless_signed_in
+
+ imported_project
+ runner
+ end
+
+ after do
+ Runtime::Feature.disable(:maven_central_request_forwarding)
+
+ runner.remove_via_api!
+ package.remove_via_api!
+ imported_project.remove_via_api!
+ end
+
+ it(
+ 'uses GitLab as a mirror of the central proxy',
+ :skip_live_env,
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/375767'
+ ) do
+ Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
+ Resource::Repository::Commit.fabricate_via_api! do |commit|
+ gitlab_ci_yaml = ERB.new(read_fixture('package_managers/maven/project/request_forwarding',
+ 'gitlab_ci.yaml.erb'
+ )
+ )
+ .result(binding)
+ settings_xml = ERB.new(read_fixture('package_managers/maven/project/request_forwarding',
+ 'settings.xml.erb'
+ )
+ )
+ .result(binding)
+
+ commit.project = imported_project
+ commit.commit_message = 'Add files'
+ commit.add_files(
+ [
+ { file_path: '.gitlab-ci.yml', content: gitlab_ci_yaml },
+ { file_path: 'settings.xml', content: settings_xml }
+ ])
+ end
+ end
+
+ imported_project.visit!
+
+ Flow::Pipeline.visit_latest_pipeline
+
+ Page::Project::Pipeline::Show.perform do |pipeline|
+ pipeline.click_job('install')
+ end
+
+ Page::Project::Job::Show.perform do |job|
+ expect(job).to be_successful(timeout: 800)
+ end
+ end
+ end
end
end
diff --git a/rubocop/ext/path_util.rb b/rubocop/ext/path_util.rb
new file mode 100644
index 00000000000..3b54f046c7b
--- /dev/null
+++ b/rubocop/ext/path_util.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module RuboCop
+ module PathUtil
+ def match_path?(pattern, path)
+ case pattern
+ when String
+ matched = if /[*{}]/.match?(pattern)
+ File.fnmatch?(pattern, path, File::FNM_PATHNAME | File::FNM_EXTGLOB)
+ else
+ pattern == path
+ end
+
+ matched || hidden_file_in_not_hidden_dir?(pattern, path)
+ when Regexp
+ begin
+ pattern.match?(path)
+ rescue ArgumentError => e
+ return false if e.message.start_with?('invalid byte sequence')
+
+ raise e
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/ext/variable_force.rb b/rubocop/ext/variable_force.rb
new file mode 100644
index 00000000000..def284513ed
--- /dev/null
+++ b/rubocop/ext/variable_force.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module RuboCop
+ module Ext
+ module VariableForce
+ def scanned_node?(node)
+ scanned_nodes.include?(node)
+ end
+
+ def scanned_nodes
+ @scanned_nodes ||= Set.new.compare_by_identity
+ end
+ end
+ end
+end
+
+RuboCop::Cop::VariableForce.prepend RuboCop::Ext::VariableForce
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index 5a5e76a87e2..6b5491b27fc 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -1,6 +1,11 @@
# rubocop:disable Naming/FileName
# frozen_string_literal: true
+# Performance improvements to be upstreamed soon:
+# See https://gitlab.com/gitlab-org/gitlab/-/issues/377469
+require_relative 'ext/path_util'
+require_relative 'ext/variable_force'
+
# Auto-require all cops under `rubocop/cop/**/*.rb`
Dir[File.join(__dir__, 'cop', '**', '*.rb')].sort.each(&method(:require))
diff --git a/spec/features/dashboard/user_filters_projects_spec.rb b/spec/features/dashboard/user_filters_projects_spec.rb
index cb9188cf171..e25da5854ab 100644
--- a/spec/features/dashboard/user_filters_projects_spec.rb
+++ b/spec/features/dashboard/user_filters_projects_spec.rb
@@ -9,8 +9,6 @@ RSpec.describe 'Dashboard > User filters projects' do
let(:project2) { create(:project, name: 'Treasure', namespace: user2.namespace, created_at: 1.second.ago, updated_at: 1.second.ago) }
before do
- stub_feature_flags(gl_listbox_for_sort_dropdowns: false)
-
project.add_maintainer(user)
sign_in(user)
@@ -147,7 +145,14 @@ RSpec.describe 'Dashboard > User filters projects' do
end
it 'filters any project' do
+ # Selecting the same option in the `GlListbox` does not emit `select` event
+ # and that is why URL update won't be triggered. Given that `Any` is a default option
+ # we need to explicitly switch from some other option (e.g. `Internal`) to `Any`
+ # to trigger the page update
+ select_dropdown_option '#filtered-search-visibility-dropdown > .dropdown', 'Internal', '.dropdown-item'
+
select_dropdown_option '#filtered-search-visibility-dropdown > .dropdown', 'Any', '.dropdown-item'
+
list = page.all('.projects-list .project-name').map(&:text)
expect(list).to contain_exactly("Internal project", "Private project", "Treasure", "Victorialand")
diff --git a/spec/features/groups/labels/sort_labels_spec.rb b/spec/features/groups/labels/sort_labels_spec.rb
index af4d39bc6fa..9d05703aae6 100644
--- a/spec/features/groups/labels/sort_labels_spec.rb
+++ b/spec/features/groups/labels/sort_labels_spec.rb
@@ -9,8 +9,6 @@ RSpec.describe 'Sort labels', :js do
let!(:label2) { create(:group_label, title: 'Bar', description: 'Fusce consequat', group: group) }
before do
- stub_feature_flags(gl_listbox_for_sort_dropdowns: false)
-
group.add_maintainer(user)
sign_in(user)
@@ -30,7 +28,7 @@ RSpec.describe 'Sort labels', :js do
it 'sorts by date' do
click_button 'Name'
- sort_options = find('ul.dropdown-menu').all('li').collect(&:text)
+ sort_options = find('ul[role="listbox"]').all('li').collect(&:text)
expect(sort_options[0]).to eq('Name')
expect(sort_options[1]).to eq('Name, descending')
@@ -39,7 +37,7 @@ RSpec.describe 'Sort labels', :js do
expect(sort_options[4]).to eq('Updated date')
expect(sort_options[5]).to eq('Oldest updated')
- click_button 'Name, descending'
+ find('li', text: 'Name, descending').click
# assert default sorting
within '.other-labels' do
diff --git a/spec/features/groups/milestones_sorting_spec.rb b/spec/features/groups/milestones_sorting_spec.rb
index 631aa940270..125bf9ce3a7 100644
--- a/spec/features/groups/milestones_sorting_spec.rb
+++ b/spec/features/groups/milestones_sorting_spec.rb
@@ -14,7 +14,6 @@ RSpec.describe 'Milestones sorting', :js do
let(:user) { create(:group_member, :maintainer, user: create(:user), group: group ).user }
before do
- stub_feature_flags(gl_listbox_for_sort_dropdowns: false)
sign_in(user)
end
@@ -30,9 +29,9 @@ RSpec.describe 'Milestones sorting', :js do
within '[data-testid=milestone_sort_by_dropdown]' do
click_button 'Due soon'
- expect(find('.gl-new-dropdown-contents').all('.gl-new-dropdown-item-text-wrapper p').map(&:text)).to eq(['Due soon', 'Due later', 'Start soon', 'Start later', 'Name, ascending', 'Name, descending'])
+ expect(find('ul[role="listbox"]').all('li').map(&:text)).to eq(['Due soon', 'Due later', 'Start soon', 'Start later', 'Name, ascending', 'Name, descending'])
- click_button 'Due later'
+ find('li', text: 'Due later').click
expect(page).to have_button('Due later')
end
diff --git a/spec/features/projects/labels/sort_labels_spec.rb b/spec/features/projects/labels/sort_labels_spec.rb
index 6a16f474056..f2f1acd2348 100644
--- a/spec/features/projects/labels/sort_labels_spec.rb
+++ b/spec/features/projects/labels/sort_labels_spec.rb
@@ -9,8 +9,6 @@ RSpec.describe 'Sort labels', :js do
let!(:label2) { create(:label, title: 'Bar', description: 'Fusce consequat', project: project) }
before do
- stub_feature_flags(gl_listbox_for_sort_dropdowns: false)
-
project.add_maintainer(user)
sign_in(user)
@@ -30,7 +28,7 @@ RSpec.describe 'Sort labels', :js do
it 'sorts by date' do
click_button 'Name'
- sort_options = find('ul.dropdown-menu').all('li').collect(&:text)
+ sort_options = find('ul[role="listbox"]').all('li').collect(&:text)
expect(sort_options[0]).to eq('Name')
expect(sort_options[1]).to eq('Name, descending')
@@ -39,7 +37,7 @@ RSpec.describe 'Sort labels', :js do
expect(sort_options[4]).to eq('Updated date')
expect(sort_options[5]).to eq('Oldest updated')
- click_button 'Name, descending'
+ find('li', text: 'Name, descending').click
# assert default sorting
within '.other-labels' do
diff --git a/spec/features/projects/milestones/milestones_sorting_spec.rb b/spec/features/projects/milestones/milestones_sorting_spec.rb
index 5c379ac1034..5ba4289fd11 100644
--- a/spec/features/projects/milestones/milestones_sorting_spec.rb
+++ b/spec/features/projects/milestones/milestones_sorting_spec.rb
@@ -21,7 +21,6 @@ RSpec.describe 'Milestones sorting', :js do
end
before do
- stub_feature_flags(gl_listbox_for_sort_dropdowns: false)
create(:milestone, start_date: 7.days.from_now, due_date: 10.days.from_now, title: "a", project: project)
create(:milestone, start_date: 6.days.from_now, due_date: 11.days.from_now, title: "c", project: project)
create(:milestone, start_date: 5.days.from_now, due_date: 12.days.from_now, title: "b", project: project)
@@ -43,10 +42,10 @@ RSpec.describe 'Milestones sorting', :js do
milestones_for_sort_by.each do |sort_by, expected_milestones|
within '[data-testid=milestone_sort_by_dropdown]' do
click_button selected_sort_order
- milestones = find('.gl-new-dropdown-contents').all('.gl-new-dropdown-item-text-wrapper p').map(&:text)
+ milestones = find('ul[role="listbox"]').all('li').map(&:text)
expect(milestones).to eq(ordered_milestones)
- click_button sort_by
+ find('li', text: sort_by).click
expect(page).to have_button(sort_by)
end
diff --git a/spec/features/projects/user_sorts_projects_spec.rb b/spec/features/projects/user_sorts_projects_spec.rb
index 49b9052ed3c..c40f01f3aa1 100644
--- a/spec/features/projects/user_sorts_projects_spec.rb
+++ b/spec/features/projects/user_sorts_projects_spec.rb
@@ -42,7 +42,6 @@ RSpec.describe 'User sorts projects and order persists' do
context "from explore projects", :js do
before do
- stub_feature_flags(gl_listbox_for_sort_dropdowns: false)
sign_in(user)
visit(explore_projects_path)
find('#sort-projects-dropdown').click
@@ -54,7 +53,6 @@ RSpec.describe 'User sorts projects and order persists' do
context 'from dashboard projects', :js do
before do
- stub_feature_flags(gl_listbox_for_sort_dropdowns: false)
sign_in(user)
visit(dashboard_projects_path)
find('#sort-projects-dropdown').click
diff --git a/spec/features/projects/wikis_spec.rb b/spec/features/projects/wikis_spec.rb
index 8ac17413df3..879ffd2932b 100644
--- a/spec/features/projects/wikis_spec.rb
+++ b/spec/features/projects/wikis_spec.rb
@@ -3,10 +3,6 @@
require "spec_helper"
RSpec.describe 'Project wikis', :js do
- before do
- stub_feature_flags(gl_listbox_for_sort_dropdowns: false)
- end
-
let_it_be(:user) { create(:user) }
let(:wiki) { create(:project_wiki, user: user, project: project) }
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/rules.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/rules.yml
index 37cae6b4264..ef604f707b5 100644
--- a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/rules.yml
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/rules.yml
@@ -15,7 +15,9 @@ rules:changes as array of strings:
# valid workflow:rules:exists
# valid rules:changes:path
+# valid workflow:name
workflow:
+ name: 'Pipeline name'
rules:
- changes:
paths:
diff --git a/spec/frontend/header_search/components/app_spec.js b/spec/frontend/header_search/components/app_spec.js
index 6a138f9a247..b0bfe2b45f0 100644
--- a/spec/frontend/header_search/components/app_spec.js
+++ b/spec/frontend/header_search/components/app_spec.js
@@ -2,6 +2,7 @@ import { GlSearchBoxByType, GlToken, GlIcon } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { mockTracking } from 'helpers/tracking_helper';
import { s__, sprintf } from '~/locale';
import HeaderSearchApp from '~/header_search/components/app.vue';
import HeaderSearchAutocompleteItems from '~/header_search/components/header_search_autocomplete_items.vue';
@@ -360,22 +361,43 @@ describe('HeaderSearchApp', () => {
describe('Header Search Input', () => {
describe('when dropdown is closed', () => {
- it('onFocus opens dropdown', async () => {
+ let trackingSpy;
+
+ beforeEach(() => {
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ });
+
+ it('onFocus opens dropdown and triggers snowplow event', async () => {
expect(findHeaderSearchDropdown().exists()).toBe(false);
findHeaderSearchInput().vm.$emit('focus');
await nextTick();
expect(findHeaderSearchDropdown().exists()).toBe(true);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'focus_input', {
+ label: 'global_search',
+ property: 'top_navigation',
+ });
});
- it('onClick opens dropdown', async () => {
+ it('onClick opens dropdown and triggers snowplow event', async () => {
expect(findHeaderSearchDropdown().exists()).toBe(false);
findHeaderSearchInput().vm.$emit('click');
await nextTick();
expect(findHeaderSearchDropdown().exists()).toBe(true);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'focus_input', {
+ label: 'global_search',
+ property: 'top_navigation',
+ });
+ });
+
+ it('onClick followed by onFocus only triggers a single snowplow event', async () => {
+ findHeaderSearchInput().vm.$emit('click');
+ findHeaderSearchInput().vm.$emit('focus');
+
+ expect(trackingSpy).toHaveBeenCalledTimes(1);
});
});
diff --git a/spec/frontend/listbox/index_spec.js b/spec/frontend/listbox/index_spec.js
index c973960e683..fd41531796b 100644
--- a/spec/frontend/listbox/index_spec.js
+++ b/spec/frontend/listbox/index_spec.js
@@ -1,6 +1,6 @@
import { nextTick } from 'vue';
-import { getAllByRole, getByRole, getByTestId } from '@testing-library/dom';
-import { GlDropdown, GlListbox } from '@gitlab/ui';
+import { getAllByRole, getByTestId } from '@testing-library/dom';
+import { GlListbox } from '@gitlab/ui';
import { createWrapper } from '@vue/test-utils';
import { initListbox, parseAttributes } from '~/listbox';
import { getFixture, setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
@@ -39,141 +39,65 @@ describe('initListbox', () => {
});
describe('given a valid element', () => {
- describe('when `glListboxForSortDropdowns` FF is enabled', () => {
- let onChangeSpy;
+ let onChangeSpy;
- const listbox = () => createWrapper(instance).findComponent(GlListbox);
- const findToggleButton = () => getByTestId(document.body, 'base-dropdown-toggle');
- const findSelectedItems = () => getAllByRole(document.body, 'option', { selected: true });
+ const listbox = () => createWrapper(instance).findComponent(GlListbox);
+ const findToggleButton = () => getByTestId(document.body, 'base-dropdown-toggle');
+ const findSelectedItems = () => getAllByRole(document.body, 'option', { selected: true });
- beforeEach(async () => {
- window.gon.features = { glListboxForSortDropdowns: true };
- setHTMLFixture(fixture);
- onChangeSpy = jest.fn();
- setup(document.querySelector('.js-redirect-listbox'), { onChange: onChangeSpy });
-
- await nextTick();
- });
+ beforeEach(async () => {
+ setHTMLFixture(fixture);
+ onChangeSpy = jest.fn();
+ setup(document.querySelector('.js-redirect-listbox'), { onChange: onChangeSpy });
- afterEach(() => {
- resetHTMLFixture();
- });
-
- it('returns an instance', () => {
- expect(instance).not.toBe(null);
- });
+ await nextTick();
+ });
- it('renders button with selected item text', () => {
- expect(findToggleButton().textContent.trim()).toBe('Bar');
- });
+ afterEach(() => {
+ resetHTMLFixture();
+ });
- it('has the correct item selected', () => {
- const selectedItems = findSelectedItems();
- expect(selectedItems).toHaveLength(1);
- expect(selectedItems[0].textContent.trim()).toBe('Bar');
- });
+ it('returns an instance', () => {
+ expect(instance).not.toBe(null);
+ });
- it('applies additional classes from the original element', () => {
- expect(instance.$el.classList).toContain('test-class-1', 'test-class-2');
- });
+ it('renders button with selected item text', () => {
+ expect(findToggleButton().textContent.trim()).toBe('Bar');
+ });
- describe.each(parsedAttributes.items)('selecting an item', (item) => {
- beforeEach(async () => {
- listbox().vm.$emit('select', item.value);
- await nextTick();
- });
-
- it('calls the onChange callback with the item', () => {
- expect(onChangeSpy).toHaveBeenCalledWith(item);
- });
-
- it('updates the toggle button text', () => {
- expect(findToggleButton().textContent.trim()).toBe(item.text);
- });
-
- it('marks the item as selected', () => {
- const selectedItems = findSelectedItems();
- expect(selectedItems).toHaveLength(1);
- expect(selectedItems[0].textContent.trim()).toBe(item.text);
- });
- });
+ it('has the correct item selected', () => {
+ const selectedItems = findSelectedItems();
+ expect(selectedItems).toHaveLength(1);
+ expect(selectedItems[0].textContent.trim()).toBe('Bar');
+ });
- it('passes the "right" prop through to the underlying component', () => {
- expect(listbox().props('right')).toBe(parsedAttributes.right);
- });
+ it('applies additional classes from the original element', () => {
+ expect(instance.$el.classList).toContain('test-class-1', 'test-class-2');
});
- describe('when `glListboxForSortDropdowns` FF is disabled', () => {
- let onChangeSpy;
-
- const ITEM_ROLE = 'menuitem';
- const dropdown = () => createWrapper(instance).findComponent(GlDropdown);
-
- const findToggleButton = () => document.body.querySelector('.gl-dropdown-toggle');
- const findItem = (text) => getByRole(document.body, ITEM_ROLE, { name: text });
- const findItems = () => getAllByRole(document.body, ITEM_ROLE);
- const findSelectedItems = () =>
- findItems().filter(
- (item) =>
- !item
- .querySelector('.gl-new-dropdown-item-check-icon')
- .classList.contains('gl-visibility-hidden'),
- );
+ describe.each(parsedAttributes.items)('selecting an item', (item) => {
beforeEach(async () => {
- window.gon.features = { glListboxForSortDropdowns: false };
- setHTMLFixture(fixture);
- onChangeSpy = jest.fn();
- setup(document.querySelector('.js-redirect-listbox'), { onChange: onChangeSpy });
-
+ listbox().vm.$emit('select', item.value);
await nextTick();
});
- afterEach(() => {
- resetHTMLFixture();
- });
-
- it('returns an instance', () => {
- expect(instance).not.toBe(null);
+ it('calls the onChange callback with the item', () => {
+ expect(onChangeSpy).toHaveBeenCalledWith(item);
});
- it('renders button with selected item text', () => {
- expect(findToggleButton().textContent.trim()).toBe('Bar');
+ it('updates the toggle button text', () => {
+ expect(findToggleButton().textContent.trim()).toBe(item.text);
});
- it('has the correct item selected', () => {
+ it('marks the item as selected', () => {
const selectedItems = findSelectedItems();
expect(selectedItems).toHaveLength(1);
- expect(selectedItems[0].textContent.trim()).toBe('Bar');
- });
-
- it('applies additional classes from the original element', () => {
- expect(instance.$el.classList).toContain('test-class-1', 'test-class-2');
- });
-
- describe.each(parsedAttributes.items)('selecting an item', (item) => {
- beforeEach(async () => {
- findItem(item.text).click();
- await nextTick();
- });
-
- it('calls the onChange callback with the item', () => {
- expect(onChangeSpy).toHaveBeenCalledWith(item);
- });
-
- it('updates the toggle button text', () => {
- expect(findToggleButton().textContent.trim()).toBe(item.text);
- });
-
- it('marks the item as selected', () => {
- const selectedItems = findSelectedItems();
- expect(selectedItems).toHaveLength(1);
- expect(selectedItems[0].textContent.trim()).toBe(item.text);
- });
+ expect(selectedItems[0].textContent.trim()).toBe(item.text);
});
+ });
- it('passes the "right" prop through to the underlying component', () => {
- expect(dropdown().props('right')).toBe(parsedAttributes.right);
- });
+ it('passes the "right" prop through to the underlying component', () => {
+ expect(listbox().props('right')).toBe(parsedAttributes.right);
});
});
});
diff --git a/spec/frontend/nav/components/top_nav_app_spec.js b/spec/frontend/nav/components/top_nav_app_spec.js
index 745707c1d28..b32ab5ebe09 100644
--- a/spec/frontend/nav/components/top_nav_app_spec.js
+++ b/spec/frontend/nav/components/top_nav_app_spec.js
@@ -1,5 +1,6 @@
import { GlNavItemDropdown } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { mount, shallowMount } from '@vue/test-utils';
+import { mockTracking } from 'helpers/tracking_helper';
import TopNavApp from '~/nav/components/top_nav_app.vue';
import TopNavDropdownMenu from '~/nav/components/top_nav_dropdown_menu.vue';
import { TEST_NAV_DATA } from '../mock_data';
@@ -8,6 +9,14 @@ describe('~/nav/components/top_nav_app.vue', () => {
let wrapper;
const createComponent = () => {
+ wrapper = mount(TopNavApp, {
+ propsData: {
+ navData: TEST_NAV_DATA,
+ },
+ });
+ };
+
+ const createComponentShallow = () => {
wrapper = shallowMount(TopNavApp, {
propsData: {
navData: TEST_NAV_DATA,
@@ -16,6 +25,7 @@ describe('~/nav/components/top_nav_app.vue', () => {
};
const findNavItemDropdown = () => wrapper.findComponent(GlNavItemDropdown);
+ const findNavItemDropdowToggle = () => findNavItemDropdown().find('.js-top-nav-dropdown-toggle');
const findMenu = () => wrapper.findComponent(TopNavDropdownMenu);
afterEach(() => {
@@ -24,7 +34,7 @@ describe('~/nav/components/top_nav_app.vue', () => {
describe('default', () => {
beforeEach(() => {
- createComponent();
+ createComponentShallow();
});
it('renders nav item dropdown', () => {
@@ -45,4 +55,18 @@ describe('~/nav/components/top_nav_app.vue', () => {
});
});
});
+
+ describe('tracking', () => {
+ it('emits a tracking event when the toggle is clicked', () => {
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ createComponent();
+
+ findNavItemDropdowToggle().trigger('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_nav', {
+ label: 'hamburger_menu',
+ property: 'top_navigation',
+ });
+ });
+ });
});
diff --git a/spec/frontend/pdf/page_spec.js b/spec/frontend/pdf/page_spec.js
index 608516385d9..4cf83a3252d 100644
--- a/spec/frontend/pdf/page_spec.js
+++ b/spec/frontend/pdf/page_spec.js
@@ -3,7 +3,7 @@ import { mount } from '@vue/test-utils';
import PageComponent from '~/pdf/page/index.vue';
jest.mock('pdfjs-dist/webpack', () => {
- return { default: jest.requireActual('pdfjs-dist/build/pdf') };
+ return { default: jest.requireActual('pdfjs-dist/legacy/build/pdf') };
});
describe('Page component', () => {
diff --git a/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js b/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js
index 7ab4aeee9bc..64f66d8f3ba 100644
--- a/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js
+++ b/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js
@@ -104,6 +104,10 @@ describe('AdminRunnerShowApp', () => {
Platform darwin
Configuration Runs untagged jobs
Maximum job timeout None
+ Token expiry
+ Runner authentication token expiration
+ Runner authentication tokens will expire based on a set interval.
+ They will automatically rotate once expired. Learn more Never expires
Tags None`.replace(/\s+/g, ' ');
expect(wrapper.text().replace(/\s+/g, ' ')).toContain(expected);
diff --git a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
index 25924296f00..7afde3bdc96 100644
--- a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
+++ b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
@@ -20,8 +20,6 @@ import AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue';
import RunnerStackedLayoutBanner from '~/runner/components/runner_stacked_layout_banner.vue';
import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
-import RunnerBulkDelete from '~/runner/components/runner_bulk_delete.vue';
-import RunnerBulkDeleteCheckbox from '~/runner/components/runner_bulk_delete_checkbox.vue';
import RunnerList from '~/runner/components/runner_list.vue';
import RunnerListEmptyState from '~/runner/components/runner_list_empty_state.vue';
import RunnerStats from '~/runner/components/stat/runner_stats.vue';
@@ -84,8 +82,6 @@ const COUNT_QUERIES = 7; // 4 tabs + 3 status queries
describe('AdminRunnersApp', () => {
let wrapper;
- let cacheConfig;
- let localMutations;
let showToast;
const findRunnerStackedLayoutBanner = () => wrapper.findComponent(RunnerStackedLayoutBanner);
@@ -93,8 +89,6 @@ describe('AdminRunnersApp', () => {
const findRunnerActionsCell = () => wrapper.findComponent(RunnerActionsCell);
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs);
- const findRunnerBulkDelete = () => wrapper.findComponent(RunnerBulkDelete);
- const findRunnerBulkDeleteCheckbox = () => wrapper.findComponent(RunnerBulkDeleteCheckbox);
const findRunnerList = () => wrapper.findComponent(RunnerList);
const findRunnerListEmptyState = () => wrapper.findComponent(RunnerListEmptyState);
const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
@@ -107,7 +101,7 @@ describe('AdminRunnersApp', () => {
provide,
...options
} = {}) => {
- ({ cacheConfig, localMutations } = createLocalState());
+ const { cacheConfig, localMutations } = createLocalState();
const handlers = [
[allRunnersQuery, mockRunnersHandler],
@@ -373,38 +367,9 @@ describe('AdminRunnersApp', () => {
await createComponent({ mountFn: mountExtended });
});
- it('runner bulk delete is available', () => {
- expect(findRunnerBulkDelete().props('runners')).toEqual(mockRunners);
- });
-
- it('runner bulk delete checkbox is available', () => {
- expect(findRunnerBulkDeleteCheckbox().props('runners')).toEqual(mockRunners);
- });
-
it('runner list is checkable', () => {
expect(findRunnerList().props('checkable')).toBe(true);
});
-
- it('responds to checked items by updating the local cache', () => {
- const setRunnerCheckedMock = jest
- .spyOn(localMutations, 'setRunnerChecked')
- .mockImplementation(() => {});
-
- const runner = mockRunners[0];
-
- expect(setRunnerCheckedMock).toHaveBeenCalledTimes(0);
-
- findRunnerList().vm.$emit('checked', {
- runner,
- isChecked: true,
- });
-
- expect(setRunnerCheckedMock).toHaveBeenCalledTimes(1);
- expect(setRunnerCheckedMock).toHaveBeenCalledWith({
- runner,
- isChecked: true,
- });
- });
});
describe('When runners are deleted', () => {
@@ -415,7 +380,7 @@ describe('AdminRunnersApp', () => {
it('count data is refetched', async () => {
expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES);
- findRunnerBulkDelete().vm.$emit('deleted', { message: 'Runners deleted' });
+ findRunnerList().vm.$emit('deleted', { message: 'Runners deleted' });
expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES * 2);
});
@@ -423,7 +388,7 @@ describe('AdminRunnersApp', () => {
it('toast is shown', async () => {
expect(showToast).toHaveBeenCalledTimes(0);
- findRunnerBulkDelete().vm.$emit('deleted', { message: 'Runners deleted' });
+ findRunnerList().vm.$emit('deleted', { message: 'Runners deleted' });
expect(showToast).toHaveBeenCalledTimes(1);
expect(showToast).toHaveBeenCalledWith('Runners deleted');
diff --git a/spec/frontend/runner/components/runner_bulk_delete_checkbox_spec.js b/spec/frontend/runner/components/runner_bulk_delete_checkbox_spec.js
index 0ac89e82314..424a4e61ccd 100644
--- a/spec/frontend/runner/components/runner_bulk_delete_checkbox_spec.js
+++ b/spec/frontend/runner/components/runner_bulk_delete_checkbox_spec.js
@@ -5,11 +5,21 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import RunnerBulkDeleteCheckbox from '~/runner/components/runner_bulk_delete_checkbox.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import { createLocalState } from '~/runner/graphql/list/local_state';
-import { allRunnersData } from '../mock_data';
Vue.use(VueApollo);
-jest.mock('~/flash');
+const makeRunner = (id, deleteRunner = true) => ({
+ id,
+ userPermissions: { deleteRunner },
+});
+
+// Multi-select checkbox possible states:
+const stateToAttrs = {
+ unchecked: { disabled: undefined, checked: undefined, indeterminate: undefined },
+ checked: { disabled: undefined, checked: 'true', indeterminate: undefined },
+ indeterminate: { disabled: undefined, checked: undefined, indeterminate: 'true' },
+ disabled: { disabled: 'true', checked: undefined, indeterminate: undefined },
+};
describe('RunnerBulkDeleteCheckbox', () => {
let wrapper;
@@ -18,12 +28,14 @@ describe('RunnerBulkDeleteCheckbox', () => {
const findCheckbox = () => wrapper.findComponent(GlFormCheckbox);
- const mockRunners = allRunnersData.data.runners.nodes;
- const mockIds = allRunnersData.data.runners.nodes.map(({ id }) => id);
- const mockId = mockIds[0];
- const mockIdAnotherPage = 'RUNNER_IN_ANOTHER_PAGE_ID';
+ const expectCheckboxToBe = (state) => {
+ const expected = stateToAttrs[state];
+ expect(findCheckbox().attributes('disabled')).toBe(expected.disabled);
+ expect(findCheckbox().attributes('checked')).toBe(expected.checked);
+ expect(findCheckbox().attributes('indeterminate')).toBe(expected.indeterminate);
+ };
- const createComponent = ({ props = {} } = {}) => {
+ const createComponent = ({ runners = [] } = {}) => {
const { cacheConfig, localMutations } = mockState;
const apolloProvider = createMockApollo(undefined, undefined, cacheConfig);
@@ -33,8 +45,7 @@ describe('RunnerBulkDeleteCheckbox', () => {
localMutations,
},
propsData: {
- runners: mockRunners,
- ...props,
+ runners,
},
});
};
@@ -49,31 +60,61 @@ describe('RunnerBulkDeleteCheckbox', () => {
jest.spyOn(mockState.localMutations, 'setRunnersChecked');
});
- describe.each`
- case | is | checkedRunnerIds | disabled | checked | indeterminate
- ${'no runners'} | ${'unchecked'} | ${[]} | ${undefined} | ${undefined} | ${undefined}
- ${'no runners in this page'} | ${'unchecked'} | ${[mockIdAnotherPage]} | ${undefined} | ${undefined} | ${undefined}
- ${'all runners'} | ${'checked'} | ${mockIds} | ${undefined} | ${'true'} | ${undefined}
- ${'some runners'} | ${'indeterminate'} | ${[mockId]} | ${undefined} | ${undefined} | ${'true'}
- ${'all plus other runners'} | ${'checked'} | ${[...mockIds, mockIdAnotherPage]} | ${undefined} | ${'true'} | ${undefined}
- `('When $case are checked', ({ is, checkedRunnerIds, disabled, checked, indeterminate }) => {
- beforeEach(async () => {
+ describe('when all runners can be deleted', () => {
+ const mockIds = ['1', '2', '3'];
+ const mockIdAnotherPage = '4';
+ const mockRunners = mockIds.map((id) => makeRunner(id));
+
+ it.each`
+ case | checkedRunnerIds | state
+ ${'no runners'} | ${[]} | ${'unchecked'}
+ ${'no runners in this page'} | ${[mockIdAnotherPage]} | ${'unchecked'}
+ ${'all runners'} | ${mockIds} | ${'checked'}
+ ${'some runners'} | ${[mockIds[0]]} | ${'indeterminate'}
+ ${'all plus other runners'} | ${[...mockIds, mockIdAnotherPage]} | ${'checked'}
+ `('if $case are checked, checkbox is $state', ({ checkedRunnerIds, state }) => {
mockCheckedRunnerIds = checkedRunnerIds;
- createComponent();
+ createComponent({ runners: mockRunners });
+ expectCheckboxToBe(state);
});
+ });
+
+ describe('when some runners cannot be deleted', () => {
+ it('all allowed runners are selected, checkbox is checked', () => {
+ mockCheckedRunnerIds = ['a', 'b', 'c'];
+ createComponent({
+ runners: [makeRunner('a'), makeRunner('b'), makeRunner('c', false)],
+ });
- it(`is ${is}`, () => {
- expect(findCheckbox().attributes('disabled')).toBe(disabled);
- expect(findCheckbox().attributes('checked')).toBe(checked);
- expect(findCheckbox().attributes('indeterminate')).toBe(indeterminate);
+ expectCheckboxToBe('checked');
+ });
+
+ it('some allowed runners are selected, checkbox is indeterminate', () => {
+ mockCheckedRunnerIds = ['a', 'b'];
+ createComponent({
+ runners: [makeRunner('a'), makeRunner('b'), makeRunner('c')],
+ });
+
+ expectCheckboxToBe('indeterminate');
+ });
+
+ it('no allowed runners are selected, checkbox is disabled', () => {
+ mockCheckedRunnerIds = ['a', 'b'];
+ createComponent({
+ runners: [makeRunner('a', false), makeRunner('b', false)],
+ });
+
+ expectCheckboxToBe('disabled');
});
});
describe('When user selects', () => {
+ const mockRunners = [makeRunner('1'), makeRunner('2')];
+
beforeEach(() => {
- mockCheckedRunnerIds = mockIds;
- createComponent();
+ mockCheckedRunnerIds = ['1', '2'];
+ createComponent({ runners: mockRunners });
});
it.each([[true], [false]])('sets checked to %s', (checked) => {
@@ -89,13 +130,11 @@ describe('RunnerBulkDeleteCheckbox', () => {
describe('When runners are loading', () => {
beforeEach(() => {
- createComponent({ props: { runners: [] } });
+ createComponent();
});
- it(`is disabled`, () => {
- expect(findCheckbox().attributes('disabled')).toBe('true');
- expect(findCheckbox().attributes('checked')).toBe(undefined);
- expect(findCheckbox().attributes('indeterminate')).toBe(undefined);
+ it('is disabled', () => {
+ expectCheckboxToBe('disabled');
});
});
});
diff --git a/spec/frontend/runner/components/runner_details_spec.js b/spec/frontend/runner/components/runner_details_spec.js
index f2281223a25..e6cc936e260 100644
--- a/spec/frontend/runner/components/runner_details_spec.js
+++ b/spec/frontend/runner/components/runner_details_spec.js
@@ -25,12 +25,7 @@ describe('RunnerDetails', () => {
const findDetailGroups = () => wrapper.findComponent(RunnerGroups);
- const createComponent = ({
- props = {},
- stubs,
- mountFn = shallowMountExtended,
- enforceRunnerTokenExpiresAt = false,
- } = {}) => {
+ const createComponent = ({ props = {}, stubs, mountFn = shallowMountExtended } = {}) => {
wrapper = mountFn(RunnerDetails, {
propsData: {
...props,
@@ -39,9 +34,6 @@ describe('RunnerDetails', () => {
RunnerDetail,
...stubs,
},
- provide: {
- glFeatures: { enforceRunnerTokenExpiresAt },
- },
});
};
@@ -82,7 +74,6 @@ describe('RunnerDetails', () => {
...runner,
},
},
- enforceRunnerTokenExpiresAt: true,
stubs: {
GlIntersperse,
GlSprintf,
@@ -135,22 +126,5 @@ describe('RunnerDetails', () => {
expect(findDetailGroups().props('runner')).toEqual(mockGroupRunner);
});
});
-
- describe('Token expiration field', () => {
- it.each`
- case | flag | shown
- ${'is shown when feature flag is enabled'} | ${true} | ${true}
- ${'is not shown when feature flag is disabled'} | ${false} | ${false}
- `('$case', ({ flag, shown }) => {
- createComponent({
- props: {
- runner: mockGroupRunner,
- },
- enforceRunnerTokenExpiresAt: flag,
- });
-
- expect(findDd('Token expiry', wrapper).exists()).toBe(shown);
- });
- });
});
});
diff --git a/spec/frontend/runner/components/runner_list_spec.js b/spec/frontend/runner/components/runner_list_spec.js
index f20644a9269..a31990f8f7e 100644
--- a/spec/frontend/runner/components/runner_list_spec.js
+++ b/spec/frontend/runner/components/runner_list_spec.js
@@ -5,9 +5,15 @@ import {
shallowMountExtended,
mountExtended,
} from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { createLocalState } from '~/runner/graphql/list/local_state';
+
import RunnerList from '~/runner/components/runner_list.vue';
+import RunnerBulkDelete from '~/runner/components/runner_bulk_delete.vue';
+import RunnerBulkDeleteCheckbox from '~/runner/components/runner_bulk_delete_checkbox.vue';
+
import { I18N_PROJECT_TYPE, I18N_STATUS_NEVER_CONTACTED } from '~/runner/constants';
import { allRunnersData, onlineContactTimeoutSecs, staleTimeoutSecs } from '../mock_data';
@@ -16,6 +22,8 @@ const mockActiveRunnersCount = mockRunners.length;
describe('RunnerList', () => {
let wrapper;
+ let cacheConfig;
+ let localMutations;
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findTable = () => wrapper.findComponent(GlTableLite);
@@ -23,18 +31,24 @@ describe('RunnerList', () => {
const findRows = () => wrapper.findAll('[data-testid^="runner-row-"]');
const findCell = ({ row = 0, fieldKey }) =>
extendedWrapper(findRows().at(row).find(`[data-testid="td-${fieldKey}"]`));
+ const findRunnerBulkDelete = () => wrapper.findComponent(RunnerBulkDelete);
+ const findRunnerBulkDeleteCheckbox = () => wrapper.findComponent(RunnerBulkDeleteCheckbox);
const createComponent = (
{ props = {}, provide = {}, ...options } = {},
mountFn = shallowMountExtended,
) => {
+ ({ cacheConfig, localMutations } = createLocalState());
+
wrapper = mountFn(RunnerList, {
+ apolloProvider: createMockApollo([], {}, cacheConfig),
propsData: {
runners: mockRunners,
activeRunnersCount: mockActiveRunnersCount,
...props,
},
provide: {
+ localMutations,
onlineContactTimeoutSecs,
staleTimeoutSecs,
...provide,
@@ -126,21 +140,40 @@ describe('RunnerList', () => {
);
});
+ it('runner bulk delete is available', () => {
+ expect(findRunnerBulkDelete().props('runners')).toEqual(mockRunners);
+ });
+
+ it('runner bulk delete checkbox is available', () => {
+ expect(findRunnerBulkDeleteCheckbox().props('runners')).toEqual(mockRunners);
+ });
+
it('Displays a checkbox field', () => {
expect(findCell({ fieldKey: 'checkbox' }).find('input').exists()).toBe(true);
});
- it('Emits a checked event', async () => {
- const checkbox = findCell({ fieldKey: 'checkbox' }).find('input');
+ it('Sets a runner as checked', async () => {
+ const runner = mockRunners[0];
+ const setRunnerCheckedMock = jest
+ .spyOn(localMutations, 'setRunnerChecked')
+ .mockImplementation(() => {});
+ const checkbox = findCell({ fieldKey: 'checkbox' }).find('input');
await checkbox.setChecked();
- expect(wrapper.emitted('checked')).toHaveLength(1);
- expect(wrapper.emitted('checked')[0][0]).toEqual({
+ expect(setRunnerCheckedMock).toHaveBeenCalledTimes(1);
+ expect(setRunnerCheckedMock).toHaveBeenCalledWith({
+ runner,
isChecked: true,
- runner: mockRunners[0],
});
});
+
+ it('Emits a deleted event', async () => {
+ const event = { message: 'Deleted!' };
+ findRunnerBulkDelete().vm.$emit('deleted', event);
+
+ expect(wrapper.emitted('deleted')).toEqual([[event]]);
+ });
});
describe('Scoped cell slots', () => {
diff --git a/spec/frontend/runner/graphql/local_state_spec.js b/spec/frontend/runner/graphql/local_state_spec.js
index ae874fef00d..915170b53f9 100644
--- a/spec/frontend/runner/graphql/local_state_spec.js
+++ b/spec/frontend/runner/graphql/local_state_spec.js
@@ -4,6 +4,13 @@ import { createLocalState } from '~/runner/graphql/list/local_state';
import getCheckedRunnerIdsQuery from '~/runner/graphql/list/checked_runner_ids.query.graphql';
import { RUNNER_TYPENAME } from '~/runner/constants';
+const makeRunner = (id, deleteRunner = true) => ({
+ id,
+ userPermissions: {
+ deleteRunner,
+ },
+});
+
describe('~/runner/graphql/list/local_state', () => {
let localState;
let apolloClient;
@@ -57,16 +64,21 @@ describe('~/runner/graphql/list/local_state', () => {
});
it('returns checked runners that have a reference in the cache', () => {
- addMockRunnerToCache('a');
- localState.localMutations.setRunnerChecked({ runner: { id: 'a' }, isChecked: true });
+ const id = 'a';
+
+ addMockRunnerToCache(id);
+ localState.localMutations.setRunnerChecked({
+ runner: makeRunner(id),
+ isChecked: true,
+ });
expect(queryCheckedRunnerIds()).toEqual(['a']);
});
it('return checked runners that are not dangling references', () => {
addMockRunnerToCache('a'); // 'b' is missing from the cache, perhaps because it was deleted
- localState.localMutations.setRunnerChecked({ runner: { id: 'a' }, isChecked: true });
- localState.localMutations.setRunnerChecked({ runner: { id: 'b' }, isChecked: true });
+ localState.localMutations.setRunnerChecked({ runner: makeRunner('a'), isChecked: true });
+ localState.localMutations.setRunnerChecked({ runner: makeRunner('b'), isChecked: true });
expect(queryCheckedRunnerIds()).toEqual(['a']);
});
@@ -81,7 +93,7 @@ describe('~/runner/graphql/list/local_state', () => {
beforeEach(() => {
inputs.forEach(([id, isChecked]) => {
addMockRunnerToCache(id);
- localState.localMutations.setRunnerChecked({ runner: { id }, isChecked });
+ localState.localMutations.setRunnerChecked({ runner: makeRunner(id), isChecked });
});
});
it(`for inputs="${inputs}" has a ids="[${expected}]"`, () => {
@@ -102,7 +114,7 @@ describe('~/runner/graphql/list/local_state', () => {
ids.forEach(addMockRunnerToCache);
localState.localMutations.setRunnersChecked({
- runners: ids.map((id) => ({ id })),
+ runners: ids.map((id) => makeRunner(id)),
isChecked,
});
});
@@ -117,7 +129,7 @@ describe('~/runner/graphql/list/local_state', () => {
it('clears all checked items', () => {
['a', 'b', 'c'].forEach((id) => {
addMockRunnerToCache(id);
- localState.localMutations.setRunnerChecked({ runner: { id }, isChecked: true });
+ localState.localMutations.setRunnerChecked({ runner: makeRunner(id), isChecked: true });
});
expect(queryCheckedRunnerIds()).toEqual(['a', 'b', 'c']);
@@ -127,4 +139,29 @@ describe('~/runner/graphql/list/local_state', () => {
expect(queryCheckedRunnerIds()).toEqual([]);
});
});
+
+ describe('when some runners cannot be deleted', () => {
+ beforeEach(() => {
+ addMockRunnerToCache('a');
+ addMockRunnerToCache('b');
+ });
+
+ it('setRunnerChecked does not check runner that cannot be deleted', () => {
+ localState.localMutations.setRunnerChecked({
+ runner: makeRunner('a', false),
+ isChecked: true,
+ });
+
+ expect(queryCheckedRunnerIds()).toEqual([]);
+ });
+
+ it('setRunnersChecked does not check runner that cannot be deleted', () => {
+ localState.localMutations.setRunnersChecked({
+ runners: [makeRunner('a', false), makeRunner('b', false)],
+ isChecked: true,
+ });
+
+ expect(queryCheckedRunnerIds()).toEqual([]);
+ });
+ });
});
diff --git a/spec/frontend/runner/group_runner_show/group_runner_show_app_spec.js b/spec/frontend/runner/group_runner_show/group_runner_show_app_spec.js
index cee1d436942..a3b67674c94 100644
--- a/spec/frontend/runner/group_runner_show/group_runner_show_app_spec.js
+++ b/spec/frontend/runner/group_runner_show/group_runner_show_app_spec.js
@@ -101,6 +101,11 @@ describe('GroupRunnerShowApp', () => {
Platform darwin
Configuration Runs untagged jobs
Maximum job timeout None
+ Token expiry
+ Runner authentication token expiration
+ Runner authentication tokens will expire based on a set interval.
+ They will automatically rotate once expired. Learn more
+ Never expires
Tags None`.replace(/\s+/g, ' ');
expect(wrapper.text().replace(/\s+/g, ' ')).toContain(expected);
diff --git a/spec/frontend/runner/group_runners/group_runners_app_spec.js b/spec/frontend/runner/group_runners/group_runners_app_spec.js
index 5f355e27d9e..7482926e151 100644
--- a/spec/frontend/runner/group_runners/group_runners_app_spec.js
+++ b/spec/frontend/runner/group_runners/group_runners_app_spec.js
@@ -14,6 +14,7 @@ import { s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory } from '~/lib/utils/url_utility';
import { upgradeStatusTokenConfig } from 'ee_else_ce/runner/components/search_tokens/upgrade_status_token_config';
+import { createLocalState } from '~/runner/graphql/list/local_state';
import RunnerStackedLayoutBanner from '~/runner/components/runner_stacked_layout_banner.vue';
import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
@@ -101,13 +102,15 @@ describe('GroupRunnersApp', () => {
mountFn = shallowMountExtended,
...options
} = {}) => {
+ const { cacheConfig, localMutations } = createLocalState();
+
const handlers = [
[groupRunnersQuery, mockGroupRunnersHandler],
[groupRunnersCountQuery, mockGroupRunnersCountHandler],
];
wrapper = mountFn(GroupRunnersApp, {
- apolloProvider: createMockApollo(handlers),
+ apolloProvider: createMockApollo(handlers, {}, cacheConfig),
propsData: {
registrationToken: mockRegistrationToken,
groupFullPath: mockGroupFullPath,
@@ -115,6 +118,7 @@ describe('GroupRunnersApp', () => {
...props,
},
provide: {
+ localMutations,
onlineContactTimeoutSecs,
staleTimeoutSecs,
emptyStateSvgPath,
@@ -260,7 +264,7 @@ describe('GroupRunnersApp', () => {
const { id: graphqlId, shortSha } = node;
const id = getIdFromGraphQLId(graphqlId);
const COUNT_QUERIES = 6; // Smart queries that display a filtered count of runners
- const FILTERED_COUNT_QUERIES = 3; // Smart queries that display a count of runners in tabs
+ const FILTERED_COUNT_QUERIES = 6; // Smart queries that display a count of runners in tabs and single stats
beforeEach(async () => {
await createComponent({ mountFn: mountExtended });
@@ -387,6 +391,11 @@ describe('GroupRunnersApp', () => {
expect(findRunnerPagination().attributes('disabled')).toBe('true');
});
+ it('runners cannot be deleted in bulk', () => {
+ createComponent();
+ expect(findRunnerList().props('checkable')).toBe(false);
+ });
+
describe('when no runners are found', () => {
beforeEach(async () => {
mockGroupRunnersHandler.mockResolvedValue({
diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js
index 3660d00cf0b..96f1c5275b2 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -9,7 +9,6 @@ import {
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import workItemWeightSubscription from 'ee_component/work_items/graphql/work_item_weight.subscription.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
@@ -35,7 +34,6 @@ import {
workItemDatesSubscriptionResponse,
workItemResponseFactory,
workItemTitleSubscriptionResponse,
- workItemWeightSubscriptionResponse,
workItemAssigneesSubscriptionResponse,
} from '../mock_data';
@@ -57,7 +55,6 @@ describe('WorkItemDetail component', () => {
const assigneesSubscriptionHandler = jest
.fn()
.mockResolvedValue(workItemAssigneesSubscriptionResponse);
- const weightSubscriptionHandler = jest.fn().mockResolvedValue(workItemWeightSubscriptionResponse);
const findAlert = () => wrapper.findComponent(GlAlert);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
@@ -95,10 +92,6 @@ describe('WorkItemDetail component', () => {
confidentialityMock,
];
- if (IS_EE) {
- handlers.push([workItemWeightSubscription, weightSubscriptionHandler]);
- }
-
wrapper = shallowMount(WorkItemDetail, {
apolloProvider: createMockApollo(handlers),
propsData: { isModal, workItemId },
@@ -113,6 +106,12 @@ describe('WorkItemDetail component', () => {
workItemsMvc2: workItemsMvc2Enabled,
},
hasIssueWeightsFeature: true,
+ hasIterationsFeature: true,
+ projectNamespace: 'namespace',
+ },
+ stubs: {
+ WorkItemWeight: true,
+ WorkItemIteration: true,
},
});
};
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
index 434c1db8a2c..ab3ea623e3e 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
@@ -28,6 +28,7 @@ describe('WorkItemLinksForm', () => {
listResponse = availableWorkItemsResponse,
typesResponse = projectWorkItemTypesQueryResponse,
parentConfidential = false,
+ hasIterationsFeature = false,
} = {}) => {
wrapper = shallowMountExtended(WorkItemLinksForm, {
apolloProvider: createMockApollo([
@@ -39,6 +40,7 @@ describe('WorkItemLinksForm', () => {
propsData: { issuableGid: 'gid://gitlab/WorkItem/1', parentConfidential },
provide: {
projectPath: 'project/path',
+ hasIterationsFeature,
},
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
index 876aedff08b..6961996f912 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
@@ -5,7 +5,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql';
+import issueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
import WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue';
import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
@@ -21,16 +21,29 @@ import {
Vue.use(VueApollo);
-const issueConfidentialityResponse = (confidential = false) => ({
+const issueDetailsResponse = (confidential = false) => ({
data: {
workspace: {
- id: '1',
- __typename: 'Project',
+ id: 'gid://gitlab/Project/1',
issuable: {
- __typename: 'Issue',
id: 'gid://gitlab/Issue/4',
confidential,
+ iteration: {
+ id: 'gid://gitlab/Iteration/1124',
+ title: null,
+ startDate: '2022-06-22',
+ dueDate: '2022-07-19',
+ webUrl: 'http://127.0.0.1:3000/groups/gitlab-org/-/iterations/1124',
+ iterationCadence: {
+ id: 'gid://gitlab/Iterations::Cadence/1101',
+ title: 'Quod voluptates quidem ea eaque eligendi ex corporis.',
+ __typename: 'IterationCadence',
+ },
+ __typename: 'Iteration',
+ },
+ __typename: 'Issue',
},
+ __typename: 'Project',
},
},
});
@@ -55,14 +68,15 @@ describe('WorkItemLinks', () => {
data = {},
fetchHandler = jest.fn().mockResolvedValue(workItemHierarchyResponse),
mutationHandler = mutationChangeParentHandler,
- confidentialQueryHandler = jest.fn().mockResolvedValue(issueConfidentialityResponse()),
+ issueDetailsQueryHandler = jest.fn().mockResolvedValue(issueDetailsResponse()),
+ hasIterationsFeature = false,
} = {}) => {
mockApollo = createMockApollo(
[
[getWorkItemLinksQuery, fetchHandler],
[changeWorkItemParentMutation, mutationHandler],
[workItemQuery, childWorkItemQueryHandler],
- [issueConfidentialQuery, confidentialQueryHandler],
+ [issueDetailsQuery, issueDetailsQueryHandler],
],
{},
{ addTypename: true },
@@ -77,6 +91,7 @@ describe('WorkItemLinks', () => {
provide: {
projectPath: 'project/path',
iid: '1',
+ hasIterationsFeature,
},
propsData: { issuableId: 1 },
apolloProvider: mockApollo,
@@ -266,7 +281,7 @@ describe('WorkItemLinks', () => {
describe('when parent item is confidential', () => {
it('passes correct confidentiality status to form', async () => {
await createComponent({
- confidentialQueryHandler: jest.fn().mockResolvedValue(issueConfidentialityResponse(true)),
+ issueDetailsQueryHandler: jest.fn().mockResolvedValue(issueDetailsResponse(true)),
});
findToggleAddFormButton().vm.$emit('click');
await nextTick();
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index 66bee44ae49..a0ed4ed1425 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -186,6 +186,7 @@ export const workItemResponseFactory = ({
datesWidgetPresent = true,
labelsWidgetPresent = true,
weightWidgetPresent = true,
+ iterationWidgetPresent = true,
confidential = false,
canInviteMembers = false,
allowsScopedLabels = false,
@@ -264,6 +265,20 @@ export const workItemResponseFactory = ({
weight: 0,
}
: { type: 'MOCK TYPE' },
+ iterationWidgetPresent
+ ? {
+ __typename: 'WorkItemWidgetIteration',
+ type: 'ITERATION',
+ iteration: {
+ description: null,
+ id: 'gid://gitlab/Iteration/1215',
+ iid: '182',
+ title: 'Iteration default title',
+ startDate: '2022-09-22',
+ dueDate: '2022-09-30',
+ },
+ }
+ : { type: 'MOCK TYPE' },
{
__typename: 'WorkItemWidgetHierarchy',
type: 'HIERARCHY',
@@ -520,6 +535,27 @@ export const workItemLabelsSubscriptionResponse = {
},
};
+export const workItemIterationSubscriptionResponse = {
+ data: {
+ issuableIterationUpdated: {
+ id: 'gid://gitlab/WorkItem/1',
+ widgets: [
+ {
+ __typename: 'WorkItemWidgetIteration',
+ iteration: {
+ description: 'Iteration description',
+ dueDate: '2022-07-29',
+ id: 'gid://gitlab/Iteration/1125',
+ iid: '95',
+ startDate: '2022-06-22',
+ title: 'Iteration subcription title',
+ },
+ },
+ ],
+ },
+ },
+};
+
export const workItemHierarchyEmptyResponse = {
data: {
workItem: {
@@ -944,3 +980,82 @@ export const projectLabelsResponse = {
},
},
};
+
+export const mockIterationWidgetResponse = {
+ description: 'Iteration description',
+ dueDate: '2022-07-19',
+ id: 'gid://gitlab/Iteration/1124',
+ iid: '91',
+ startDate: '2022-06-22',
+ title: 'Iteration title widget',
+};
+
+export const groupIterationsResponse = {
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Group/22',
+ attributes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Iteration/1124',
+ title: null,
+ startDate: '2022-06-22',
+ dueDate: '2022-07-19',
+ webUrl: 'http://127.0.0.1:3000/groups/gitlab-org/-/iterations/1124',
+ iterationCadence: {
+ id: 'gid://gitlab/Iterations::Cadence/1101',
+ title: 'Quod voluptates quidem ea eaque eligendi ex corporis.',
+ __typename: 'IterationCadence',
+ },
+ __typename: 'Iteration',
+ state: 'current',
+ },
+ {
+ id: 'gid://gitlab/Iteration/1185',
+ title: null,
+ startDate: '2022-07-06',
+ dueDate: '2022-07-19',
+ webUrl: 'http://127.0.0.1:3000/groups/gitlab-org/-/iterations/1185',
+ iterationCadence: {
+ id: 'gid://gitlab/Iterations::Cadence/1144',
+ title: 'Quo velit perspiciatis saepe aut omnis voluptas ab eos.',
+ __typename: 'IterationCadence',
+ },
+ __typename: 'Iteration',
+ state: 'current',
+ },
+ {
+ id: 'gid://gitlab/Iteration/1194',
+ title: null,
+ startDate: '2022-07-06',
+ dueDate: '2022-07-19',
+ webUrl: 'http://127.0.0.1:3000/groups/gitlab-org/-/iterations/1194',
+ iterationCadence: {
+ id: 'gid://gitlab/Iterations::Cadence/1152',
+ title:
+ 'Minima aut consequatur magnam vero doloremque accusamus maxime repellat voluptatem qui.',
+ __typename: 'IterationCadence',
+ },
+ __typename: 'Iteration',
+ state: 'current',
+ },
+ ],
+ __typename: 'IterationConnection',
+ },
+ __typename: 'Group',
+ },
+ },
+};
+
+export const groupIterationsResponseWithNoIterations = {
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Group/22',
+ attributes: {
+ nodes: [],
+ __typename: 'IterationConnection',
+ },
+ __typename: 'Group',
+ },
+ },
+};
diff --git a/spec/lib/gitlab/ci/config/entry/workflow_spec.rb b/spec/lib/gitlab/ci/config/entry/workflow_spec.rb
index 3d19832e13d..97ac199f47d 100644
--- a/spec/lib/gitlab/ci/config/entry/workflow_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/workflow_spec.rb
@@ -65,6 +65,54 @@ RSpec.describe Gitlab::Ci::Config::Entry::Workflow do
end
end
end
+
+ context 'with workflow name' do
+ let(:factory) { Gitlab::Config::Entry::Factory.new(described_class).value(workflow_hash) }
+
+ context 'with a blank name' do
+ let(:workflow_hash) do
+ { name: '' }
+ end
+
+ it 'is invalid' do
+ expect(config).not_to be_valid
+ end
+
+ it 'returns error about invalid name' do
+ expect(config.errors).to include('workflow name is too short (minimum is 1 character)')
+ end
+ end
+
+ context 'with too long name' do
+ let(:workflow_hash) do
+ { name: 'a' * 256 }
+ end
+
+ it 'is invalid' do
+ expect(config).not_to be_valid
+ end
+
+ it 'returns error about invalid name' do
+ expect(config.errors).to include('workflow name is too long (maximum is 255 characters)')
+ end
+ end
+
+ context 'when name is nil' do
+ let(:workflow_hash) { { name: nil } }
+
+ it 'is valid' do
+ expect(config).to be_valid
+ end
+ end
+
+ context 'when name is not provided' do
+ let(:workflow_hash) { { rules: [{ if: '$VAR' }] } }
+
+ it 'is valid' do
+ expect(config).to be_valid
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb
index 055114769ea..475503de7da 100644
--- a/spec/lib/gitlab/ci/config_spec.rb
+++ b/spec/lib/gitlab/ci/config_spec.rb
@@ -889,4 +889,31 @@ RSpec.describe Gitlab::Ci::Config do
it { is_expected.to eq([{ if: '$CI_COMMIT_REF_NAME == "master"' }]) }
end
+
+ describe '#workflow_name' do
+ subject(:workflow_name) { config.workflow_name }
+
+ let(:yml) do
+ <<-EOS
+ workflow:
+ name: 'Pipeline name'
+
+ rspec:
+ script: exit 0
+ EOS
+ end
+
+ it { is_expected.to eq('Pipeline name') }
+
+ context 'with no name' do
+ let(:yml) do
+ <<-EOS
+ rspec:
+ script: exit 0
+ EOS
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
index 62de4d2e96d..51d1661b586 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
@@ -236,4 +236,47 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Populate do
end
end
end
+
+ context 'with pipeline name' do
+ let(:config) do
+ { workflow: { name: ' Pipeline name ' }, rspec: { script: 'rspec' } }
+ end
+
+ context 'with feature flag disabled' do
+ before do
+ stub_feature_flags(pipeline_name: false)
+ end
+
+ it 'does not build pipeline_metadata' do
+ run_chain
+
+ expect(pipeline.pipeline_metadata).to be_nil
+ end
+ end
+
+ context 'with feature flag enabled' do
+ before do
+ stub_feature_flags(pipeline_name: true)
+ end
+
+ it 'builds pipeline_metadata' do
+ run_chain
+
+ expect(pipeline.pipeline_metadata.title).to eq('Pipeline name')
+ expect(pipeline.pipeline_metadata.project).to eq(pipeline.project)
+ end
+
+ context 'with empty name' do
+ let(:config) do
+ { workflow: { name: ' ' }, rspec: { script: 'rspec' } }
+ end
+
+ it 'strips whitespace from name' do
+ run_chain
+
+ expect(pipeline.pipeline_metadata).to be_nil
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index 398f8b16f95..ebf8422489e 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -514,6 +514,35 @@ module Gitlab
expect(subject.root_variables).to eq([])
end
end
+
+ context 'with name' do
+ let(:config) do
+ <<-EOYML
+ workflow:
+ name: 'Pipeline name'
+
+ hello:
+ script: echo world
+ EOYML
+ end
+
+ it 'parses the workflow:name as workflow_name' do
+ expect(subject.workflow_name).to eq('Pipeline name')
+ end
+ end
+
+ context 'with no name' do
+ let(:config) do
+ <<-EOYML
+ hello:
+ script: echo world
+ EOYML
+ end
+
+ it 'parses the workflow:name' do
+ expect(subject.workflow_name).to be_nil
+ end
+ end
end
describe '#warnings' do
diff --git a/spec/lib/gitlab/profiler_spec.rb b/spec/lib/gitlab/profiler_spec.rb
index bfe1a588489..7c365990627 100644
--- a/spec/lib/gitlab/profiler_spec.rb
+++ b/spec/lib/gitlab/profiler_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe Gitlab::Profiler do
end
it 'returns a profile result' do
- expect(described_class.profile('/')).to be_an_instance_of(RubyProf::Profile)
+ expect(described_class.profile('/')).to be_an_instance_of(File)
end
it 'uses the custom logger given' do
@@ -59,28 +59,26 @@ RSpec.describe Gitlab::Profiler do
described_class.profile('/', user: user, private_token: private_token)
end
- context 'with sampling profiler' do
- it 'generates sampling data' do
- user = double(:user)
- temp_data = Tempfile.new
+ it 'generates sampling data' do
+ user = double(:user)
+ temp_data = Tempfile.new
- expect(described_class).to receive(:with_user).with(user).and_call_original
- described_class.profile('/', user: user, sampling_mode: true, profiler_options: { out: temp_data.path })
+ expect(described_class).to receive(:with_user).with(user).and_call_original
+ described_class.profile('/', user: user, profiler_options: { out: temp_data.path })
- expect(File.stat(temp_data).size).to be > 0
- File.unlink(temp_data)
- end
+ expect(File.stat(temp_data).size).to be > 0
+ File.unlink(temp_data)
+ end
- it 'saves sampling data with a randomly-generated filename' do
- user = double(:user)
+ it 'saves sampling data with a randomly-generated filename' do
+ user = double(:user)
- expect(described_class).to receive(:with_user).with(user).and_call_original
- result = described_class.profile('/', user: user, sampling_mode: true)
+ expect(described_class).to receive(:with_user).with(user).and_call_original
+ result = described_class.profile('/', user: user)
- expect(result).to be_a(File)
- expect(File.stat(result.path).size).to be > 0
- File.unlink(result.path)
- end
+ expect(result).to be_a(File)
+ expect(File.stat(result.path).size).to be > 0
+ File.unlink(result.path)
end
end
@@ -211,54 +209,4 @@ RSpec.describe Gitlab::Profiler do
expect(described_class.log_load_times_by_model(null_logger)).to be_nil
end
end
-
- describe '.print_by_total_time' do
- let(:stdout) { StringIO.new }
- let(:regexp) { /^\s+\d+\.\d+\s+(\d+\.\d+)/ }
-
- let(:output) do
- stdout.rewind
- stdout.read
- end
-
- let_it_be(:result) do
- Thread.new { sleep 1 }
-
- RubyProf.profile do
- sleep 0.1
- 1.to_s
- end
- end
-
- around do |example|
- original_stdout = $stdout
-
- $stdout = stdout # rubocop: disable RSpec/ExpectOutput
- example.run
- $stdout = original_stdout # rubocop: disable RSpec/ExpectOutput
- end
-
- it 'prints a profile result sorted by total time' do
- described_class.print_by_total_time(result)
-
- expect(output).to include('Kernel#sleep')
-
- thread_profiles = output.split('Sort by: total_time').select { |x| x =~ regexp }
-
- thread_profiles.each do |profile|
- total_times =
- profile
- .scan(regexp)
- .map { |(total)| total.to_f }
-
- expect(total_times).to eq(total_times.sort.reverse)
- end
- end
-
- it 'accepts a max_percent option' do
- described_class.print_by_total_time(result, max_percent: 50)
-
- expect(output).not_to include('Kernel#sleep')
- end
- end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index d854943e277..751a303739c 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -2983,6 +2983,24 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) }
+ it 'logs the event' do
+ allow(Gitlab::AppJsonLogger).to receive(:info)
+
+ pipeline.cancel_running
+
+ expect(Gitlab::AppJsonLogger)
+ .to have_received(:info)
+ .with(
+ a_hash_including(
+ event: 'pipeline_cancel_running',
+ pipeline_id: pipeline.id,
+ auto_canceled_by_pipeline_id: nil,
+ cascade_to_children: true,
+ execute_async: true
+ )
+ )
+ end
+
context 'when there is a running external job and a regular job' do
before do
create(:ci_build, :running, pipeline: pipeline)
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index 3f6bbe795cc..e8db83b7144 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -314,52 +314,22 @@ RSpec.describe Ci::Runner, 'TokenAuthenticatable', :freeze_time do
describe '#token_expired?' do
subject { runner.token_expired? }
- context 'when enforce_runner_token_expires_at feature flag is disabled' do
- before do
- stub_feature_flags(enforce_runner_token_expires_at: false)
- end
-
- context 'when runner has no token expiration' do
- let(:runner) { non_expirable_runner }
-
- it { is_expected.to eq(false) }
- end
-
- context 'when runner token is not expired' do
- let(:runner) { non_expired_runner }
+ context 'when runner has no token expiration' do
+ let(:runner) { non_expirable_runner }
- it { is_expected.to eq(false) }
- end
-
- context 'when runner token is expired' do
- let(:runner) { expired_runner }
-
- it { is_expected.to eq(false) }
- end
+ it { is_expected.to eq(false) }
end
- context 'when enforce_runner_token_expires_at feature flag is enabled' do
- before do
- stub_feature_flags(enforce_runner_token_expires_at: true)
- end
-
- context 'when runner has no token expiration' do
- let(:runner) { non_expirable_runner }
-
- it { is_expected.to eq(false) }
- end
+ context 'when runner token is not expired' do
+ let(:runner) { non_expired_runner }
- context 'when runner token is not expired' do
- let(:runner) { non_expired_runner }
-
- it { is_expected.to eq(false) }
- end
+ it { is_expected.to eq(false) }
+ end
- context 'when runner token is expired' do
- let(:runner) { expired_runner }
+ context 'when runner token is expired' do
+ let(:runner) { expired_runner }
- it { is_expected.to eq(true) }
- end
+ it { is_expected.to eq(true) }
end
end
@@ -386,52 +356,22 @@ RSpec.describe Ci::Runner, 'TokenAuthenticatable', :freeze_time do
describe '.find_by_token' do
subject { Ci::Runner.find_by_token(runner.token) }
- context 'when enforce_runner_token_expires_at feature flag is disabled' do
- before do
- stub_feature_flags(enforce_runner_token_expires_at: false)
- end
-
- context 'when runner has no token expiration' do
- let(:runner) { non_expirable_runner }
-
- it { is_expected.to eq(non_expirable_runner) }
- end
-
- context 'when runner token is not expired' do
- let(:runner) { non_expired_runner }
-
- it { is_expected.to eq(non_expired_runner) }
- end
-
- context 'when runner token is expired' do
- let(:runner) { expired_runner }
+ context 'when runner has no token expiration' do
+ let(:runner) { non_expirable_runner }
- it { is_expected.to eq(expired_runner) }
- end
+ it { is_expected.to eq(non_expirable_runner) }
end
- context 'when enforce_runner_token_expires_at feature flag is enabled' do
- before do
- stub_feature_flags(enforce_runner_token_expires_at: true)
- end
-
- context 'when runner has no token expiration' do
- let(:runner) { non_expirable_runner }
-
- it { is_expected.to eq(non_expirable_runner) }
- end
-
- context 'when runner token is not expired' do
- let(:runner) { non_expired_runner }
+ context 'when runner token is not expired' do
+ let(:runner) { non_expired_runner }
- it { is_expected.to eq(non_expired_runner) }
- end
+ it { is_expected.to eq(non_expired_runner) }
+ end
- context 'when runner token is expired' do
- let(:runner) { expired_runner }
+ context 'when runner token is expired' do
+ let(:runner) { expired_runner }
- it { is_expected.to be_nil }
- end
+ it { is_expected.to be_nil }
end
end
end
diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb
index 93c47422f37..6419a9fc9d9 100644
--- a/spec/models/hooks/web_hook_spec.rb
+++ b/spec/models/hooks/web_hook_spec.rb
@@ -316,7 +316,7 @@ RSpec.describe WebHook do
end
it 'is twice the initial value' do
- expect(hook.next_backoff).to eq(20.minutes)
+ expect(hook.next_backoff).to eq(2 * described_class::INITIAL_BACKOFF)
end
end
@@ -326,7 +326,7 @@ RSpec.describe WebHook do
end
it 'grows exponentially' do
- expect(hook.next_backoff).to eq(80.minutes)
+ expect(hook.next_backoff).to eq(2 * 2 * 2 * described_class::INITIAL_BACKOFF)
end
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 39da84ed20a..d593e369d27 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -1044,6 +1044,14 @@ RSpec.describe API::MergeRequests do
it_behaves_like 'a non-cached MergeRequest api request', 1
end
+ context 'when the label changes' do
+ before do
+ merge_request.labels << create(:label, project: merge_request.project)
+ end
+
+ it_behaves_like 'a non-cached MergeRequest api request', 1
+ end
+
context 'when the assignees change' do
before do
merge_request.assignees << create(:user)
diff --git a/spec/views/projects/hooks/edit.html.haml_spec.rb b/spec/views/projects/hooks/edit.html.haml_spec.rb
index c4ec2149794..2a95656645e 100644
--- a/spec/views/projects/hooks/edit.html.haml_spec.rb
+++ b/spec/views/projects/hooks/edit.html.haml_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe 'projects/hooks/edit' do
it 'renders alert' do
render
- expect(rendered).to have_text(s_('Webhooks|Webhook was automatically disabled'))
+ expect(rendered).to have_text(s_('Webhooks|Webhook rate limit has been reached'))
end
end
diff --git a/yarn.lock b/yarn.lock
index bb1811bb625..3e6bb447351 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4913,6 +4913,11 @@ domhandler@^4.0.0, domhandler@^4.2.0:
dependencies:
domelementtype "^2.2.0"
+dommatrix@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/dommatrix/-/dommatrix-1.0.3.tgz#e7c18e8d6f3abdd1fef3dd4aa74c4d2e620a0525"
+ integrity sha512-l32Xp/TLgWb8ReqbVJAFIvXmY7go4nTxxlWiAFyhoQw9RKEOHBZNnyGvJWqDVSPmq3Y9HlM4npqF/T6VMOXhww==
+
dompurify@2.3.8:
version "2.3.8"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.8.tgz#224fe9ae57d7ebd9a1ae1ac18c1c1ca3f532226f"
@@ -9182,11 +9187,6 @@ node-domexception@1.0.0:
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
-node-ensure@^0.0.0:
- version "0.0.0"
- resolved "https://registry.yarnpkg.com/node-ensure/-/node-ensure-0.0.0.tgz#ecae764150de99861ec5c810fd5d096b183932a7"
- integrity sha1-7K52QVDemYYexcgQ/V0Jaxg5Mqc=
-
node-fetch@^2.6.1, node-fetch@^2.6.7:
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
@@ -9737,13 +9737,13 @@ pbkdf2@^3.0.3:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
-pdfjs-dist@^2.0.943:
- version "2.1.266"
- resolved "https://registry.yarnpkg.com/pdfjs-dist/-/pdfjs-dist-2.1.266.tgz#cded02268b389559e807f410d2a729db62160026"
- integrity sha512-Jy7o1wE3NezPxozexSbq4ltuLT0Z21ew/qrEiAEeUZzHxMHGk4DUV1D7RuCXg5vJDvHmjX1YssN+we9QfRRgXQ==
+pdfjs-dist@^2.16.105:
+ version "2.16.105"
+ resolved "https://registry.yarnpkg.com/pdfjs-dist/-/pdfjs-dist-2.16.105.tgz#937b9c4a918f03f3979c88209d84c1ce90122c2a"
+ integrity sha512-J4dn41spsAwUxCpEoVf6GVoz908IAA3mYiLmNxg8J9kfRXc2jxpbUepcP0ocp0alVNLFthTAM8DZ1RaHh8sU0A==
dependencies:
- node-ensure "^0.0.0"
- worker-loader "^2.0.0"
+ dommatrix "^1.0.3"
+ web-streams-polyfill "^3.2.1"
picocolors@^0.2.1:
version "0.2.1"
@@ -10666,9 +10666,9 @@ saxes@^5.0.1:
xmlchars "^2.2.0"
schema-utils@^0.4.0:
- version "0.4.5"
- resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.5.tgz#21836f0608aac17b78f9e3e24daff14a5ca13a3e"
- integrity sha512-yYrjb9TX2k/J1Y5UNy3KYdZq10xhYcF8nMpAW6o3hy6Q8WSIEf9lJHG/ePnOBfziPM3fvQwfOwa13U/Fh8qTfA==
+ version "0.4.7"
+ resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.7.tgz#ba74f597d2be2ea880131746ee17d0a093c68187"
+ integrity sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==
dependencies:
ajv "^6.1.0"
ajv-keywords "^3.1.0"
@@ -11725,7 +11725,7 @@ tsconfig-paths@^3.14.1:
minimist "^1.2.6"
strip-bom "^3.0.0"
-tslib@2.3.0, tslib@~2.3.0:
+tslib@2.3.0, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0, tslib@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
@@ -11735,7 +11735,7 @@ tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
-tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.4.0:
+tslib@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
@@ -12349,10 +12349,10 @@ web-streams-polyfill@4.0.0-beta.1:
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.1.tgz#3b19b9817374b7cee06d374ba7eeb3aeb80e8c95"
integrity sha512-3ux37gEX670UUphBF9AMCq8XM6iQ8Ac6A+DSRRjDoRBm1ufCkaCDdNVbaqq60PsEkdNlLKrGtv/YBP4EJXqNtQ==
-web-streams-polyfill@^3.2.0:
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz#a6b74026b38e4885869fb5c589e90b95ccfc7965"
- integrity sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA==
+web-streams-polyfill@^3.2.0, web-streams-polyfill@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6"
+ integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==
web-vitals@^0.2.4:
version "0.2.4"