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:
-rw-r--r--.rubocop_todo/layout/argument_alignment.yml18
-rw-r--r--.rubocop_todo/rspec/missing_feature_category.yml2
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/boards/boards_util.js15
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue99
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_form_fields.vue87
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_list_empty_state.vue75
-rw-r--r--app/assets/javascripts/ci/runner/constants.js20
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue197
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql3
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_files.query.graphql3
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue152
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_details_header.vue446
-rw-r--r--app/assets/javascripts/projects/commit_box/info/components/commit_refs.vue12
-rw-r--r--app/assets/javascripts/projects/commit_box/info/components/refs_list.vue11
-rw-r--r--app/assets/javascripts/projects/commit_box/info/constants.js4
-rw-r--r--app/assets/javascripts/security_configuration/components/app.vue11
-rw-r--r--app/assets/javascripts/security_configuration/components/auto_dev_ops_alert.vue2
-rw-r--r--app/assets/javascripts/security_configuration/components/feature_card.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_badge_link.vue76
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/user_select/user_select.vue2
-rw-r--r--app/graphql/types/ci/runner_manager_type.rb2
-rw-r--r--app/models/integrations/chat_message/push_message.rb4
-rw-r--r--app/presenters/ci/pipeline_presenter.rb4
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml2
-rw-r--r--app/views/protected_branches/_create_protected_branch.html.haml4
-rw-r--r--config/metrics/counts_28d/20210216175113_merge_request_action_monthly.yml2
-rw-r--r--config/metrics/counts_28d/20210216181446_g_project_management_issue_comment_added_monthly.yml2
-rw-r--r--config/metrics/counts_all/20210216180750_groups.yml2
-rw-r--r--config/routes.rb5
-rw-r--r--db/post_migrate/20230602063059_remove_broadcast_messages_namespace_id_column.rb19
-rw-r--r--db/schema_migrations/202306020630591
-rw-r--r--db/structure.sql5
-rw-r--r--doc/api/graphql/reference/index.md45
-rw-r--r--doc/architecture/blueprints/runner_tokens/index.md50
-rw-r--r--doc/ci/variables/predefined_variables.md2
-rw-r--r--doc/ci/yaml/workflow.md2
-rw-r--r--doc/development/pipelines/internals.md4
-rw-r--r--doc/tutorials/more_tutorials.md4
-rw-r--r--lib/gitlab/ci/status/scheduled.rb4
-rw-r--r--lib/gitlab/ci/status/success_warning.rb2
-rw-r--r--locale/gitlab.pot64
-rw-r--r--qa/qa/page/element.rb22
-rw-r--r--qa/qa/page/merge_request/show.rb4
-rw-r--r--qa/qa/page/project/secure/configuration_form.rb48
-rw-r--r--qa/qa/page/project/settings/protected_branches.rb6
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/source_editor/source_editor_toolbar_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_with_image_pull_policy_spec.rb6
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_project_level_spec.rb26
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/package_registry/nuget/nuget_project_level_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/package_registry/rubygems_registry_spec.rb2
-rw-r--r--qa/spec/page/element_spec.rb8
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb6
-rw-r--r--spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb14
-rw-r--r--spec/features/merge_request/user_sees_pipelines_spec.rb2
-rw-r--r--spec/features/projects/commit/user_sees_pipelines_tab_spec.rb2
-rw-r--r--spec/features/projects/jobs/user_browses_jobs_spec.rb8
-rw-r--r--spec/features/projects/jobs_spec.rb2
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb16
-rw-r--r--spec/frontend/boards/components/board_content_spec.js51
-rw-r--r--spec/frontend/boards/mock_data.js1
-rw-r--r--spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js82
-rw-r--r--spec/frontend/ci/runner/components/runner_form_fields_spec.js10
-rw-r--r--spec/frontend/ci/runner/components/runner_list_empty_state_spec.js197
-rw-r--r--spec/frontend/commit/components/refs_list_spec.js2
-rw-r--r--spec/frontend/commit/mock_data.js1
-rw-r--r--spec/frontend/fixtures/pipeline_header.rb27
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js341
-rw-r--r--spec/frontend/packages_and_registries/package_registry/mock_data.js9
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/details_spec.js253
-rw-r--r--spec/frontend/pipelines/mock_data.js27
-rw-r--r--spec/frontend/pipelines/pipeline_details_header_spec.js210
-rw-r--r--spec/frontend/pipelines/pipeline_multi_actions_spec.js122
-rw-r--r--spec/frontend/vue_shared/components/ci_badge_link_spec.js58
-rw-r--r--spec/graphql/types/ci/runner_manager_type_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/build/factory_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/scheduled_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/status/success_warning_spec.rb4
-rw-r--r--spec/models/integrations/chat_message/push_message_spec.rb24
-rw-r--r--spec/models/integrations/discord_spec.rb2
-rw-r--r--spec/presenters/ci/pipeline_presenter_spec.rb4
-rw-r--r--spec/requests/api/graphql/ci/jobs_spec.rb4
88 files changed, 2013 insertions, 1089 deletions
diff --git a/.rubocop_todo/layout/argument_alignment.yml b/.rubocop_todo/layout/argument_alignment.yml
index 6fac8aff19c..aa5946c1932 100644
--- a/.rubocop_todo/layout/argument_alignment.yml
+++ b/.rubocop_todo/layout/argument_alignment.yml
@@ -1653,24 +1653,6 @@ Layout/ArgumentAlignment:
- 'qa/qa/specs/features/api/4_verify/api_variable_inheritance_with_forward_pipeline_variables_spec.rb'
- 'qa/qa/specs/features/browser_ui/1_manage/integrations/jenkins/jenkins_build_status_spec.rb'
- 'qa/qa/specs/features/browser_ui/1_manage/integrations/jira/jira_basic_integration_spec.rb'
- - 'qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb'
- - 'qa/qa/specs/features/browser_ui/3_create/pages/new_static_page_spec.rb'
- - 'qa/qa/specs/features/browser_ui/3_create/project_wiki/project_based_content_creation_spec.rb'
- - 'qa/qa/specs/features/browser_ui/3_create/project_wiki/project_based_content_manipulation_spec.rb'
- - 'qa/qa/specs/features/browser_ui/3_create/project_wiki/project_based_directory_management_spec.rb'
- - 'qa/qa/specs/features/browser_ui/3_create/project_wiki/project_based_list_spec.rb'
- - 'qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb'
- - 'qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb'
- - 'qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb'
- - 'qa/qa/specs/features/browser_ui/3_create/source_editor/source_editor_toolbar_spec.rb'
- - 'qa/qa/specs/features/browser_ui/3_create/web_ide/add_new_directory_in_web_ide_spec.rb'
- - 'qa/qa/specs/features/browser_ui/3_create/web_ide_old/add_file_template_spec.rb'
- - 'qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_with_image_pull_policy_spec.rb'
- - 'qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_project_level_spec.rb'
- - 'qa/qa/specs/features/browser_ui/5_package/package_registry/nuget/nuget_project_level_spec.rb'
- - 'qa/qa/specs/features/browser_ui/5_package/package_registry/rubygems_registry_spec.rb'
- - 'qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb'
- - 'qa/qa/specs/features/ee/api/12_systems/geo/geo_nodes_spec.rb'
- 'qa/qa/specs/features/ee/api/1_manage/integrations/group_webhook_events_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/10_govern/change_vulnerability_status_spec.rb'
- 'qa/qa/specs/features/ee/browser_ui/10_govern/create_merge_request_with_secure_spec.rb'
diff --git a/.rubocop_todo/rspec/missing_feature_category.yml b/.rubocop_todo/rspec/missing_feature_category.yml
index a5cee560ef1..e257f9e2de1 100644
--- a/.rubocop_todo/rspec/missing_feature_category.yml
+++ b/.rubocop_todo/rspec/missing_feature_category.yml
@@ -3301,13 +3301,11 @@ RSpec/MissingFeatureCategory:
- 'spec/lib/gitlab/ci/status/pipeline/factory_spec.rb'
- 'spec/lib/gitlab/ci/status/preparing_spec.rb'
- 'spec/lib/gitlab/ci/status/running_spec.rb'
- - 'spec/lib/gitlab/ci/status/scheduled_spec.rb'
- 'spec/lib/gitlab/ci/status/skipped_spec.rb'
- 'spec/lib/gitlab/ci/status/stage/common_spec.rb'
- 'spec/lib/gitlab/ci/status/stage/factory_spec.rb'
- 'spec/lib/gitlab/ci/status/stage/play_manual_spec.rb'
- 'spec/lib/gitlab/ci/status/success_spec.rb'
- - 'spec/lib/gitlab/ci/status/success_warning_spec.rb'
- 'spec/lib/gitlab/ci/status/waiting_for_resource_spec.rb'
- 'spec/lib/gitlab/ci/tags/bulk_insert_spec.rb'
- 'spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb'
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index ff544fd4272..729fda5e8c0 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-42c93313900baabb0621ef2fb95feae8050ca418
+81496efc0d26dba7799d1392c80b06bce943cc29
diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js
index fcd1440841b..93bd97e691b 100644
--- a/app/assets/javascripts/boards/boards_util.js
+++ b/app/assets/javascripts/boards/boards_util.js
@@ -1,4 +1,4 @@
-import { sortBy, cloneDeep, find } from 'lodash';
+import { sortBy, cloneDeep, find, inRange } from 'lodash';
import {
TYPENAME_BOARD,
TYPENAME_ITERATION,
@@ -12,7 +12,7 @@ import {
AssigneeFilterType,
MilestoneFilterType,
boardQuery,
-} from './constants';
+} from 'ee_else_ce/boards/constants';
export function getMilestone() {
return null;
@@ -30,6 +30,17 @@ export function updateListPosition(listObj) {
return { ...listObj, position };
}
+export function calculateNewPosition(listPosition, initialPosition, targetPosition) {
+ if (
+ listPosition === null ||
+ !(inRange(listPosition, initialPosition, targetPosition) || listPosition === targetPosition)
+ ) {
+ return listPosition;
+ }
+ const offset = initialPosition < targetPosition ? -1 : 1;
+ return listPosition + offset;
+}
+
export function formatBoardLists(lists) {
return lists.nodes.reduce((map, list) => {
return {
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index ba4e3af79b5..a51e4ddc8f8 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -1,12 +1,19 @@
<script>
import { GlAlert } from '@gitlab/ui';
import { sortBy } from 'lodash';
+import produce from 'immer';
import Draggable from 'vuedraggable';
import { mapState, mapActions } from 'vuex';
import eventHub from '~/boards/eventhub';
import BoardAddNewColumn from 'ee_else_ce/boards/components/board_add_new_column.vue';
import { defaultSortableOptions } from '~/sortable/constants';
-import { DraggableItemTypes, flashAnimationDuration } from 'ee_else_ce/boards/constants';
+import {
+ DraggableItemTypes,
+ flashAnimationDuration,
+ listsQuery,
+ updateListQueries,
+} from 'ee_else_ce/boards/constants';
+import { calculateNewPosition } from 'ee_else_ce/boards/boards_util';
import BoardColumn from './board_column.vue';
export default {
@@ -20,7 +27,15 @@ export default {
EpicsSwimlanes: () => import('ee_component/boards/components/epics_swimlanes.vue'),
GlAlert,
},
- inject: ['canAdminList', 'isIssueBoard', 'isEpicBoard', 'disabled', 'isApolloBoard'],
+ inject: [
+ 'boardType',
+ 'canAdminList',
+ 'isIssueBoard',
+ 'isEpicBoard',
+ 'disabled',
+ 'issuableType',
+ 'isApolloBoard',
+ ],
props: {
boardId: {
type: String,
@@ -117,6 +132,83 @@ export default {
this.highlightedLists = this.highlightedLists.filter((id) => id !== listId);
}, flashAnimationDuration);
},
+ updateListPosition({
+ item: {
+ dataset: { listId: movedListId, draggableItemType },
+ },
+ newIndex,
+ to: { children },
+ }) {
+ if (!this.isApolloBoard) {
+ this.moveList({
+ item: {
+ dataset: { listId: movedListId, draggableItemType },
+ },
+ newIndex,
+ to: { children },
+ });
+ return;
+ }
+
+ if (draggableItemType !== DraggableItemTypes.list) {
+ return;
+ }
+
+ const displacedListId = children[newIndex].dataset.listId;
+
+ if (movedListId === displacedListId) {
+ return;
+ }
+ const initialPosition = this.boardListsById[movedListId].position;
+ const targetPosition = this.boardListsById[displacedListId].position;
+
+ try {
+ this.$apollo.mutate({
+ mutation: updateListQueries[this.issuableType].mutation,
+ variables: {
+ listId: movedListId,
+ position: targetPosition,
+ },
+ update: (store) => {
+ const sourceData = store.readQuery({
+ query: listsQuery[this.issuableType].query,
+ variables: this.listQueryVariables,
+ });
+ const data = produce(sourceData, (draftData) => {
+ // for current list, new position is already set by Apollo via automatic update
+ const affectedNodes = draftData[this.boardType].board.lists.nodes.filter(
+ (node) => node.id !== movedListId,
+ );
+ affectedNodes.forEach((node) => {
+ // eslint-disable-next-line no-param-reassign
+ node.position = calculateNewPosition(
+ node.position,
+ initialPosition,
+ targetPosition,
+ );
+ });
+ });
+ store.writeQuery({
+ query: listsQuery[this.issuableType].query,
+ variables: this.listQueryVariables,
+ data,
+ });
+ },
+ optimisticResponse: {
+ updateBoardList: {
+ __typename: 'UpdateBoardListPayload',
+ errors: [],
+ list: {
+ ...this.boardListsApollo[movedListId],
+ position: targetPosition,
+ },
+ },
+ },
+ });
+ } catch {
+ // handle error
+ }
+ },
},
};
</script>
@@ -136,7 +228,7 @@ export default {
ref="list"
v-bind="draggableOptions"
class="boards-list gl-w-full gl-py-5 gl-pr-3 gl-white-space-nowrap gl-overflow-x-auto"
- @end="moveList"
+ @end="updateListPosition"
>
<board-column
v-for="(list, index) in boardListsToUse"
@@ -173,6 +265,7 @@ export default {
:filters="filterParams"
:highlighted-lists="highlightedLists"
@setActiveList="$emit('setActiveList', $event)"
+ @move-list="updateListPosition"
>
<board-add-new-column
v-if="addColumnFormVisible"
diff --git a/app/assets/javascripts/ci/runner/components/runner_form_fields.vue b/app/assets/javascripts/ci/runner/components/runner_form_fields.vue
index 180c41e7ed6..d090a562ff7 100644
--- a/app/assets/javascripts/ci/runner/components/runner_form_fields.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_form_fields.vue
@@ -74,11 +74,56 @@ export default {
<template>
<div>
<h2 class="gl-font-size-h2 gl-my-5">
+ {{ s__('Runners|Tags') }}
+ </h2>
+ <gl-skeleton-loader v-if="loading" :lines="12" />
+ <template v-else-if="model">
+ <gl-form-group :label="__('Tags')" label-for="runner-tags">
+ <template #description>
+ <gl-sprintf
+ :message="
+ s__('Runners|Multiple tags must be separated by a comma. For example, %{example}.')
+ "
+ >
+ <template #example>
+ <!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
+ <code>macos, shared</code>
+ </template>
+ </gl-sprintf>
+ </template>
+ <template #label-description>
+ <gl-sprintf
+ :message="
+ s__(
+ 'Runners|Add tags for the types of jobs the runner processes to ensure that the runner only runs jobs that you intend it to. %{helpLinkStart}Learn more.%{helpLinkEnd}',
+ )
+ "
+ >
+ <template #helpLink="{ content }">
+ <gl-link :href="$options.HELP_LABELS_PAGE_PATH" target="_blank">{{
+ content
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </template>
+ <gl-form-input id="runner-tags" v-model="model.tagList" name="tags" />
+ </gl-form-group>
+ <gl-form-checkbox v-model="model.runUntagged" name="run-untagged">
+ {{ __('Run untagged jobs') }}
+ <template #help>
+ {{ s__('Runners|Use the runner for jobs without tags in addition to tagged jobs.') }}
+ </template>
+ </gl-form-checkbox>
+ </template>
+
+ <hr aria-hidden="true" />
+
+ <h2 class="gl-font-size-h2 gl-my-5">
{{ s__('Runners|Details') }}
{{ __('(optional)') }}
</h2>
- <gl-skeleton-loader v-if="loading" :lines="9" />
+ <gl-skeleton-loader v-if="loading" :lines="15" />
<template v-else-if="model">
<gl-form-group :label="s__('Runners|Runner description')" label-for="runner-description">
<gl-form-input id="runner-description" v-model="model.description" name="description" />
@@ -93,7 +138,7 @@ export default {
{{ __('(optional)') }}
</h2>
- <gl-skeleton-loader v-if="loading" :lines="27" />
+ <gl-skeleton-loader v-if="loading" :lines="15" />
<template v-else-if="model">
<div class="gl-mb-5">
<gl-form-checkbox v-model="model.paused" name="paused">
@@ -115,13 +160,6 @@ export default {
</template>
</gl-form-checkbox>
- <gl-form-checkbox v-model="model.runUntagged" name="run-untagged">
- {{ __('Run untagged jobs') }}
- <template #help>
- {{ s__('Runners|Use the runner for jobs without tags in addition to tagged jobs.') }}
- </template>
- </gl-form-checkbox>
-
<gl-form-checkbox v-if="canBeLockedToProject" v-model="model.locked" name="locked">
{{ __('Lock to current projects') }} <gl-icon name="lock" />
<template #help>
@@ -134,37 +172,6 @@ export default {
</gl-form-checkbox>
</div>
- <gl-form-group :label="__('Tags')" label-for="runner-tags">
- <template #description>
- <gl-sprintf
- :message="
- s__('Runners|Multiple tags must be separated by a comma. For example, %{example}.')
- "
- >
- <template #example>
- <!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
- <code>macos, shared</code>
- </template>
- </gl-sprintf>
- </template>
- <template #label-description>
- <gl-sprintf
- :message="
- s__(
- 'Runners|Add tags for the types of jobs the runner processes to ensure that the runner only runs jobs that you intend it to. %{helpLinkStart}Learn more.%{helpLinkEnd}',
- )
- "
- >
- <template #helpLink="{ content }">
- <gl-link :href="$options.HELP_LABELS_PAGE_PATH" target="_blank">{{
- content
- }}</gl-link>
- </template>
- </gl-sprintf>
- </template>
- <gl-form-input id="runner-tags" v-model="model.tagList" name="tags" />
- </gl-form-group>
-
<gl-form-group
:label="__('Maximum job timeout')"
:label-description="
diff --git a/app/assets/javascripts/ci/runner/components/runner_list_empty_state.vue b/app/assets/javascripts/ci/runner/components/runner_list_empty_state.vue
index ab2dc1b8ba3..d2836962a97 100644
--- a/app/assets/javascripts/ci/runner/components/runner_list_empty_state.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_list_empty_state.vue
@@ -5,6 +5,16 @@ import FILTERED_SVG_URL from '@gitlab/svgs/dist/illustrations/empty-state/empty-
import { GlEmptyState, GlLink, GlSprintf, GlModalDirective } from '@gitlab/ui';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
+import {
+ I18N_GET_STARTED,
+ I18N_RUNNERS_ARE_AGENTS,
+ I18N_CREATE_RUNNER_LINK,
+ I18N_STILL_USING_REGISTRATION_TOKENS,
+ I18N_CONTACT_ADMIN_TO_REGISTER,
+ I18N_FOLLOW_REGISTRATION_INSTRUCTIONS,
+ I18N_NO_RESULTS,
+ I18N_EDIT_YOUR_SEARCH,
+} from '~/ci/runner/constants';
export default {
components: {
@@ -38,9 +48,8 @@ export default {
shouldShowCreateRunnerWorkflow() {
// create_runner_workflow_for_admin or create_runner_workflow_for_namespace
return (
- this.newRunnerPath &&
- (this.glFeatures?.createRunnerWorkflowForAdmin ||
- this.glFeatures?.createRunnerWorkflowForNamespace)
+ this.glFeatures?.createRunnerWorkflowForAdmin ||
+ this.glFeatures?.createRunnerWorkflowForNamespace
);
},
},
@@ -48,35 +57,59 @@ export default {
svgHeight: 145,
EMPTY_STATE_SVG_URL,
FILTERED_SVG_URL,
+
+ I18N_GET_STARTED,
+ I18N_RUNNERS_ARE_AGENTS,
+ I18N_CREATE_RUNNER_LINK,
+ I18N_STILL_USING_REGISTRATION_TOKENS,
+ I18N_CONTACT_ADMIN_TO_REGISTER,
+ I18N_FOLLOW_REGISTRATION_INSTRUCTIONS,
+ I18N_NO_RESULTS,
+ I18N_EDIT_YOUR_SEARCH,
};
</script>
<template>
<gl-empty-state
v-if="isSearchFiltered"
- :title="s__('Runners|No results found')"
+ :title="$options.I18N_NO_RESULTS"
:svg-path="$options.FILTERED_SVG_URL"
:svg-height="$options.svgHeight"
- :description="s__('Runners|Edit your search and try again')"
+ :description="$options.I18N_EDIT_YOUR_SEARCH"
/>
<gl-empty-state
v-else
- :title="s__('Runners|Get started with runners')"
+ :title="$options.I18N_GET_STARTED"
:svg-path="$options.EMPTY_STATE_SVG_URL"
:svg-height="$options.svgHeight"
>
- <template v-if="registrationToken" #description>
+ <template #description>
+ {{ $options.I18N_RUNNERS_ARE_AGENTS }}
+ <template v-if="shouldShowCreateRunnerWorkflow">
+ <gl-sprintf v-if="newRunnerPath" :message="$options.I18N_CREATE_RUNNER_LINK">
+ <template #link="{ content }">
+ <gl-link :href="newRunnerPath">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ <template v-if="registrationToken">
+ <br />
+ <gl-link v-gl-modal="$options.modalId">{{
+ $options.I18N_STILL_USING_REGISTRATION_TOKENS
+ }}</gl-link>
+ <runner-instructions-modal
+ :modal-id="$options.modalId"
+ :registration-token="registrationToken"
+ />
+ </template>
+ <template v-if="!newRunnerPath && !registrationToken">
+ {{ $options.I18N_CONTACT_ADMIN_TO_REGISTER }}
+ </template>
+ </template>
<gl-sprintf
- :message="
- s__(
- 'Runners|Runners are the agents that run your CI/CD jobs. Follow the %{linkStart}installation and registration instructions%{linkEnd} to set up a runner.',
- )
- "
+ v-else-if="registrationToken"
+ :message="$options.I18N_FOLLOW_REGISTRATION_INSTRUCTIONS"
>
- <template v-if="shouldShowCreateRunnerWorkflow" #link="{ content }">
- <gl-link :href="newRunnerPath">{{ content }}</gl-link>
- </template>
- <template v-else #link="{ content }">
+ <template #link="{ content }">
<gl-link v-gl-modal="$options.modalId">{{ content }}</gl-link>
<runner-instructions-modal
:modal-id="$options.modalId"
@@ -84,13 +117,9 @@ export default {
/>
</template>
</gl-sprintf>
- </template>
- <template v-else #description>
- {{
- s__(
- 'Runners|Runners are the agents that run your CI/CD jobs. To register new runners, please contact your administrator.',
- )
- }}
+ <template v-else>
+ {{ $options.I18N_CONTACT_ADMIN_TO_REGISTER }}
+ </template>
</template>
</gl-empty-state>
</template>
diff --git a/app/assets/javascripts/ci/runner/constants.js b/app/assets/javascripts/ci/runner/constants.js
index 28263b5cfd9..395d9ac0d8e 100644
--- a/app/assets/javascripts/ci/runner/constants.js
+++ b/app/assets/javascripts/ci/runner/constants.js
@@ -102,6 +102,26 @@ export const I18N_CREATED_AT_BY_LABEL = s__('Runners|Created %{timeAgo} by %{ava
export const I18N_SHOW_ONLY_INHERITED = s__('Runners|Show only inherited');
export const I18N_ADMIN = s__('Runners|Administrator');
+// No runners registered
+export const I18N_GET_STARTED = s__('Runners|Get started with runners');
+export const I18N_RUNNERS_ARE_AGENTS = s__(
+ 'Runners|Runners are the agents that run your CI/CD jobs.',
+);
+export const I18N_CREATE_RUNNER_LINK = s__(
+ 'Runners|%{linkStart}Create a new runner%{linkEnd} to get started.',
+);
+export const I18N_STILL_USING_REGISTRATION_TOKENS = s__('Runners|Still using registration tokens?');
+export const I18N_CONTACT_ADMIN_TO_REGISTER = s__(
+ 'Runners|To register new runners, contact your administrator.',
+);
+export const I18N_FOLLOW_REGISTRATION_INSTRUCTIONS = s__(
+ 'Runners|Follow the %{linkStart}installation and registration instructions%{linkEnd} to set up a runner.',
+);
+
+// No runners found
+export const I18N_NO_RESULTS = s__('Runners|No results found');
+export const I18N_EDIT_YOUR_SEARCH = s__('Runners|Edit your search and try again');
+
// Runner details
export const JOBS_ROUTE_PATH = '/jobs'; // vue-router route path
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
index f2dac15a99e..8c8293eb09e 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
@@ -41,7 +41,7 @@ export default {
viewType: {
type: String,
required: false,
- default: 'child',
+ default: 'root',
},
canCreatePipelineInTargetProject: {
type: Boolean,
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue
index 10ac4c5383b..3157653648b 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue
@@ -8,8 +8,10 @@ import {
GlButton,
GlFormCheckbox,
GlLoadingIcon,
+ GlModal,
+ GlSprintf,
} from '@gitlab/ui';
-import { last } from 'lodash';
+import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/alert';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { __, s__ } from '~/locale';
import FileSha from '~/packages_and_registries/package_registry/components/details/file_sha.vue';
@@ -22,10 +24,22 @@ import {
GRAPHQL_PACKAGE_FILES_PAGE_SIZE,
REQUEST_DELETE_SELECTED_PACKAGE_FILE_TRACKING_ACTION,
SELECT_PACKAGE_FILE_TRACKING_ACTION,
+ DOWNLOAD_PACKAGE_ASSET_TRACKING_ACTION,
+ CANCEL_DELETE_PACKAGE_FILE_TRACKING_ACTION,
+ DELETE_PACKAGE_FILE_TRACKING_ACTION,
+ REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION,
TRACKING_LABEL_PACKAGE_ASSET,
TRACKING_ACTION_EXPAND_PACKAGE_ASSET,
+ DELETE_PACKAGE_FILE_ERROR_MESSAGE,
+ DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
+ DELETE_PACKAGE_FILES_ERROR_MESSAGE,
+ DELETE_PACKAGE_FILES_SUCCESS_MESSAGE,
+ DELETE_PACKAGE_FILES_TRACKING_ACTION,
+ DELETE_ALL_PACKAGE_FILES_MODAL_CONTENT,
+ DELETE_LAST_PACKAGE_FILE_MODAL_CONTENT,
} from '~/packages_and_registries/package_registry/constants';
import getPackageFilesQuery from '~/packages_and_registries/package_registry/graphql/queries/get_package_files.query.graphql';
+import destroyPackageFilesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_files.mutation.graphql';
export default {
name: 'PackageFiles',
@@ -38,22 +52,25 @@ export default {
GlFormCheckbox,
GlButton,
GlLoadingIcon,
+ GlModal,
+ GlSprintf,
FileIcon,
TimeAgoTooltip,
FileSha,
},
mixins: [Tracking.mixin()],
+ trackingActions: {
+ DELETE_PACKAGE_FILE_TRACKING_ACTION,
+ REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION,
+ CANCEL_DELETE_PACKAGE_FILE_TRACKING_ACTION,
+ DOWNLOAD_PACKAGE_ASSET_TRACKING_ACTION,
+ },
props: {
canDelete: {
type: Boolean,
required: false,
default: false,
},
- isLoading: {
- type: Boolean,
- required: false,
- default: false,
- },
packageId: {
type: String,
required: true,
@@ -62,6 +79,10 @@ export default {
type: String,
required: true,
},
+ projectPath: {
+ type: String,
+ required: true,
+ },
},
apollo: {
packageFiles: {
@@ -73,7 +94,7 @@ export default {
return this.queryVariables;
},
update(data) {
- return data.package?.packageFiles?.nodes || [];
+ return data.package?.packageFiles ?? {};
},
error() {
this.fetchPackageFilesError = true;
@@ -83,29 +104,33 @@ export default {
data() {
return {
fetchPackageFilesError: false,
- packageFiles: [],
+ filesToDelete: [],
+ packageFiles: {},
+ mutationLoading: false,
selectedReferences: [],
};
},
computed: {
+ files() {
+ return this.packageFiles?.nodes ?? [];
+ },
areFilesSelected() {
return this.selectedReferences.length > 0;
},
areAllFilesSelected() {
- return this.packageFiles.length > 0 && this.packageFiles.every(this.isSelected);
+ return this.files.length > 0 && this.files.every(this.isSelected);
},
filesTableRows() {
- return this.packageFiles.map((pf) => ({
+ return this.files.map((pf) => ({
...pf,
size: this.formatSize(pf.size),
- pipeline: last(pf.pipelines),
}));
},
hasSelectedSomeFiles() {
return this.areFilesSelected && !this.areAllFilesSelected;
},
- loading() {
- return this.$apollo.queries.packageFiles.loading || this.isLoading;
+ isLoading() {
+ return this.$apollo.queries.packageFiles.loading || this.mutationLoading;
},
filesTableHeaderFields() {
return [
@@ -148,6 +173,29 @@ export default {
category: packageTypeToTrackCategory(this.packageType),
};
},
+ refetchQueriesData() {
+ return [
+ {
+ query: getPackageFilesQuery,
+ variables: this.queryVariables,
+ },
+ ];
+ },
+ modalAction() {
+ return this.hasOneItem(this.filesToDelete)
+ ? this.$options.modal.fileDeletePrimaryAction
+ : this.$options.modal.filesDeletePrimaryAction;
+ },
+ modalTitle() {
+ return this.hasOneItem(this.filesToDelete)
+ ? this.$options.i18n.deleteFileModalTitle
+ : this.$options.i18n.deleteFilesModalTitle;
+ },
+ modalDescription() {
+ return this.hasOneItem(this.filesToDelete)
+ ? this.$options.i18n.deleteFileModalContent
+ : this.$options.i18n.deleteFilesModalContent;
+ },
},
methods: {
formatSize(size) {
@@ -170,15 +218,97 @@ export default {
},
handleFileDeleteSelected() {
this.track(REQUEST_DELETE_SELECTED_PACKAGE_FILE_TRACKING_ACTION);
- this.$emit('delete-files', this.selectedReferences);
+ this.handleFileDelete(this.selectedReferences);
+ },
+ async deletePackageFiles(ids) {
+ this.mutationLoading = true;
+ try {
+ const { data } = await this.$apollo.mutate({
+ mutation: destroyPackageFilesMutation,
+ variables: {
+ projectPath: this.projectPath,
+ ids,
+ },
+ awaitRefetchQueries: true,
+ refetchQueries: this.refetchQueriesData,
+ });
+ if (data?.destroyPackageFiles?.errors[0]) {
+ throw data.destroyPackageFiles.errors[0];
+ }
+ createAlert({
+ message: this.hasOneItem(ids)
+ ? DELETE_PACKAGE_FILE_SUCCESS_MESSAGE
+ : DELETE_PACKAGE_FILES_SUCCESS_MESSAGE,
+ variant: VARIANT_SUCCESS,
+ });
+ } catch (error) {
+ createAlert({
+ message: this.hasOneItem(ids)
+ ? DELETE_PACKAGE_FILE_ERROR_MESSAGE
+ : DELETE_PACKAGE_FILES_ERROR_MESSAGE,
+ variant: VARIANT_WARNING,
+ captureError: true,
+ error,
+ });
+ } finally {
+ this.mutationLoading = false;
+ this.filesToDelete = [];
+ this.selectedReferences = [];
+ }
+ },
+ handleFileDelete(files) {
+ this.track(REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION);
+ if (files.length === this.files.length && !this.packageFiles?.pageInfo?.hasNextPage) {
+ this.$emit(
+ 'delete-all-files',
+ this.hasOneItem(files)
+ ? DELETE_LAST_PACKAGE_FILE_MODAL_CONTENT
+ : DELETE_ALL_PACKAGE_FILES_MODAL_CONTENT,
+ );
+ } else {
+ this.filesToDelete = files;
+ this.$refs.deleteFilesModal.show();
+ }
+ },
+ hasOneItem(items) {
+ return items.length === 1;
+ },
+ confirmFilesDelete() {
+ if (this.hasOneItem(this.filesToDelete)) {
+ this.track(DELETE_PACKAGE_FILE_TRACKING_ACTION);
+ } else {
+ this.track(DELETE_PACKAGE_FILES_TRACKING_ACTION);
+ }
+ this.deletePackageFiles(this.filesToDelete.map((file) => file.id));
},
},
i18n: {
- deleteFile: __('Delete asset'),
+ deleteFile: s__('PackageRegistry|Delete asset'),
+ deleteFileModalTitle: s__('PackageRegistry|Delete package asset'),
+ deleteFileModalContent: s__(
+ 'PackageRegistry|You are about to delete %{filename}. This is a destructive action that may render your package unusable. Are you sure?',
+ ),
+ deleteFilesModalTitle: s__('PackageRegistry|Delete %{count} assets'),
+ deleteFilesModalContent: s__(
+ 'PackageRegistry|You are about to delete %{count} assets. This operation is irreversible.',
+ ),
deleteSelected: s__('PackageRegistry|Delete selected'),
moreActionsText: __('More actions'),
fetchPackageFilesErrorMessage: FETCH_PACKAGE_FILES_ERROR_MESSAGE,
},
+ modal: {
+ fileDeletePrimaryAction: {
+ text: __('Delete'),
+ attributes: { variant: 'danger', category: 'primary' },
+ },
+ filesDeletePrimaryAction: {
+ text: s__('PackageRegistry|Permanently delete assets'),
+ attributes: { variant: 'danger', category: 'primary' },
+ },
+ cancelAction: {
+ text: __('Cancel'),
+ },
+ },
};
</script>
@@ -188,7 +318,7 @@ export default {
<h3 class="gl-font-lg gl-mt-5">{{ __('Assets') }}</h3>
<gl-button
v-if="!fetchPackageFilesError && canDelete"
- :disabled="loading || !areFilesSelected"
+ :disabled="isLoading || !areFilesSelected"
category="secondary"
variant="danger"
data-testid="delete-selected"
@@ -206,7 +336,7 @@ export default {
</gl-alert>
<gl-table
v-else
- :busy="loading"
+ :busy="isLoading"
:fields="filesTableHeaderFields"
:items="filesTableRows"
show-empty
@@ -255,7 +385,7 @@ export default {
:href="item.downloadPath"
class="gl-text-gray-500"
data-testid="download-link"
- @click="$emit('download-file')"
+ @click="track($options.trackingActions.DOWNLOAD_PACKAGE_ASSET_TRACKING_ACTION)"
>
<file-icon
:file-name="item.fileName"
@@ -279,7 +409,7 @@ export default {
no-caret
right
>
- <gl-dropdown-item data-testid="delete-file" @click="$emit('delete-files', [item])">
+ <gl-dropdown-item data-testid="delete-file" @click="handleFileDelete([item])">
{{ $options.i18n.deleteFile }}
</gl-dropdown-item>
</gl-dropdown>
@@ -300,5 +430,34 @@ export default {
</div>
</template>
</gl-table>
+
+ <gl-modal
+ ref="deleteFilesModal"
+ size="sm"
+ modal-id="delete-files-modal"
+ :action-primary="modalAction"
+ :action-cancel="$options.modal.cancelAction"
+ data-testid="delete-files-modal"
+ @primary="confirmFilesDelete"
+ @canceled="track($options.trackingActions.CANCEL_DELETE_PACKAGE_FILE)"
+ >
+ <template #modal-title>
+ <gl-sprintf :message="modalTitle">
+ <template #count>
+ {{ filesToDelete.length }}
+ </template>
+ </gl-sprintf>
+ </template>
+
+ <gl-sprintf :message="modalDescription">
+ <template #filename>
+ <strong>{{ filesToDelete[0].fileName }}</strong>
+ </template>
+
+ <template #count>
+ {{ filesToDelete.length }}
+ </template>
+ </gl-sprintf>
+ </gl-modal>
</div>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
index e5ef9265f3e..2a9cfb955a7 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
@@ -47,9 +47,6 @@ query getPackageDetails($id: PackagesPackageID!) {
}
}
packageFiles(first: 100) {
- pageInfo {
- hasNextPage
- }
nodes {
id
size
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_files.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_files.query.graphql
index 7851cd39200..e6f292ec1d3 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_files.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_files.query.graphql
@@ -2,6 +2,9 @@ query getPackageFiles($id: PackagesPackageID!, $first: Int) {
package(id: $id) {
id
packageFiles(first: $first) {
+ pageInfo {
+ hasNextPage
+ }
nodes {
id
fileMd5
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
index 48a45956ef1..922886fa9cd 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
@@ -11,7 +11,7 @@ import {
GlTabs,
GlSprintf,
} from '@gitlab/ui';
-import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/alert';
+import { createAlert } from '~/alert';
import { TYPENAME_PACKAGES_PACKAGE } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { numberToHumanSize } from '~/lib/utils/number_utils';
@@ -33,27 +33,15 @@ import {
DELETE_PACKAGE_TRACKING_ACTION,
REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
CANCEL_DELETE_PACKAGE_TRACKING_ACTION,
- DELETE_PACKAGE_FILE_TRACKING_ACTION,
- DELETE_PACKAGE_FILES_TRACKING_ACTION,
- REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION,
REQUEST_FORWARDING_HELP_PAGE_PATH,
- CANCEL_DELETE_PACKAGE_FILE_TRACKING_ACTION,
SHOW_DELETE_SUCCESS_ALERT,
FETCH_PACKAGE_DETAILS_ERROR_MESSAGE,
- DELETE_PACKAGE_FILE_ERROR_MESSAGE,
- DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
- DELETE_PACKAGE_FILES_ERROR_MESSAGE,
- DELETE_PACKAGE_FILES_SUCCESS_MESSAGE,
DELETE_PACKAGE_REQUEST_FORWARDING_MODAL_CONTENT,
- DOWNLOAD_PACKAGE_ASSET_TRACKING_ACTION,
DELETE_MODAL_TITLE,
DELETE_MODAL_CONTENT,
- DELETE_ALL_PACKAGE_FILES_MODAL_CONTENT,
- DELETE_LAST_PACKAGE_FILE_MODAL_CONTENT,
GRAPHQL_PAGE_SIZE,
} from '~/packages_and_registries/package_registry/constants';
-import destroyPackageFilesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_files.mutation.graphql';
import getPackageDetails from '~/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql';
import getPackageVersionsQuery from '~/packages_and_registries/package_registry/graphql/queries/get_package_versions.query.graphql';
import Tracking from '~/tracking';
@@ -92,10 +80,6 @@ export default {
DELETE_PACKAGE_TRACKING_ACTION,
REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
CANCEL_DELETE_PACKAGE_TRACKING_ACTION,
- DELETE_PACKAGE_FILE_TRACKING_ACTION,
- REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION,
- CANCEL_DELETE_PACKAGE_FILE_TRACKING_ACTION,
- DOWNLOAD_PACKAGE_ASSET_TRACKING_ACTION,
},
data() {
return {
@@ -158,9 +142,6 @@ export default {
isLoading() {
return this.$apollo.queries.packageEntity.loading;
},
- packageFilesMutationLoading() {
- return this.mutationLoading;
- },
isValidPackage() {
return this.isLoading || Boolean(this.packageEntity.name);
},
@@ -196,14 +177,6 @@ export default {
PACKAGE_TYPE_PYPI,
].includes(this.packageType);
},
- refetchQueriesData() {
- return [
- {
- query: getPackageDetails,
- variables: this.queryVariables,
- },
- ];
- },
refetchVersionsQueryData() {
return [
{
@@ -230,71 +203,9 @@ export default {
window.location.replace(`${returnTo}?${modalQuery}`);
},
- async deletePackageFiles(ids) {
- this.mutationLoading = true;
- try {
- const { data } = await this.$apollo.mutate({
- mutation: destroyPackageFilesMutation,
- variables: {
- projectPath: this.projectPath,
- ids,
- },
- awaitRefetchQueries: true,
- refetchQueries: this.refetchQueriesData,
- });
- if (data?.destroyPackageFiles?.errors[0]) {
- throw data.destroyPackageFiles.errors[0];
- }
- createAlert({
- message: this.isLastItem(ids)
- ? DELETE_PACKAGE_FILE_SUCCESS_MESSAGE
- : DELETE_PACKAGE_FILES_SUCCESS_MESSAGE,
- variant: VARIANT_SUCCESS,
- });
- } catch (error) {
- createAlert({
- message: this.isLastItem(ids)
- ? DELETE_PACKAGE_FILE_ERROR_MESSAGE
- : DELETE_PACKAGE_FILES_ERROR_MESSAGE,
- variant: VARIANT_WARNING,
- captureError: true,
- error,
- });
- }
- this.mutationLoading = false;
- },
- handleFileDelete(files) {
- this.track(REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION);
- if (
- files.length === this.packageFiles.length &&
- !this.packageEntity.packageFiles?.pageInfo?.hasNextPage
- ) {
- if (this.isLastItem(files)) {
- this.deletePackageModalContent = DELETE_LAST_PACKAGE_FILE_MODAL_CONTENT;
- } else {
- this.deletePackageModalContent = DELETE_ALL_PACKAGE_FILES_MODAL_CONTENT;
- }
- this.$refs.deleteModal.show();
- } else {
- this.filesToDelete = files;
- if (this.isLastItem(files)) {
- this.$refs.deleteFileModal.show();
- } else if (files.length > 1) {
- this.$refs.deleteFilesModal.show();
- }
- }
- },
- isLastItem(items) {
- return items.length === 1;
- },
- confirmFilesDelete() {
- if (this.isLastItem(this.filesToDelete)) {
- this.track(DELETE_PACKAGE_FILE_TRACKING_ACTION);
- } else {
- this.track(DELETE_PACKAGE_FILES_TRACKING_ACTION);
- }
- this.deletePackageFiles(this.filesToDelete.map((file) => file.id));
- this.filesToDelete = [];
+ handleAllFilesDelete(content) {
+ this.deletePackageModalContent = content;
+ this.$refs.deleteModal.show();
},
resetDeleteModalContent() {
this.deletePackageModalContent = DELETE_MODAL_CONTENT;
@@ -302,10 +213,6 @@ export default {
},
i18n: {
DELETE_MODAL_TITLE,
- deleteFileModalTitle: s__(`PackageRegistry|Delete package asset`),
- deleteFileModalContent: s__(
- `PackageRegistry|You are about to delete %{filename}. This is a destructive action that may render your package unusable. Are you sure?`,
- ),
otherVersionsTabTitle: s__('PackageRegistry|Other versions'),
},
links: {
@@ -374,11 +281,10 @@ export default {
<package-files
v-if="showFiles"
:can-delete="packageEntity.canDestroy"
- :is-loading="packageFilesMutationLoading"
:package-id="packageEntity.id"
:package-type="packageType"
- @download-file="track($options.trackingActions.DOWNLOAD_PACKAGE_ASSET_TRACKING_ACTION)"
- @delete-files="handleFileDelete"
+ :project-path="projectPath"
+ @delete-all-files="handleAllFilesDelete"
/>
</div>
</gl-tab>
@@ -471,51 +377,5 @@ export default {
</gl-modal>
</template>
</delete-packages>
-
- <gl-modal
- ref="deleteFileModal"
- size="sm"
- modal-id="delete-file-modal"
- :action-primary="$options.modal.fileDeletePrimaryAction"
- :action-cancel="$options.modal.cancelAction"
- data-testid="delete-file-modal"
- @primary="confirmFilesDelete"
- @canceled="track($options.trackingActions.CANCEL_DELETE_PACKAGE_FILE)"
- >
- <template #modal-title>{{ $options.i18n.deleteFileModalTitle }}</template>
- <gl-sprintf v-if="isLastItem(filesToDelete)" :message="$options.i18n.deleteFileModalContent">
- <template #filename>
- <strong>{{ filesToDelete[0].fileName }}</strong>
- </template>
- </gl-sprintf>
- </gl-modal>
-
- <gl-modal
- ref="deleteFilesModal"
- size="sm"
- modal-id="delete-files-modal"
- :action-primary="$options.modal.filesDeletePrimaryAction"
- :action-cancel="$options.modal.cancelAction"
- data-testid="delete-files-modal"
- @primary="confirmFilesDelete"
- @canceled="track($options.trackingActions.CANCEL_DELETE_PACKAGE_FILE)"
- >
- <template #modal-title>{{
- n__(
- `PackageRegistry|Delete 1 asset`,
- `PackageRegistry|Delete %d assets`,
- filesToDelete.length,
- )
- }}</template>
- <span v-if="filesToDelete.length > 0">
- {{
- n__(
- `PackageRegistry|You are about to delete 1 asset. This operation is irreversible.`,
- `PackageRegistry|You are about to delete %d assets. This operation is irreversible.`,
- filesToDelete.length,
- )
- }}
- </span>
- </gl-modal>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_details_header.vue b/app/assets/javascripts/pipelines/components/pipeline_details_header.vue
index 7d4395dd579..3030a14d1d5 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_details_header.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_details_header.vue
@@ -1,30 +1,61 @@
<script>
-import { GlBadge, GlIcon, GlLink, GlLoadingIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
+import {
+ GlAlert,
+ GlBadge,
+ GlButton,
+ GlIcon,
+ GlLink,
+ GlLoadingIcon,
+ GlModal,
+ GlModalDirective,
+ GlSprintf,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+import { setUrlFragment, redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import { __, s__, sprintf } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import SafeHtml from '~/vue_shared/directives/safe_html';
-import { LOAD_FAILURE, POST_FAILURE, DELETE_FAILURE, DEFAULT } from '../constants';
+import {
+ LOAD_FAILURE,
+ POST_FAILURE,
+ DELETE_FAILURE,
+ DEFAULT,
+ BUTTON_TOOLTIP_RETRY,
+ BUTTON_TOOLTIP_CANCEL,
+} from '../constants';
+import cancelPipelineMutation from '../graphql/mutations/cancel_pipeline.mutation.graphql';
+import deletePipelineMutation from '../graphql/mutations/delete_pipeline.mutation.graphql';
+import retryPipelineMutation from '../graphql/mutations/retry_pipeline.mutation.graphql';
import getPipelineQuery from '../graphql/queries/get_pipeline_header_data.query.graphql';
import TimeAgo from './pipelines_list/time_ago.vue';
import { getQueryHeaders } from './graph/utils';
+const DELETE_MODAL_ID = 'pipeline-delete-modal';
const POLL_INTERVAL = 10000;
export default {
name: 'PipelineDetailsHeader',
+ BUTTON_TOOLTIP_RETRY,
+ BUTTON_TOOLTIP_CANCEL,
+ pipelineCancel: 'pipelineCancel',
+ pipelineRetry: 'pipelineRetry',
finishedStatuses: ['FAILED', 'SUCCESS', 'CANCELED'],
components: {
CiBadgeLink,
ClipboardButton,
+ GlAlert,
GlBadge,
+ GlButton,
GlIcon,
GlLink,
GlLoadingIcon,
+ GlModal,
GlSprintf,
TimeAgo,
},
directives: {
+ GlModal: GlModalDirective,
GlTooltip: GlTooltipDirective,
SafeHtml,
},
@@ -51,6 +82,12 @@ export default {
),
stuckBadgeText: s__('Pipelines|stuck'),
stuckBadgeTooltip: s__('Pipelines|This pipeline is stuck'),
+ computeCreditsTooltip: s__('Pipelines|Total amount of compute credits used for the pipeline'),
+ totalJobsTooltip: s__('Pipelines|Total number of jobs for the pipeline'),
+ retryPipelineText: __('Retry'),
+ cancelPipelineText: __('Cancel pipeline'),
+ deletePipelineText: __('Delete'),
+ clipboardTooltip: __('Copy commit SHA'),
},
errorTexts: {
[LOAD_FAILURE]: __('We are currently unable to fetch data for the pipeline header.'),
@@ -58,6 +95,22 @@ export default {
[DELETE_FAILURE]: __('An error occurred while deleting the pipeline.'),
[DEFAULT]: __('An unknown error occurred.'),
},
+ modal: {
+ id: DELETE_MODAL_ID,
+ title: __('Delete pipeline'),
+ deleteConfirmationText: __(
+ 'Are you sure you want to delete this pipeline? Doing so will expire all pipeline caches and delete all related objects, such as builds, logs, artifacts, and triggers. This action cannot be undone.',
+ ),
+ actionPrimary: {
+ text: __('Delete pipeline'),
+ attributes: {
+ variant: 'danger',
+ },
+ },
+ actionCancel: {
+ text: __('Cancel'),
+ },
+ },
inject: {
graphqlResourceEtag: {
default: '',
@@ -224,141 +277,306 @@ export default {
queuedDuration: this.pipeline?.queuedDuration || 0,
});
},
+ canRetryPipeline() {
+ const { retryable, userPermissions } = this.pipeline;
+
+ return retryable && userPermissions.updatePipeline;
+ },
+ canCancelPipeline() {
+ const { cancelable, userPermissions } = this.pipeline;
+
+ return cancelable && userPermissions.updatePipeline;
+ },
},
methods: {
reportFailure(errorType, errorMessages = []) {
this.failureType = errorType;
this.failureMessages = errorMessages;
},
+ async postPipelineAction(name, mutation) {
+ try {
+ const {
+ data: {
+ [name]: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation,
+ variables: { id: this.pipeline.id },
+ });
+
+ if (errors.length > 0) {
+ this.isRetrying = false;
+
+ this.reportFailure(POST_FAILURE, errors);
+ } else {
+ await this.$apollo.queries.pipeline.refetch();
+ if (!this.isFinished) {
+ this.$apollo.queries.pipeline.startPolling(POLL_INTERVAL);
+ }
+ }
+ } catch {
+ this.isRetrying = false;
+
+ this.reportFailure(POST_FAILURE);
+ }
+ },
+ cancelPipeline() {
+ this.isCanceling = true;
+ this.postPipelineAction(this.$options.pipelineCancel, cancelPipelineMutation);
+ },
+ retryPipeline() {
+ this.isRetrying = true;
+ this.postPipelineAction(this.$options.pipelineRetry, retryPipelineMutation);
+ },
+ async deletePipeline() {
+ this.isDeleting = true;
+ this.$apollo.queries.pipeline.stopPolling();
+
+ try {
+ const {
+ data: {
+ pipelineDestroy: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: deletePipelineMutation,
+ variables: {
+ id: this.pipeline.id,
+ },
+ });
+
+ if (errors.length > 0) {
+ this.reportFailure(DELETE_FAILURE, errors);
+ this.isDeleting = false;
+ } else {
+ redirectTo(setUrlFragment(this.paths.pipelinesPath, 'delete_success')); // eslint-disable-line import/no-deprecated
+ }
+ } catch {
+ this.$apollo.queries.pipeline.startPolling(POLL_INTERVAL);
+ this.reportFailure(DELETE_FAILURE);
+ this.isDeleting = false;
+ }
+ },
},
};
</script>
<template>
- <div class="gl-mt-3">
+ <div class="gl-my-4">
+ <gl-alert v-if="hasError" :title="failure.text" :variant="failure.variant" :dismissible="false">
+ <div v-for="(failureMessage, index) in failureMessages" :key="`failure-message-${index}`">
+ {{ failureMessage }}
+ </div>
+ </gl-alert>
<gl-loading-icon v-if="loading" class="gl-text-left" size="lg" />
- <template v-else>
- <h3 v-if="name" class="gl-mt-0 gl-mb-2" data-testid="pipeline-name">{{ name }}</h3>
+ <div v-else class="gl-display-flex gl-justify-content-space-between">
<div>
- <ci-badge-link :status="detailedStatus" />
- <div class="gl-ml-2 gl-mb-2 gl-display-inline-block gl-h-6">
- <gl-sprintf :message="triggeredText">
- <template #link="{ content }">
- <gl-link
- :href="userPath"
- class="gl-text-gray-900 gl-font-weight-bold"
- target="_blank"
- >
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- <gl-link
- :href="commitPath"
- class="gl-bg-blue-50 gl-rounded-base gl-px-2 gl-mx-2"
- data-testid="commit-link"
+ <h3 v-if="name" class="gl-mt-0 gl-mb-2" data-testid="pipeline-name">{{ name }}</h3>
+ <div>
+ <ci-badge-link :status="detailedStatus" />
+ <div class="gl-ml-2 gl-mb-2 gl-display-inline-block gl-h-6">
+ <gl-sprintf :message="triggeredText">
+ <template #link="{ content }">
+ <gl-link
+ :href="userPath"
+ class="gl-text-gray-900 gl-font-weight-bold"
+ target="_blank"
+ >
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ <gl-link
+ :href="commitPath"
+ class="gl-bg-blue-50 gl-rounded-base gl-px-2 gl-mx-2"
+ data-testid="commit-link"
+ >
+ {{ shortId }}
+ </gl-link>
+ <clipboard-button
+ :text="shortId"
+ category="tertiary"
+ :title="$options.i18n.clipboardTooltip"
+ size="small"
+ />
+ <time-ago
+ v-if="isFinished"
+ :pipeline="pipeline"
+ class="gl-display-inline gl-mb-0"
+ :display-calendar-icon="false"
+ font-size="gl-font-md"
+ />
+ </div>
+ </div>
+ <div v-safe-html="refText" class="gl-mb-2" data-testid="pipeline-ref-text"></div>
+ <div>
+ <gl-badge
+ v-if="badges.schedule"
+ v-gl-tooltip
+ :title="$options.i18n.scheduleBadgeTooltip"
+ variant="info"
+ size="sm"
+ >
+ {{ $options.i18n.scheduleBadgeText }}
+ </gl-badge>
+ <gl-badge
+ v-if="badges.child"
+ v-gl-tooltip
+ :title="$options.i18n.childBadgeTooltip"
+ variant="info"
+ size="sm"
+ >
+ <gl-sprintf :message="$options.i18n.childBadgeText">
+ <template #link="{ content }">
+ <gl-link :href="paths.triggeredByPath" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </gl-badge>
+ <gl-badge
+ v-if="badges.latest"
+ v-gl-tooltip
+ :title="$options.i18n.latestBadgeTooltip"
+ variant="success"
+ size="sm"
+ >
+ {{ $options.i18n.latestBadgeText }}
+ </gl-badge>
+ <gl-badge
+ v-if="badges.mergeTrainPipeline"
+ v-gl-tooltip
+ :title="$options.i18n.mergeTrainBadgeTooltip"
+ variant="info"
+ size="sm"
+ >
+ {{ $options.i18n.mergeTrainBadgeText }}
+ </gl-badge>
+ <gl-badge
+ v-if="badges.invalid"
+ v-gl-tooltip
+ :title="yamlErrors"
+ variant="danger"
+ size="sm"
>
- {{ shortId }}
- </gl-link>
- <clipboard-button
- :text="shortId"
- category="tertiary"
- :title="__('Copy commit SHA')"
- size="small"
- />
- <time-ago
+ {{ $options.i18n.invalidBadgeText }}
+ </gl-badge>
+ <gl-badge
+ v-if="badges.failed"
+ v-gl-tooltip
+ :title="failureReason"
+ variant="danger"
+ size="sm"
+ >
+ {{ $options.i18n.failedBadgeText }}
+ </gl-badge>
+ <gl-badge
+ v-if="badges.autoDevops"
+ v-gl-tooltip
+ :title="$options.i18n.autoDevopsBadgeTooltip"
+ variant="info"
+ size="sm"
+ >
+ {{ $options.i18n.autoDevopsBadgeText }}
+ </gl-badge>
+ <gl-badge
+ v-if="badges.detached"
+ v-gl-tooltip
+ :title="$options.i18n.detachedBadgeTooltip"
+ variant="info"
+ size="sm"
+ data-qa-selector="merge_request_badge_tag"
+ >
+ {{ $options.i18n.detachedBadgeText }}
+ </gl-badge>
+ <gl-badge
+ v-if="badges.stuck"
+ v-gl-tooltip
+ :title="$options.i18n.stuckBadgeTooltip"
+ variant="warning"
+ size="sm"
+ >
+ {{ $options.i18n.stuckBadgeText }}
+ </gl-badge>
+ <span
+ v-gl-tooltip
+ :title="$options.i18n.totalJobsTooltip"
+ class="gl-ml-2"
+ data-testid="total-jobs"
+ >
+ <gl-icon name="pipeline" />
+ {{ totalJobsText }}
+ </span>
+ <span
v-if="isFinished"
- :pipeline="pipeline"
- class="gl-display-inline gl-mb-0"
- :display-calendar-icon="false"
- font-size="gl-font-md"
- />
+ v-gl-tooltip
+ :title="$options.i18n.computeCreditsTooltip"
+ class="gl-ml-2"
+ data-testid="compute-credits"
+ >
+ <gl-icon name="quota" />
+ {{ computeCredits }}
+ </span>
+ <span v-if="inProgress" class="gl-ml-2" data-testid="pipeline-running-text">
+ <gl-icon name="timer" />
+ {{ inProgressText }}
+ </span>
</div>
</div>
- <div v-safe-html="refText" class="gl-mb-2" data-testid="pipeline-ref-text"></div>
<div>
- <gl-badge
- v-if="badges.schedule"
+ <gl-button
+ v-if="canRetryPipeline"
v-gl-tooltip
- :title="$options.i18n.scheduleBadgeTooltip"
- variant="info"
+ :aria-label="$options.BUTTON_TOOLTIP_RETRY"
+ :title="$options.BUTTON_TOOLTIP_RETRY"
+ :loading="isRetrying"
+ :disabled="isRetrying"
+ variant="confirm"
+ data-testid="retry-pipeline"
+ class="js-retry-button"
+ @click="retryPipeline()"
>
- {{ $options.i18n.scheduleBadgeText }}
- </gl-badge>
- <gl-badge
- v-if="badges.child"
- v-gl-tooltip
- :title="$options.i18n.childBadgeTooltip"
- variant="info"
- >
- <gl-sprintf :message="$options.i18n.childBadgeText">
- <template #link="{ content }">
- <gl-link :href="paths.triggeredByPath" target="_blank">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </gl-badge>
- <gl-badge
- v-if="badges.latest"
- v-gl-tooltip
- :title="$options.i18n.latestBadgeTooltip"
- variant="success"
- >
- {{ $options.i18n.latestBadgeText }}
- </gl-badge>
- <gl-badge
- v-if="badges.mergeTrainPipeline"
- v-gl-tooltip
- :title="$options.i18n.mergeTrainBadgeTooltip"
- variant="info"
- >
- {{ $options.i18n.mergeTrainBadgeText }}
- </gl-badge>
- <gl-badge v-if="badges.invalid" v-gl-tooltip :title="yamlErrors" variant="danger">
- {{ $options.i18n.invalidBadgeText }}
- </gl-badge>
- <gl-badge v-if="badges.failed" v-gl-tooltip :title="failureReason" variant="danger">
- {{ $options.i18n.failedBadgeText }}
- </gl-badge>
- <gl-badge
- v-if="badges.autoDevops"
- v-gl-tooltip
- :title="$options.i18n.autoDevopsBadgeTooltip"
- variant="info"
- >
- {{ $options.i18n.autoDevopsBadgeText }}
- </gl-badge>
- <gl-badge
- v-if="badges.detached"
+ {{ $options.i18n.retryPipelineText }}
+ </gl-button>
+
+ <gl-button
+ v-if="canCancelPipeline"
v-gl-tooltip
- :title="$options.i18n.detachedBadgeTooltip"
- variant="info"
- data-qa-selector="merge_request_badge_tag"
+ :aria-label="$options.BUTTON_TOOLTIP_CANCEL"
+ :title="$options.BUTTON_TOOLTIP_CANCEL"
+ :loading="isCanceling"
+ :disabled="isCanceling"
+ class="gl-ml-3"
+ variant="danger"
+ data-testid="cancel-pipeline"
+ @click="cancelPipeline()"
>
- {{ $options.i18n.detachedBadgeText }}
- </gl-badge>
- <gl-badge
- v-if="badges.stuck"
- v-gl-tooltip
- :title="$options.i18n.stuckBadgeTooltip"
- variant="warning"
+ {{ $options.i18n.cancelPipelineText }}
+ </gl-button>
+
+ <gl-button
+ v-if="pipeline.userPermissions.destroyPipeline"
+ v-gl-modal="$options.modal.id"
+ :loading="isDeleting"
+ :disabled="isDeleting"
+ class="gl-ml-3"
+ variant="danger"
+ category="secondary"
+ data-testid="delete-pipeline"
>
- {{ $options.i18n.stuckBadgeText }}
- </gl-badge>
- <span class="gl-ml-2" data-testid="total-jobs">
- <gl-icon name="pipeline" />
- {{ totalJobsText }}
- </span>
- <span v-if="isFinished" class="gl-ml-2" data-testid="compute-credits">
- <gl-icon name="quota" />
- {{ computeCredits }}
- </span>
- <span v-if="inProgress" class="gl-ml-2" data-testid="pipeline-running-text">
- <gl-icon name="timer" />
- {{ inProgressText }}
- </span>
+ {{ $options.i18n.deletePipelineText }}
+ </gl-button>
</div>
- </template>
+ </div>
+ <gl-modal
+ :modal-id="$options.modal.id"
+ :title="$options.modal.title"
+ :action-primary="$options.modal.actionPrimary"
+ :action-cancel="$options.modal.actionCancel"
+ @primary="deletePipeline()"
+ >
+ <p>
+ {{ $options.modal.deleteConfirmationText }}
+ </p>
+ </gl-modal>
</div>
</template>
diff --git a/app/assets/javascripts/projects/commit_box/info/components/commit_refs.vue b/app/assets/javascripts/projects/commit_box/info/components/commit_refs.vue
index 4258332ed6e..25af4cc8082 100644
--- a/app/assets/javascripts/projects/commit_box/info/components/commit_refs.vue
+++ b/app/assets/javascripts/projects/commit_box/info/components/commit_refs.vue
@@ -9,6 +9,8 @@ import {
TAGS,
FETCH_CONTAINING_REFS_EVENT,
FETCH_COMMIT_REFERENCES_ERROR,
+ BRANCHES_REF_TYPE,
+ TAGS_REF_TYPE,
} from '../constants';
import RefsList from './refs_list.vue';
@@ -98,7 +100,9 @@ export default {
tags: TAGS,
errorMessage: FETCH_COMMIT_REFERENCES_ERROR,
},
- fetchContainingRefsEvent: FETCH_CONTAINING_REFS_EVENT,
+ FETCH_CONTAINING_REFS_EVENT,
+ BRANCHES_REF_TYPE,
+ TAGS_REF_TYPE,
};
</script>
@@ -112,7 +116,8 @@ export default {
:containing-refs="containingBranches"
:namespace="$options.i18n.branches"
:url-part="commitsUrlPart"
- @[$options.fetchContainingRefsEvent]="fetchContainingBranches"
+ :ref-type="$options.BRANCHES_REF_TYPE"
+ @[$options.FETCH_CONTAINING_REFS_EVENT]="fetchContainingBranches"
/>
<refs-list
v-if="hasTags"
@@ -122,7 +127,8 @@ export default {
:containing-refs="containingTags"
:namespace="$options.i18n.tags"
:url-part="commitsUrlPart"
- @[$options.fetchContainingRefsEvent]="fetchContainingTags"
+ :ref-type="$options.TAGS_REF_TYPE"
+ @[$options.FETCH_CONTAINING_REFS_EVENT]="fetchContainingTags"
/>
</div>
</template>
diff --git a/app/assets/javascripts/projects/commit_box/info/components/refs_list.vue b/app/assets/javascripts/projects/commit_box/info/components/refs_list.vue
index 7e21040a3b1..8ceab9cb60b 100644
--- a/app/assets/javascripts/projects/commit_box/info/components/refs_list.vue
+++ b/app/assets/javascripts/projects/commit_box/info/components/refs_list.vue
@@ -16,6 +16,10 @@ export default {
type: String,
required: true,
},
+ refType: {
+ type: String,
+ required: true,
+ },
containingRefs: {
type: Array,
required: false,
@@ -60,6 +64,9 @@ export default {
this.toggleCollapse();
this.$emit(FETCH_CONTAINING_REFS_EVENT);
},
+ getRefUrl(ref) {
+ return `${this.urlPart}${ref}?ref_type=${this.refType}`;
+ },
},
i18n: {
containingCommit: CONTAINING_COMMIT,
@@ -73,7 +80,7 @@ export default {
<gl-badge
v-for="ref in tippingRefs"
:key="ref"
- :href="`${urlPart}${ref}`"
+ :href="getRefUrl(ref)"
class="gl-mt-2 gl-mr-2"
size="sm"
>{{ ref }}</gl-badge
@@ -94,7 +101,7 @@ export default {
<gl-badge
v-for="ref in containingRefs"
:key="ref"
- :href="`${urlPart}${ref}`"
+ :href="getRefUrl(ref)"
class="gl-mt-3 gl-mr-2"
size="sm"
>{{ ref }}</gl-badge
diff --git a/app/assets/javascripts/projects/commit_box/info/constants.js b/app/assets/javascripts/projects/commit_box/info/constants.js
index f255d6c3877..4b74fbe19e1 100644
--- a/app/assets/javascripts/projects/commit_box/info/constants.js
+++ b/app/assets/javascripts/projects/commit_box/info/constants.js
@@ -17,3 +17,7 @@ export const FETCH_CONTAINING_REFS_EVENT = 'fetch-containing-refs';
export const FETCH_COMMIT_REFERENCES_ERROR = s__(
'Commit|There was an error fetching the commit references. Please try again later.',
);
+
+export const BRANCHES_REF_TYPE = 'heads';
+
+export const TAGS_REF_TYPE = 'tags';
diff --git a/app/assets/javascripts/security_configuration/components/app.vue b/app/assets/javascripts/security_configuration/components/app.vue
index d57b3fda342..e7d97989195 100644
--- a/app/assets/javascripts/security_configuration/components/app.vue
+++ b/app/assets/javascripts/security_configuration/components/app.vue
@@ -164,7 +164,7 @@ export default {
<gl-tabs
content-class="gl-pt-0"
- data-qa-selector="security_configuration_container"
+ data-testid="security-configuration-container"
sync-active-tab-with-query-params
lazy
>
@@ -196,12 +196,9 @@ export default {
{{ $options.i18n.description }}
</p>
<p v-if="canViewCiHistory">
- <gl-link
- data-testid="security-view-history-link"
- data-qa-selector="security_configuration_history_link"
- :href="gitlabCiHistoryPath"
- >{{ $options.i18n.configurationHistory }}</gl-link
- >
+ <gl-link data-testid="security-view-history-link" :href="gitlabCiHistoryPath">{{
+ $options.i18n.configurationHistory
+ }}</gl-link>
</p>
</template>
diff --git a/app/assets/javascripts/security_configuration/components/auto_dev_ops_alert.vue b/app/assets/javascripts/security_configuration/components/auto_dev_ops_alert.vue
index 315f676e659..c01df3573c5 100644
--- a/app/assets/javascripts/security_configuration/components/auto_dev_ops_alert.vue
+++ b/app/assets/javascripts/security_configuration/components/auto_dev_ops_alert.vue
@@ -28,7 +28,7 @@ export default {
variant="info"
:primary-button-link="autoDevopsPath"
:primary-button-text="$options.i18n.primaryButtonText"
- data-qa-selector="autodevops_container"
+ data-testid="autodevops-container"
@dismiss="dismissMethod"
>
<gl-sprintf :message="$options.i18n.body">
diff --git a/app/assets/javascripts/security_configuration/components/feature_card.vue b/app/assets/javascripts/security_configuration/components/feature_card.vue
index d1b705fe2fc..a757657339b 100644
--- a/app/assets/javascripts/security_configuration/components/feature_card.vue
+++ b/app/assets/javascripts/security_configuration/components/feature_card.vue
@@ -122,7 +122,7 @@ export default {
v-if="isNotSastIACTemporaryHack"
:class="statusClasses"
data-testid="feature-status"
- :data-qa-selector="`${feature.type}_status`"
+ :data-qa-feature="`${feature.type}_${enabled}_status`"
>
<feature-card-badge
v-if="hasBadge"
@@ -164,7 +164,7 @@ export default {
:href="feature.configurationPath"
variant="confirm"
:category="configurationButton.category"
- :data-qa-selector="`${feature.type}_enable_button`"
+ :data-testid="`${feature.type}_enable_button`"
class="gl-mt-5"
>
{{ configurationButton.text }}
@@ -176,7 +176,7 @@ export default {
variant="confirm"
:category="manageViaMrButtonCategory"
class="gl-mt-5"
- :data-qa-selector="`${feature.type}_mr_button`"
+ :data-testid="`${feature.type}_mr_button`"
@error="onError"
/>
diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
index 7b5ded9348f..9023807eba3 100644
--- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTooltipDirective, GlLink } from '@gitlab/ui';
+import { GlBadge, GlTooltipDirective } from '@gitlab/ui';
import CiIcon from './ci_icon.vue';
/**
* Renders CI Badge link with CI icon and status text based on
@@ -26,8 +26,8 @@ import CiIcon from './ci_icon.vue';
export default {
components: {
- GlLink,
CiIcon,
+ GlBadge,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -42,6 +42,11 @@ export default {
required: false,
default: true,
},
+ badgeSize: {
+ type: String,
+ required: false,
+ default: 'md',
+ },
},
computed: {
title() {
@@ -51,27 +56,76 @@ export default {
// For now, this can either come from graphQL with camelCase or REST API in snake_case
return this.status.detailsPath || this.status.details_path;
},
- cssClass() {
- const className = this.status.group;
- return className ? `ci-status ci-${className}` : 'ci-status';
+ badgeStyles() {
+ switch (this.status.icon) {
+ case 'status_success':
+ return {
+ textColor: 'gl-text-green-700',
+ variant: 'success',
+ };
+ case 'status_warning':
+ return {
+ textColor: 'gl-text-orange-700',
+ variant: 'warning',
+ };
+ case 'status_failed':
+ return {
+ textColor: 'gl-text-red-700',
+ variant: 'danger',
+ };
+ case 'status_running':
+ return {
+ textColor: 'gl-text-blue-700',
+ variant: 'info',
+ };
+ case 'status_pending':
+ return {
+ textColor: 'gl-text-orange-700',
+ variant: 'warning',
+ };
+ case 'status_canceled':
+ return {
+ textColor: 'gl-text-gray-700',
+ variant: 'neutral',
+ };
+ case 'status_manual':
+ return {
+ textColor: 'gl-text-gray-700',
+ variant: 'neutral',
+ };
+ // default covers the styles for the remainder of CI
+ // statuses that are not explicitly stated here
+ default:
+ return {
+ textColor: 'gl-text-gray-600',
+ variant: 'muted',
+ };
+ }
},
},
};
</script>
<template>
- <gl-link
+ <gl-badge
v-gl-tooltip
- class="gl-display-inline-flex gl-align-items-center gl-line-height-0 gl-px-3 gl-py-2 gl-rounded-base"
- :class="cssClass"
:title="title"
- data-qa-selector="status_badge_link"
:href="detailsPath"
+ :size="badgeSize"
+ :variant="badgeStyles.variant"
+ :data-testid="`ci-badge-${status.text}`"
+ data-qa-selector="status_badge_link"
@click="$emit('ciStatusBadgeClick')"
>
<ci-icon :status="status" />
<template v-if="showText">
- <span class="gl-ml-2 gl-white-space-nowrap">{{ status.text }}</span>
+ <span
+ class="gl-ml-2 gl-white-space-nowrap"
+ :class="badgeStyles.textColor"
+ data-testid="ci-badge-text"
+ >
+ {{ status.text }}
+ </span>
</template>
- </gl-link>
+ </gl-badge>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
index faa50a50c69..3bb168e9051 100644
--- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
@@ -131,7 +131,6 @@ export default {
ref="search"
:value="searchTerm"
:placeholder="searchText"
- class="js-dropdown-input-field"
@input="setSearchTerm"
/>
</slot>
diff --git a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
index ac4d1517d52..4879baced0d 100644
--- a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
+++ b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
@@ -308,7 +308,7 @@ export default {
<gl-search-box-by-type
ref="search"
:value="search"
- class="js-dropdown-input-field"
+ data-testid="user-search-input"
@input="debouncedSearchKeyUpdate"
/>
</template>
diff --git a/app/graphql/types/ci/runner_manager_type.rb b/app/graphql/types/ci/runner_manager_type.rb
index 2a5053f8f07..9c89b6537ea 100644
--- a/app/graphql/types/ci/runner_manager_type.rb
+++ b/app/graphql/types/ci/runner_manager_type.rb
@@ -47,3 +47,5 @@ module Types
end
end
end
+
+Types::Ci::RunnerManagerType.prepend_mod_with('Types::Ci::RunnerManagerType')
diff --git a/app/models/integrations/chat_message/push_message.rb b/app/models/integrations/chat_message/push_message.rb
index 60a3105d1c0..b17e28bb6c6 100644
--- a/app/models/integrations/chat_message/push_message.rb
+++ b/app/models/integrations/chat_message/push_message.rb
@@ -82,12 +82,12 @@ module Integrations
if ref_type == 'tag'
"#{project_url}/-/tags/#{ref}"
else
- "#{project_url}/commits/#{ref}"
+ "#{project_url}/-/commits/#{ref}"
end
end
def compare_url
- "#{project_url}/compare/#{before}...#{after}"
+ "#{project_url}/-/compare/#{before}...#{after}"
end
def ref_link
diff --git a/app/presenters/ci/pipeline_presenter.rb b/app/presenters/ci/pipeline_presenter.rb
index 4ad88188e45..3aba5a2c7ed 100644
--- a/app/presenters/ci/pipeline_presenter.rb
+++ b/app/presenters/ci/pipeline_presenter.rb
@@ -89,13 +89,13 @@ module Ci
def ref_text
if pipeline.detached_merge_request_pipeline?
- _("For merge request %{link_to_merge_request} to merge %{link_to_merge_request_source_branch}")
+ _("Related merge request %{link_to_merge_request} to merge %{link_to_merge_request_source_branch}")
.html_safe % {
link_to_merge_request: link_to_merge_request,
link_to_merge_request_source_branch: link_to_merge_request_source_branch
}
elsif pipeline.merged_result_pipeline?
- _("For merge request %{link_to_merge_request} to merge %{link_to_merge_request_source_branch} into %{link_to_merge_request_target_branch}")
+ _("Related merge request %{link_to_merge_request} to merge %{link_to_merge_request_source_branch} into %{link_to_merge_request_target_branch}")
.html_safe % {
link_to_merge_request: link_to_merge_request,
link_to_merge_request_source_branch: link_to_merge_request_source_branch,
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index 15339becb74..dfa582f4c60 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -26,7 +26,7 @@
.detail-page-header-actions.gl-align-self-start.is-merge-request.js-issuable-actions.gl-display-flex
- if can_update_merge_request
- = render Pajamas::ButtonComponent.new(href: edit_project_merge_request_path(@project, @merge_request), button_options: {class: "gl-display-none gl-md-display-block js-issuable-edit", data: { qa_selector: "edit_button" }}) do
+ = render Pajamas::ButtonComponent.new(href: edit_project_merge_request_path(@project, @merge_request), button_options: {class: "gl-display-none gl-md-display-block js-issuable-edit", data: { qa_selector: "edit_title_button" }}) do
= _('Edit')
- if @merge_request.source_project
diff --git a/app/views/protected_branches/_create_protected_branch.html.haml b/app/views/protected_branches/_create_protected_branch.html.haml
index b4765ab49c2..799f6aa6031 100644
--- a/app/views/protected_branches/_create_protected_branch.html.haml
+++ b/app/views/protected_branches/_create_protected_branch.html.haml
@@ -3,12 +3,12 @@
= dropdown_tag(_('Select'),
options: { toggle_class: 'js-allowed-to-merge wide',
dropdown_class: 'dropdown-menu-selectable capitalize-header', dropdown_qa_selector: 'allowed_to_merge_dropdown_content', dropdown_testid: 'allowed-to-merge-dropdown',
- data: { field_name: 'protected_branch[merge_access_levels_attributes][0][access_level]', input_id: 'merge_access_levels_attributes', qa_selector: 'allowed_to_merge_dropdown' }})
+ data: { field_name: 'protected_branch[merge_access_levels_attributes][0][access_level]', input_id: 'merge_access_levels_attributes', qa_selector: 'select_allowed_to_merge_dropdown' }})
- content_for :push_access_levels do
.push_access_levels-container
= dropdown_tag(_('Select'),
options: { toggle_class: "js-allowed-to-push js-multiselect wide",
dropdown_class: 'dropdown-menu-selectable capitalize-header', dropdown_qa_selector: 'allowed_to_push_dropdown_content' , dropdown_testid: 'allowed-to-push-dropdown',
- data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes', qa_selector: 'allowed_to_push_dropdown' }})
+ data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes', qa_selector: 'select_allowed_to_push_dropdown' }})
= render 'protected_branches/shared/create_protected_branch', protected_branch_entity: protected_branch_entity
diff --git a/config/metrics/counts_28d/20210216175113_merge_request_action_monthly.yml b/config/metrics/counts_28d/20210216175113_merge_request_action_monthly.yml
index 7ee99925c98..a53c14f9b21 100644
--- a/config/metrics/counts_28d/20210216175113_merge_request_action_monthly.yml
+++ b/config/metrics/counts_28d/20210216175113_merge_request_action_monthly.yml
@@ -1,5 +1,5 @@
---
-data_category: optional
+data_category: operational
key_path: redis_hll_counters.source_code.merge_request_action_monthly
description: Count of unique users who perform an action on a merge request
product_section: dev
diff --git a/config/metrics/counts_28d/20210216181446_g_project_management_issue_comment_added_monthly.yml b/config/metrics/counts_28d/20210216181446_g_project_management_issue_comment_added_monthly.yml
index 3062544003d..28850b5285b 100644
--- a/config/metrics/counts_28d/20210216181446_g_project_management_issue_comment_added_monthly.yml
+++ b/config/metrics/counts_28d/20210216181446_g_project_management_issue_comment_added_monthly.yml
@@ -1,5 +1,5 @@
---
-data_category: optional
+data_category: operational
key_path: redis_hll_counters.issues_edit.g_project_management_issue_comment_added_monthly
description: Count of MAU commenting on an issue
product_section: dev
diff --git a/config/metrics/counts_all/20210216180750_groups.yml b/config/metrics/counts_all/20210216180750_groups.yml
index 72d5c97ccb1..904aa534ed6 100644
--- a/config/metrics/counts_all/20210216180750_groups.yml
+++ b/config/metrics/counts_all/20210216180750_groups.yml
@@ -1,5 +1,5 @@
---
-data_category: optional
+data_category: operational
key_path: counts.groups
description: Total count of groups as of usage ping snapshot
product_section: dev
diff --git a/config/routes.rb b/config/routes.rb
index 3a09bf4b136..cf5476b0c77 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -66,11 +66,6 @@ InitializerConnections.raise_if_new_database_connection do
Gitlab.ee do
resource :company, only: [:new, :create], controller: 'company'
- # TODO: remove next line and the controller after the deployment
- # https://gitlab.com/gitlab-org/gitlab/-/issues/411208
- resources :groups_projects, only: [:create] do
- post :import, on: :collection
- end
resources :groups, only: [:new, :create] do
post :import, on: :collection
end
diff --git a/db/post_migrate/20230602063059_remove_broadcast_messages_namespace_id_column.rb b/db/post_migrate/20230602063059_remove_broadcast_messages_namespace_id_column.rb
new file mode 100644
index 00000000000..ad7e23b7cb1
--- /dev/null
+++ b/db/post_migrate/20230602063059_remove_broadcast_messages_namespace_id_column.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class RemoveBroadcastMessagesNamespaceIdColumn < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'index_broadcast_messages_on_namespace_id'
+
+ def up
+ remove_column :broadcast_messages, :namespace_id
+ end
+
+ def down
+ # rubocop:disable Migration/SchemaAdditionMethodsNoPost
+ add_column :broadcast_messages, :namespace_id, :bigint unless column_exists?(:broadcast_messages, :namespace_id)
+ # rubocop:enable Migration/SchemaAdditionMethodsNoPost
+
+ add_concurrent_index :broadcast_messages, :namespace_id, name: INDEX_NAME
+ end
+end
diff --git a/db/schema_migrations/20230602063059 b/db/schema_migrations/20230602063059
new file mode 100644
index 00000000000..53ae46fb8f5
--- /dev/null
+++ b/db/schema_migrations/20230602063059
@@ -0,0 +1 @@
+915530f0de68a448bb9c88572896dc0979a38b5624dc5006811a4c635e35c71e \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 7f46cc56198..9856a06a66f 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -12761,8 +12761,7 @@ CREATE TABLE broadcast_messages (
broadcast_type smallint DEFAULT 1 NOT NULL,
dismissable boolean,
target_access_levels integer[] DEFAULT '{}'::integer[] NOT NULL,
- theme smallint DEFAULT 0 NOT NULL,
- namespace_id bigint
+ theme smallint DEFAULT 0 NOT NULL
);
CREATE SEQUENCE broadcast_messages_id_seq
@@ -29976,8 +29975,6 @@ CREATE INDEX index_boards_on_project_id ON boards USING btree (project_id);
CREATE INDEX index_broadcast_message_on_ends_at_and_broadcast_type_and_id ON broadcast_messages USING btree (ends_at, broadcast_type, id);
-CREATE INDEX index_broadcast_messages_on_namespace_id ON broadcast_messages USING btree (namespace_id);
-
CREATE INDEX index_btree_namespaces_traversal_ids ON namespaces USING btree (traversal_ids);
CREATE INDEX index_bulk_import_batch_trackers_on_tracker_id ON bulk_import_batch_trackers USING btree (tracker_id);
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 810dae24f25..816fb8ab439 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -10348,6 +10348,29 @@ The edge type for [`ProductAnalyticsDashboardPanel`](#productanalyticsdashboardp
| <a id="productanalyticsdashboardpaneledgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="productanalyticsdashboardpaneledgenode"></a>`node` | [`ProductAnalyticsDashboardPanel`](#productanalyticsdashboardpanel) | The item at the end of the edge. |
+#### `ProductAnalyticsDashboardVisualizationConnection`
+
+The connection type for [`ProductAnalyticsDashboardVisualization`](#productanalyticsdashboardvisualization).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="productanalyticsdashboardvisualizationconnectionedges"></a>`edges` | [`[ProductAnalyticsDashboardVisualizationEdge]`](#productanalyticsdashboardvisualizationedge) | A list of edges. |
+| <a id="productanalyticsdashboardvisualizationconnectionnodes"></a>`nodes` | [`[ProductAnalyticsDashboardVisualization]`](#productanalyticsdashboardvisualization) | A list of nodes. |
+| <a id="productanalyticsdashboardvisualizationconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
+
+#### `ProductAnalyticsDashboardVisualizationEdge`
+
+The edge type for [`ProductAnalyticsDashboardVisualization`](#productanalyticsdashboardvisualization).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="productanalyticsdashboardvisualizationedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
+| <a id="productanalyticsdashboardvisualizationedgenode"></a>`node` | [`ProductAnalyticsDashboardVisualization`](#productanalyticsdashboardvisualization) | The item at the end of the edge. |
+
#### `ProjectConnection`
The connection type for [`Project`](#project).
@@ -12941,6 +12964,7 @@ Returns [`CiRunnerStatus!`](#cirunnerstatus).
| <a id="cirunnermanagerrunner"></a>`runner` | [`CiRunner`](#cirunner) | Runner configuration for the runner manager. |
| <a id="cirunnermanagerstatus"></a>`status` | [`CiRunnerStatus!`](#cirunnerstatus) | Status of the runner manager. |
| <a id="cirunnermanagersystemid"></a>`systemId` | [`String!`](#string) | System ID associated with the runner manager. |
+| <a id="cirunnermanagerupgradestatus"></a>`upgradeStatus` **{warning-solid}** | [`CiRunnerUpgradeStatus`](#cirunnerupgradestatus) | **Introduced** in 16.1. This feature is an Experiment. It can be changed or removed at any time. Availability of upgrades for the runner manager. |
| <a id="cirunnermanagerversion"></a>`version` | [`String`](#string) | Version of the runner. |
### `CiSecureFileRegistry`
@@ -19505,6 +19529,7 @@ Represents a product analytics dashboard visualization.
| ---- | ---- | ----------- |
| <a id="productanalyticsdashboardvisualizationdata"></a>`data` | [`JSON!`](#json) | Data of the visualization. |
| <a id="productanalyticsdashboardvisualizationoptions"></a>`options` | [`JSON!`](#json) | Options of the visualization. |
+| <a id="productanalyticsdashboardvisualizationslug"></a>`slug` | [`String!`](#string) | Slug of the visualization. |
| <a id="productanalyticsdashboardvisualizationtype"></a>`type` | [`String!`](#string) | Type of the visualization. |
### `Project`
@@ -20562,6 +20587,26 @@ four standard [pagination arguments](#connection-pagination-arguments):
| ---- | ---- | ----------- |
| <a id="projectproductanalyticsdashboardsslug"></a>`slug` | [`String`](#string) | Find by dashboard slug. |
+##### `Project.productAnalyticsVisualizations`
+
+Visualizations of the project or associated configuration project.
+
+WARNING:
+**Introduced** in 16.1.
+This feature is an Experiment. It can be changed or removed at any time.
+
+Returns [`ProductAnalyticsDashboardVisualizationConnection`](#productanalyticsdashboardvisualizationconnection).
+
+This field returns a [connection](#connections). It accepts the
+four standard [pagination arguments](#connection-pagination-arguments):
+`before: String`, `after: String`, `first: Int`, `last: Int`.
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="projectproductanalyticsvisualizationsslug"></a>`slug` | [`String`](#string) | Slug of the visualization to return. |
+
##### `Project.projectMembers`
Members of the project.
diff --git a/doc/architecture/blueprints/runner_tokens/index.md b/doc/architecture/blueprints/runner_tokens/index.md
index 0dc592531a3..39130e3384b 100644
--- a/doc/architecture/blueprints/runner_tokens/index.md
+++ b/doc/architecture/blueprints/runner_tokens/index.md
@@ -411,31 +411,32 @@ scope.
### Stage 5 - Optional disabling of registration token
-| Component | Milestone | Changes |
-|------------------|----------:|---------|
-| GitLab Rails app | `%16.0` | Adapt `register_{group|project}_runner` permissions to take [application setting](https://gitlab.com/gitlab-org/gitlab/-/issues/386712) in consideration. |
-| GitLab Rails app | | Add UI to allow disabling use of registration tokens at project or group level. |
-| GitLab Rails app | | Introduce `:enforce_create_runner_workflow` feature flag (disabled by default) to control whether use of registration tokens is allowed. |
-| GitLab Rails app | | Make [`POST /api/v4/runners` endpoint](../../../api/runners.md#register-a-new-runner) permanently return `HTTP 410 Gone` if either `allow_runner_registration_token` setting or `:enforce_create_runner_workflow` feature flag disables registration tokens.<br/>A future v5 version of the API should return `HTTP 404 Not Found`. |
-| GitLab Rails app | | Hide legacy UI showing registration with a registration token, if `:enforce_create_runner_workflow` feature flag disables registration tokens. |
+| Component | Milestone | Changes |
+|------------------|----------:|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| GitLab Rails app | `%16.0` | Adapt `register_{group|project}_runner` permissions to take [application setting](https://gitlab.com/gitlab-org/gitlab/-/issues/386712) in consideration. |
+| GitLab Rails app | | Add UI to allow disabling use of registration tokens in top-level group settings. |
+| GitLab Rails app | | Make [`POST /api/v4/runners` endpoint](../../../api/runners.md#register-a-new-runner) permanently return `HTTP 410 Gone` if either `allow_runner_registration_token` setting disables registration tokens.<br/>A future v5 version of the API should return `HTTP 404 Not Found`. |
+| GitLab Rails app | | Hide legacy UI showing registration with a registration token, if it disabled on in top-level group settings or by admins. |
### Stage 6 - Enforcement
-| Component | Milestone | Changes |
-|------------------|----------:|---------|
-| GitLab Rails app | `%16.6` | Enable `:enforce_create_runner_workflow` feature flag by default. |
-| GitLab Rails app | | Implement new `:create_runner` PPGAT scope so that we don't require a full `api` scope. |
-| GitLab Rails app | | Document gotchas when [automatically rotating runner tokens](../../../ci/runners/configure_runners.md#automatically-rotate-authentication-tokens) with multiple machines. |
+| Component | Milestone | Changes |
+|------------------|----------:|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| GitLab Rails app | `%16.6` | Disable registration tokens for all groups by running database migration (only on GitLab.com) | |
+| GitLab Rails app | `%16.6` | Disable registration tokens on the instance level by running database migration (except GitLab.com) | |
+| GitLab Rails app | `%16.8` | Disable registration tokens on the instance level for GitLab.com | |
+| GitLab Rails app | | Implement new `:create_runner` PPGAT scope so that we don't require a full `api` scope. |
+| GitLab Rails app | | Document gotchas when [automatically rotating runner tokens](../../../ci/runners/configure_runners.md#automatically-rotate-authentication-tokens) with multiple machines. |
### Stage 7 - Removals
-| Component | Milestone | Changes |
-|------------------|----------:|---------|
-| GitLab Rails app | `17.0` | Remove legacy UI showing registration with a registration token. |
-| GitLab Runner | `17.0` | Remove runner model arguments from `register` command (for example `--run-untagged`, `--tag-list`, etc.) |
-| GitLab Rails app | `17.0` | Create database migrations to drop `allow_runner_registration_token` setting columns from `application_settings` and `namespace_settings` tables. |
+| Component | Milestone | Changes |
+|------------------|----------:|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| GitLab Rails app | `17.0` | Remove UI enabling registration tokens on the group and instance levels. |
+| GitLab Rails app | `17.0` | Remove legacy UI showing registration with a registration token. |
+| GitLab Runner | `17.0` | Remove runner model arguments from `register` command (for example `--run-untagged`, `--tag-list`, etc.) |
+| GitLab Rails app | `17.0` | Create database migrations to drop `allow_runner_registration_token` setting columns from `application_settings` and `namespace_settings` tables. |
| GitLab Rails app | `17.0` | Create database migrations to drop:<br/>- `runners_registration_token`/`runners_registration_token_encrypted` columns from `application_settings`;<br/>- `runners_token`/`runners_token_encrypted` from `namespaces` table;<br/>- `runners_token`/`runners_token_encrypted` from `projects` table. |
-| GitLab Rails app | `17.0` | Remove `:enforce_create_runner_workflow` feature flag. |
## FAQ
@@ -444,13 +445,16 @@ scope.
If no action is taken before your GitLab instance is upgraded to 16.6, then your runner registration
workflow will break.
Until then, both the new and the old workflow will coexist side-by-side.
-For self-managed instances, to continue using the previous runner registration process,
-you can disable the `enforce_create_runner_workflow` feature flag until GitLab 17.0.
To avoid a broken workflow, you need to first create a runner in the GitLab runners admin page.
After that, you'll need to replace the registration token you're using in your runner registration
workflow with the obtained runner authentication token.
+### Can I use the old runner registration process after 15.6?
+
+- If you're using GitLab.com, you'll be able to manually re-enable the previous runner registration process in the top-level group settings until GitLab 16.8.
+- If you're running GitLab self-managed, you'll be able re-enable the previous runner registration process in admin settings until GitLab 17.0.
+
### What is the new runner registration process?
When the new runner registration process is introduced, you will:
@@ -476,12 +480,6 @@ This allows the GitLab instance to display which system executed a given job.
- In GitLab 15.10, we plan to implement runner creation directly in the runners administration page,
and prepare the runner to follow the new workflow.
- In GitLab 16.6, we plan to disable registration tokens.
- For self-managed instances, to continue using
- registration tokens, you can disable the `enforce_create_runner_workflow` feature flag until
- GitLab 17.0.
-
- Previous `gitlab-runner` versions (that don't include the new `system_id` value) will start to be
- rejected by the GitLab instance;
- In GitLab 17.0, we plan to completely remove support for runner registration tokens.
### How will the `gitlab-runner register` command syntax change?
diff --git a/doc/ci/variables/predefined_variables.md b/doc/ci/variables/predefined_variables.md
index 001a599776a..67430da0739 100644
--- a/doc/ci/variables/predefined_variables.md
+++ b/doc/ci/variables/predefined_variables.md
@@ -35,7 +35,7 @@ as it can cause the pipeline to behave unexpectedly.
| `CI_COMMIT_DESCRIPTION` | 10.8 | all | The description of the commit. If the title is shorter than 100 characters, the message without the first line. |
| `CI_COMMIT_MESSAGE` | 10.8 | all | The full commit message. |
| `CI_COMMIT_REF_NAME` | 9.0 | all | The branch or tag name for which project is built. |
-| `CI_COMMIT_REF_PROTECTED` | 11.11 | all | `true` if the job is running for a protected reference. |
+| `CI_COMMIT_REF_PROTECTED` | 11.11 | all | `true` if the job is running for a protected reference, `false` otherwise. |
| `CI_COMMIT_REF_SLUG` | 9.0 | all | `CI_COMMIT_REF_NAME` in lowercase, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. No leading / trailing `-`. Use in URLs, host names and domain names. |
| `CI_COMMIT_SHA` | 9.0 | all | The commit revision the project is built for. |
| `CI_COMMIT_SHORT_SHA` | 11.7 | all | The first eight characters of `CI_COMMIT_SHA`. |
diff --git a/doc/ci/yaml/workflow.md b/doc/ci/yaml/workflow.md
index 82144e55216..e88a96ae1f5 100644
--- a/doc/ci/yaml/workflow.md
+++ b/doc/ci/yaml/workflow.md
@@ -129,7 +129,7 @@ workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_TAG
- - if: $CI_COMMIT_REF_PROTECTED
+ - if: $CI_COMMIT_REF_PROTECTED == "true"
```
This example assumes that your long-lived branches are [protected](../../user/project/protected_branches.md).
diff --git a/doc/development/pipelines/internals.md b/doc/development/pipelines/internals.md
index 9e511fb88f6..4cdaf50641e 100644
--- a/doc/development/pipelines/internals.md
+++ b/doc/development/pipelines/internals.md
@@ -136,9 +136,9 @@ that are scoped to a single [configuration keyword](../../ci/yaml/index.md#job-k
| `.qa-cache` | Allows a job to use a default `cache` definition suitable for QA tasks. |
| `.yarn-cache` | Allows a job to use a default `cache` definition suitable for frontend jobs that do a `yarn install`. |
| `.assets-compile-cache` | Allows a job to use a default `cache` definition suitable for frontend jobs that compile assets. |
-| `.use-pg13` | Allows a job to use the `postgres` 13 and `redis` services (see [`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/global.gitlab-ci.yml) for the specific versions of the services). |
+| `.use-pg13` | Allows a job to use the `postgres` 13, `redis`, and `rediscluster` services (see [`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/global.gitlab-ci.yml) for the specific versions of the services). |
| `.use-pg13-ee` | Same as `.use-pg13` but also use an `elasticsearch` service (see [`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/global.gitlab-ci.yml) for the specific version of the service). |
-| `.use-pg14` | Allows a job to use the `postgres` 14 and `redis` services (see [`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/global.gitlab-ci.yml) for the specific versions of the services). |
+| `.use-pg14` | Allows a job to use the `postgres` 14, `redis`, and `rediscluster` services (see [`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/global.gitlab-ci.yml) for the specific versions of the services). |
| `.use-pg14-ee` | Same as `.use-pg14` but also use an `elasticsearch` service (see [`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/global.gitlab-ci.yml) for the specific version of the service). |
| `.use-kaniko` | Allows a job to use the `kaniko` tool to build Docker images. |
| `.as-if-foss` | Simulate the FOSS project by setting the `FOSS_ONLY='1'` CI/CD variable. |
diff --git a/doc/tutorials/more_tutorials.md b/doc/tutorials/more_tutorials.md
index c52de180bff..19b3a709ab7 100644
--- a/doc/tutorials/more_tutorials.md
+++ b/doc/tutorials/more_tutorials.md
@@ -13,8 +13,8 @@ If you're learning about GitLab, to find more tutorial content:
- Find recent tutorials on the GitLab blog by [searching by the `tutorial` tag](https://about.gitlab.com/blog/tags.html#tutorial).
-- Browse the **Learn@GitLab** [playlist on YouTube](https://www.youtube.com/playlist?list=PLFGfElNsQthYDx0A_FaNNfUm9NHsK6zED)
+- Browse the **GitLab Snapshots** [playlist on YouTube](https://www.youtube.com/playlist?list=PLFGfElNsQthYDx0A_FaNNfUm9NHsK6zED)
to find video tutorials.
If you find an article, video, or other resource that would be a
-great addition to this page, add it in a [merge request](../development/documentation/index.md).
+great addition to the tutorial pages, add it in a [merge request](../development/documentation/index.md).
diff --git a/lib/gitlab/ci/status/scheduled.rb b/lib/gitlab/ci/status/scheduled.rb
index e9068c326cf..8526becfef9 100644
--- a/lib/gitlab/ci/status/scheduled.rb
+++ b/lib/gitlab/ci/status/scheduled.rb
@@ -5,11 +5,11 @@ module Gitlab
module Status
class Scheduled < Status::Core
def text
- s_('CiStatusText|delayed')
+ s_('CiStatusText|scheduled')
end
def label
- s_('CiStatusLabel|delayed')
+ s_('CiStatusLabel|scheduled')
end
def icon
diff --git a/lib/gitlab/ci/status/success_warning.rb b/lib/gitlab/ci/status/success_warning.rb
index 47623ad945f..84a0e52f518 100644
--- a/lib/gitlab/ci/status/success_warning.rb
+++ b/lib/gitlab/ci/status/success_warning.rb
@@ -9,7 +9,7 @@ module Gitlab
#
class SuccessWarning < Status::Extended
def text
- s_('CiStatusText|passed')
+ s_('CiStatusText|warning')
end
def label
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 04f4852b9a2..95b4baadf46 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -9619,9 +9619,6 @@ msgstr ""
msgid "CiStatusLabel|created"
msgstr ""
-msgid "CiStatusLabel|delayed"
-msgstr ""
-
msgid "CiStatusLabel|failed"
msgstr ""
@@ -9640,6 +9637,9 @@ msgstr ""
msgid "CiStatusLabel|preparing"
msgstr ""
+msgid "CiStatusLabel|scheduled"
+msgstr ""
+
msgid "CiStatusLabel|skipped"
msgstr ""
@@ -9679,12 +9679,18 @@ msgstr ""
msgid "CiStatusText|preparing"
msgstr ""
+msgid "CiStatusText|scheduled"
+msgstr ""
+
msgid "CiStatusText|skipped"
msgstr ""
msgid "CiStatusText|waiting"
msgstr ""
+msgid "CiStatusText|warning"
+msgstr ""
+
msgid "CiStatus|running"
msgstr ""
@@ -19289,12 +19295,6 @@ msgstr ""
msgid "For investigating IT service disruptions or outages"
msgstr ""
-msgid "For merge request %{link_to_merge_request} to merge %{link_to_merge_request_source_branch}"
-msgstr ""
-
-msgid "For merge request %{link_to_merge_request} to merge %{link_to_merge_request_source_branch} into %{link_to_merge_request_target_branch}"
-msgstr ""
-
msgid "For more info, read the documentation."
msgstr ""
@@ -32155,14 +32155,15 @@ msgstr ""
msgid "PackageRegistry|Debian"
msgstr ""
-msgid "PackageRegistry|Delete 1 asset"
-msgid_plural "PackageRegistry|Delete %d assets"
-msgstr[0] ""
-msgstr[1] ""
+msgid "PackageRegistry|Delete %{count} assets"
+msgstr ""
msgid "PackageRegistry|Delete Package Version"
msgstr ""
+msgid "PackageRegistry|Delete asset"
+msgstr ""
+
msgid "PackageRegistry|Delete package"
msgstr ""
@@ -32471,6 +32472,9 @@ msgstr ""
msgid "PackageRegistry|Yes, delete selected packages"
msgstr ""
+msgid "PackageRegistry|You are about to delete %{count} assets. This operation is irreversible."
+msgstr ""
+
msgid "PackageRegistry|You are about to delete %{count} packages. This operation is irreversible."
msgstr ""
@@ -32480,11 +32484,6 @@ msgstr ""
msgid "PackageRegistry|You are about to delete %{name}, are you sure?"
msgstr ""
-msgid "PackageRegistry|You are about to delete 1 asset. This operation is irreversible."
-msgid_plural "PackageRegistry|You are about to delete %d assets. This operation is irreversible."
-msgstr[0] ""
-msgstr[1] ""
-
msgid "PackageRegistry|You are about to delete version %{version} of %{name}. Are you sure?"
msgstr ""
@@ -33574,6 +33573,12 @@ msgstr ""
msgid "Pipelines|Token"
msgstr ""
+msgid "Pipelines|Total amount of compute credits used for the pipeline"
+msgstr ""
+
+msgid "Pipelines|Total number of jobs for the pipeline"
+msgstr ""
+
msgid "Pipelines|Trigger user has insufficient permissions to project"
msgstr ""
@@ -37681,6 +37686,12 @@ msgstr ""
msgid "Related issues"
msgstr ""
+msgid "Related merge request %{link_to_merge_request} to merge %{link_to_merge_request_source_branch}"
+msgstr ""
+
+msgid "Related merge request %{link_to_merge_request} to merge %{link_to_merge_request_source_branch} into %{link_to_merge_request_target_branch}"
+msgstr ""
+
msgid "Related merge requests"
msgstr ""
@@ -39078,6 +39089,9 @@ msgid_plural "Runners|%{highlightStart}%{duration}%{highlightEnd} seconds"
msgstr[0] ""
msgstr[1] ""
+msgid "Runners|%{linkStart}Create a new runner%{linkEnd} to get started."
+msgstr ""
+
msgid "Runners|%{link_start}These runners%{link_end} are available to all groups and projects."
msgstr ""
@@ -39306,6 +39320,9 @@ msgstr ""
msgid "Runners|Filter projects"
msgstr ""
+msgid "Runners|Follow the %{linkStart}installation and registration instructions%{linkEnd} to set up a runner."
+msgstr ""
+
msgid "Runners|Get started with runners"
msgstr ""
@@ -39632,10 +39649,7 @@ msgstr ""
msgid "Runners|Runners are grouped when they have the same authentication token. This happens when you re-use a runner configuration in more than one runner manager. %{linkStart}How does this work?%{linkEnd}"
msgstr ""
-msgid "Runners|Runners are the agents that run your CI/CD jobs. Follow the %{linkStart}installation and registration instructions%{linkEnd} to set up a runner."
-msgstr ""
-
-msgid "Runners|Runners are the agents that run your CI/CD jobs. To register new runners, please contact your administrator."
+msgid "Runners|Runners are the agents that run your CI/CD jobs."
msgstr ""
msgid "Runners|Runners performance"
@@ -39707,6 +39721,9 @@ msgstr ""
msgid "Runners|Step 3 (optional)"
msgstr ""
+msgid "Runners|Still using registration tokens?"
+msgstr ""
+
msgid "Runners|Stop the runner from accepting new jobs."
msgstr ""
@@ -39778,6 +39795,9 @@ msgstr ""
msgid "Runners|To install Runner in a container follow the instructions described in the GitLab documentation"
msgstr ""
+msgid "Runners|To register new runners, contact your administrator."
+msgstr ""
+
msgid "Runners|To register them, go to the %{link_start}group's Runners page%{link_end}."
msgstr ""
diff --git a/qa/qa/page/element.rb b/qa/qa/page/element.rb
index 6bfdf98587b..db2f2153560 100644
--- a/qa/qa/page/element.rb
+++ b/qa/qa/page/element.rb
@@ -13,9 +13,7 @@ module QA
@attributes[:pattern] ||= selector
options.each do |option|
- if option.is_a?(String) || option.is_a?(Regexp)
- @attributes[:pattern] = option
- end
+ @attributes[:pattern] = option if option.is_a?(String) || option.is_a?(Regexp)
end
end
@@ -28,7 +26,7 @@ module QA
end
def selector_css
- %Q([data-qa-selector="#{@name}"]#{additional_selectors},.#{selector})
+ %(#{qa_selector}#{additional_selectors},.#{selector})
end
def expression
@@ -40,14 +38,26 @@ module QA
end
def matches?(line)
- !!(line =~ /["']#{name}['"]|#{expression}/)
+ !!(line =~ /["']#{name}['"]|["']#{convert_to_kebabcase(name)}['"]|#{expression}/)
end
private
+ def convert_to_kebabcase(text)
+ text.to_s.tr('_', '-')
+ end
+
+ def qa_selector
+ [
+ %([data-testid="#{name}"]#{additional_selectors}),
+ %([data-testid="#{convert_to_kebabcase(name)}"]#{additional_selectors}),
+ %([data-qa-selector="#{name}"]#{additional_selectors})
+ ].join(',')
+ end
+
def additional_selectors
@attributes.dup.delete_if { |attr| attr == :pattern || attr == :required }.map do |key, value|
- %Q([data-qa-#{key.to_s.tr('_', '-')}="#{value}"])
+ %([data-qa-#{key.to_s.tr('_', '-')}="#{value}"])
end.join
end
end
diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb
index d5d8c52a0e3..9eb55989ea8 100644
--- a/qa/qa/page/merge_request/show.rb
+++ b/qa/qa/page/merge_request/show.rb
@@ -118,7 +118,7 @@ module QA
end
view 'app/views/projects/merge_requests/_mr_title.html.haml' do
- element :edit_button
+ element :edit_title_button
element :title_content, required: true
end
@@ -211,7 +211,7 @@ module QA
# Click by JS is needed to bypass the Moved MR actions popover
# Change back to regular click_element when moved_mr_sidebar FF is removed
# Rollout issue: https://gitlab.com/gitlab-org/gitlab/-/issues/385460
- click_by_javascript(find_element(:edit_button))
+ click_by_javascript(find_element(:edit_title_button))
end
def fast_forward_not_possible?
diff --git a/qa/qa/page/project/secure/configuration_form.rb b/qa/qa/page/project/secure/configuration_form.rb
index 493ec08d023..70eff31bfa9 100644
--- a/qa/qa/page/project/secure/configuration_form.rb
+++ b/qa/qa/page/project/secure/configuration_form.rb
@@ -9,15 +9,13 @@ module QA
view 'app/assets/javascripts/security_configuration/components/app.vue' do
element :security_configuration_container
- element :security_configuration_history_link
+ element :security_view_history_link
end
view 'app/assets/javascripts/security_configuration/components/feature_card.vue' do
- element :dependency_scanning_status, "`${feature.type}_status`" # rubocop:disable QA/ElementWithPattern
- element :sast_status, "`${feature.type}_status`" # rubocop:disable QA/ElementWithPattern
+ element :feature_status
element :sast_enable_button, "`${feature.type}_enable_button`" # rubocop:disable QA/ElementWithPattern
element :dependency_scanning_mr_button, "`${feature.type}_mr_button`" # rubocop:disable QA/ElementWithPattern
- element :license_scanning_status, "`${feature.type}_status`" # rubocop:disable QA/ElementWithPattern
end
view 'app/assets/javascripts/security_configuration/components/auto_dev_ops_alert.vue' do
@@ -25,15 +23,15 @@ module QA
end
def has_security_configuration_history_link?
- has_element?(:security_configuration_history_link)
+ has_element?(:security_view_history_link)
end
def has_no_security_configuration_history_link?
- has_no_element?(:security_configuration_history_link)
+ has_no_element?(:security_view_history_link)
end
def click_security_configuration_history_link
- click_element(:security_configuration_history_link)
+ click_element(:security_view_history_link)
end
def click_sast_enable_button
@@ -44,40 +42,20 @@ module QA
click_element(:dependency_scanning_mr_button)
end
- def has_sast_status?(status_text)
- within_element(:sast_status) do
- has_text?(status_text)
- end
- end
-
- def has_no_sast_status?(status_text)
- within_element(:sast_status) do
- has_no_text?(status_text)
- end
- end
-
- def has_dependency_scanning_status?(status_text)
- within_element(:dependency_scanning_status) do
- has_text?(status_text)
- end
+ def has_true_sast_status?
+ has_element?(:feature_status, feature: 'sast_true_status')
end
- def has_no_dependency_scanning_status?(status_text)
- within_element(:dependency_scanning_status) do
- has_no_text?(status_text)
- end
+ def has_false_sast_status?
+ has_element?(:feature_status, feature: 'sast_false_status')
end
- def has_license_compliance_status?(status_text)
- within_element(:license_scanning_status) do
- has_text?(status_text)
- end
+ def has_true_dependency_scanning_status?
+ has_element?(:feature_status, feature: 'dependency_scanning_true_status')
end
- def has_no_license_compliance_status?(status_text)
- within_element(:license_scanning_status) do
- has_no_text?(status_text)
- end
+ def has_false_dependency_scanning_status?
+ has_element?(:feature_status, feature: 'dependency_scanning_false_status')
end
def has_auto_devops_container?
diff --git a/qa/qa/page/project/settings/protected_branches.rb b/qa/qa/page/project/settings/protected_branches.rb
index 3eddd0fd33a..e6b13ed77a0 100644
--- a/qa/qa/page/project/settings/protected_branches.rb
+++ b/qa/qa/page/project/settings/protected_branches.rb
@@ -11,9 +11,9 @@ module QA
end
view 'app/views/protected_branches/_create_protected_branch.html.haml' do
- element :allowed_to_push_dropdown
+ element :select_allowed_to_push_dropdown
element :allowed_to_push_dropdown_content
- element :allowed_to_merge_dropdown
+ element :select_allowed_to_merge_dropdown
element :allowed_to_merge_dropdown_content
end
@@ -45,7 +45,7 @@ module QA
private
def select_allowed(action, allowed)
- click_element :"allowed_to_#{action}_dropdown"
+ click_element :"select_allowed_to_#{action}_dropdown"
allowed[:roles] = Resource::ProtectedBranch::Roles::NO_ONE unless allowed.key?(:roles)
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb
index fd818c3797b..b2dca4fc312 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb
@@ -43,7 +43,7 @@ module QA
end
it 'mentions another user in an issue',
-testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347988' do
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347988' do
Page::Project::Issue::Show.perform do |show|
at_username = "@#{user.username}"
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb
index 0ec231ed66e..41ef38d2d66 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb
@@ -4,7 +4,7 @@ module QA
RSpec.describe 'Create' do
describe 'Git push over HTTP', :smoke, :skip_fips_env, product_group: :source_code do
it 'user using a personal access token pushes code to the repository',
-testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347749' do
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347749' do
Flow::Login.sign_in
access_token = Resource::PersonalAccessToken.fabricate!.token
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb
index efa1f9fe2c9..edc85849356 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb
@@ -4,7 +4,7 @@ module QA
RSpec.describe 'Create' do
describe 'Git push over HTTP', product_group: :source_code do
it 'user pushes code to the repository', :smoke, :skip_fips_env,
- testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347747' do
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347747' do
Flow::Login.sign_in
Resource::Repository::ProjectPush.fabricate! do |push|
@@ -20,7 +20,7 @@ module QA
end
it 'pushes to a project using a specific Praefect repository storage', :smoke, :skip_fips_env, :requires_admin,
- :requires_praefect, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347789' do
+ :requires_praefect, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347789' do
Flow::Login.sign_in_as_admin
project = Resource::Project.fabricate_via_api! do |storage_project|
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb
index f281f441e8a..dcee723a1c4 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb
@@ -27,7 +27,7 @@ module QA
end
it 'pushes code to the repository via SSH', :smoke, :skip_fips_env,
- testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347825' do
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347825' do
Resource::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.ssh_key = @key
@@ -43,7 +43,7 @@ module QA
end
it 'pushes multiple branches and tags together', :smoke, :skip_fips_env,
- testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347826' do
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347826' do
branches = []
tags = []
Git::Repository.perform do |repository|
diff --git a/qa/qa/specs/features/browser_ui/3_create/source_editor/source_editor_toolbar_spec.rb b/qa/qa/specs/features/browser_ui/3_create/source_editor/source_editor_toolbar_spec.rb
index ffe340eb0dd..9b1df337065 100644
--- a/qa/qa/specs/features/browser_ui/3_create/source_editor/source_editor_toolbar_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/source_editor/source_editor_toolbar_spec.rb
@@ -17,7 +17,7 @@ module QA
end
it 'can preview markdown side-by-side while editing',
- testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/367749' do
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/367749' do
project.visit!
Page::Project::Show.perform do |project|
project.click_file('README.md')
diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_with_image_pull_policy_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_with_image_pull_policy_spec.rb
index aaaa11ef867..c693a57605e 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_with_image_pull_policy_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_with_image_pull_policy_spec.rb
@@ -71,10 +71,10 @@ module QA
if pull_image
expect(job_log).to have_content(message),
- "Expected to find #{message} in #{job_log}, but didn't."
+ "Expected to find #{message} in #{job_log}, but didn't."
else
expect(job_log).not_to have_content(message),
- "Found #{message} in #{job_log}, but didn't expect to."
+ "Found #{message} in #{job_log}, but didn't expect to."
end
end
end
@@ -96,7 +96,7 @@ module QA
visit_job
expect(job_log).to include(text1, text2),
- "Expected to find contents #{text1} and #{text2} in #{job_log}, but didn't."
+ "Expected to find contents #{text1} and #{text2} in #{job_log}, but didn't."
end
end
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 27bca6c17a2..908585c8423 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
@@ -2,10 +2,10 @@
module QA
RSpec.describe 'Package', :orchestrated, :requires_admin, :packages, :object_storage, :reliable,
- feature_flag: {
- name: 'maven_central_request_forwarding',
- scope: :global
- } do
+ feature_flag: {
+ name: 'maven_central_request_forwarding',
+ scope: :global
+ } do
describe 'Maven project level endpoint', product_group: :package_registry do
include Runtime::Fixtures
include Support::Helpers::MaskToken
@@ -218,16 +218,14 @@ module QA
) 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)
+ 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'
diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/nuget/nuget_project_level_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/nuget/nuget_project_level_spec.rb
index ad5835d8c9d..adb299eaab5 100644
--- a/qa/qa/specs/features/browser_ui/5_package/package_registry/nuget/nuget_project_level_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/nuget/nuget_project_level_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Package', :skip_live_env, :orchestrated, :packages, :object_storage,
-product_group: :package_registry do
+ product_group: :package_registry do
describe 'NuGet project level endpoint' do
include Support::Helpers::MaskToken
diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/rubygems_registry_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/rubygems_registry_spec.rb
index 1af1fc7c231..374111b3498 100644
--- a/qa/qa/specs/features/browser_ui/5_package/package_registry/rubygems_registry_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/rubygems_registry_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Package', :orchestrated, :packages, :object_storage,
- feature_flag: { name: 'rubygem_packages', scope: :project } do
+ feature_flag: { name: 'rubygem_packages', scope: :project } do
describe 'RubyGems Repository', product_group: :package_registry do
include Runtime::Fixtures
diff --git a/qa/spec/page/element_spec.rb b/qa/spec/page/element_spec.rb
index fbf58b5e18a..da1fd224564 100644
--- a/qa/spec/page/element_spec.rb
+++ b/qa/spec/page/element_spec.rb
@@ -73,7 +73,7 @@ RSpec.describe QA::Page::Element do
subject { described_class.new(:something, /link_to 'something'/) }
it 'has an attribute[pattern] of the pattern' do
- expect(subject.attributes[:pattern]).to eq /link_to 'something'/
+ expect(subject.attributes[:pattern]).to eq(/link_to 'something'/)
end
it 'is not required by default' do
@@ -98,7 +98,7 @@ RSpec.describe QA::Page::Element do
subject { described_class.new(:something, /link_to 'something_else_entirely'/, required: true) }
it 'has an attribute[pattern] of the passed pattern' do
- expect(subject.attributes[:pattern]).to eq /link_to 'something_else_entirely'/
+ expect(subject.attributes[:pattern]).to eq(/link_to 'something_else_entirely'/)
end
it 'is required' do
@@ -118,6 +118,10 @@ RSpec.describe QA::Page::Element do
expect(subject.selector_css).to include(%q([data-qa-selector="my_element"]))
end
+ it 'properly translates to a data-testid' do
+ expect(subject.selector_css).to include(%q([data-testid="my_element"]))
+ end
+
context 'additional selectors' do
let(:element) { described_class.new(:my_element, index: 3, another_match: 'something') }
let(:required_element) { described_class.new(:my_element, required: true, index: 3) }
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index ee71181fba2..4cf558b04cc 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -162,7 +162,7 @@ RSpec.describe 'Issue Sidebar', feature_category: :team_planning do
open_assignees_dropdown
page.within '.dropdown-menu-user' do
- find('.js-dropdown-input-field').find('input').set(user2.name)
+ find('[data-testid="user-search-input"]').set(user2.name)
wait_for_requests
@@ -182,7 +182,7 @@ RSpec.describe 'Issue Sidebar', feature_category: :team_planning do
it 'keeps your filtered term after filtering and dismissing the dropdown' do
open_assignees_dropdown
- find('.js-dropdown-input-field').find('input').set(user2.name)
+ find('[data-testid="user-search-input"]').set(user2.name)
wait_for_requests
page.within '.dropdown-menu-user' do
@@ -199,7 +199,7 @@ RSpec.describe 'Issue Sidebar', feature_category: :team_planning do
expect(page.all('[data-testid="selected-participant"]').length).to eq(1)
end
- expect(find('.js-dropdown-input-field').find('input').value).to eq(user2.name)
+ expect(find('[data-testid="user-search-input"]').value).to eq(user2.name)
end
end
end
diff --git a/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
index 7d024103943..ca12e0e2b65 100644
--- a/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
@@ -67,7 +67,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees branch pipelines and detached merge request pipelines in correct order' do
page.within('.ci-table') do
- expect(page).to have_selector('.ci-created', count: 2)
+ expect(page).to have_selector('[data-testid="ci-badge-created"]', count: 2)
expect(first('[data-testid="pipeline-url-link"]')).to have_content("##{detached_merge_request_pipeline.id}")
end
end
@@ -103,7 +103,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees branch pipelines and detached merge request pipelines in correct order' do
page.within('.ci-table') do
- expect(page).to have_selector('.ci-pending', count: 4)
+ expect(page).to have_selector('[data-testid="ci-badge-pending"]', count: 4)
expect(all('[data-testid="pipeline-url-link"]')[0])
.to have_content("##{detached_merge_request_pipeline_2.id}")
@@ -246,7 +246,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees a branch pipeline in pipeline tab' do
page.within('.ci-table') do
- expect(page).to have_selector('.ci-created', count: 1)
+ expect(page).to have_selector('[data-testid="ci-badge-created"]', count: 1)
expect(first('[data-testid="pipeline-url-link"]')).to have_content("##{push_pipeline.id}")
end
end
@@ -299,7 +299,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees branch pipelines and detached merge request pipelines in correct order' do
page.within('.ci-table') do
- expect(page).to have_selector('.ci-pending', count: 2)
+ expect(page).to have_selector('[data-testid="ci-badge-pending"]', count: 2)
expect(first('[data-testid="pipeline-url-link"]')).to have_content("##{detached_merge_request_pipeline.id}")
end
end
@@ -315,7 +315,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees pipeline list in forked project' do
visit project_pipelines_path(forked_project)
- expect(page).to have_selector('.ci-pending', count: 2)
+ expect(page).to have_selector('[data-testid="ci-badge-pending"]', count: 2)
end
context 'when a user updated a merge request from a forked project to the parent project' do
@@ -341,7 +341,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees branch pipelines and detached merge request pipelines in correct order' do
page.within('.ci-table') do
- expect(page).to have_selector('.ci-pending', count: 4)
+ expect(page).to have_selector('[data-testid="ci-badge-pending"]', count: 4)
expect(all('[data-testid="pipeline-url-link"]')[0])
.to have_content("##{detached_merge_request_pipeline_2.id}")
@@ -384,7 +384,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees pipeline list in forked project' do
visit project_pipelines_path(forked_project)
- expect(page).to have_selector('.ci-pending', count: 4)
+ expect(page).to have_selector('[data-testid="ci-badge-pending"]', count: 4)
end
end
diff --git a/spec/features/merge_request/user_sees_pipelines_spec.rb b/spec/features/merge_request/user_sees_pipelines_spec.rb
index f92ce3865a9..a2796cd250b 100644
--- a/spec/features/merge_request/user_sees_pipelines_spec.rb
+++ b/spec/features/merge_request/user_sees_pipelines_spec.rb
@@ -42,7 +42,7 @@ RSpec.describe 'Merge request > User sees pipelines', :js, feature_category: :co
wait_for_requests
page.within('[data-testid="pipeline-table-row"]') do
- expect(page).to have_selector('.ci-success')
+ expect(page).to have_selector('[data-testid="ci-badge-passed"]')
expect(page).to have_content(pipeline.id)
expect(page).to have_content('API')
expect(page).to have_css('[data-testid="pipeline-mini-graph"]')
diff --git a/spec/features/projects/commit/user_sees_pipelines_tab_spec.rb b/spec/features/projects/commit/user_sees_pipelines_tab_spec.rb
index da83bbcb63a..e44364c7f2d 100644
--- a/spec/features/projects/commit/user_sees_pipelines_tab_spec.rb
+++ b/spec/features/projects/commit/user_sees_pipelines_tab_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe 'Commit > Pipelines tab', :js, feature_category: :source_code_man
wait_for_requests
page.within('[data-testid="pipeline-table-row"]') do
- expect(page).to have_selector('.ci-success')
+ expect(page).to have_selector('[data-testid="ci-badge-passed"]')
expect(page).to have_content(pipeline.id)
expect(page).to have_content('API')
expect(page).to have_css('[data-testid="pipeline-mini-graph"]')
diff --git a/spec/features/projects/jobs/user_browses_jobs_spec.rb b/spec/features/projects/jobs/user_browses_jobs_spec.rb
index d74a3d28068..aeba53c22b6 100644
--- a/spec/features/projects/jobs/user_browses_jobs_spec.rb
+++ b/spec/features/projects/jobs/user_browses_jobs_spec.rb
@@ -72,7 +72,7 @@ RSpec.describe 'User browses jobs', feature_category: :groups_and_projects do
wait_for_requests
- expect(page).to have_selector('.ci-canceled')
+ expect(page).to have_selector('[data-testid="ci-badge-canceled"]')
expect(page).not_to have_selector('[data-testid="jobs-table-error-alert"]')
end
end
@@ -94,7 +94,7 @@ RSpec.describe 'User browses jobs', feature_category: :groups_and_projects do
wait_for_requests
- expect(page).to have_selector('.ci-pending')
+ expect(page).to have_selector('[data-testid="ci-badge-pending"]')
end
end
@@ -134,7 +134,7 @@ RSpec.describe 'User browses jobs', feature_category: :groups_and_projects do
wait_for_requests
- expect(page).to have_selector('.ci-pending')
+ expect(page).to have_selector('[data-testid="ci-badge-pending"]')
end
it 'unschedules a job successfully' do
@@ -142,7 +142,7 @@ RSpec.describe 'User browses jobs', feature_category: :groups_and_projects do
wait_for_requests
- expect(page).to have_selector('.ci-manual')
+ expect(page).to have_selector('[data-testid="ci-badge-manual"]')
end
end
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index 01f8f2166ac..a16db71354c 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -66,7 +66,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state, feature_category: :grou
wait_for_requests
- expect(page).to have_css('.ci-status.ci-success', text: 'passed')
+ expect(page).to have_css('[data-testid="ci-badge-passed"]', text: 'passed')
end
it 'shows commit`s data', :js do
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index b8bb81991fc..70f9961ced8 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -120,7 +120,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :groups_and_projects do
it 'indicates that pipeline can be canceled' do
expect(page).to have_selector('.js-pipelines-cancel-button')
- expect(page).to have_selector('.ci-running')
+ expect(page).to have_selector('[data-testid="ci-badge-running"]')
end
context 'when canceling' do
@@ -132,7 +132,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :groups_and_projects do
it 'indicated that pipelines was canceled', :sidekiq_might_not_need_inline do
expect(page).not_to have_selector('.js-pipelines-cancel-button')
- expect(page).to have_selector('.ci-canceled')
+ expect(page).to have_selector('[data-testid="ci-badge-canceled"]')
end
end
end
@@ -150,7 +150,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :groups_and_projects do
it 'indicates that pipeline can be retried' do
expect(page).to have_selector('.js-pipelines-retry-button')
- expect(page).to have_selector('.ci-failed')
+ expect(page).to have_selector('[data-testid="ci-badge-failed"]')
end
context 'when retrying' do
@@ -161,7 +161,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :groups_and_projects do
it 'shows running pipeline that is not retryable' do
expect(page).not_to have_selector('.js-pipelines-retry-button')
- expect(page).to have_selector('.ci-running')
+ expect(page).to have_selector('[data-testid="ci-badge-running"]')
end
end
end
@@ -400,7 +400,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :groups_and_projects do
end
it 'shows the pipeline as preparing' do
- expect(page).to have_selector('.ci-preparing')
+ expect(page).to have_selector('[data-testid="ci-badge-preparing"]')
end
end
@@ -421,7 +421,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :groups_and_projects do
end
it 'has pipeline running' do
- expect(page).to have_selector('.ci-running')
+ expect(page).to have_selector('[data-testid="ci-badge-running"]')
end
context 'when canceling' do
@@ -432,7 +432,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :groups_and_projects do
it 'indicates that pipeline was canceled', :sidekiq_might_not_need_inline do
expect(page).not_to have_selector('.js-pipelines-cancel-button')
- expect(page).to have_selector('.ci-canceled')
+ expect(page).to have_selector('[data-testid="ci-badge-canceled"]')
end
end
end
@@ -454,7 +454,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :groups_and_projects do
end
it 'has failed pipeline', :sidekiq_might_not_need_inline do
- expect(page).to have_selector('.ci-failed')
+ expect(page).to have_selector('[data-testid="ci-badge-failed"]')
end
end
end
diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js
index 23b3b13d69c..9260718a94b 100644
--- a/spec/frontend/boards/components/board_content_spec.js
+++ b/spec/frontend/boards/components/board_content_spec.js
@@ -1,9 +1,11 @@
import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
import Vue from 'vue';
import Draggable from 'vuedraggable';
import Vuex from 'vuex';
import eventHub from '~/boards/eventhub';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue';
@@ -11,9 +13,18 @@ import getters from 'ee_else_ce/boards/stores/getters';
import BoardColumn from '~/boards/components/board_column.vue';
import BoardContent from '~/boards/components/board_content.vue';
import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue';
+import updateBoardListMutation from '~/boards/graphql/board_list_update.mutation.graphql';
import BoardAddNewColumn from 'ee_else_ce/boards/components/board_add_new_column.vue';
-import { mockLists, mockListsById } from '../mock_data';
-
+import { DraggableItemTypes } from 'ee_else_ce/boards/constants';
+import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql';
+import {
+ mockLists,
+ mockListsById,
+ updateBoardListResponse,
+ boardListsQueryResponse,
+} from '../mock_data';
+
+Vue.use(VueApollo);
Vue.use(Vuex);
const actions = {
@@ -22,6 +33,9 @@ const actions = {
describe('BoardContent', () => {
let wrapper;
+ let mockApollo;
+
+ const updateListHandler = jest.fn().mockResolvedValue(updateBoardListResponse);
const defaultState = {
isShowingEpicsSwimlanes: false,
@@ -47,21 +61,32 @@ describe('BoardContent', () => {
isIssueBoard = true,
isEpicBoard = false,
} = {}) => {
+ mockApollo = createMockApollo([[updateBoardListMutation, updateListHandler]]);
+ const listQueryVariables = { isProject: true };
+
+ mockApollo.clients.defaultClient.writeQuery({
+ query: boardListsQuery,
+ variables: listQueryVariables,
+ data: boardListsQueryResponse.data,
+ });
+
const store = createStore({
...defaultState,
...state,
});
wrapper = shallowMount(BoardContent, {
+ apolloProvider: mockApollo,
propsData: {
boardId: 'gid://gitlab/Board/1',
filterParams: {},
isSwimlanesOn: false,
boardListsApollo: mockListsById,
- listQueryVariables: {},
+ listQueryVariables,
addColumnFormVisible: false,
...props,
},
provide: {
+ boardType: 'project',
canAdminList,
issuableType,
isIssueBoard,
@@ -81,6 +106,7 @@ describe('BoardContent', () => {
const findBoardColumns = () => wrapper.findAllComponents(BoardColumn);
const findBoardAddNewColumn = () => wrapper.findComponent(BoardAddNewColumn);
+ const findDraggable = () => wrapper.findComponent(Draggable);
describe('default', () => {
beforeEach(() => {
@@ -128,7 +154,7 @@ describe('BoardContent', () => {
});
it('renders draggable component', () => {
- expect(wrapper.findComponent(Draggable).exists()).toBe(true);
+ expect(findDraggable().exists()).toBe(true);
});
});
@@ -138,7 +164,7 @@ describe('BoardContent', () => {
});
it('does not render draggable component', () => {
- expect(wrapper.findComponent(Draggable).exists()).toBe(false);
+ expect(findDraggable().exists()).toBe(false);
});
});
@@ -164,6 +190,21 @@ describe('BoardContent', () => {
expect(eventHub.$on).toHaveBeenCalledWith('updateBoard', wrapper.vm.refetchLists);
});
+
+ it('reorders lists', async () => {
+ const movableListsOrder = [mockLists[0].id, mockLists[1].id];
+
+ findDraggable().vm.$emit('end', {
+ item: { dataset: { listId: mockLists[0].id, draggableItemType: DraggableItemTypes.list } },
+ newIndex: 1,
+ to: {
+ children: movableListsOrder.map((listId) => ({ dataset: { listId } })),
+ },
+ });
+ await waitForPromises();
+
+ expect(updateListHandler).toHaveBeenCalled();
+ });
});
describe('when "add column" form is visible', () => {
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index 60f906d2157..68f665e004c 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -1023,6 +1023,7 @@ export const updateBoardListResponse = {
data: {
updateBoardList: {
list: mockList,
+ errors: [],
},
},
};
diff --git a/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
index cc4a022c2df..89ce3a2e18c 100644
--- a/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
+++ b/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
@@ -1,5 +1,6 @@
+import Vue from 'vue';
import { GlAlert, GlButton, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
@@ -53,9 +54,6 @@ jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(),
}));
-const localVue = createLocalVue();
-localVue.use(VueApollo);
-
const defaultProvide = {
ciConfigPath: mockCiConfigPath,
defaultBranch: mockDefaultBranch,
@@ -74,24 +72,10 @@ describe('Pipeline editor app component', () => {
let mockLatestCommitShaQuery;
let mockPipelineQuery;
- const createComponent = ({
- blobLoading = false,
- options = {},
- provide = {},
- stubs = {},
- } = {}) => {
+ const createComponent = ({ options = {}, provide = {}, stubs = {} } = {}) => {
wrapper = shallowMount(PipelineEditorApp, {
provide: { ...defaultProvide, ...provide },
stubs,
- mocks: {
- $apollo: {
- queries: {
- initialCiFileContent: {
- loading: blobLoading,
- },
- },
- },
- },
...options,
});
};
@@ -101,6 +85,8 @@ describe('Pipeline editor app component', () => {
stubs = {},
withUndefinedBranch = false,
} = {}) => {
+ Vue.use(VueApollo);
+
const handlers = [
[getBlobContent, mockBlobContentData],
[getCiConfigData, mockCiConfigData],
@@ -137,7 +123,6 @@ describe('Pipeline editor app component', () => {
});
const options = {
- localVue,
mocks: {},
apolloProvider: mockApollo,
};
@@ -164,7 +149,7 @@ describe('Pipeline editor app component', () => {
describe('loading state', () => {
it('displays a loading icon if the blob query is loading', () => {
- createComponent({ blobLoading: true });
+ createComponentWithApollo();
expect(findLoadingIcon().exists()).toBe(true);
expect(findEditorHome().exists()).toBe(false);
@@ -246,10 +231,6 @@ describe('Pipeline editor app component', () => {
describe('when file exists', () => {
beforeEach(async () => {
await createComponentWithApollo();
-
- jest
- .spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling')
- .mockImplementation(jest.fn());
});
it('shows pipeline editor home component', () => {
@@ -268,8 +249,8 @@ describe('Pipeline editor app component', () => {
});
});
- it('does not poll for the commit sha', () => {
- expect(wrapper.vm.$apollo.queries.commitSha.startPolling).toHaveBeenCalledTimes(0);
+ it('calls once and does not start poll for the commit sha', () => {
+ expect(mockLatestCommitShaQuery).toHaveBeenCalledTimes(1);
});
});
@@ -281,10 +262,6 @@ describe('Pipeline editor app component', () => {
PipelineEditorEmptyState,
},
});
-
- jest
- .spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling')
- .mockImplementation(jest.fn());
});
it('shows an empty state and does not show editor home component', () => {
@@ -293,8 +270,8 @@ describe('Pipeline editor app component', () => {
expect(findEditorHome().exists()).toBe(false);
});
- it('does not poll for the commit sha', () => {
- expect(wrapper.vm.$apollo.queries.commitSha.startPolling).toHaveBeenCalledTimes(0);
+ it('calls once and does not start poll for the commit sha', () => {
+ expect(mockLatestCommitShaQuery).toHaveBeenCalledTimes(1);
});
describe('because of a fetching error', () => {
@@ -381,38 +358,27 @@ describe('Pipeline editor app component', () => {
});
it('polls for commit sha while pipeline data is not yet available for current branch', async () => {
- jest
- .spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling')
- .mockImplementation(jest.fn());
-
- // simulate a commit to the current branch
findEditorHome().vm.$emit('updateCommitSha');
await waitForPromises();
- expect(wrapper.vm.$apollo.queries.commitSha.startPolling).toHaveBeenCalledTimes(1);
+ expect(mockLatestCommitShaQuery).toHaveBeenCalledTimes(2);
});
it('stops polling for commit sha when pipeline data is available for newly committed branch', async () => {
- jest
- .spyOn(wrapper.vm.$apollo.queries.commitSha, 'stopPolling')
- .mockImplementation(jest.fn());
-
mockLatestCommitShaQuery.mockResolvedValue(mockCommitShaResults);
- await wrapper.vm.$apollo.queries.commitSha.refetch();
+ await waitForPromises();
+
+ await findEditorHome().vm.$emit('updateCommitSha');
- expect(wrapper.vm.$apollo.queries.commitSha.stopPolling).toHaveBeenCalledTimes(1);
+ expect(mockLatestCommitShaQuery).toHaveBeenCalledTimes(2);
});
it('stops polling for commit sha when pipeline data is available for current branch', async () => {
- jest
- .spyOn(wrapper.vm.$apollo.queries.commitSha, 'stopPolling')
- .mockImplementation(jest.fn());
-
mockLatestCommitShaQuery.mockResolvedValue(mockNewCommitShaResults);
findEditorHome().vm.$emit('updateCommitSha');
await waitForPromises();
- expect(wrapper.vm.$apollo.queries.commitSha.stopPolling).toHaveBeenCalledTimes(1);
+ expect(mockLatestCommitShaQuery).toHaveBeenCalledTimes(2);
});
});
@@ -497,15 +463,12 @@ describe('Pipeline editor app component', () => {
it('refetches blob content', async () => {
await createComponentWithApollo();
- jest
- .spyOn(wrapper.vm.$apollo.queries.initialCiFileContent, 'refetch')
- .mockImplementation(jest.fn());
- expect(wrapper.vm.$apollo.queries.initialCiFileContent.refetch).toHaveBeenCalledTimes(0);
+ expect(mockBlobContentData).toHaveBeenCalledTimes(1);
- await wrapper.vm.refetchContent();
+ findEditorHome().vm.$emit('refetchContent');
- expect(wrapper.vm.$apollo.queries.initialCiFileContent.refetch).toHaveBeenCalledTimes(1);
+ expect(mockBlobContentData).toHaveBeenCalledTimes(2);
});
it('hides start screen when refetch fetches CI file', async () => {
@@ -516,7 +479,8 @@ describe('Pipeline editor app component', () => {
expect(findEditorHome().exists()).toBe(false);
mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
- await wrapper.vm.$apollo.queries.initialCiFileContent.refetch();
+ findEmptyState().vm.$emit('refetchContent');
+ await waitForPromises();
expect(findEmptyState().exists()).toBe(false);
expect(findEditorHome().exists()).toBe(true);
@@ -573,10 +537,6 @@ describe('Pipeline editor app component', () => {
mockGetTemplate.mockResolvedValue(mockCiTemplateQueryResponse);
await createComponentWithApollo();
-
- jest
- .spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling')
- .mockImplementation(jest.fn());
});
it('skips empty state and shows editor home component', () => {
diff --git a/spec/frontend/ci/runner/components/runner_form_fields_spec.js b/spec/frontend/ci/runner/components/runner_form_fields_spec.js
index 98f170d8f18..93be4d9d35e 100644
--- a/spec/frontend/ci/runner/components/runner_form_fields_spec.js
+++ b/spec/frontend/ci/runner/components/runner_form_fields_spec.js
@@ -21,6 +21,7 @@ describe('RunnerFormFields', () => {
const findInput = (name) => wrapper.find(`input[name="${name}"]`);
const expectRendersFields = () => {
+ expect(wrapper.text()).toContain(s__('Runners|Tags'));
expect(wrapper.text()).toContain(s__('Runners|Details'));
expect(wrapper.text()).toContain(s__('Runners|Configuration'));
@@ -42,10 +43,11 @@ describe('RunnerFormFields', () => {
});
it('renders a loading frame', () => {
+ expect(wrapper.text()).toContain(s__('Runners|Tags'));
expect(wrapper.text()).toContain(s__('Runners|Details'));
expect(wrapper.text()).toContain(s__('Runners|Configuration'));
- expect(wrapper.findAllComponents(GlSkeletonLoader)).toHaveLength(2);
+ expect(wrapper.findAllComponents(GlSkeletonLoader)).toHaveLength(3);
expect(wrapper.findAll('input')).toHaveLength(0);
});
@@ -101,23 +103,23 @@ describe('RunnerFormFields', () => {
it('checks checkbox fields', async () => {
createComponent({
value: {
+ runUntagged: false,
paused: false,
accessLevel: ACCESS_LEVEL_NOT_PROTECTED,
- runUntagged: false,
},
});
+ findInput('run-untagged').setChecked(true);
findInput('paused').setChecked(true);
findInput('protected').setChecked(true);
- findInput('run-untagged').setChecked(true);
await nextTick();
expect(wrapper.emitted('input').at(-1)).toEqual([
{
+ runUntagged: true,
paused: true,
accessLevel: ACCESS_LEVEL_REF_PROTECTED,
- runUntagged: true,
},
]);
});
diff --git a/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js b/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js
index 9d521b0b8ca..22797433b58 100644
--- a/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js
+++ b/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js
@@ -1,27 +1,46 @@
import EMPTY_STATE_SVG_URL from '@gitlab/svgs/dist/illustrations/empty-state/empty-pipeline-md.svg?url';
import FILTERED_SVG_URL from '@gitlab/svgs/dist/illustrations/empty-state/empty-search-md.svg?url';
import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
-import { s__ } from '~/locale';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
-
-import { mockRegistrationToken, newRunnerPath } from 'jest/ci/runner/mock_data';
+import {
+ I18N_GET_STARTED,
+ I18N_RUNNERS_ARE_AGENTS,
+ I18N_CREATE_RUNNER_LINK,
+ I18N_STILL_USING_REGISTRATION_TOKENS,
+ I18N_CONTACT_ADMIN_TO_REGISTER,
+ I18N_FOLLOW_REGISTRATION_INSTRUCTIONS,
+ I18N_NO_RESULTS,
+ I18N_EDIT_YOUR_SEARCH,
+} from '~/ci/runner/constants';
+
+import {
+ mockRegistrationToken,
+ newRunnerPath as mockNewRunnerPath,
+} from 'jest/ci/runner/mock_data';
import RunnerListEmptyState from '~/ci/runner/components/runner_list_empty_state.vue';
describe('RunnerListEmptyState', () => {
let wrapper;
+ let glFeatures;
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findLinks = () => wrapper.findAllComponents(GlLink);
const findLink = () => wrapper.findComponent(GlLink);
const findRunnerInstructionsModal = () => wrapper.findComponent(RunnerInstructionsModal);
- const createComponent = ({ props, mountFn = shallowMountExtended, ...options } = {}) => {
+ const expectTitleToBe = (title) => {
+ expect(findEmptyState().find('h1').text()).toBe(title);
+ };
+ const expectDescriptionToBe = (sentences) => {
+ expect(findEmptyState().find('p').text()).toMatchInterpolatedText(sentences.join(' '));
+ };
+
+ const createComponent = ({ props, mountFn = shallowMountExtended } = {}) => {
wrapper = mountFn(RunnerListEmptyState, {
propsData: {
- registrationToken: mockRegistrationToken,
- newRunnerPath,
...props,
},
directives: {
@@ -30,109 +49,146 @@ describe('RunnerListEmptyState', () => {
stubs: {
GlEmptyState,
GlSprintf,
- GlLink,
},
- ...options,
+ provide: { glFeatures },
});
};
- describe('when search is not filtered', () => {
- const title = s__('Runners|Get started with runners');
+ beforeEach(() => {
+ glFeatures = null;
+ });
- describe('when there is a registration token', () => {
+ describe('when search is not filtered', () => {
+ describe.each([
+ { createRunnerWorkflowForAdmin: true },
+ { createRunnerWorkflowForNamespace: true },
+ ])('when createRunnerWorkflow is enabled by %o', (currentGlFeatures) => {
beforeEach(() => {
- createComponent();
- });
-
- it('renders an illustration', () => {
- expect(findEmptyState().props('svgPath')).toBe(EMPTY_STATE_SVG_URL);
- });
-
- it('displays "no results" text with instructions', () => {
- const desc = s__(
- 'Runners|Runners are the agents that run your CI/CD jobs. Follow the %{linkStart}installation and registration instructions%{linkEnd} to set up a runner.',
- );
-
- expect(findEmptyState().text()).toMatchInterpolatedText(`${title} ${desc}`);
+ glFeatures = currentGlFeatures;
});
- describe.each([
- { createRunnerWorkflowForAdmin: true },
- { createRunnerWorkflowForNamespace: true },
- ])('when %o', (glFeatures) => {
- describe('when newRunnerPath is defined', () => {
+ describe.each`
+ newRunnerPath | registrationToken | expectedMessages
+ ${mockNewRunnerPath} | ${mockRegistrationToken} | ${[I18N_CREATE_RUNNER_LINK, I18N_STILL_USING_REGISTRATION_TOKENS]}
+ ${mockNewRunnerPath} | ${null} | ${[I18N_CREATE_RUNNER_LINK]}
+ ${null} | ${mockRegistrationToken} | ${[I18N_STILL_USING_REGISTRATION_TOKENS]}
+ ${null} | ${null} | ${[I18N_CONTACT_ADMIN_TO_REGISTER]}
+ `(
+ 'when newRunnerPath is $newRunnerPath and registrationToken is $registrationToken',
+ ({ newRunnerPath, registrationToken, expectedMessages }) => {
beforeEach(() => {
createComponent({
- provide: {
- glFeatures,
+ props: {
+ newRunnerPath,
+ registrationToken,
},
});
});
- it('shows a link to the new runner page', () => {
- expect(findLink().attributes('href')).toBe(newRunnerPath);
+ it('shows title', () => {
+ expectTitleToBe(I18N_GET_STARTED);
});
- });
- describe('when newRunnerPath not defined', () => {
- beforeEach(() => {
- createComponent({
- props: {
- newRunnerPath: null,
- },
- provide: {
- glFeatures,
- },
- });
+ it('renders an illustration', () => {
+ expect(findEmptyState().props('svgPath')).toBe(EMPTY_STATE_SVG_URL);
});
- it('opens a runner registration instructions modal with a link', () => {
- const { value } = getBinding(findLink().element, 'gl-modal');
+ it(`shows description: "${expectedMessages.join(' ')}"`, () => {
+ expectDescriptionToBe([I18N_RUNNERS_ARE_AGENTS, ...expectedMessages]);
+ });
+ },
+ );
- expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
+ describe('with newRunnerPath and registration token', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ registrationToken: mockRegistrationToken,
+ newRunnerPath: mockNewRunnerPath,
+ },
});
});
+
+ it('shows links to the new runner page and registration instructions', () => {
+ expect(findLinks().at(0).attributes('href')).toBe(mockNewRunnerPath);
+
+ const { value } = getBinding(findLinks().at(1).element, 'gl-modal');
+ expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
+ });
});
- describe.each([
- { createRunnerWorkflowForAdmin: false },
- { createRunnerWorkflowForNamespace: false },
- ])('when %o', (glFeatures) => {
+ describe('with newRunnerPath and no registration token', () => {
beforeEach(() => {
createComponent({
- provide: {
- glFeatures,
+ props: {
+ registrationToken: mockRegistrationToken,
+ newRunnerPath: null,
},
});
});
it('opens a runner registration instructions modal with a link', () => {
const { value } = getBinding(findLink().element, 'gl-modal');
-
expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
});
});
- });
- describe('when there is no registration token', () => {
- beforeEach(() => {
- createComponent({ props: { registrationToken: null } });
- });
+ describe('with no newRunnerPath nor registration token', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ registrationToken: null,
+ newRunnerPath: null,
+ },
+ });
+ });
- it('renders an illustration', () => {
- expect(findEmptyState().props('svgPath')).toBe(EMPTY_STATE_SVG_URL);
+ it('has no link', () => {
+ expect(findLink().exists()).toBe(false);
+ });
});
+ });
+
+ describe('when createRunnerWorkflow is disabled', () => {
+ describe('when there is a registration token', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ registrationToken: mockRegistrationToken,
+ },
+ });
+ });
+
+ it('renders an illustration', () => {
+ expect(findEmptyState().props('svgPath')).toBe(EMPTY_STATE_SVG_URL);
+ });
+
+ it('opens a runner registration instructions modal with a link', () => {
+ const { value } = getBinding(findLink().element, 'gl-modal');
+ expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
+ });
- it('displays "no results" text', () => {
- const desc = s__(
- 'Runners|Runners are the agents that run your CI/CD jobs. To register new runners, please contact your administrator.',
- );
+ it('displays text with registration instructions', () => {
+ expectTitleToBe(I18N_GET_STARTED);
- expect(findEmptyState().text()).toMatchInterpolatedText(`${title} ${desc}`);
+ expectDescriptionToBe([I18N_RUNNERS_ARE_AGENTS, I18N_FOLLOW_REGISTRATION_INSTRUCTIONS]);
+ });
});
- it('has no registration instructions link', () => {
- expect(findLink().exists()).toBe(false);
+ describe('when there is no registration token', () => {
+ beforeEach(() => {
+ createComponent({ props: { registrationToken: null } });
+ });
+
+ it('displays "contact admin" text', () => {
+ expectTitleToBe(I18N_GET_STARTED);
+
+ expectDescriptionToBe([I18N_RUNNERS_ARE_AGENTS, I18N_CONTACT_ADMIN_TO_REGISTER]);
+ });
+
+ it('has no registration instructions link', () => {
+ expect(findLink().exists()).toBe(false);
+ });
});
});
});
@@ -147,8 +203,9 @@ describe('RunnerListEmptyState', () => {
});
it('displays "no filtered results" text', () => {
- expect(findEmptyState().text()).toContain(s__('Runners|No results found'));
- expect(findEmptyState().text()).toContain(s__('Runners|Edit your search and try again'));
+ expectTitleToBe(I18N_NO_RESULTS);
+
+ expectDescriptionToBe([I18N_EDIT_YOUR_SEARCH]);
});
});
});
diff --git a/spec/frontend/commit/components/refs_list_spec.js b/spec/frontend/commit/components/refs_list_spec.js
index 594f8827d58..cc783dc3b58 100644
--- a/spec/frontend/commit/components/refs_list_spec.js
+++ b/spec/frontend/commit/components/refs_list_spec.js
@@ -61,7 +61,7 @@ describe('Commit references component', () => {
it('renders links to refs', () => {
const index = 0;
const refBadge = findTippingRefs().at(index);
- const refUrl = `${refsListPropsMock.urlPart}${refsListPropsMock.tippingRefs[index]}`;
+ const refUrl = `${refsListPropsMock.urlPart}${refsListPropsMock.tippingRefs[index]}?ref_type=${refsListPropsMock.refType}`;
expect(refBadge.attributes('href')).toBe(refUrl);
});
diff --git a/spec/frontend/commit/mock_data.js b/spec/frontend/commit/mock_data.js
index 9c8f9266986..2a618e08c50 100644
--- a/spec/frontend/commit/mock_data.js
+++ b/spec/frontend/commit/mock_data.js
@@ -289,4 +289,5 @@ export const refsListPropsMock = {
tippingRefs: tippingBranchesMock,
isLoading: false,
urlPart: '/some/project/-/commits/',
+ refType: 'heads',
};
diff --git a/spec/frontend/fixtures/pipeline_header.rb b/spec/frontend/fixtures/pipeline_header.rb
index a4fba7e8675..d25bf12623f 100644
--- a/spec/frontend/fixtures/pipeline_header.rb
+++ b/spec/frontend/fixtures/pipeline_header.rb
@@ -51,6 +51,8 @@ RSpec.describe "GraphQL Pipeline Header", '(JavaScript fixtures)', type: :reques
)
end
+ let_it_be(:build) { create(:ci_build, :running, pipeline: pipeline, ref: 'master') }
+
it "graphql/pipelines/pipeline_header_running.json" do
query = get_graphql_query_as_string(query_path)
@@ -59,4 +61,29 @@ RSpec.describe "GraphQL Pipeline Header", '(JavaScript fixtures)', type: :reques
expect_graphql_errors_to_be_empty
end
end
+
+ context 'with failed pipeline' do
+ let_it_be(:pipeline) do
+ create(
+ :ci_pipeline,
+ project: project,
+ sha: commit.id,
+ ref: 'master',
+ user: user,
+ status: :failed,
+ started_at: 1.hour.ago,
+ finished_at: Time.current
+ )
+ end
+
+ let_it_be(:build) { create(:ci_build, :canceled, pipeline: pipeline, ref: 'master') }
+
+ it "graphql/pipelines/pipeline_header_failed.json" do
+ query = get_graphql_query_as_string(query_path)
+
+ post_graphql(query, current_user: user, variables: { fullPath: project.full_path, iid: pipeline.iid })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
end
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js
index 5c36dbf9c9c..2b60684e60a 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js
@@ -1,22 +1,37 @@
-import { GlAlert, GlDropdown, GlButton, GlFormCheckbox, GlLoadingIcon } from '@gitlab/ui';
+import { GlAlert, GlDropdown, GlButton, GlFormCheckbox, GlLoadingIcon, GlModal } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { stubComponent } from 'helpers/stub_component';
import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import Tracking from '~/tracking';
import { s__ } from '~/locale';
+import { createAlert } from '~/alert';
import {
packageFiles as packageFilesMock,
packageFilesQuery,
+ packageDestroyFilesMutation,
+ packageDestroyFilesMutationError,
} from 'jest/packages_and_registries/package_registry/mock_data';
+import {
+ DOWNLOAD_PACKAGE_ASSET_TRACKING_ACTION,
+ DELETE_ALL_PACKAGE_FILES_MODAL_CONTENT,
+ DELETE_LAST_PACKAGE_FILE_MODAL_CONTENT,
+ DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
+ DELETE_PACKAGE_FILE_ERROR_MESSAGE,
+ DELETE_PACKAGE_FILES_SUCCESS_MESSAGE,
+ DELETE_PACKAGE_FILES_ERROR_MESSAGE,
+} from '~/packages_and_registries/package_registry/constants';
import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import getPackageFiles from '~/packages_and_registries/package_registry/graphql/queries/get_package_files.query.graphql';
+import destroyPackageFilesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_files.mutation.graphql';
Vue.use(VueApollo);
+jest.mock('~/alert');
describe('Package Files', () => {
let wrapper;
@@ -24,6 +39,7 @@ describe('Package Files', () => {
const findAllRows = () => wrapper.findAllByTestId('file-row');
const findDeleteSelectedButton = () => wrapper.findByTestId('delete-selected');
+ const findDeleteFilesModal = () => wrapper.findByTestId('delete-files-modal');
const findFirstRow = () => extendedWrapper(findAllRows().at(0));
const findSecondRow = () => extendedWrapper(findAllRows().at(1));
const findPackageFilesAlert = () => wrapper.findComponent(GlAlert);
@@ -41,27 +57,39 @@ describe('Package Files', () => {
const files = packageFilesMock();
const [file] = files;
+ const showMock = jest.fn();
+ const eventCategory = 'UI::NpmPackages';
+
const createComponent = ({
packageId = '1',
packageType = 'NPM',
- isLoading = false,
+ projectPath = 'gitlab-test',
canDelete = true,
stubs,
- resolver = jest.fn().mockResolvedValue(packageFilesQuery([file])),
+ resolver = jest.fn().mockResolvedValue(packageFilesQuery({ files: [file] })),
+ filesDeleteMutationResolver = jest.fn().mockResolvedValue(packageDestroyFilesMutation()),
} = {}) => {
- const requestHandlers = [[getPackageFiles, resolver]];
+ const requestHandlers = [
+ [getPackageFiles, resolver],
+ [destroyPackageFilesMutation, filesDeleteMutationResolver],
+ ];
apolloProvider = createMockApollo(requestHandlers);
wrapper = mountExtended(PackageFiles, {
apolloProvider,
propsData: {
canDelete,
- isLoading,
packageId,
packageType,
+ projectPath,
},
stubs: {
GlTable: false,
+ GlModal: stubComponent(GlModal, {
+ methods: {
+ show: showMock,
+ },
+ }),
...stubs,
},
});
@@ -122,10 +150,16 @@ describe('Package Files', () => {
expect(findFirstRowDownloadLink().attributes('href')).toBe(file.downloadPath);
});
- it('emits "download-file" event on click', () => {
+ it('tracks "download-file" event on click', () => {
+ const eventSpy = jest.spyOn(Tracking, 'event');
+
findFirstRowDownloadLink().vm.$emit('click');
- expect(wrapper.emitted('download-file')).toEqual([[]]);
+ expect(eventSpy).toHaveBeenCalledWith(
+ eventCategory,
+ DOWNLOAD_PACKAGE_ASSET_TRACKING_ACTION,
+ expect.any(Object),
+ );
});
});
@@ -179,12 +213,14 @@ describe('Package Files', () => {
expect(findActionMenuDelete().exists()).toBe(true);
});
- it('emits a delete event when clicked', async () => {
+ it('shows delete file confirmation modal', async () => {
await findActionMenuDelete().trigger('click');
- const [[items]] = wrapper.emitted('delete-files');
- const [{ id }] = items;
- expect(id).toBe(file.id);
+ expect(showMock).toHaveBeenCalledTimes(1);
+
+ expect(findDeleteFilesModal().text()).toBe(
+ 'You are about to delete foo-1.0.1.tgz. This is a destructive action that may render your package unusable. Are you sure?',
+ );
});
});
});
@@ -213,21 +249,6 @@ describe('Package Files', () => {
expect(findDeleteSelectedButton().props('disabled')).toBe(true);
});
- it('delete selected button exists & is disabled when isLoading prop is true', async () => {
- createComponent();
- await waitForPromises();
- const first = findAllRowCheckboxes().at(0);
-
- await first.setChecked(true);
-
- expect(findDeleteSelectedButton().props('disabled')).toBe(false);
-
- await wrapper.setProps({ isLoading: true });
-
- expect(findDeleteSelectedButton().props('disabled')).toBe(true);
- expect(findLoadingIcon().exists()).toBe(true);
- });
-
it('checkboxes to select file are visible', async () => {
createComponent({ resolver: jest.fn().mockResolvedValue(packageFilesQuery()) });
await waitForPromises();
@@ -295,7 +316,7 @@ describe('Package Files', () => {
});
});
- it('emits a delete event when selected', async () => {
+ it('shows delete modal with single file confirmation text when delete selected is clicked', async () => {
createComponent();
await waitForPromises();
@@ -305,12 +326,14 @@ describe('Package Files', () => {
await findDeleteSelectedButton().trigger('click');
- const [[items]] = wrapper.emitted('delete-files');
- const [{ id }] = items;
- expect(id).toBe(file.id);
+ expect(showMock).toHaveBeenCalledTimes(1);
+
+ expect(findDeleteFilesModal().text()).toBe(
+ 'You are about to delete foo-1.0.1.tgz. This is a destructive action that may render your package unusable. Are you sure?',
+ );
});
- it('emits delete event with both items when all are selected', async () => {
+ it('shows delete modal with multiple files confirmation text when delete selected is clicked', async () => {
createComponent({ resolver: jest.fn().mockResolvedValue(packageFilesQuery()) });
await waitForPromises();
@@ -318,8 +341,63 @@ describe('Package Files', () => {
await findDeleteSelectedButton().trigger('click');
- const [[items]] = wrapper.emitted('delete-files');
- expect(items).toHaveLength(2);
+ expect(showMock).toHaveBeenCalledTimes(1);
+
+ expect(findDeleteFilesModal().text()).toMatchInterpolatedText(
+ 'You are about to delete 2 assets. This operation is irreversible.',
+ );
+ });
+
+ describe('emits delete-all-files event', () => {
+ it('with right content for last file in package', async () => {
+ createComponent({
+ resolver: jest.fn().mockResolvedValue(
+ packageFilesQuery({
+ files: [file],
+ pageInfo: {
+ hasNextPage: false,
+ },
+ }),
+ ),
+ });
+ await waitForPromises();
+ const first = findAllRowCheckboxes().at(0);
+
+ await first.setChecked(true);
+
+ await findDeleteSelectedButton().trigger('click');
+
+ expect(showMock).toHaveBeenCalledTimes(0);
+
+ expect(wrapper.emitted('delete-all-files')).toHaveLength(1);
+ expect(wrapper.emitted('delete-all-files')[0]).toEqual([
+ DELETE_LAST_PACKAGE_FILE_MODAL_CONTENT,
+ ]);
+ });
+
+ it('with right content for all files in package', async () => {
+ createComponent({
+ resolver: jest.fn().mockResolvedValue(
+ packageFilesQuery({
+ pageInfo: {
+ hasNextPage: false,
+ },
+ }),
+ ),
+ });
+ await waitForPromises();
+
+ await findCheckAllCheckbox().setChecked(true);
+
+ await findDeleteSelectedButton().trigger('click');
+
+ expect(showMock).toHaveBeenCalledTimes(0);
+
+ expect(wrapper.emitted('delete-all-files')).toHaveLength(1);
+ expect(wrapper.emitted('delete-all-files')[0]).toEqual([
+ DELETE_ALL_PACKAGE_FILES_MODAL_CONTENT,
+ ]);
+ });
});
});
@@ -343,6 +421,195 @@ describe('Package Files', () => {
});
});
+ describe('deleting a file', () => {
+ const doDeleteFile = async () => {
+ const first = findAllRowCheckboxes().at(0);
+
+ await first.setChecked(true);
+
+ await findDeleteSelectedButton().trigger('click');
+
+ findDeleteFilesModal().vm.$emit('primary');
+ };
+
+ it('confirming on the modal sets the loading state', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ await doDeleteFile();
+
+ await nextTick();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ it('confirming on the modal deletes the file and shows a success message', async () => {
+ const resolver = jest.fn().mockResolvedValue(packageFilesQuery({ files: [file] }));
+ const filesDeleteMutationResolver = jest
+ .fn()
+ .mockResolvedValue(packageDestroyFilesMutation());
+ createComponent({ resolver, filesDeleteMutationResolver });
+
+ await waitForPromises();
+
+ await doDeleteFile();
+
+ await waitForPromises();
+
+ expect(findLoadingIcon().exists()).toBe(false);
+
+ expect(createAlert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
+ }),
+ );
+
+ expect(filesDeleteMutationResolver).toHaveBeenCalledWith({
+ ids: [file.id],
+ projectPath: 'gitlab-test',
+ });
+
+ // we are re-fetching the package files, so we expect the resolver to have been called twice
+ expect(resolver).toHaveBeenCalledTimes(2);
+ expect(resolver).toHaveBeenCalledWith({
+ id: '1',
+ first: 100,
+ });
+ });
+
+ describe('errors', () => {
+ it('shows an error when the mutation request fails', async () => {
+ createComponent({ filesDeleteMutationResolver: jest.fn().mockRejectedValue() });
+ await waitForPromises();
+
+ await doDeleteFile();
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: DELETE_PACKAGE_FILE_ERROR_MESSAGE,
+ }),
+ );
+ });
+
+ it('shows an error when the mutation request returns an error payload', async () => {
+ createComponent({
+ filesDeleteMutationResolver: jest
+ .fn()
+ .mockResolvedValue(packageDestroyFilesMutationError()),
+ });
+ await waitForPromises();
+
+ await doDeleteFile();
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: DELETE_PACKAGE_FILE_ERROR_MESSAGE,
+ }),
+ );
+ });
+ });
+ });
+
+ describe('deleting multiple files', () => {
+ const doDeleteFiles = async () => {
+ await findCheckAllCheckbox().setChecked(true);
+
+ await findDeleteSelectedButton().trigger('click');
+
+ findDeleteFilesModal().vm.$emit('primary');
+ };
+
+ it('confirming on the modal sets the loading state', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ await doDeleteFiles();
+
+ await nextTick();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ it('confirming on the modal deletes the file and shows a success message', async () => {
+ const resolver = jest.fn().mockResolvedValue(packageFilesQuery());
+ const filesDeleteMutationResolver = jest
+ .fn()
+ .mockResolvedValue(packageDestroyFilesMutation());
+ createComponent({ resolver, filesDeleteMutationResolver });
+
+ await waitForPromises();
+
+ await doDeleteFiles();
+
+ await waitForPromises();
+
+ expect(findLoadingIcon().exists()).toBe(false);
+
+ expect(createAlert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: DELETE_PACKAGE_FILES_SUCCESS_MESSAGE,
+ }),
+ );
+
+ expect(filesDeleteMutationResolver).toHaveBeenCalledWith({
+ ids: files.map(({ id }) => id),
+ projectPath: 'gitlab-test',
+ });
+
+ // we are re-fetching the package files, so we expect the resolver to have been called twice
+ expect(resolver).toHaveBeenCalledTimes(2);
+ expect(resolver).toHaveBeenCalledWith({
+ id: '1',
+ first: 100,
+ });
+ });
+
+ describe('errors', () => {
+ it('shows an error when the mutation request fails', async () => {
+ const resolver = jest.fn().mockResolvedValue(packageFilesQuery());
+ createComponent({ filesDeleteMutationResolver: jest.fn().mockRejectedValue(), resolver });
+ await waitForPromises();
+
+ await doDeleteFiles();
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: DELETE_PACKAGE_FILES_ERROR_MESSAGE,
+ }),
+ );
+ });
+
+ it('shows an error when the mutation request returns an error payload', async () => {
+ const resolver = jest.fn().mockResolvedValue(packageFilesQuery());
+ createComponent({
+ filesDeleteMutationResolver: jest
+ .fn()
+ .mockResolvedValue(packageDestroyFilesMutationError()),
+ resolver,
+ });
+ await waitForPromises();
+
+ await doDeleteFiles();
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: DELETE_PACKAGE_FILES_ERROR_MESSAGE,
+ }),
+ );
+ });
+ });
+ });
+
describe('additional details', () => {
describe('details toggle button', () => {
it('exists', async () => {
@@ -357,7 +624,9 @@ describe('Package Files', () => {
noShaFile.fileSha256 = null;
noShaFile.fileMd5 = null;
noShaFile.fileSha1 = null;
- createComponent({ resolver: jest.fn().mockResolvedValue(packageFilesQuery([noShaFile])) });
+ createComponent({
+ resolver: jest.fn().mockResolvedValue(packageFilesQuery({ files: [noShaFile] })),
+ });
await waitForPromises();
expect(findFirstToggleDetailsButton().exists()).toBe(false);
@@ -410,7 +679,9 @@ describe('Package Files', () => {
const { ...missingMd5 } = file;
missingMd5.fileMd5 = null;
- createComponent({ resolver: jest.fn().mockResolvedValue(packageFilesQuery([missingMd5])) });
+ createComponent({
+ resolver: jest.fn().mockResolvedValue(packageFilesQuery({ files: [missingMd5] })),
+ });
await waitForPromises();
await showShaFiles();
diff --git a/spec/frontend/packages_and_registries/package_registry/mock_data.js b/spec/frontend/packages_and_registries/package_registry/mock_data.js
index fa6a69b1a1f..f1dab38a9e6 100644
--- a/spec/frontend/packages_and_registries/package_registry/mock_data.js
+++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js
@@ -254,9 +254,6 @@ export const packageDetailsQuery = ({
__typename: 'PipelineConnection',
},
packageFiles: {
- pageInfo: {
- hasNextPage: true,
- },
nodes: packageFiles().map(({ id, size }) => ({ id, size })),
__typename: 'PackageFileConnection',
},
@@ -285,11 +282,15 @@ export const packagePipelinesQuery = (pipelines = packagePipelines()) => ({
},
});
-export const packageFilesQuery = (files = packageFiles()) => ({
+export const packageFilesQuery = ({ files = packageFiles(), pageInfo = {} } = {}) => ({
data: {
package: {
id: 'gid://gitlab/Packages::Package/111',
packageFiles: {
+ pageInfo: {
+ hasNextPage: true,
+ ...pageInfo,
+ },
nodes: files,
__typename: 'PackageFileConnection',
},
diff --git a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
index 8b15dfd7d4a..0f91a7aeb50 100644
--- a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
@@ -21,10 +21,7 @@ import {
REQUEST_FORWARDING_HELP_PAGE_PATH,
FETCH_PACKAGE_DETAILS_ERROR_MESSAGE,
PACKAGE_TYPE_COMPOSER,
- DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
- DELETE_PACKAGE_FILE_ERROR_MESSAGE,
- DELETE_PACKAGE_FILES_SUCCESS_MESSAGE,
- DELETE_PACKAGE_FILES_ERROR_MESSAGE,
+ DELETE_ALL_PACKAGE_FILES_MODAL_CONTENT,
PACKAGE_TYPE_NUGET,
PACKAGE_TYPE_MAVEN,
PACKAGE_TYPE_CONAN,
@@ -32,7 +29,6 @@ import {
PACKAGE_TYPE_NPM,
} from '~/packages_and_registries/package_registry/constants';
-import destroyPackageFilesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_files.mutation.graphql';
import getPackageDetails from '~/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql';
import getPackageVersionsQuery from '~/packages_and_registries/package_registry/graphql//queries/get_package_versions.query.graphql';
import {
@@ -41,9 +37,6 @@ import {
packageVersions,
dependencyLinks,
emptyPackageDetailsQuery,
- packageFiles,
- packageDestroyFilesMutation,
- packageDestroyFilesMutationError,
defaultPackageGroupSettings,
} from '../mock_data';
@@ -74,13 +67,9 @@ describe('PackagesApp', () => {
function createComponent({
resolver = jest.fn().mockResolvedValue(packageDetailsQuery()),
- filesDeleteMutationResolver = jest.fn().mockResolvedValue(packageDestroyFilesMutation()),
routeId = '1',
} = {}) {
- const requestHandlers = [
- [getPackageDetails, resolver],
- [destroyPackageFilesMutation, filesDeleteMutationResolver],
- ];
+ const requestHandlers = [[getPackageDetails, resolver]];
apolloProvider = createMockApollo(requestHandlers);
wrapper = shallowMountExtended(PackagesApp, {
@@ -117,8 +106,6 @@ describe('PackagesApp', () => {
const findDeleteModal = () => wrapper.findByTestId('delete-modal');
const findDeleteButton = () => wrapper.findByTestId('delete-package');
const findPackageFiles = () => wrapper.findComponent(PackageFiles);
- const findDeleteFileModal = () => wrapper.findByTestId('delete-file-modal');
- const findDeleteFilesModal = () => wrapper.findByTestId('delete-files-modal');
const findVersionsList = () => wrapper.findComponent(PackageVersionsList);
const findVersionsCountBadge = () => wrapper.findByTestId('other-versions-badge');
const findNoVersionsMessage = () => wrapper.findByTestId('no-versions-message');
@@ -336,9 +323,9 @@ describe('PackagesApp', () => {
expect(findPackageFiles().props()).toMatchObject({
canDelete: packageData().canDestroy,
- isLoading: false,
packageId: packageData().id,
packageType: packageData().packageType,
+ projectPath: 'gitlab-test',
});
});
@@ -356,250 +343,26 @@ describe('PackagesApp', () => {
expect(findPackageFiles().exists()).toBe(false);
});
- describe('deleting a file', () => {
- const [fileToDelete] = packageFiles();
-
- const doDeleteFile = () => {
- findPackageFiles().vm.$emit('delete-files', [fileToDelete]);
-
- findDeleteFileModal().vm.$emit('primary');
-
- return waitForPromises();
- };
-
- it('opens delete file confirmation modal', async () => {
- createComponent();
-
- await waitForPromises();
-
- findPackageFiles().vm.$emit('delete-files', [fileToDelete]);
-
- expect(showMock).toHaveBeenCalledTimes(1);
-
- await waitForPromises();
-
- expect(findDeleteFileModal().text()).toBe(
- 'You are about to delete foo-1.0.1.tgz. This is a destructive action that may render your package unusable. Are you sure?',
- );
- });
-
- it('when its the only file opens delete package confirmation modal', async () => {
- const [packageFile] = packageFiles();
+ describe('emits delete-all-files event', () => {
+ it('opens the delete package confirmation modal and shows confirmation text', async () => {
const resolver = jest.fn().mockResolvedValue(
packageDetailsQuery({
- extendPackage: {
- packageFiles: {
- pageInfo: {
- hasNextPage: false,
- },
- nodes: [packageFile],
- __typename: 'PackageFileConnection',
- },
- },
+ extendPackage: {},
packageSettings: {
...defaultPackageGroupSettings,
npmPackageRequestsForwarding: false,
},
}),
);
-
- createComponent({
- resolver,
- });
-
- await waitForPromises();
-
- findPackageFiles().vm.$emit('delete-files', [fileToDelete]);
-
- expect(showMock).toHaveBeenCalledTimes(1);
-
- await waitForPromises();
-
- expect(findDeleteModal().text()).toBe(
- 'Deleting the last package asset will remove version 1.0.0 of @gitlab-org/package-15. Are you sure?',
- );
- });
-
- it('confirming on the modal sets the loading state', async () => {
- createComponent();
-
- await waitForPromises();
-
- findPackageFiles().vm.$emit('delete-files', [fileToDelete]);
-
- findDeleteFileModal().vm.$emit('primary');
-
- await nextTick();
-
- expect(findPackageFiles().props('isLoading')).toEqual(true);
- });
-
- it('confirming on the modal deletes the file and shows a success message', async () => {
- const resolver = jest.fn().mockResolvedValue(packageDetailsQuery());
- createComponent({ resolver });
-
- await waitForPromises();
-
- await doDeleteFile();
-
- expect(createAlert).toHaveBeenCalledWith(
- expect.objectContaining({
- message: DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
- }),
- );
- // we are re-fetching the package details, so we expect the resolver to have been called twice
- expect(resolver).toHaveBeenCalledTimes(2);
- });
-
- describe('errors', () => {
- it('shows an error when the mutation request fails', async () => {
- createComponent({ filesDeleteMutationResolver: jest.fn().mockRejectedValue() });
- await waitForPromises();
-
- await doDeleteFile();
-
- expect(createAlert).toHaveBeenCalledWith(
- expect.objectContaining({
- message: DELETE_PACKAGE_FILE_ERROR_MESSAGE,
- }),
- );
- });
-
- it('shows an error when the mutation request returns an error payload', async () => {
- createComponent({
- filesDeleteMutationResolver: jest
- .fn()
- .mockResolvedValue(packageDestroyFilesMutationError()),
- });
- await waitForPromises();
-
- await doDeleteFile();
-
- expect(createAlert).toHaveBeenCalledWith(
- expect.objectContaining({
- message: DELETE_PACKAGE_FILE_ERROR_MESSAGE,
- }),
- );
- });
- });
- });
-
- describe('deleting multiple files', () => {
- const doDeleteFiles = () => {
- findPackageFiles().vm.$emit('delete-files', packageFiles());
-
- findDeleteFilesModal().vm.$emit('primary');
-
- return waitForPromises();
- };
-
- it('opens delete files confirmation modal', async () => {
- createComponent();
-
- await waitForPromises();
-
- const showDeleteFilesSpy = jest.spyOn(wrapper.vm.$refs.deleteFilesModal, 'show');
-
- findPackageFiles().vm.$emit('delete-files', packageFiles());
-
- expect(showDeleteFilesSpy).toHaveBeenCalled();
- });
-
- it('confirming on the modal sets the loading state', async () => {
- createComponent();
-
- await waitForPromises();
-
- findPackageFiles().vm.$emit('delete-files', packageFiles());
-
- findDeleteFilesModal().vm.$emit('primary');
-
- await nextTick();
-
- expect(findPackageFiles().props('isLoading')).toEqual(true);
- });
-
- it('confirming on the modal deletes the file and shows a success message', async () => {
- const resolver = jest.fn().mockResolvedValue(packageDetailsQuery());
createComponent({ resolver });
await waitForPromises();
- await doDeleteFiles();
-
- expect(resolver).toHaveBeenCalledTimes(2);
-
- expect(createAlert).toHaveBeenCalledWith(
- expect.objectContaining({
- message: DELETE_PACKAGE_FILES_SUCCESS_MESSAGE,
- }),
- );
- // we are re-fetching the package details, so we expect the resolver to have been called twice
- expect(resolver).toHaveBeenCalledTimes(2);
- });
-
- describe('errors', () => {
- it('shows an error when the mutation request fails', async () => {
- createComponent({ filesDeleteMutationResolver: jest.fn().mockRejectedValue() });
- await waitForPromises();
-
- await doDeleteFiles();
-
- expect(createAlert).toHaveBeenCalledWith(
- expect.objectContaining({
- message: DELETE_PACKAGE_FILES_ERROR_MESSAGE,
- }),
- );
- });
-
- it('shows an error when the mutation request returns an error payload', async () => {
- createComponent({
- filesDeleteMutationResolver: jest
- .fn()
- .mockResolvedValue(packageDestroyFilesMutationError()),
- });
- await waitForPromises();
-
- await doDeleteFiles();
-
- expect(createAlert).toHaveBeenCalledWith(
- expect.objectContaining({
- message: DELETE_PACKAGE_FILES_ERROR_MESSAGE,
- }),
- );
- });
- });
- });
-
- describe('deleting all files', () => {
- it('opens the delete package confirmation modal', async () => {
- const resolver = jest.fn().mockResolvedValue(
- packageDetailsQuery({
- extendPackage: {
- packageFiles: {
- pageInfo: {
- hasNextPage: false,
- },
- nodes: packageFiles(),
- },
- },
- packageSettings: {
- ...defaultPackageGroupSettings,
- npmPackageRequestsForwarding: false,
- },
- }),
- );
- createComponent({
- resolver,
- });
-
- await waitForPromises();
-
- findPackageFiles().vm.$emit('delete-files', packageFiles());
+ findPackageFiles().vm.$emit('delete-all-files', DELETE_ALL_PACKAGE_FILES_MODAL_CONTENT);
expect(showMock).toHaveBeenCalledTimes(1);
- await waitForPromises();
+ await nextTick();
expect(findDeleteModal().text()).toBe(
'Deleting all package assets will remove version 1.0.0 of @gitlab-org/package-15. Are you sure?',
diff --git a/spec/frontend/pipelines/mock_data.js b/spec/frontend/pipelines/mock_data.js
index fd654eb6f10..8bbe0ef78c0 100644
--- a/spec/frontend/pipelines/mock_data.js
+++ b/spec/frontend/pipelines/mock_data.js
@@ -1,5 +1,6 @@
import pipelineHeaderSuccess from 'test_fixtures/graphql/pipelines/pipeline_header_success.json';
import pipelineHeaderRunning from 'test_fixtures/graphql/pipelines/pipeline_header_running.json';
+import pipelineHeaderFailed from 'test_fixtures/graphql/pipelines/pipeline_header_failed.json';
const PIPELINE_RUNNING = 'RUNNING';
const PIPELINE_CANCELED = 'CANCELED';
@@ -8,7 +9,31 @@ const PIPELINE_FAILED = 'FAILED';
const threeWeeksAgo = new Date();
threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21);
-export { pipelineHeaderSuccess, pipelineHeaderRunning };
+export { pipelineHeaderSuccess, pipelineHeaderRunning, pipelineHeaderFailed };
+
+export const pipelineRetryMutationResponseSuccess = {
+ data: { pipelineRetry: { errors: [] } },
+};
+
+export const pipelineRetryMutationResponseFailed = {
+ data: { pipelineRetry: { errors: ['error'] } },
+};
+
+export const pipelineCancelMutationResponseSuccess = {
+ data: { pipelineRetry: { errors: [] } },
+};
+
+export const pipelineCancelMutationResponseFailed = {
+ data: { pipelineRetry: { errors: ['error'] } },
+};
+
+export const pipelineDeleteMutationResponseSuccess = {
+ data: { pipelineRetry: { errors: [] } },
+};
+
+export const pipelineDeleteMutationResponseFailed = {
+ data: { pipelineRetry: { errors: ['error'] } },
+};
export const mockPipelineHeader = {
detailedStatus: {},
diff --git a/spec/frontend/pipelines/pipeline_details_header_spec.js b/spec/frontend/pipelines/pipeline_details_header_spec.js
index 08ae35fe808..7141e10fb17 100644
--- a/spec/frontend/pipelines/pipeline_details_header_spec.js
+++ b/spec/frontend/pipelines/pipeline_details_header_spec.js
@@ -1,23 +1,59 @@
-import { GlBadge, GlLoadingIcon } from '@gitlab/ui';
-import Vue from 'vue';
+import { GlAlert, GlBadge, GlLoadingIcon, GlModal } from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import PipelineDetailsHeader from '~/pipelines/components/pipeline_details_header.vue';
+import { BUTTON_TOOLTIP_RETRY, BUTTON_TOOLTIP_CANCEL } from '~/pipelines/constants';
import TimeAgo from '~/pipelines/components/pipelines_list/time_ago.vue';
import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import cancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql';
+import deletePipelineMutation from '~/pipelines/graphql/mutations/delete_pipeline.mutation.graphql';
+import retryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.mutation.graphql';
import getPipelineDetailsQuery from '~/pipelines/graphql/queries/get_pipeline_header_data.query.graphql';
-import { pipelineHeaderSuccess, pipelineHeaderRunning } from './mock_data';
+import {
+ pipelineHeaderSuccess,
+ pipelineHeaderRunning,
+ pipelineHeaderFailed,
+ pipelineRetryMutationResponseSuccess,
+ pipelineCancelMutationResponseSuccess,
+ pipelineDeleteMutationResponseSuccess,
+ pipelineRetryMutationResponseFailed,
+ pipelineCancelMutationResponseFailed,
+ pipelineDeleteMutationResponseFailed,
+} from './mock_data';
Vue.use(VueApollo);
describe('Pipeline details header', () => {
let wrapper;
+ let glModalDirective;
const successHandler = jest.fn().mockResolvedValue(pipelineHeaderSuccess);
const runningHandler = jest.fn().mockResolvedValue(pipelineHeaderRunning);
+ const failedHandler = jest.fn().mockResolvedValue(pipelineHeaderFailed);
+ const retryMutationHandlerSuccess = jest
+ .fn()
+ .mockResolvedValue(pipelineRetryMutationResponseSuccess);
+ const cancelMutationHandlerSuccess = jest
+ .fn()
+ .mockResolvedValue(pipelineCancelMutationResponseSuccess);
+ const deleteMutationHandlerSuccess = jest
+ .fn()
+ .mockResolvedValue(pipelineDeleteMutationResponseSuccess);
+ const retryMutationHandlerFailed = jest
+ .fn()
+ .mockResolvedValue(pipelineRetryMutationResponseFailed);
+ const cancelMutationHandlerFailed = jest
+ .fn()
+ .mockResolvedValue(pipelineCancelMutationResponseFailed);
+ const deleteMutationHandlerFailed = jest
+ .fn()
+ .mockResolvedValue(pipelineDeleteMutationResponseFailed);
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
const findStatus = () => wrapper.findComponent(CiBadgeLink);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findTimeAgo = () => wrapper.findComponent(TimeAgo);
@@ -28,6 +64,10 @@ describe('Pipeline details header', () => {
const findCommitLink = () => wrapper.findByTestId('commit-link');
const findPipelineRunningText = () => wrapper.findByTestId('pipeline-running-text').text();
const findPipelineRefText = () => wrapper.findByTestId('pipeline-ref-text').text();
+ const findRetryButton = () => wrapper.findByTestId('retry-pipeline');
+ const findCancelButton = () => wrapper.findByTestId('cancel-pipeline');
+ const findDeleteButton = () => wrapper.findByTestId('delete-pipeline');
+ const findDeleteModal = () => wrapper.findComponent(GlModal);
const defaultHandlers = [[getPipelineDetailsQuery, successHandler]];
@@ -58,7 +98,7 @@ describe('Pipeline details header', () => {
stuck: false,
},
refText:
- 'For merge request <a class="mr-iid" href="/root/ci-project/-/merge_requests/1">!1</a> to merge <a class="ref-name" href="/root/ci-project/-/commits/test">test</a>',
+ 'Related merge request <a class="mr-iid" href="/root/ci-project/-/merge_requests/1">!1</a> to merge <a class="ref-name" href="/root/ci-project/-/commits/test">test</a>',
};
const createMockApolloProvider = (handlers) => {
@@ -66,6 +106,8 @@ describe('Pipeline details header', () => {
};
const createComponent = (handlers = defaultHandlers, props = defaultProps) => {
+ glModalDirective = jest.fn();
+
wrapper = shallowMountExtended(PipelineDetailsHeader, {
provide: {
...defaultProvideOptions,
@@ -73,6 +115,13 @@ describe('Pipeline details header', () => {
propsData: {
...props,
},
+ directives: {
+ glModal: {
+ bind(_, { value }) {
+ glModalDirective(value);
+ },
+ },
+ },
apolloProvider: createMockApolloProvider(handlers),
});
};
@@ -125,7 +174,7 @@ describe('Pipeline details header', () => {
});
it('displays ref text', () => {
- expect(findPipelineRefText()).toBe('For merge request !1 to merge test');
+ expect(findPipelineRefText()).toBe('Related merge request !1 to merge test');
});
});
@@ -164,4 +213,155 @@ describe('Pipeline details header', () => {
expect(findPipelineRunningText()).toBe('In progress, queued for 3600 seconds');
});
});
+
+ describe('actions', () => {
+ describe('retry action', () => {
+ beforeEach(async () => {
+ createComponent([
+ [getPipelineDetailsQuery, failedHandler],
+ [retryPipelineMutation, retryMutationHandlerSuccess],
+ ]);
+
+ await waitForPromises();
+ });
+
+ it('should call retryPipeline Mutation with pipeline id', () => {
+ findRetryButton().vm.$emit('click');
+
+ expect(retryMutationHandlerSuccess).toHaveBeenCalledWith({
+ id: pipelineHeaderFailed.data.project.pipeline.id,
+ });
+ expect(findAlert().exists()).toBe(false);
+ });
+
+ it('should render retry action tooltip', () => {
+ expect(findRetryButton().attributes('title')).toBe(BUTTON_TOOLTIP_RETRY);
+ });
+ });
+
+ describe('retry action failed', () => {
+ beforeEach(async () => {
+ createComponent([
+ [getPipelineDetailsQuery, failedHandler],
+ [retryPipelineMutation, retryMutationHandlerFailed],
+ ]);
+
+ await waitForPromises();
+ });
+
+ it('should display error message on failure', async () => {
+ findRetryButton().vm.$emit('click');
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ });
+
+ it('retry button loading state should reset on error', async () => {
+ findRetryButton().vm.$emit('click');
+
+ await nextTick();
+
+ expect(findRetryButton().props('loading')).toBe(true);
+
+ await waitForPromises();
+
+ expect(findRetryButton().props('loading')).toBe(false);
+ });
+ });
+
+ describe('cancel action', () => {
+ it('should call cancelPipeline Mutation with pipeline id', async () => {
+ createComponent([
+ [getPipelineDetailsQuery, runningHandler],
+ [cancelPipelineMutation, cancelMutationHandlerSuccess],
+ ]);
+
+ await waitForPromises();
+
+ findCancelButton().vm.$emit('click');
+
+ expect(cancelMutationHandlerSuccess).toHaveBeenCalledWith({
+ id: pipelineHeaderRunning.data.project.pipeline.id,
+ });
+ expect(findAlert().exists()).toBe(false);
+ });
+
+ it('should render cancel action tooltip', async () => {
+ createComponent([
+ [getPipelineDetailsQuery, runningHandler],
+ [cancelPipelineMutation, cancelMutationHandlerSuccess],
+ ]);
+
+ await waitForPromises();
+
+ expect(findCancelButton().attributes('title')).toBe(BUTTON_TOOLTIP_CANCEL);
+ });
+
+ it('should display error message on failure', async () => {
+ createComponent([
+ [getPipelineDetailsQuery, runningHandler],
+ [cancelPipelineMutation, cancelMutationHandlerFailed],
+ ]);
+
+ await waitForPromises();
+
+ findCancelButton().vm.$emit('click');
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ });
+ });
+
+ describe('delete action', () => {
+ it('displays delete modal when clicking on delete and does not call the delete action', async () => {
+ createComponent([
+ [getPipelineDetailsQuery, successHandler],
+ [deletePipelineMutation, deleteMutationHandlerSuccess],
+ ]);
+
+ await waitForPromises();
+
+ findDeleteButton().vm.$emit('click');
+
+ const modalId = 'pipeline-delete-modal';
+
+ expect(findDeleteModal().props('modalId')).toBe(modalId);
+ expect(glModalDirective).toHaveBeenCalledWith(modalId);
+ expect(deleteMutationHandlerSuccess).not.toHaveBeenCalled();
+ expect(findAlert().exists()).toBe(false);
+ });
+
+ it('should call deletePipeline Mutation with pipeline id when modal is submitted', async () => {
+ createComponent([
+ [getPipelineDetailsQuery, successHandler],
+ [deletePipelineMutation, deleteMutationHandlerSuccess],
+ ]);
+
+ await waitForPromises();
+
+ findDeleteModal().vm.$emit('primary');
+
+ expect(deleteMutationHandlerSuccess).toHaveBeenCalledWith({
+ id: pipelineHeaderSuccess.data.project.pipeline.id,
+ });
+ });
+
+ it('should display error message on failure', async () => {
+ createComponent([
+ [getPipelineDetailsQuery, successHandler],
+ [deletePipelineMutation, deleteMutationHandlerFailed],
+ ]);
+
+ await waitForPromises();
+
+ findDeleteModal().vm.$emit('primary');
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ });
+ });
+ });
});
diff --git a/spec/frontend/pipelines/pipeline_multi_actions_spec.js b/spec/frontend/pipelines/pipeline_multi_actions_spec.js
index e3c9983aa52..43336bbc748 100644
--- a/spec/frontend/pipelines/pipeline_multi_actions_spec.js
+++ b/spec/frontend/pipelines/pipeline_multi_actions_spec.js
@@ -1,9 +1,11 @@
+import { nextTick } from 'vue';
import { GlAlert, GlDropdown, GlSprintf, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { stubComponent } from 'helpers/stub_component';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import PipelineMultiActions, {
@@ -14,6 +16,7 @@ import { TRACKING_CATEGORIES } from '~/pipelines/constants';
describe('Pipeline Multi Actions Dropdown', () => {
let wrapper;
let mockAxios;
+ const focusInputMock = jest.fn();
const artifacts = [
{
@@ -30,7 +33,7 @@ describe('Pipeline Multi Actions Dropdown', () => {
const artifactsEndpoint = `endpoint/${artifactsEndpointPlaceholder}/artifacts.json`;
const pipelineId = 108;
- const createComponent = ({ mockData = {} } = {}) => {
+ const createComponent = () => {
wrapper = extendedWrapper(
shallowMount(PipelineMultiActions, {
provide: {
@@ -40,14 +43,12 @@ describe('Pipeline Multi Actions Dropdown', () => {
propsData: {
pipelineId,
},
- data() {
- return {
- ...mockData,
- };
- },
stubs: {
GlSprintf,
GlDropdown,
+ GlSearchBoxByType: stubComponent(GlSearchBoxByType, {
+ methods: { focusInput: focusInputMock },
+ }),
},
}),
);
@@ -76,70 +77,91 @@ describe('Pipeline Multi Actions Dropdown', () => {
});
describe('Artifacts', () => {
- it('should fetch artifacts and show search box on dropdown click', async () => {
- const endpoint = artifactsEndpoint.replace(artifactsEndpointPlaceholder, pipelineId);
- mockAxios.onGet(endpoint).replyOnce(HTTP_STATUS_OK, { artifacts });
- createComponent();
- findDropdown().vm.$emit('show');
- await waitForPromises();
+ const endpoint = artifactsEndpoint.replace(artifactsEndpointPlaceholder, pipelineId);
- expect(mockAxios.history.get).toHaveLength(1);
- expect(wrapper.vm.artifacts).toEqual(artifacts);
- expect(findSearchBox().exists()).toBe(true);
- });
+ describe('while loading artifacts', () => {
+ beforeEach(() => {
+ mockAxios.onGet(endpoint).replyOnce(HTTP_STATUS_OK, { artifacts });
+ });
- it('should focus the search box when opened with artifacts', () => {
- createComponent({ mockData: { artifacts } });
- wrapper.vm.$refs.searchInput.focusInput = jest.fn();
+ it('should render a loading spinner and no empty message', async () => {
+ createComponent();
- findDropdown().vm.$emit('shown');
+ findDropdown().vm.$emit('show');
+ await nextTick();
- expect(wrapper.vm.$refs.searchInput.focusInput).toHaveBeenCalled();
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(findEmptyMessage().exists()).toBe(false);
+ });
});
- it('should render all the provided artifacts when search query is empty', () => {
- const searchQuery = '';
- createComponent({ mockData: { searchQuery, artifacts } });
+ describe('artifacts loaded successfully', () => {
+ describe('artifacts exist', () => {
+ beforeEach(async () => {
+ mockAxios.onGet(endpoint).replyOnce(HTTP_STATUS_OK, { artifacts });
- expect(findAllArtifactItems()).toHaveLength(artifacts.length);
- expect(findEmptyMessage().exists()).toBe(false);
- });
+ createComponent();
- it('should render filtered artifacts when search query is not empty', () => {
- const searchQuery = 'job-2';
- createComponent({ mockData: { searchQuery, artifacts } });
+ findDropdown().vm.$emit('show');
+ await waitForPromises();
+ });
- expect(findAllArtifactItems()).toHaveLength(1);
- expect(findEmptyMessage().exists()).toBe(false);
- });
+ it('should fetch artifacts and show search box on dropdown click', () => {
+ expect(mockAxios.history.get).toHaveLength(1);
+ expect(findSearchBox().exists()).toBe(true);
+ });
- it('should render the correct artifact name and path', () => {
- createComponent({ mockData: { artifacts } });
+ it('should focus the search box when opened with artifacts', () => {
+ findDropdown().vm.$emit('shown');
- expect(findFirstArtifactItem().attributes('href')).toBe(artifacts[0].path);
- expect(findFirstArtifactItem().text()).toBe(artifacts[0].name);
- });
+ expect(focusInputMock).toHaveBeenCalled();
+ });
- it('should render empty message and no search box when no artifacts are found', () => {
- createComponent({ mockData: { artifacts: [] } });
+ it('should render all the provided artifacts when search query is empty', () => {
+ findSearchBox().vm.$emit('input', '');
- expect(findEmptyMessage().exists()).toBe(true);
- expect(findSearchBox().exists()).toBe(false);
- });
+ expect(findAllArtifactItems()).toHaveLength(artifacts.length);
+ expect(findEmptyMessage().exists()).toBe(false);
+ });
- describe('while loading artifacts', () => {
- it('should render a loading spinner and no empty message', () => {
- createComponent({ mockData: { isLoading: true, artifacts: [] } });
+ it('should render filtered artifacts when search query is not empty', async () => {
+ findSearchBox().vm.$emit('input', 'job-2');
+ await waitForPromises();
- expect(findLoadingIcon().exists()).toBe(true);
- expect(findEmptyMessage().exists()).toBe(false);
+ expect(findAllArtifactItems()).toHaveLength(1);
+ expect(findEmptyMessage().exists()).toBe(false);
+ });
+
+ it('should render the correct artifact name and path', () => {
+ expect(findFirstArtifactItem().attributes('href')).toBe(artifacts[0].path);
+ expect(findFirstArtifactItem().text()).toBe(artifacts[0].name);
+ });
+ });
+
+ describe('artifacts list is empty', () => {
+ beforeEach(() => {
+ mockAxios.onGet(endpoint).replyOnce(HTTP_STATUS_OK, { artifacts: [] });
+ });
+
+ it('should render empty message and no search box when no artifacts are found', async () => {
+ createComponent();
+
+ findDropdown().vm.$emit('show');
+ await waitForPromises();
+
+ expect(findEmptyMessage().exists()).toBe(true);
+ expect(findSearchBox().exists()).toBe(false);
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
});
});
describe('with a failing request', () => {
- it('should render an error message', async () => {
- const endpoint = artifactsEndpoint.replace(artifactsEndpointPlaceholder, pipelineId);
+ beforeEach(() => {
mockAxios.onGet(endpoint).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+ });
+
+ it('should render an error message', async () => {
createComponent();
findDropdown().vm.$emit('show');
await waitForPromises();
diff --git a/spec/frontend/vue_shared/components/ci_badge_link_spec.js b/spec/frontend/vue_shared/components/ci_badge_link_spec.js
index afb509b9fe6..8c860c9b06f 100644
--- a/spec/frontend/vue_shared/components/ci_badge_link_spec.js
+++ b/spec/frontend/vue_shared/components/ci_badge_link_spec.js
@@ -1,4 +1,4 @@
-import { GlLink } from '@gitlab/ui';
+import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
@@ -46,6 +46,13 @@ describe('CI Badge Link Component', () => {
icon: 'status_pending',
details_path: 'status/pending',
},
+ preparing: {
+ text: 'preparing',
+ label: 'preparing',
+ group: 'preparing',
+ icon: 'status_preparing',
+ details_path: 'status/preparing',
+ },
running: {
text: 'running',
label: 'running',
@@ -53,6 +60,13 @@ describe('CI Badge Link Component', () => {
icon: 'status_running',
details_path: 'status/running',
},
+ scheduled: {
+ text: 'scheduled',
+ label: 'scheduled',
+ group: 'scheduled',
+ icon: 'status_scheduled',
+ details_path: 'status/scheduled',
+ },
skipped: {
text: 'skipped',
label: 'skipped',
@@ -61,8 +75,8 @@ describe('CI Badge Link Component', () => {
details_path: 'status/skipped',
},
success_warining: {
- text: 'passed',
- label: 'passed',
+ text: 'warning',
+ label: 'passed with warnings',
group: 'success-with-warnings',
icon: 'status_warning',
details_path: 'status/warning',
@@ -77,6 +91,8 @@ describe('CI Badge Link Component', () => {
};
const findIcon = () => wrapper.findComponent(CiIcon);
+ const findBadge = () => wrapper.findComponent(GlBadge);
+ const findBadgeText = () => wrapper.find('[data-testid="ci-badge-text"');
const createComponent = (propsData) => {
wrapper = shallowMount(CiBadgeLink, { propsData });
@@ -87,22 +103,50 @@ describe('CI Badge Link Component', () => {
expect(wrapper.attributes('href')).toBe(statuses[status].details_path);
expect(wrapper.text()).toBe(statuses[status].text);
- expect(wrapper.classes()).toContain('ci-status');
- expect(wrapper.classes()).toContain(`ci-${statuses[status].group}`);
+ expect(findBadge().props('size')).toBe('md');
expect(findIcon().exists()).toBe(true);
});
+ it.each`
+ status | textColor | variant
+ ${statuses.success} | ${'gl-text-green-700'} | ${'success'}
+ ${statuses.success_warining} | ${'gl-text-orange-700'} | ${'warning'}
+ ${statuses.failed} | ${'gl-text-red-700'} | ${'danger'}
+ ${statuses.running} | ${'gl-text-blue-700'} | ${'info'}
+ ${statuses.pending} | ${'gl-text-orange-700'} | ${'warning'}
+ ${statuses.preparing} | ${'gl-text-gray-600'} | ${'muted'}
+ ${statuses.canceled} | ${'gl-text-gray-700'} | ${'neutral'}
+ ${statuses.scheduled} | ${'gl-text-gray-600'} | ${'muted'}
+ ${statuses.skipped} | ${'gl-text-gray-600'} | ${'muted'}
+ ${statuses.manual} | ${'gl-text-gray-700'} | ${'neutral'}
+ ${statuses.created} | ${'gl-text-gray-600'} | ${'muted'}
+ `(
+ 'should contain correct badge class and variant for status: $status.text',
+ ({ status, textColor, variant }) => {
+ createComponent({ status });
+
+ expect(findBadgeText().classes()).toContain(textColor);
+ expect(findBadge().props('variant')).toBe(variant);
+ },
+ );
+
it('should not render label', () => {
createComponent({ status: statuses.canceled, showText: false });
expect(wrapper.text()).toBe('');
});
- it('should emit ciStatusBadgeClick event', async () => {
+ it('should emit ciStatusBadgeClick event', () => {
createComponent({ status: statuses.success });
- await wrapper.findComponent(GlLink).vm.$emit('click');
+ findBadge().vm.$emit('click');
expect(wrapper.emitted('ciStatusBadgeClick')).toEqual([[]]);
});
+
+ it('should render dynamic badge size', () => {
+ createComponent({ status: statuses.success, badgeSize: 'lg' });
+
+ expect(findBadge().props('size')).toBe('lg');
+ });
});
diff --git a/spec/graphql/types/ci/runner_manager_type_spec.rb b/spec/graphql/types/ci/runner_manager_type_spec.rb
index 240e1edbf78..6f73171cd8f 100644
--- a/spec/graphql/types/ci/runner_manager_type_spec.rb
+++ b/spec/graphql/types/ci/runner_manager_type_spec.rb
@@ -13,6 +13,6 @@ RSpec.describe GitlabSchema.types['CiRunnerManager'], feature_category: :runner_
runner status system_id version
]
- expect(described_class).to have_graphql_fields(*expected_fields)
+ expect(described_class).to include_graphql_fields(*expected_fields)
end
end
diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb
index 21eca97331e..f71f3d47452 100644
--- a/spec/lib/gitlab/ci/status/build/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb
@@ -370,7 +370,7 @@ RSpec.describe Gitlab::Ci::Status::Build::Factory do
end
it 'fabricates status with correct details' do
- expect(status.text).to eq s_('CiStatusText|delayed')
+ expect(status.text).to eq s_('CiStatusText|scheduled')
expect(status.group).to eq 'scheduled'
expect(status.icon).to eq 'status_scheduled'
expect(status.favicon).to eq 'favicon_status_scheduled'
diff --git a/spec/lib/gitlab/ci/status/scheduled_spec.rb b/spec/lib/gitlab/ci/status/scheduled_spec.rb
index 8a923faf3f9..df72455d3c1 100644
--- a/spec/lib/gitlab/ci/status/scheduled_spec.rb
+++ b/spec/lib/gitlab/ci/status/scheduled_spec.rb
@@ -2,17 +2,17 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Status::Scheduled do
+RSpec.describe Gitlab::Ci::Status::Scheduled, feature_category: :continuous_integration do
subject do
described_class.new(double('subject'), double('user'))
end
describe '#text' do
- it { expect(subject.text).to eq 'delayed' }
+ it { expect(subject.text).to eq 'scheduled' }
end
describe '#label' do
- it { expect(subject.label).to eq 'delayed' }
+ it { expect(subject.label).to eq 'scheduled' }
end
describe '#icon' do
diff --git a/spec/lib/gitlab/ci/status/success_warning_spec.rb b/spec/lib/gitlab/ci/status/success_warning_spec.rb
index 86b826ad272..1725f90a0cf 100644
--- a/spec/lib/gitlab/ci/status/success_warning_spec.rb
+++ b/spec/lib/gitlab/ci/status/success_warning_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Status::SuccessWarning do
+RSpec.describe Gitlab::Ci::Status::SuccessWarning, feature_category: :continuous_integration do
let(:status) { double('status') }
subject do
@@ -10,7 +10,7 @@ RSpec.describe Gitlab::Ci::Status::SuccessWarning do
end
describe '#test' do
- it { expect(subject.text).to eq 'passed' }
+ it { expect(subject.text).to eq 'warning' }
end
describe '#label' do
diff --git a/spec/models/integrations/chat_message/push_message_spec.rb b/spec/models/integrations/chat_message/push_message_spec.rb
index 8d2d0f9f9a8..5c9c5c64d7e 100644
--- a/spec/models/integrations/chat_message/push_message_spec.rb
+++ b/spec/models/integrations/chat_message/push_message_spec.rb
@@ -38,8 +38,8 @@ RSpec.describe Integrations::ChatMessage::PushMessage do
context 'without markdown' do
it 'returns a message regarding pushes' do
expect(subject.pretext).to eq(
- 'test.user pushed to branch <http://url.com/commits/master|master> of '\
- '<http://url.com|project_name> (<http://url.com/compare/before...after|Compare changes>)')
+ 'test.user pushed to branch <http://url.com/-/commits/master|master> of '\
+ '<http://url.com|project_name> (<http://url.com/-/compare/before...after|Compare changes>)')
expect(subject.attachments).to eq([{
text: "<http://url1.com|abcdefgh>: message1 - author1\n\n"\
"<http://url2.com|12345678>: message2 w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w ... - author2",
@@ -55,13 +55,13 @@ RSpec.describe Integrations::ChatMessage::PushMessage do
it 'returns a message regarding pushes' do
expect(subject.pretext).to eq(
- 'test.user pushed to branch [master](http://url.com/commits/master) of [project_name](http://url.com) ([Compare changes](http://url.com/compare/before...after))')
+ 'test.user pushed to branch [master](http://url.com/-/commits/master) of [project_name](http://url.com) ([Compare changes](http://url.com/-/compare/before...after))')
expect(subject.attachments).to eq(
"[abcdefgh](http://url1.com): message1 - author1\n\n[12345678](http://url2.com): message2 w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w ... - author2")
expect(subject.activity).to eq(
- title: 'test.user pushed to branch [master](http://url.com/commits/master)',
+ title: 'test.user pushed to branch [master](http://url.com/-/commits/master)',
subtitle: 'in [project_name](http://url.com)',
- text: '[Compare changes](http://url.com/compare/before...after)',
+ text: '[Compare changes](http://url.com/-/compare/before...after)',
image: 'http://someavatar.com'
)
end
@@ -102,7 +102,7 @@ RSpec.describe Integrations::ChatMessage::PushMessage do
expect(subject.activity).to eq(
title: 'test.user pushed new tag [new_tag](http://url.com/-/tags/new_tag)',
subtitle: 'in [project_name](http://url.com)',
- text: '[Compare changes](http://url.com/compare/0000000000000000000000000000000000000000...after)',
+ text: '[Compare changes](http://url.com/-/compare/0000000000000000000000000000000000000000...after)',
image: 'http://someavatar.com'
)
end
@@ -143,7 +143,7 @@ RSpec.describe Integrations::ChatMessage::PushMessage do
expect(subject.activity).to eq(
title: 'test.user removed tag new_tag',
subtitle: 'in [project_name](http://url.com)',
- text: '[Compare changes](http://url.com/compare/before...0000000000000000000000000000000000000000)',
+ text: '[Compare changes](http://url.com/-/compare/before...0000000000000000000000000000000000000000)',
image: 'http://someavatar.com'
)
end
@@ -158,7 +158,7 @@ RSpec.describe Integrations::ChatMessage::PushMessage do
context 'without markdown' do
it 'returns a message regarding a new branch' do
expect(subject.pretext).to eq(
- 'test.user pushed new branch <http://url.com/commits/master|master> to '\
+ 'test.user pushed new branch <http://url.com/-/commits/master|master> to '\
'<http://url.com|project_name>')
expect(subject.attachments).to be_empty
end
@@ -171,12 +171,12 @@ RSpec.describe Integrations::ChatMessage::PushMessage do
it 'returns a message regarding a new branch' do
expect(subject.pretext).to eq(
- 'test.user pushed new branch [master](http://url.com/commits/master) to [project_name](http://url.com)')
+ 'test.user pushed new branch [master](http://url.com/-/commits/master) to [project_name](http://url.com)')
expect(subject.attachments).to be_empty
expect(subject.activity).to eq(
- title: 'test.user pushed new branch [master](http://url.com/commits/master)',
+ title: 'test.user pushed new branch [master](http://url.com/-/commits/master)',
subtitle: 'in [project_name](http://url.com)',
- text: '[Compare changes](http://url.com/compare/0000000000000000000000000000000000000000...after)',
+ text: '[Compare changes](http://url.com/-/compare/0000000000000000000000000000000000000000...after)',
image: 'http://someavatar.com'
)
end
@@ -208,7 +208,7 @@ RSpec.describe Integrations::ChatMessage::PushMessage do
expect(subject.activity).to eq(
title: 'test.user removed branch master',
subtitle: 'in [project_name](http://url.com)',
- text: '[Compare changes](http://url.com/compare/before...0000000000000000000000000000000000000000)',
+ text: '[Compare changes](http://url.com/-/compare/before...0000000000000000000000000000000000000000)',
image: 'http://someavatar.com'
)
end
diff --git a/spec/models/integrations/discord_spec.rb b/spec/models/integrations/discord_spec.rb
index 138a56d1872..42ea4a287fe 100644
--- a/spec/models/integrations/discord_spec.rb
+++ b/spec/models/integrations/discord_spec.rb
@@ -52,7 +52,7 @@ RSpec.describe Integrations::Discord do
subject.execute(sample_data)
expect(builder.to_json_hash[:embeds].first).to include(
- description: start_with("#{user.name} pushed to branch [master](http://localhost/#{project.namespace.path}/#{project.path}/commits/master) of"),
+ description: start_with("#{user.name} pushed to branch [master](http://localhost/#{project.namespace.path}/#{project.path}/-/commits/master) of"),
author: hash_including(
icon_url: start_with('https://www.gravatar.com/avatar/'),
name: user.name
diff --git a/spec/presenters/ci/pipeline_presenter_spec.rb b/spec/presenters/ci/pipeline_presenter_spec.rb
index 86e4bb703dc..cc68cdff7c1 100644
--- a/spec/presenters/ci/pipeline_presenter_spec.rb
+++ b/spec/presenters/ci/pipeline_presenter_spec.rb
@@ -217,7 +217,7 @@ RSpec.describe Ci::PipelinePresenter do
let(:pipeline) { merge_request.all_pipelines.last }
it 'returns a correct ref text' do
- is_expected.to eq("For merge request <a class=\"mr-iid\" href=\"#{project_merge_request_path(merge_request.project, merge_request)}\">#{merge_request.to_reference}</a> " \
+ is_expected.to eq("Related merge request <a class=\"mr-iid\" href=\"#{project_merge_request_path(merge_request.project, merge_request)}\">#{merge_request.to_reference}</a> " \
"to merge <a class=\"ref-name gl-link gl-bg-blue-50 gl-rounded-base gl-px-2\" href=\"#{project_commits_path(merge_request.source_project, merge_request.source_branch)}\">#{merge_request.source_branch}</a>")
end
end
@@ -227,7 +227,7 @@ RSpec.describe Ci::PipelinePresenter do
let(:pipeline) { merge_request.all_pipelines.last }
it 'returns a correct ref text' do
- is_expected.to eq("For merge request <a class=\"mr-iid\" href=\"#{project_merge_request_path(merge_request.project, merge_request)}\">#{merge_request.to_reference}</a> " \
+ is_expected.to eq("Related merge request <a class=\"mr-iid\" href=\"#{project_merge_request_path(merge_request.project, merge_request)}\">#{merge_request.to_reference}</a> " \
"to merge <a class=\"ref-name gl-link gl-bg-blue-50 gl-rounded-base gl-px-2\" href=\"#{project_commits_path(merge_request.source_project, merge_request.source_branch)}\">#{merge_request.source_branch}</a> " \
"into <a class=\"ref-name gl-link gl-bg-blue-50 gl-rounded-base gl-px-2\" href=\"#{project_commits_path(merge_request.target_project, merge_request.target_branch)}\">#{merge_request.target_branch}</a>")
end
diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb
index f237516021d..756fcd8b7cd 100644
--- a/spec/requests/api/graphql/ci/jobs_spec.rb
+++ b/spec/requests/api/graphql/ci/jobs_spec.rb
@@ -433,8 +433,6 @@ RSpec.describe 'Query.project.pipeline', feature_category: :continuous_integrati
end
it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do
- admin2 = create(:admin)
-
control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
post_graphql(query, current_user: admin)
end
@@ -442,7 +440,7 @@ RSpec.describe 'Query.project.pipeline', feature_category: :continuous_integrati
runner_manager2 = create(:ci_runner_machine)
create(:ci_build, pipeline: pipeline, name: 'my test job2', runner_manager: runner_manager2)
- expect { post_graphql(query, current_user: admin2) }.not_to exceed_all_query_limit(control)
+ expect { post_graphql(query, current_user: admin) }.not_to exceed_all_query_limit(control)
end
end