diff --git a/.gitlab/ci/qa.gitlab-ci.yml b/.gitlab/ci/qa.gitlab-ci.yml
index 0dcb3083700..36b8b6f6d6c 100644
--- a/.gitlab/ci/qa.gitlab-ci.yml
+++ b/.gitlab/ci/qa.gitlab-ci.yml
@@ -239,6 +239,7 @@ e2e:test-product-analytics:
PIPELINE_NAME: E2E Product Analytics
GDK_IMAGE: "${CI_REGISTRY_IMAGE}/gitlab-qa-gdk:${CI_COMMIT_SHA}"
GITLAB_QA_IMAGE: "${CI_REGISTRY_IMAGE}/gitlab-ee-qa:${CI_COMMIT_SHA}"
+ UPSTREAM_COMMIT_SHA: $CI_COMMIT_SHA
needs:
- build-gdk-image
- build-qa-image
diff --git a/.gitlab/ci/release-environments/security.gitlab-ci.yml b/.gitlab/ci/release-environments/security.gitlab-ci.yml
index 100ecffaf44..01c5b64e835 100644
--- a/.gitlab/ci/release-environments/security.gitlab-ci.yml
+++ b/.gitlab/ci/release-environments/security.gitlab-ci.yml
@@ -6,7 +6,7 @@ include:
inputs:
cng_path: 'charts/components/images'
- project: 'gitlab-org/quality/pipeline-common'
- ref: '8.22.0'
+ ref: '9.0.0'
file: ci/base.gitlab-ci.yml
stages:
diff --git a/GITLAB_KAS_VERSION b/GITLAB_KAS_VERSION
index dad4ea2e43f..edff6c0b280 100644
--- a/GITLAB_KAS_VERSION
+++ b/GITLAB_KAS_VERSION
@@ -1 +1 @@
-a2ca345cd681ef39094623d8f4b6ed65996de57d
+a3988141dd517bc75af2775a168033df1bed742c
diff --git a/app/assets/javascripts/boards/components/board_drawer_wrapper.vue b/app/assets/javascripts/boards/components/board_drawer_wrapper.vue
index 0ac407a53da..71b2c970a47 100644
--- a/app/assets/javascripts/boards/components/board_drawer_wrapper.vue
+++ b/app/assets/javascripts/boards/components/board_drawer_wrapper.vue
@@ -143,7 +143,6 @@ export default {
onAttributeUpdated: this.onAttributeUpdated,
onIssuableDeleted: this.refetchActiveIssuableLists,
onStateUpdated: this.onStateUpdated,
- modalWorkItemFullPath: this.modalWorkItemFullPath,
});
},
};
diff --git a/app/assets/javascripts/organizations/shared/graphql/fragments/base_project.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/base_project.fragment.graphql
similarity index 100%
rename from app/assets/javascripts/organizations/shared/graphql/fragments/base_project.fragment.graphql
rename to app/assets/javascripts/graphql_shared/fragments/base_project.fragment.graphql
diff --git a/app/assets/javascripts/organizations/shared/graphql/fragments/project.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/project.fragment.graphql
similarity index 100%
rename from app/assets/javascripts/organizations/shared/graphql/fragments/project.fragment.graphql
rename to app/assets/javascripts/graphql_shared/fragments/project.fragment.graphql
diff --git a/app/assets/javascripts/organizations/shared/components/projects_view.vue b/app/assets/javascripts/organizations/shared/components/projects_view.vue
index c71b9bd12cb..b9b1f801f94 100644
--- a/app/assets/javascripts/organizations/shared/components/projects_view.vue
+++ b/app/assets/javascripts/organizations/shared/components/projects_view.vue
@@ -3,6 +3,7 @@ import { GlLoadingIcon, GlKeysetPagination } from '@gitlab/ui';
import projectsEmptyStateSvgPath from '@gitlab/svgs/dist/illustrations/empty-state/empty-projects-md.svg?url';
import { s__, __ } from '~/locale';
import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue';
+import { formatGraphQLProjects } from '~/vue_shared/components/projects_list/utils';
import { ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
import { DEFAULT_PER_PAGE } from '~/api';
import { deleteProject } from '~/rest_api';
@@ -10,7 +11,6 @@ import { createAlert } from '~/alert';
import {
renderDeleteSuccessToast,
deleteParams,
- formatProjects,
timestampType,
} from 'ee_else_ce/organizations/shared/utils';
import { SORT_ITEM_NAME, SORT_DIRECTION_ASC } from '../constants';
@@ -109,7 +109,7 @@ export default {
},
}) {
return {
- nodes: formatProjects(nodes),
+ nodes: formatGraphQLProjects(nodes),
pageInfo,
};
},
diff --git a/app/assets/javascripts/organizations/shared/graphql/queries/projects.query.graphql b/app/assets/javascripts/organizations/shared/graphql/queries/projects.query.graphql
index 886ca7fdc87..090ed59ac5b 100644
--- a/app/assets/javascripts/organizations/shared/graphql/queries/projects.query.graphql
+++ b/app/assets/javascripts/organizations/shared/graphql/queries/projects.query.graphql
@@ -1,5 +1,5 @@
#import "~/graphql_shared/fragments/page_info.fragment.graphql"
-#import "ee_else_ce/organizations/shared/graphql/fragments/project.fragment.graphql"
+#import "ee_else_ce/graphql_shared/fragments/project.fragment.graphql"
query getOrganizationProjects(
$id: OrganizationsOrganizationID!
diff --git a/app/assets/javascripts/organizations/shared/utils.js b/app/assets/javascripts/organizations/shared/utils.js
index ebfb545fc61..38eabe193a6 100644
--- a/app/assets/javascripts/organizations/shared/utils.js
+++ b/app/assets/javascripts/organizations/shared/utils.js
@@ -13,20 +13,6 @@ import {
QUERY_PARAM_START_CURSOR,
} from './constants';
-const availableProjectActions = (userPermissions) => {
- const baseActions = [];
-
- if (userPermissions.viewEditPage) {
- baseActions.push(ACTION_EDIT);
- }
-
- if (userPermissions.removeProject) {
- baseActions.push(ACTION_DELETE);
- }
-
- return baseActions;
-};
-
const availableGroupActions = (userPermissions) => {
const baseActions = [];
@@ -41,37 +27,6 @@ const availableGroupActions = (userPermissions) => {
return baseActions;
};
-export const formatProjects = (projects) =>
- projects.map(
- ({
- id,
- nameWithNamespace,
- mergeRequestsAccessLevel,
- issuesAccessLevel,
- forkingAccessLevel,
- webUrl,
- userPermissions,
- maxAccessLevel: accessLevel,
- organizationEditPath: editPath,
- ...project
- }) => ({
- ...project,
- id: getIdFromGraphQLId(id),
- name: nameWithNamespace,
- mergeRequestsAccessLevel: mergeRequestsAccessLevel.stringValue,
- issuesAccessLevel: issuesAccessLevel.stringValue,
- forkingAccessLevel: forkingAccessLevel.stringValue,
- webUrl,
- isForked: false,
- accessLevel,
- editPath,
- availableActions: availableProjectActions(userPermissions),
- actionLoadingStates: {
- [ACTION_DELETE]: false,
- },
- }),
- );
-
export const formatGroups = (groups) =>
groups.map(
({
diff --git a/app/assets/javascripts/projects/your_work/components/app.vue b/app/assets/javascripts/projects/your_work/components/app.vue
index 9b5fc0317d2..73403688999 100644
--- a/app/assets/javascripts/projects/your_work/components/app.vue
+++ b/app/assets/javascripts/projects/your_work/components/app.vue
@@ -1,23 +1,25 @@
+
+
+
+
+
diff --git a/app/assets/javascripts/projects/your_work/constants.js b/app/assets/javascripts/projects/your_work/constants.js
index c21f76459fb..18edfb6a5e2 100644
--- a/app/assets/javascripts/projects/your_work/constants.js
+++ b/app/assets/javascripts/projects/your_work/constants.js
@@ -1,8 +1,11 @@
import { __ } from '~/locale';
+import contributedProjectsQuery from './graphql/queries/contributed_projects.query.graphql';
export const CONTRIBUTED_TAB = {
text: __('Contributed'),
value: 'contributed',
+ query: contributedProjectsQuery,
+ queryPath: 'currentUser.contributedProjects',
};
export const STARRED_TAB = {
diff --git a/app/assets/javascripts/projects/your_work/graphql/queries/contributed_projects.query.graphql b/app/assets/javascripts/projects/your_work/graphql/queries/contributed_projects.query.graphql
new file mode 100644
index 00000000000..29588e0948a
--- /dev/null
+++ b/app/assets/javascripts/projects/your_work/graphql/queries/contributed_projects.query.graphql
@@ -0,0 +1,12 @@
+#import "ee_else_ce/graphql_shared/fragments/project.fragment.graphql"
+
+query getContributedProjects {
+ currentUser {
+ id
+ contributedProjects {
+ nodes {
+ ...Project
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/projects/your_work/index.js b/app/assets/javascripts/projects/your_work/index.js
index 484e73725dd..8222975f7e2 100644
--- a/app/assets/javascripts/projects/your_work/index.js
+++ b/app/assets/javascripts/projects/your_work/index.js
@@ -1,5 +1,7 @@
import Vue from 'vue';
import VueRouter from 'vue-router';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
import routes from './routes';
import YourWorkProjectsApp from './components/app.vue';
@@ -20,9 +22,14 @@ export const initYourWorkProjects = () => {
if (!el) return false;
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+
return new Vue({
el,
router: createRouter(),
+ apolloProvider,
name: 'YourWorkProjectsRoot',
render(createElement) {
return createElement(YourWorkProjectsApp);
diff --git a/app/assets/javascripts/vue_shared/components/projects_list/utils.js b/app/assets/javascripts/vue_shared/components/projects_list/utils.js
new file mode 100644
index 00000000000..a3d0885c0e7
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/projects_list/utils.js
@@ -0,0 +1,47 @@
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
+
+const availableGraphQLProjectActions = (userPermissions) => {
+ const baseActions = [];
+
+ if (userPermissions.viewEditPage) {
+ baseActions.push(ACTION_EDIT);
+ }
+
+ if (userPermissions.removeProject) {
+ baseActions.push(ACTION_DELETE);
+ }
+
+ return baseActions;
+};
+
+export const formatGraphQLProjects = (projects) =>
+ projects.map(
+ ({
+ id,
+ nameWithNamespace,
+ mergeRequestsAccessLevel,
+ issuesAccessLevel,
+ forkingAccessLevel,
+ webUrl,
+ userPermissions,
+ maxAccessLevel: accessLevel,
+ organizationEditPath: editPath,
+ ...project
+ }) => ({
+ ...project,
+ id: getIdFromGraphQLId(id),
+ name: nameWithNamespace,
+ mergeRequestsAccessLevel: mergeRequestsAccessLevel.stringValue,
+ issuesAccessLevel: issuesAccessLevel.stringValue,
+ forkingAccessLevel: forkingAccessLevel.stringValue,
+ webUrl,
+ isForked: false,
+ accessLevel,
+ editPath,
+ availableActions: availableGraphQLProjectActions(userPermissions),
+ actionLoadingStates: {
+ [ACTION_DELETE]: false,
+ },
+ }),
+ );
diff --git a/app/assets/stylesheets/page_bundles/admin/application_settings_metrics_and_profiling.scss b/app/assets/stylesheets/page_bundles/admin/application_settings_metrics_and_profiling.scss
deleted file mode 100644
index db81cc7fdd4..00000000000
--- a/app/assets/stylesheets/page_bundles/admin/application_settings_metrics_and_profiling.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-.service-data-payload-container {
- max-height: 400px;
-}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index d051db96f31..e4b7f8436c5 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -252,8 +252,7 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
}
}
- .note-created-ago,
- .note-updated-at {
+ .note-created-ago {
white-space: normal;
}
@@ -301,11 +300,6 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
.timeline-entry-inner {
opacity: 0.5;
}
-
- .dummy-avatar {
- background-color: $gray-100;
- border: 1px solid darken($gray-100, 25%);
- }
}
.editing-spinner {
@@ -949,10 +943,6 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
}
}
-.note-role {
- margin: 0 8px;
-}
-
/**
* Line note button on the side of diffs
*/
@@ -1032,11 +1022,6 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
color: $note-disabled-comment-color;
padding: $gl-padding-8 0;
- &.discussion-locked {
- border: 0;
- background-color: $white;
- }
-
a:not(.learn-more) {
color: $blue-600;
}
diff --git a/app/views/admin/application_settings/metrics_and_profiling.html.haml b/app/views/admin/application_settings/metrics_and_profiling.html.haml
index 5dcb4d8a243..6b808325eba 100644
--- a/app/views/admin/application_settings/metrics_and_profiling.html.haml
+++ b/app/views/admin/application_settings/metrics_and_profiling.html.haml
@@ -1,5 +1,3 @@
-- add_page_specific_style 'page_bundles/admin/application_settings_metrics_and_profiling'
-
- breadcrumb_title _("Metrics and profiling")
- page_title _("Metrics and profiling")
- add_page_specific_style 'page_bundles/settings'
diff --git a/config/application.rb b/config/application.rb
index 27ce2d6cfa6..a6eb423d158 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -289,7 +289,6 @@ module Gitlab
config.assets.precompile << "mailers/notify.css"
config.assets.precompile << "mailers/notify_enhanced.css"
config.assets.precompile << "page_bundles/_mixins_and_variables_and_functions.css"
- config.assets.precompile << "page_bundles/admin/application_settings_metrics_and_profiling.css"
config.assets.precompile << "page_bundles/admin/elasticsearch_form.css"
config.assets.precompile << "page_bundles/admin/geo_sites.css"
config.assets.precompile << "page_bundles/admin/geo_replicable.css"
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 1765b8f9f7d..923cd65fefc 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -481,8 +481,6 @@
- 2
- - mailers
- 2
-- - members_destroy
- - 1
- - members_destroyer_clean_up_group_protected_branch_rules
- 1
- - members_expiring_email_notification
diff --git a/data/deprecations/17-4-secure-container-registries.yml b/data/deprecations/17-4-secure-container-registries.yml
new file mode 100644
index 00000000000..d946a3c9e48
--- /dev/null
+++ b/data/deprecations/17-4-secure-container-registries.yml
@@ -0,0 +1,28 @@
+- title: "Public use of Secure container registries is deprecated"
+ removal_milestone: "18.0"
+ announcement_milestone: "17.4"
+ breaking_change: true
+ reporter: thiagocsf
+ stage: secure
+ issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/470641
+ impact: low
+ scope: instance
+ resolution_role: Developer
+ manual_task: true
+ body: | # (required) Don't change this line.
+ Container registries under `registry.gitlab.com/gitlab-org/security-products/`
+ are no longer accessible in GitLab 18.0. [Since GitLab 14.8](https://docs.gitlab.com/ee/update/deprecations.html#secure-and-protect-analyzer-images-published-in-new-location)
+ the correct location is under `registry.gitlab.com/security-products` (note the absence of
+ `gitlab-org` in the address).
+
+ This change improves the security of the release process for GitLab [vulnerability scanners](https://docs.gitlab.com/ee/user/application_security/#vulnerability-scanner-maintenance).
+
+ Users are advised to use the equivalent registry under `registry.gitlab.com/security-products/`,
+ which is the canonical location for GitLab security scanner images. The relevant GitLab CI
+ templates already use this location, so no changes should be necessary for users that use the
+ unmodified templates.
+
+ Offline deployments should review the [specific scanner instructions](https://docs.gitlab.com/ee/user/application_security/offline_deployments/#specific-scanner-instructions)
+ to ensure the correct locations are being used to mirror the required scanner images.
+ tiers: [Free, Premium, Ultimate]
+ documentation_url: https://docs.gitlab.com/ee/user/application_security/index.html#vulnerability-scanner-maintenance
diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md
index 8848722ab77..089a911428f 100644
--- a/doc/administration/gitaly/index.md
+++ b/doc/administration/gitaly/index.md
@@ -128,7 +128,7 @@ Accessing Git repositories directly is done at your own risk and is not supporte
The following shows GitLab set up to use direct access to Gitaly:
-
+
In this example:
@@ -230,7 +230,7 @@ customers.
The following shows GitLab set up to access `storage-1`, a virtual storage provided by Gitaly
Cluster:
-
+
In this example:
@@ -456,7 +456,7 @@ Gitaly Cluster consists of multiple components:
Praefect is a router and transaction manager for Gitaly, and a required
component for running a Gitaly Cluster.
-
+
For more information, see [Gitaly High Availability (HA) Design](https://gitlab.com/gitlab-org/gitaly/-/blob/master/doc/design_ha.md).
diff --git a/doc/ci/variables/index.md b/doc/ci/variables/index.md
index ae624d534a8..dfd47df2d10 100644
--- a/doc/ci/variables/index.md
+++ b/doc/ci/variables/index.md
@@ -564,7 +564,7 @@ test-job1:
script:
- echo "$BUILD_VERSION" # Output is: 'v1.0.0'
dependencies:
- - build
+ - build-job1
test-job2:
stage: test
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index 602982065ac..4933512ecb3 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -83,7 +83,7 @@ We make the following assumption with regards to automatically being considered
- Team members working on a specific feature (for example, search) are considered domain experts for that feature.
We default to assigning reviews to team members with domain expertise for code reviews. UX reviews default to the recommended reviewer from the Review Roulette. Due to designer capacity limits, areas not supported by a Product Designer will no longer require a UX review unless it is a community contribution.
-When a suitable [domain expert](#domain-experts) isn't available, you can choose any team member to review the MR, or follow the [Reviewer roulette](#reviewer-roulette) recommendation (see above for UX reviews).
+When a suitable [domain expert](#domain-experts) isn't available, you can choose any team member to review the MR, or follow the [Reviewer roulette](#reviewer-roulette) recommendation (see above for UX reviews). Double check if the person is OOO before assigning them.
To find a domain expert:
@@ -378,7 +378,7 @@ This saves reviewers time and helps authors catch mistakes earlier.
Reviewers are responsible for reviewing the specifics of the chosen solution.
-If you are unavailable to review an assigned merge request:
+If you are unavailable to review an assigned merge request within the [Review-response SLO](https://handbook.gitlab.com/handbook/engineering/workflow/code-review/#review-response-slo):
1. Inform the author that you're not available.
1. Use the [GitLab Review Workload Dashboard](https://gitlab-org.gitlab.io/gitlab-roulette/) to select a new reviewer.
diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md
index b692a56a6d0..0009fcd7b1b 100644
--- a/doc/update/deprecations.md
+++ b/doc/update/deprecations.md
@@ -458,6 +458,35 @@ The project page will be removed entirely from the group settings in 18.0.
+### Public use of Secure container registries is deprecated
+
+
+
+- Announced in GitLab 17.4
+- Removal in GitLab 18.0 ([breaking change](https://docs.gitlab.com/ee/update/terminology.html#breaking-change))
+- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/470641).
+
+
+
+Container registries under `registry.gitlab.com/gitlab-org/security-products/`
+are no longer accessible in GitLab 18.0. [Since GitLab 14.8](https://docs.gitlab.com/ee/update/deprecations.html#secure-and-protect-analyzer-images-published-in-new-location)
+the correct location is under `registry.gitlab.com/security-products` (note the absence of
+`gitlab-org` in the address).
+
+This change improves the security of the release process for GitLab [vulnerability scanners](https://docs.gitlab.com/ee/user/application_security/#vulnerability-scanner-maintenance).
+
+Users are advised to use the equivalent registry under `registry.gitlab.com/security-products/`,
+which is the canonical location for GitLab security scanner images. The relevant GitLab CI
+templates already use this location, so no changes should be necessary for users that use the
+unmodified templates.
+
+Offline deployments should review the [specific scanner instructions](https://docs.gitlab.com/ee/user/application_security/offline_deployments/#specific-scanner-instructions)
+to ensure the correct locations are being used to mirror the required scanner images.
+
+
+
+
+
### Rate limits for common User, Project, and Group API endpoints
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 54b6dec1ff1..3ce4f06045e 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3095,9 +3095,6 @@ msgstr ""
msgid "Active project access tokens"
msgstr ""
-msgid "Active tab: %{tab}"
-msgstr ""
-
msgid "Activity"
msgstr ""
@@ -5810,6 +5807,9 @@ msgstr ""
msgid "An error occurred fetching the public deploy keys. Please try again."
msgstr ""
+msgid "An error occurred loading the projects. Please refresh the page to try again."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -28133,12 +28133,18 @@ msgstr ""
msgid "InProductMarketing|%{upper_start}Free 30-day trial%{upper_end} %{lower_start}GitLab Ultimate%{lower_end}"
msgstr ""
+msgid "InProductMarketing|%{upper_start}Free 60-day trial%{upper_end} %{lower_start}GitLab Ultimate & GitLab Duo Enterprise%{lower_end} %{last_start}Sign up for a free trial of the most comprehensive AI-powered DevSecOps Platform%{last_end}"
+msgstr ""
+
msgid "InProductMarketing|Accelerate your digital transformation"
msgstr ""
msgid "InProductMarketing|Blog"
msgstr ""
+msgid "InProductMarketing|Boost efficiency and collaboration"
+msgstr ""
+
msgid "InProductMarketing|Build in security"
msgstr ""
@@ -28148,6 +28154,9 @@ msgstr ""
msgid "InProductMarketing|Deliver software faster"
msgstr ""
+msgid "InProductMarketing|End-to-end security and compliance"
+msgstr ""
+
msgid "InProductMarketing|Ensure compliance"
msgstr ""
@@ -28157,6 +28166,9 @@ msgstr ""
msgid "InProductMarketing|Free guest users"
msgstr ""
+msgid "InProductMarketing|GitLab Duo Enterprise: AI across the software development lifecycle"
+msgstr ""
+
msgid "InProductMarketing|If you don't want to receive marketing emails directly from GitLab, %{marketing_preference_link}."
msgstr ""
@@ -28175,6 +28187,12 @@ msgstr ""
msgid "InProductMarketing|No credit card required."
msgstr ""
+msgid "InProductMarketing|One platform for Dev, Sec, and Ops teams"
+msgstr ""
+
+msgid "InProductMarketing|Ship secure software faster"
+msgstr ""
+
msgid "InProductMarketing|Start a Self-Managed trial"
msgstr ""
diff --git a/qa/Gemfile b/qa/Gemfile
index 810380958e3..8791f8d396a 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -24,7 +24,7 @@ gem 'rainbow', '~> 3.1.1'
gem 'rspec-parameterized', '~> 1.0.2'
gem 'octokit', '~> 9.1.0', require: false
gem "faraday-retry", "~> 2.2", ">= 2.2.1"
-gem 'zeitwerk', '~> 2.6', '>= 2.6.17'
+gem 'zeitwerk', '~> 2.6', '>= 2.6.18'
gem 'influxdb-client', '~> 3.1'
gem 'terminal-table', '~> 3.0.2', require: false
gem 'slack-notifier', '~> 2.4', require: false
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index 14f8ba8300a..852d7dd29b9 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -358,7 +358,7 @@ GEM
wisper (2.0.1)
xpath (3.2.0)
nokogiri (~> 1.8)
- zeitwerk (2.6.17)
+ zeitwerk (2.6.18)
PLATFORMS
ruby
@@ -400,7 +400,7 @@ DEPENDENCIES
slack-notifier (~> 2.4)
terminal-table (~> 3.0.2)
warning (~> 1.4)
- zeitwerk (~> 2.6, >= 2.6.17)
+ zeitwerk (~> 2.6, >= 2.6.18)
BUNDLED WITH
2.5.11
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
index 0af3199fd32..c5758e5d743 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe 'Dashboard Projects', :js, feature_category: :groups_and_projects
visit dashboard_projects_path
expect(page).to have_content('Projects')
- expect(page).to have_content('Active tab: Contributed')
+ expect(page).to have_selector('a[aria-selected="true"]', text: 'Contributed')
end
end
diff --git a/spec/frontend/fixtures/projects.rb b/spec/frontend/fixtures/projects.rb
index 195ce9b6d31..115f939f39d 100644
--- a/spec/frontend/fixtures/projects.rb
+++ b/spec/frontend/fixtures/projects.rb
@@ -6,15 +6,12 @@ RSpec.describe 'Projects (JavaScript fixtures)', type: :controller, feature_cate
include ApiHelpers
include JavaScriptFixturesHelpers
- runners_token = 'runnerstoken:intabulasreferre'
-
let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) do
create(
:project,
namespace: namespace,
path: 'builds-project',
- runners_token: runners_token,
avatar: fixture_file_upload('spec/fixtures/dk.png', 'image/png')
)
end
@@ -28,15 +25,6 @@ RSpec.describe 'Projects (JavaScript fixtures)', type: :controller, feature_cate
)
end
- let(:project_variable_populated) do
- create(
- :project,
- namespace: namespace,
- path: 'builds-project2',
- runners_token: runners_token
- )
- end
-
let(:user) { project.first_owner }
render_views
@@ -70,28 +58,6 @@ RSpec.describe 'Projects (JavaScript fixtures)', type: :controller, feature_cate
end
end
- describe GraphQL::Query, type: :request do
- include GraphqlHelpers
-
- context 'for access token projects query' do
- before do
- project_variable_populated.add_maintainer(user)
- end
-
- base_input_path = 'access_tokens/graphql/queries/'
- base_output_path = 'graphql/projects/access_tokens/'
- query_name = 'get_projects.query.graphql'
-
- it "#{base_output_path}#{query_name}.json" do
- query = get_graphql_query_as_string("#{base_input_path}#{query_name}")
-
- post_graphql(query, current_user: user, variables: { search: '', first: 2 })
-
- expect_graphql_errors_to_be_empty
- end
- end
- end
-
describe 'Storage', feature_category: :consumables_cost_management do
describe GraphQL::Query, type: :request do
include GraphqlHelpers
@@ -152,3 +118,68 @@ RSpec.describe API::Projects, '(JavaScript fixtures)', type: :request, feature_c
expect(response).to have_gitlab_http_status(:bad_request)
end
end
+
+RSpec.describe GraphQL::Query, type: :request, feature_category: :groups_and_projects do
+ include JavaScriptFixturesHelpers
+ include GraphqlHelpers
+
+ runners_token = 'runnerstoken:intabulasreferre'
+
+ let_it_be(:project_variable_populated) do
+ create(
+ :project,
+ runners_token: runners_token
+ )
+ end
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:project2) { create(:project) }
+
+ let_it_be(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ context 'for access token projects query' do
+ before_all do
+ project_variable_populated.add_maintainer(user)
+ end
+
+ base_input_path = 'access_tokens/graphql/queries/'
+ base_output_path = 'graphql/projects/access_tokens/'
+ query_name = 'get_projects.query.graphql'
+
+ it "#{base_output_path}#{query_name}.json" do
+ query = get_graphql_query_as_string("#{base_input_path}#{query_name}")
+
+ post_graphql(query, current_user: user, variables: { search: '', first: 2 })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+
+ context 'for your work -> projects -> contributed' do
+ before_all do
+ project.add_maintainer(user)
+ project2.add_maintainer(user)
+ end
+
+ before do
+ create(:push_event, project: project, author: user)
+ create(:push_event, project: project2, author: user)
+ end
+
+ base_input_path = 'projects/your_work/graphql/queries/'
+ base_output_path = 'graphql/projects/your_work/'
+ query_name = 'contributed_projects.query.graphql'
+
+ it "#{base_output_path}#{query_name}.json" do
+ query = get_graphql_query_as_string("#{base_input_path}#{query_name}")
+
+ post_graphql(query, current_user: user)
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+end
diff --git a/spec/frontend/organizations/shared/components/projects_view_spec.js b/spec/frontend/organizations/shared/components/projects_view_spec.js
index fe8d0042c6b..5be5b9a3321 100644
--- a/spec/frontend/organizations/shared/components/projects_view_spec.js
+++ b/spec/frontend/organizations/shared/components/projects_view_spec.js
@@ -7,12 +7,9 @@ import { SORT_DIRECTION_ASC, SORT_ITEM_NAME } from '~/organizations/shared/const
import NewProjectButton from '~/organizations/shared/components/new_project_button.vue';
import GroupsAndProjectsEmptyState from '~/organizations/shared/components/groups_and_projects_empty_state.vue';
import projectsQuery from '~/organizations/shared/graphql/queries/projects.query.graphql';
-import {
- renderDeleteSuccessToast,
- deleteParams,
- formatProjects,
-} from 'ee_else_ce/organizations/shared/utils';
+import { renderDeleteSuccessToast, deleteParams } from 'ee_else_ce/organizations/shared/utils';
import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue';
+import { formatGraphQLProjects } from '~/vue_shared/components/projects_list/utils';
import { ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
import { TIMESTAMP_TYPE_CREATED_AT } from '~/vue_shared/components/resource_lists/constants';
import { createAlert } from '~/alert';
@@ -185,7 +182,7 @@ describe('ProjectsView', () => {
await waitForPromises();
expect(findProjectsList().props()).toMatchObject({
- projects: formatProjects(nodes),
+ projects: formatGraphQLProjects(nodes),
showProjectIcon: true,
listItemClass: defaultPropsData.listItemClass,
timestampType: TIMESTAMP_TYPE_CREATED_AT,
@@ -330,7 +327,7 @@ describe('ProjectsView', () => {
});
describe('Deleting project', () => {
- const MOCK_PROJECT = formatProjects(nodes)[0];
+ const MOCK_PROJECT = formatGraphQLProjects(nodes)[0];
describe('when API call is successful', () => {
beforeEach(async () => {
diff --git a/spec/frontend/organizations/shared/utils_spec.js b/spec/frontend/organizations/shared/utils_spec.js
index 8dd18b31300..fc2613a6e79 100644
--- a/spec/frontend/organizations/shared/utils_spec.js
+++ b/spec/frontend/organizations/shared/utils_spec.js
@@ -1,7 +1,6 @@
import organizationGroupsGraphQlResponse from 'test_fixtures/graphql/organizations/groups.query.graphql.json';
import organizationProjectsGraphQlResponse from 'test_fixtures/graphql/organizations/projects.query.graphql.json';
import {
- formatProjects,
formatGroups,
onPageChange,
deleteParams,
@@ -11,6 +10,7 @@ import {
import { SORT_CREATED_AT, SORT_UPDATED_AT, SORT_NAME } from '~/organizations/shared/constants';
import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { formatGraphQLProjects } from '~/vue_shared/components/projects_list/utils';
import toast from '~/vue_shared/plugins/global_toast';
import {
TIMESTAMP_TYPE_CREATED_AT,
@@ -35,47 +35,6 @@ const {
},
} = organizationProjectsGraphQlResponse;
-describe('formatProjects', () => {
- it('correctly formats the projects', () => {
- const [firstMockProject] = organizationProjects;
- const formattedProjects = formatProjects(organizationProjects);
- const [firstFormattedProject] = formattedProjects;
-
- expect(firstFormattedProject).toMatchObject({
- id: getIdFromGraphQLId(firstMockProject.id),
- name: firstMockProject.nameWithNamespace,
- mergeRequestsAccessLevel: firstMockProject.mergeRequestsAccessLevel.stringValue,
- issuesAccessLevel: firstMockProject.issuesAccessLevel.stringValue,
- forkingAccessLevel: firstMockProject.forkingAccessLevel.stringValue,
- accessLevel: {
- integerValue: 50,
- },
- availableActions: [ACTION_EDIT, ACTION_DELETE],
- actionLoadingStates: {
- [ACTION_DELETE]: false,
- },
- });
-
- expect(formattedProjects.length).toBe(organizationProjects.length);
- });
-
- describe('when project does not have delete permissions', () => {
- const nonDeletableFormattedProject = formatProjects(organizationProjects)[1];
-
- it('does not include delete action in `availableActions`', () => {
- expect(nonDeletableFormattedProject.availableActions).toEqual([]);
- });
- });
-
- describe('when project does not have edit permissions', () => {
- const nonEditableFormattedProject = formatProjects(organizationProjects)[1];
-
- it('does not include edit action in `availableActions`', () => {
- expect(nonEditableFormattedProject.availableActions).toEqual([]);
- });
- });
-});
-
describe('formatGroups', () => {
it('correctly formats the groups with edit and delete permissions', () => {
const [firstMockGroup] = organizationGroups;
@@ -150,7 +109,7 @@ describe('onPageChange', () => {
});
describe('renderDeleteSuccessToast', () => {
- const [MOCK_PROJECT] = formatProjects(organizationProjects);
+ const [MOCK_PROJECT] = formatGraphQLProjects(organizationProjects);
const MOCK_TYPE = 'Project';
it('calls toast correctly', () => {
diff --git a/spec/frontend/projects/your_work/components/app_spec.js b/spec/frontend/projects/your_work/components/app_spec.js
index 91934cfacea..19293559834 100644
--- a/spec/frontend/projects/your_work/components/app_spec.js
+++ b/spec/frontend/projects/your_work/components/app_spec.js
@@ -3,7 +3,9 @@ import VueRouter from 'vue-router';
import { GlTabs } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import YourWorkProjectsApp from '~/projects/your_work/components/app.vue';
+import TabView from '~/projects/your_work/components/tab_view.vue';
import { createRouter } from '~/projects/your_work';
+import { stubComponent } from 'helpers/stub_component';
import {
ROOT_ROUTE_NAME,
DASHBOARD_ROUTE_NAME,
@@ -31,13 +33,16 @@ describe('YourWorkProjectsApp', () => {
wrapper = mountExtended(YourWorkProjectsApp, {
router,
+ stubs: {
+ TabView: stubComponent(TabView),
+ },
});
};
const findPageTitle = () => wrapper.find('h1');
const findGlTabs = () => wrapper.findComponent(GlTabs);
const findAllTabTitles = () => wrapper.findAllByTestId('projects-dashboard-tab-title');
- const findActiveTab = () => wrapper.find('.tab-pane.active');
+ const findActiveTab = () => wrapper.findByRole('tab', { selected: true });
afterEach(() => {
router = null;
@@ -81,6 +86,12 @@ describe('YourWorkProjectsApp', () => {
it('initializes to the correct tab', () => {
expect(findActiveTab().text()).toContain(expectedTab.text);
});
+
+ if (expectedTab.query) {
+ it('renders `TabView` component and passes `tab` prop', () => {
+ expect(wrapper.findComponent(TabView).props('tab')).toMatchObject(expectedTab);
+ });
+ }
});
describe('onTabUpdate', () => {
diff --git a/spec/frontend/projects/your_work/components/tab_view_spec.js b/spec/frontend/projects/your_work/components/tab_view_spec.js
new file mode 100644
index 00000000000..ea0159eb340
--- /dev/null
+++ b/spec/frontend/projects/your_work/components/tab_view_spec.js
@@ -0,0 +1,83 @@
+import Vue from 'vue';
+import { GlLoadingIcon } from '@gitlab/ui';
+import VueApollo from 'vue-apollo';
+import contributedProjectsGraphQlResponse from 'test_fixtures/graphql/projects/your_work/contributed_projects.query.graphql.json';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import TabView from '~/projects/your_work/components/tab_view.vue';
+import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue';
+import contributedProjectsQuery from '~/projects/your_work/graphql/queries/contributed_projects.query.graphql';
+import { formatGraphQLProjects } from '~/vue_shared/components/projects_list/utils';
+import { createAlert } from '~/alert';
+import { CONTRIBUTED_TAB } from 'ee_else_ce/projects/your_work/constants';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+
+jest.mock('~/alert');
+
+Vue.use(VueApollo);
+
+describe('TabView', () => {
+ let wrapper;
+ let mockApollo;
+
+ const createComponent = ({ handler, propsData }) => {
+ mockApollo = createMockApollo([handler]);
+
+ wrapper = mountExtended(TabView, {
+ apolloProvider: mockApollo,
+ propsData,
+ });
+ };
+
+ afterEach(() => {
+ mockApollo = null;
+ });
+
+ describe.each`
+ tab | handler | expectedProjects
+ ${CONTRIBUTED_TAB} | ${[contributedProjectsQuery, jest.fn().mockResolvedValue(contributedProjectsGraphQlResponse)]} | ${contributedProjectsGraphQlResponse.data.currentUser.contributedProjects.nodes}
+ `('onMount when route name is $tab.value', ({ tab, handler, expectedProjects }) => {
+ describe('when GraphQL request is loading', () => {
+ beforeEach(() => {
+ createComponent({ handler, propsData: { tab } });
+ });
+
+ it('shows loading icon', () => {
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ });
+ });
+
+ describe('when GraphQL request is successful', () => {
+ beforeEach(async () => {
+ createComponent({ handler, propsData: { tab } });
+ await waitForPromises();
+ });
+
+ it('passes projects to `ProjectsList` component', () => {
+ expect(wrapper.findComponent(ProjectsList).props('projects')).toEqual(
+ formatGraphQLProjects(expectedProjects),
+ );
+ });
+ });
+
+ describe('when GraphQL request is not successful', () => {
+ const error = new Error();
+
+ beforeEach(async () => {
+ createComponent({
+ handler: [handler[0], jest.fn().mockRejectedValue(error)],
+ propsData: { tab },
+ });
+ await waitForPromises();
+ });
+
+ it('displays error alert', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'An error occurred loading the projects. Please refresh the page to try again.',
+ error,
+ captureError: true,
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/projects_list/utils_spec.js b/spec/frontend/vue_shared/components/projects_list/utils_spec.js
new file mode 100644
index 00000000000..906157e54d8
--- /dev/null
+++ b/spec/frontend/vue_shared/components/projects_list/utils_spec.js
@@ -0,0 +1,53 @@
+import projectsGraphQLResponse from 'test_fixtures/graphql/organizations/projects.query.graphql.json';
+import { formatGraphQLProjects } from '~/vue_shared/components/projects_list/utils';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
+
+const {
+ data: {
+ organization: {
+ projects: { nodes: projects },
+ },
+ },
+} = projectsGraphQLResponse;
+
+describe('formatGraphQLProjects', () => {
+ it('correctly formats the projects', () => {
+ const [firstMockProject] = projects;
+ const formattedProjects = formatGraphQLProjects(projects);
+ const [firstFormattedProject] = formattedProjects;
+
+ expect(firstFormattedProject).toMatchObject({
+ id: getIdFromGraphQLId(firstMockProject.id),
+ name: firstMockProject.nameWithNamespace,
+ mergeRequestsAccessLevel: firstMockProject.mergeRequestsAccessLevel.stringValue,
+ issuesAccessLevel: firstMockProject.issuesAccessLevel.stringValue,
+ forkingAccessLevel: firstMockProject.forkingAccessLevel.stringValue,
+ accessLevel: {
+ integerValue: 50,
+ },
+ availableActions: [ACTION_EDIT, ACTION_DELETE],
+ actionLoadingStates: {
+ [ACTION_DELETE]: false,
+ },
+ });
+
+ expect(formattedProjects.length).toBe(projects.length);
+ });
+
+ describe('when project does not have delete permissions', () => {
+ const nonDeletableFormattedProject = formatGraphQLProjects(projects)[1];
+
+ it('does not include delete action in `availableActions`', () => {
+ expect(nonDeletableFormattedProject.availableActions).toEqual([]);
+ });
+ });
+
+ describe('when project does not have edit permissions', () => {
+ const nonEditableFormattedProject = formatGraphQLProjects(projects)[1];
+
+ it('does not include edit action in `availableActions`', () => {
+ expect(nonEditableFormattedProject.availableActions).toEqual([]);
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_detail_modal_spec.js b/spec/frontend/work_items/components/work_item_detail_modal_spec.js
index d2a8e2fe396..15e64898a62 100644
--- a/spec/frontend/work_items/components/work_item_detail_modal_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_modal_spec.js
@@ -4,7 +4,6 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
-import { stubComponent } from 'helpers/stub_component';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
import deleteWorkItemMutation from '~/work_items/graphql/delete_work_item.mutation.graphql';
import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
@@ -47,10 +46,13 @@ describe('WorkItemDetailModal component', () => {
},
provide: {
fullPath: 'group/project',
+ reportAbusePath: 'report/abuse',
+ groupPath: '',
+ hasSubepicsFeature: false,
},
stubs: {
GlModal,
- WorkItemDetail: stubComponent(WorkItemDetail),
+ WorkItemDetail,
},
});
};
diff --git a/spec/frontend/work_items/components/work_item_prefetch_spec.js b/spec/frontend/work_items/components/work_item_prefetch_spec.js
index 444b237234a..05ffc6b1310 100644
--- a/spec/frontend/work_items/components/work_item_prefetch_spec.js
+++ b/spec/frontend/work_items/components/work_item_prefetch_spec.js
@@ -34,15 +34,13 @@ describe('WorkItemPrefetch component', () => {
},
scopedSlots: {
default: `
-
-
- Hover item
-
-
+
+ Hover item
+
`,
},
});