Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-09-04 03:11:53 +00:00
parent 02bb524d05
commit ae68f3fe9c
37 changed files with 474 additions and 194 deletions

View File

@ -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

View File

@ -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:

View File

@ -1 +1 @@
a2ca345cd681ef39094623d8f4b6ed65996de57d
a3988141dd517bc75af2775a168033df1bed742c

View File

@ -143,7 +143,6 @@ export default {
onAttributeUpdated: this.onAttributeUpdated,
onIssuableDeleted: this.refetchActiveIssuableLists,
onStateUpdated: this.onStateUpdated,
modalWorkItemFullPath: this.modalWorkItemFullPath,
});
},
};

View File

@ -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,
};
},

View File

@ -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!

View File

@ -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(
({

View File

@ -1,23 +1,25 @@
<script>
import { GlTabs, GlTab, GlBadge, GlSprintf } from '@gitlab/ui';
import { GlTabs, GlTab, GlBadge } from '@gitlab/ui';
import { __ } from '~/locale';
import { TIMESTAMP_TYPE_UPDATED_AT } from '~/vue_shared/components/resource_lists/constants';
import {
PROJECT_DASHBOARD_TABS,
CONTRIBUTED_TAB,
CUSTOM_DASHBOARD_ROUTE_NAMES,
PROJECT_DASHBOARD_TABS,
} from 'ee_else_ce/projects/your_work/constants';
import TabView from './tab_view.vue';
export default {
name: 'YourWorkProjectsApp',
TIMESTAMP_TYPE_UPDATED_AT,
i18n: {
heading: __('Projects'),
activeTab: __('Active tab: %{tab}'),
},
components: {
GlTabs,
GlTab,
GlBadge,
GlSprintf,
TabView,
},
data() {
return {
@ -54,7 +56,7 @@ export default {
<h1 class="page-title gl-mt-5 gl-text-size-h-display">{{ $options.i18n.heading }}</h1>
<gl-tabs :value="activeTabIndex" @input="onTabUpdate">
<gl-tab v-for="tab in formattedTabs" :key="tab.text">
<gl-tab v-for="tab in formattedTabs" :key="tab.text" lazy>
<template #title>
<span data-testid="projects-dashboard-tab-title">
<span>{{ tab.text }}</span>
@ -62,11 +64,8 @@ export default {
</span>
</template>
<gl-sprintf :message="$options.i18n.activeTab">
<template #tab>
{{ tab.text }}
</template>
</gl-sprintf>
<tab-view v-if="tab.query" :tab="tab" />
<template v-else>{{ tab.text }}</template>
</gl-tab>
</gl-tabs>
</div>

View File

@ -0,0 +1,71 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { get } from 'lodash';
import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue';
import { __ } from '~/locale';
import { createAlert } from '~/alert';
import { formatGraphQLProjects } from '~/vue_shared/components/projects_list/utils';
import { TIMESTAMP_TYPE_UPDATED_AT } from '~/vue_shared/components/resource_lists/constants';
export default {
name: 'YourWorkProjectsTabView',
TIMESTAMP_TYPE_UPDATED_AT,
i18n: {
errorMessage: __(
'An error occurred loading the projects. Please refresh the page to try again.',
),
},
components: {
GlLoadingIcon,
ProjectsList,
},
props: {
tab: {
required: true,
type: Object,
},
},
data() {
return {
projects: {},
};
},
apollo: {
projects() {
return {
query: this.tab.query,
update(response) {
const { nodes, pageInfo } = get(response, this.tab.queryPath);
return {
nodes: formatGraphQLProjects(nodes),
pageInfo,
};
},
error(error) {
createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
},
};
},
},
computed: {
nodes() {
return this.projects.nodes || [];
},
isLoading() {
return this.$apollo.queries.projects.loading;
},
},
};
</script>
<template>
<gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" />
<projects-list
v-else-if="nodes.length"
:projects="nodes"
show-project-icon
list-item-class="gl-px-5"
:timestamp-type="$options.TIMESTAMP_TYPE_UPDATED_AT"
/>
</template>

View File

@ -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 = {

View File

@ -0,0 +1,12 @@
#import "ee_else_ce/graphql_shared/fragments/project.fragment.graphql"
query getContributedProjects {
currentUser {
id
contributedProjects {
nodes {
...Project
}
}
}
}

View File

@ -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);

View File

@ -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,
},
}),
);

View File

@ -1,3 +0,0 @@
.service-data-payload-container {
max-height: 400px;
}

View File

@ -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;
}

View File

@ -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'

View File

@ -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"

View File

@ -481,8 +481,6 @@
- 2
- - mailers
- 2
- - members_destroy
- 1
- - members_destroyer_clean_up_group_protected_branch_rules
- 1
- - members_expiring_email_notification

View File

@ -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

View File

@ -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:
![Shard example](img/shard_example_v13_3.png)
![GitLab application interacting with Gitaly storage shards](img/shard_example_v13_3.png)
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:
![Cluster example](img/cluster_example_v13_3.png)
![GitLab application interacting with virtual Gitaly storage, which interacts with Gitaly physical storage](img/cluster_example_v13_3.png)
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.
![Architecture diagram](img/praefect_architecture_v12_10.png)
![Praefect distributing incoming connections to Gitaly cluster nodes](img/praefect_architecture_v12_10.png)
For more information, see [Gitaly High Availability (HA) Design](https://gitlab.com/gitlab-org/gitaly/-/blob/master/doc/design_ha.md).

View File

@ -564,7 +564,7 @@ test-job1:
script:
- echo "$BUILD_VERSION" # Output is: 'v1.0.0'
dependencies:
- build
- build-job1
test-job2:
stage: test

View File

@ -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.

View File

@ -458,6 +458,35 @@ The project page will be removed entirely from the group settings in 18.0.
<div class="deprecation breaking-change" data-milestone="18.0">
### Public use of Secure container registries is deprecated
<div class="deprecation-notes">
- Announced in GitLab <span class="milestone">17.4</span>
- Removal in GitLab <span class="milestone">18.0</span> ([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).
</div>
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.
</div>
<div class="deprecation breaking-change" data-milestone="18.0">
### Rate limits for common User, Project, and Group API endpoints
<div class="deprecation-notes">

View File

@ -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 ""

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 () => {

View File

@ -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', () => {

View File

@ -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', () => {

View File

@ -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,
});
});
});
});
});

View File

@ -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([]);
});
});
});

View File

@ -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,
},
});
};

View File

@ -34,15 +34,13 @@ describe('WorkItemPrefetch component', () => {
},
scopedSlots: {
default: `
<template #default="{ prefetchWorkItem, clearPrefetching }">
<span
@mouseover="prefetchWorkItem"
@mouseleave="clearPrefetching"
data-testid="prefetch-trigger"
>
Hover item
</span>
</template>
<span
@mouseover="props.prefetchWorkItem"
@mouseleave="props.clearPrefetching"
data-testid="prefetch-trigger"
>
Hover item
</span>
`,
},
});