Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
961bdc8763
commit
eb5145f05d
|
|
@ -85,7 +85,7 @@
|
|||
{"name":"colored2","version":"3.1.2","platform":"ruby","checksum":"b13c2bd7eeae2cf7356a62501d398e72fde78780bd26aec6a979578293c28b4a"},
|
||||
{"name":"commonmarker","version":"0.23.11","platform":"ruby","checksum":"9d1d35d358740151bce29235aebfecc63314fb57dd89a83e72d4061b4fe3d2bf"},
|
||||
{"name":"concurrent-ruby","version":"1.2.3","platform":"ruby","checksum":"82fdd3f8a0816e28d513e637bb2b90a45d7b982bdf4f3a0511722d2e495801e2"},
|
||||
{"name":"connection_pool","version":"2.5.0","platform":"ruby","checksum":"233b92f8d38e038c1349ccea65dd3772727d669d6d2e71f9897c8bf5cd53ebfc"},
|
||||
{"name":"connection_pool","version":"2.5.1","platform":"ruby","checksum":"ae802a90a4b5a081101b39d618e69921a9a50bea9ac3420a5b8c71f1befa3e9c"},
|
||||
{"name":"console","version":"1.29.2","platform":"ruby","checksum":"afd9b75a1b047059dda22df0e3c0a386e96f50f6752c87c4b00b1a9fcbe77cd6"},
|
||||
{"name":"cork","version":"0.3.0","platform":"ruby","checksum":"a0a0ac50e262f8514d1abe0a14e95e71c98b24e3378690e5d044daf0013ad4bc"},
|
||||
{"name":"cose","version":"1.3.0","platform":"ruby","checksum":"63247c66a5bc76e53926756574fe3724cc0a88707e358c90532ae2a320e98601"},
|
||||
|
|
|
|||
|
|
@ -453,7 +453,7 @@ GEM
|
|||
colored2 (3.1.2)
|
||||
commonmarker (0.23.11)
|
||||
concurrent-ruby (1.2.3)
|
||||
connection_pool (2.5.0)
|
||||
connection_pool (2.5.1)
|
||||
console (1.29.2)
|
||||
fiber-annotation
|
||||
fiber-local (~> 1.1)
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@
|
|||
{"name":"colored2","version":"3.1.2","platform":"ruby","checksum":"b13c2bd7eeae2cf7356a62501d398e72fde78780bd26aec6a979578293c28b4a"},
|
||||
{"name":"commonmarker","version":"0.23.11","platform":"ruby","checksum":"9d1d35d358740151bce29235aebfecc63314fb57dd89a83e72d4061b4fe3d2bf"},
|
||||
{"name":"concurrent-ruby","version":"1.2.3","platform":"ruby","checksum":"82fdd3f8a0816e28d513e637bb2b90a45d7b982bdf4f3a0511722d2e495801e2"},
|
||||
{"name":"connection_pool","version":"2.5.0","platform":"ruby","checksum":"233b92f8d38e038c1349ccea65dd3772727d669d6d2e71f9897c8bf5cd53ebfc"},
|
||||
{"name":"connection_pool","version":"2.5.1","platform":"ruby","checksum":"ae802a90a4b5a081101b39d618e69921a9a50bea9ac3420a5b8c71f1befa3e9c"},
|
||||
{"name":"console","version":"1.29.2","platform":"ruby","checksum":"afd9b75a1b047059dda22df0e3c0a386e96f50f6752c87c4b00b1a9fcbe77cd6"},
|
||||
{"name":"cork","version":"0.3.0","platform":"ruby","checksum":"a0a0ac50e262f8514d1abe0a14e95e71c98b24e3378690e5d044daf0013ad4bc"},
|
||||
{"name":"cose","version":"1.3.0","platform":"ruby","checksum":"63247c66a5bc76e53926756574fe3724cc0a88707e358c90532ae2a320e98601"},
|
||||
|
|
|
|||
|
|
@ -465,7 +465,7 @@ GEM
|
|||
colored2 (3.1.2)
|
||||
commonmarker (0.23.11)
|
||||
concurrent-ruby (1.2.3)
|
||||
connection_pool (2.5.0)
|
||||
connection_pool (2.5.1)
|
||||
console (1.29.2)
|
||||
fiber-annotation
|
||||
fiber-local (~> 1.1)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
GlDisclosureDropdownGroup,
|
||||
GlLoadingIcon,
|
||||
GlTooltipDirective,
|
||||
GlSearchBoxByType,
|
||||
} from '@gitlab/ui';
|
||||
import { createAlert } from '~/alert';
|
||||
import { s__, __, sprintf } from '~/locale';
|
||||
|
|
@ -17,6 +18,8 @@ import { PIPELINE_POLL_INTERVAL_DEFAULT, FAILED_STATUS } from '~/ci/constants';
|
|||
import JobDropdownItem from '~/ci/common/private/job_dropdown_item.vue';
|
||||
import getPipelineStageJobsQuery from './graphql/queries/get_pipeline_stage_jobs.query.graphql';
|
||||
|
||||
const searchItemsThreshold = 12;
|
||||
|
||||
export default {
|
||||
name: 'PipelineStageDropdown',
|
||||
components: {
|
||||
|
|
@ -25,6 +28,7 @@ export default {
|
|||
GlDisclosureDropdown,
|
||||
GlDisclosureDropdownGroup,
|
||||
GlLoadingIcon,
|
||||
GlSearchBoxByType,
|
||||
JobDropdownItem,
|
||||
},
|
||||
directives: {
|
||||
|
|
@ -46,6 +50,7 @@ export default {
|
|||
return {
|
||||
isDropdownOpen: false,
|
||||
stageJobs: [],
|
||||
search: '',
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
|
|
@ -99,6 +104,14 @@ export default {
|
|||
passedJobs() {
|
||||
return this.stageJobs.filter((job) => job.detailedStatus.group !== FAILED_STATUS);
|
||||
},
|
||||
searchedJobs() {
|
||||
return this.stageJobs.filter((job) =>
|
||||
job.name.toLowerCase().includes(this.search.toLowerCase()),
|
||||
);
|
||||
},
|
||||
searchVisible() {
|
||||
return !this.isLoading && this.stageJobs.length > searchItemsThreshold;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
toggleQueryPollingByVisibility(this.$apollo.queries.stageJobs);
|
||||
|
|
@ -107,6 +120,7 @@ export default {
|
|||
onHideDropdown() {
|
||||
this.isDropdownOpen = false;
|
||||
this.$apollo.queries.stageJobs.stopPolling();
|
||||
this.search = '';
|
||||
},
|
||||
onShowDropdown() {
|
||||
this.isDropdownOpen = true;
|
||||
|
|
@ -149,6 +163,7 @@ export default {
|
|||
>
|
||||
<span>{{ dropdownHeaderText }}</span>
|
||||
</div>
|
||||
<gl-search-box-by-type v-if="searchVisible" v-model="search" class="gl-m-2" borderless />
|
||||
</template>
|
||||
|
||||
<div v-if="isLoading" class="gl-flex gl-gap-3 gl-px-4 gl-py-3">
|
||||
|
|
@ -161,27 +176,43 @@ export default {
|
|||
data-testid="pipeline-mini-graph-dropdown-menu-list"
|
||||
@click.stop
|
||||
>
|
||||
<gl-disclosure-dropdown-group v-if="hasFailedJobs">
|
||||
<template #group-label>{{ s__('Pipelines|Failed jobs') }}</template>
|
||||
<job-dropdown-item
|
||||
v-for="job in failedJobs"
|
||||
:key="job.id"
|
||||
:job="job"
|
||||
@jobActionExecuted="$emit('jobActionExecuted')"
|
||||
/>
|
||||
</gl-disclosure-dropdown-group>
|
||||
<gl-disclosure-dropdown-group
|
||||
v-if="hasPassedJobs"
|
||||
:bordered="hasFailedJobs"
|
||||
data-testid="passed-jobs"
|
||||
>
|
||||
<job-dropdown-item
|
||||
v-for="job in passedJobs"
|
||||
:key="job.id"
|
||||
:job="job"
|
||||
@jobActionExecuted="$emit('jobActionExecuted')"
|
||||
/>
|
||||
</gl-disclosure-dropdown-group>
|
||||
<template v-if="search">
|
||||
<gl-disclosure-dropdown-group
|
||||
v-if="searchedJobs.length"
|
||||
key="searched-jobs"
|
||||
data-testid="searched-jobs"
|
||||
>
|
||||
<job-dropdown-item
|
||||
v-for="job in searchedJobs"
|
||||
:key="job.id + 'searched'"
|
||||
:job="job"
|
||||
@jobActionExecuted="$emit('jobActionExecuted')"
|
||||
/>
|
||||
</gl-disclosure-dropdown-group>
|
||||
</template>
|
||||
<template v-else>
|
||||
<gl-disclosure-dropdown-group v-if="hasFailedJobs">
|
||||
<template #group-label>{{ s__('Pipelines|Failed jobs') }}</template>
|
||||
<job-dropdown-item
|
||||
v-for="job in failedJobs"
|
||||
:key="job.id"
|
||||
:job="job"
|
||||
@jobActionExecuted="$emit('jobActionExecuted')"
|
||||
/>
|
||||
</gl-disclosure-dropdown-group>
|
||||
<gl-disclosure-dropdown-group
|
||||
v-if="hasPassedJobs"
|
||||
:bordered="hasFailedJobs"
|
||||
data-testid="passed-jobs"
|
||||
>
|
||||
<job-dropdown-item
|
||||
v-for="job in passedJobs"
|
||||
:key="job.id"
|
||||
:job="job"
|
||||
@jobActionExecuted="$emit('jobActionExecuted')"
|
||||
/>
|
||||
</gl-disclosure-dropdown-group>
|
||||
</template>
|
||||
</ul>
|
||||
|
||||
<template #footer>
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@
|
|||
import { GlCollapsibleListbox } from '@gitlab/ui';
|
||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
import { createAlert } from '~/alert';
|
||||
import { __, s__ } from '~/locale';
|
||||
import { s__ } from '~/locale';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
|
||||
export default {
|
||||
ORGANIZATIONS_PER_PAGE: 25, // Same value as PAGE_LENGTH in `app/controllers/import/github_groups_controller.rb`
|
||||
components: {
|
||||
GlCollapsibleListbox,
|
||||
},
|
||||
|
|
@ -17,15 +18,22 @@ export default {
|
|||
},
|
||||
},
|
||||
data() {
|
||||
return { organizationsLoading: true, organizations: [], organizationFilter: '' };
|
||||
return {
|
||||
page: 1,
|
||||
hasMoreOrganizations: false,
|
||||
isLoading: true,
|
||||
isLoadingMore: false,
|
||||
organizations: [],
|
||||
organizationFilter: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
toggleText() {
|
||||
return this.value || this.$options.i18n.allOrganizations;
|
||||
return this.value || s__('ImportProjects|All organizations');
|
||||
},
|
||||
dropdownItems() {
|
||||
return [
|
||||
{ text: this.$options.i18n.allOrganizations, value: '' },
|
||||
{ text: s__('ImportProjects|All organizations'), value: '' },
|
||||
...this.organizations
|
||||
.filter((entry) =>
|
||||
entry.name.toLowerCase().includes(this.organizationFilter.toLowerCase()),
|
||||
|
|
@ -38,36 +46,64 @@ export default {
|
|||
},
|
||||
},
|
||||
async mounted() {
|
||||
try {
|
||||
this.organizationsLoading = true;
|
||||
const {
|
||||
data: { provider_groups: organizations },
|
||||
} = await axios.get(this.statusImportGithubGroupPath);
|
||||
this.organizations = organizations;
|
||||
} catch (e) {
|
||||
createAlert({
|
||||
message: __('Something went wrong on our end.'),
|
||||
});
|
||||
Sentry.captureException(e);
|
||||
} finally {
|
||||
this.organizationsLoading = false;
|
||||
}
|
||||
this.loadInitialOrganizations();
|
||||
},
|
||||
i18n: {
|
||||
allOrganizations: s__('ImportProjects|All organizations'),
|
||||
methods: {
|
||||
async fetchOrganizations(page = this.page) {
|
||||
try {
|
||||
const {
|
||||
data: { provider_groups: organizations },
|
||||
} = await axios.get(this.statusImportGithubGroupPath, {
|
||||
params: { page },
|
||||
});
|
||||
|
||||
this.hasMoreOrganizations = organizations.length === this.$options.ORGANIZATIONS_PER_PAGE;
|
||||
|
||||
return organizations;
|
||||
} catch (e) {
|
||||
createAlert({
|
||||
message: s__('GithubImporter|Something went wrong while fetching GitHub organizations.'),
|
||||
});
|
||||
Sentry.captureException(e);
|
||||
|
||||
this.hasMoreOrganizations = false; // Stop loading more after error
|
||||
|
||||
return [];
|
||||
}
|
||||
},
|
||||
async loadInitialOrganizations() {
|
||||
this.organizations = await this.fetchOrganizations();
|
||||
this.isLoading = false;
|
||||
},
|
||||
async loadMoreOrganizations() {
|
||||
if (!this.hasMoreOrganizations) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoadingMore = true;
|
||||
const nextPageOrganizations = await this.fetchOrganizations(this.page + 1);
|
||||
if (nextPageOrganizations.length > 0) {
|
||||
this.organizations.push(...nextPageOrganizations);
|
||||
this.page += 1;
|
||||
}
|
||||
this.isLoadingMore = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-collapsible-listbox
|
||||
:loading="organizationsLoading"
|
||||
:loading="isLoading"
|
||||
:toggle-text="toggleText"
|
||||
:header-text="s__('ImportProjects|Organizations')"
|
||||
:items="dropdownItems"
|
||||
searchable
|
||||
:infinite-scroll="hasMoreOrganizations"
|
||||
:infinite-scroll-loading="isLoadingMore"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@search="organizationFilter = $event"
|
||||
@select="$emit('input', $event)"
|
||||
@bottom-reached="loadMoreOrganizations"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ export default {
|
|||
<import-projects-table v-bind="$attrs">
|
||||
<template #filter="{ importAllButtonText, showImportAllModal }">
|
||||
<gl-tabs v-model="selectedRelationTypeTabIdx" content-class="!gl-py-0 gl-mb-3">
|
||||
<gl-tab v-for="tab in $options.relationTypes" :key="tab.title" :title="tab.title">
|
||||
<gl-tab v-for="tab in $options.relationTypes" :key="tab.title" :title="tab.title" lazy>
|
||||
<div
|
||||
class="gl-flex gl-flex-wrap gl-justify-between gl-gap-3 gl-border-0 gl-border-b-1 gl-border-solid gl-border-b-default gl-bg-subtle gl-p-5"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
PACKAGE_ERROR_STATUS,
|
||||
PACKAGE_DEFAULT_STATUS,
|
||||
PACKAGE_DEPRECATED_STATUS,
|
||||
PACKAGE_TYPE_CONAN,
|
||||
WARNING_TEXT,
|
||||
} from '~/packages_and_registries/package_registry/constants';
|
||||
import { getPackageTypeLabel } from '~/packages_and_registries/package_registry/utils';
|
||||
|
|
@ -103,6 +104,9 @@ export default {
|
|||
'gl-font-normal': this.errorStatusRow,
|
||||
};
|
||||
},
|
||||
isPackageTypeConan() {
|
||||
return this.packageEntity.packageType === PACKAGE_TYPE_CONAN;
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
createdAt: __('Created %{timestamp}'),
|
||||
|
|
@ -127,18 +131,30 @@ export default {
|
|||
</template>
|
||||
<template #left-primary>
|
||||
<div class="gl-mr-5 gl-flex gl-min-w-0 gl-items-center gl-gap-3" data-testid="package-name">
|
||||
<router-link
|
||||
v-if="containsWebPathLink"
|
||||
:class="errorPackageStyle"
|
||||
class="gl-min-w-0 gl-break-all gl-text-default"
|
||||
data-testid="details-link"
|
||||
:to="{ name: 'details', params: { id: packageId } }"
|
||||
>
|
||||
{{ packageEntity.name }}
|
||||
</router-link>
|
||||
<span v-else :class="errorPackageStyle">
|
||||
{{ packageEntity.name }}
|
||||
</span>
|
||||
<div class="gl-gap-2 sm:gl-flex">
|
||||
<router-link
|
||||
v-if="containsWebPathLink"
|
||||
:class="errorPackageStyle"
|
||||
class="gl-min-w-0 gl-break-all gl-text-default"
|
||||
data-testid="details-link"
|
||||
:to="{ name: 'details', params: { id: packageId } }"
|
||||
>
|
||||
{{ packageEntity.name }}
|
||||
</router-link>
|
||||
<span v-else :class="errorPackageStyle">
|
||||
{{ packageEntity.name }}
|
||||
</span>
|
||||
|
||||
<div v-if="isPackageTypeConan" data-testid="conan-metadata">
|
||||
<span class="gl-hidden sm:gl-inline">·</span>
|
||||
<span class="gl-font-normal gl-text-subtle" data-testid="package-username">
|
||||
{{ packageEntity.metadata.packageUsername }}
|
||||
</span>
|
||||
<gl-badge class="gl-ml-2" variant="info" data-testid="package-channel">
|
||||
{{ packageEntity.metadata.packageChannel }}
|
||||
</gl-badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showBadges" class="gl-flex gl-gap-3">
|
||||
<package-tags
|
||||
|
|
|
|||
|
|
@ -31,6 +31,13 @@ query getPackages(
|
|||
count
|
||||
nodes {
|
||||
...PackageData
|
||||
metadata {
|
||||
... on ConanMetadata {
|
||||
id
|
||||
packageChannel
|
||||
packageUsername
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
...PageInfo
|
||||
|
|
@ -58,6 +65,13 @@ query getPackages(
|
|||
name
|
||||
webUrl
|
||||
}
|
||||
metadata {
|
||||
... on ConanMetadata {
|
||||
id
|
||||
packageChannel
|
||||
packageUsername
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
...PageInfo
|
||||
|
|
|
|||
|
|
@ -127,12 +127,15 @@ export default {
|
|||
async mounted() {
|
||||
eventHub.$on('convert-task-list-item', this.convertTaskListItem);
|
||||
eventHub.$on('delete-task-list-item', this.deleteTaskListItem);
|
||||
window.addEventListener('hashchange', (e) => this.truncateOrScrollToAnchor(e));
|
||||
|
||||
await this.$nextTick();
|
||||
this.truncateOrScrollToAnchor();
|
||||
},
|
||||
beforeDestroy() {
|
||||
eventHub.$off('convert-task-list-item', this.convertTaskListItem);
|
||||
eventHub.$off('delete-task-list-item', this.deleteTaskListItem);
|
||||
window.removeEventListener('hashchange', this.truncateOrScrollToAnchor);
|
||||
this.removeAllPointerEventListeners();
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -158,13 +161,18 @@ export default {
|
|||
* If yes, it will prevent description from truncating and will scroll the page to the anchor.
|
||||
* If no, it will truncate the description as per default behaviour.
|
||||
*/
|
||||
truncateOrScrollToAnchor() {
|
||||
truncateOrScrollToAnchor({ type } = {}) {
|
||||
const hash = getLocationHash();
|
||||
const hashSelector = `href="#${hash}"`;
|
||||
const isLocationHashAnchoredInDescription =
|
||||
hash && this.descriptionHtml?.includes(hashSelector);
|
||||
|
||||
if (isLocationHashAnchoredInDescription) {
|
||||
// Link was referred from current page itself,
|
||||
// so we expand the description and then scroll to it.
|
||||
if (type === 'hashchange') {
|
||||
this.showAll();
|
||||
}
|
||||
handleLocationHash();
|
||||
} else {
|
||||
this.truncateLongDescription();
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ module Import
|
|||
before_action :provider_auth, only: [:status]
|
||||
feature_category :importers
|
||||
|
||||
# Same value as ORGANIZATIONS_PER_PAGE in
|
||||
# `app/assets/javascripts/import_entities/import_projects/components/github_organizations_box.vue`
|
||||
PAGE_LENGTH = 25
|
||||
|
||||
def status
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ module Snippets
|
|||
cannot_be_used_together: 'and snippet files cannot be used together',
|
||||
invalid_data: 'have invalid data'
|
||||
}.freeze
|
||||
SNIPPET_ACCESS_ERROR = :snippet_access_error
|
||||
|
||||
CreateRepositoryError = Class.new(StandardError)
|
||||
|
||||
|
|
@ -32,7 +33,7 @@ module Snippets
|
|||
def forbidden_visibility_error(snippet)
|
||||
deny_visibility_level(snippet)
|
||||
|
||||
snippet_error_response(snippet, 403)
|
||||
snippet_error_response(snippet, SNIPPET_ACCESS_ERROR)
|
||||
end
|
||||
|
||||
def valid_params?
|
||||
|
|
@ -50,7 +51,6 @@ module Snippets
|
|||
snippet.errors.add(:snippet_actions, INVALID_PARAMS_MESSAGES[:invalid_data])
|
||||
end
|
||||
|
||||
# Callers need to interpret into 422
|
||||
snippet_error_response(snippet, INVALID_PARAMS_ERROR)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
removal_milestone: '18.0'
|
||||
announcement_milestone: '17.4'
|
||||
breaking_change: true
|
||||
window: 1
|
||||
window: 2
|
||||
reporter: lwanko
|
||||
stage: Fulfillment
|
||||
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/476858
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
removal_milestone: '18.0'
|
||||
announcement_milestone: '17.5'
|
||||
breaking_change: true
|
||||
window: 1
|
||||
window: 2
|
||||
reporter: lwanko
|
||||
stage: Fulfillment
|
||||
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/489850
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
- title: "Resource owner password credentials grant is deprecated"
|
||||
# The milestones for the deprecation announcement, and the removal.
|
||||
removal_milestone: "19.0"
|
||||
announcement_milestone: "18.0"
|
||||
# Change breaking_change to false if needed.
|
||||
breaking_change: true
|
||||
window: # Can be 1, 2, or 3 - The window when the breaking change will be deployed on GitLab.com
|
||||
reporter: hsutor # The GitLab username of the person reporting the change
|
||||
stage: software_supply_chain_security
|
||||
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/457353
|
||||
# Use the impact calculator https://gitlab-com.gitlab.io/gl-infra/breaking-change-impact-calculator/?
|
||||
impact: medium
|
||||
scope: instance
|
||||
resolution_role: Admin
|
||||
manual_task: true
|
||||
body: | # (required) Don't change this line.
|
||||
Using the resource owner password credentials (ROPC) grant as an OAuth flow is deprecated, and support will be fully removed in GitLab 19.0. We have added a setting that can be enabled or disabled by administrators to use this grant type with client credentials only, in their instances. This allows users who would like to opt out of using ROPC without client credentials to do so prior to 19.0. ROPC will be completely removed in 19.0 and cannot be used even with client credentials after that point.
|
||||
|
||||
GitLab has [required client authentication for ROPC on GitLab.com](https://about.gitlab.com/blog/2025/04/01/improving-oauth-ropc-security-on-gitlab-com/) since April 8, 2025 for security reasons. Fully removing ROPC support keeps security in line with the OAuth RFC version 2.1.
|
||||
|
|
@ -80,6 +80,7 @@ Valid reports are:
|
|||
- `api_fuzzing`
|
||||
- `coverage_fuzzing`
|
||||
- `sast`
|
||||
- `secret_detection`
|
||||
|
||||
For example, here is the definition of a SAST job that generates a file named `gl-sast-report.json`,
|
||||
and uploads it as a SAST report:
|
||||
|
|
|
|||
|
|
@ -174,14 +174,25 @@ For more information, see [GitLab Duo add-on seat management with LDAP](../admin
|
|||
|
||||
## View assigned GitLab Duo users
|
||||
|
||||
{{< history >}}
|
||||
|
||||
- Last GitLab Duo activity field [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/455761) in GitLab 18.0.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must purchase a GitLab Duo add-on, or have an active GitLab Duo trial.
|
||||
|
||||
After you purchase GitLab Duo, you can assign seats to users to grant access to the add-on.
|
||||
After you purchase GitLab Duo, you can assign seats to users to grant access to the add-on. Then you can view details of assigned GitLab Duo users.
|
||||
|
||||
To retrieve the **Last Duo activity** date for assigned GitLab Duo users,
|
||||
you can also use the [GraphQL API](../api/graphql/reference/_index.md#addonuser) with the `AddOnUser` type.
|
||||
The GitLab Duo seat utilization page shows the following information for each user:
|
||||
|
||||
- User's full name and username
|
||||
- Seat assignment status
|
||||
- Public email address
|
||||
- Last GitLab activity
|
||||
- Last GitLab Duo activity
|
||||
|
||||
### For GitLab.com
|
||||
|
||||
|
|
|
|||
|
|
@ -26,8 +26,6 @@ This window takes place on April 21 - 23, 2025 from 09:00 UTC to 22:00 UTC.
|
|||
|-------------|--------|-------|-------|------------------------|
|
||||
| [CI/CD job token - **Limit access from your project** setting removal](deprecations.md#cicd-job-token---limit-access-from-your-project-setting-removal) | High | Software supply chain security | Project | Refer to the [Understanding this change](https://gitlab.com/gitlab-org/gitlab/-/issues/395708#understanding-this-change) section for details. |
|
||||
| [CI/CD job token - **Authorized groups and projects** allowlist enforcement](deprecations.md#cicd-job-token---authorized-groups-and-projects-allowlist-enforcement) | High | Software supply chain security | Project | Refer to the [Understanding this change](https://gitlab.com/gitlab-org/gitlab/-/issues/383084#understanding-this-change) section for details. |
|
||||
| [Replace `add_on_purchase` GraphQL field with `add_on_purchases`](deprecations.md#replace-add_on_purchase-graphql-field-with-add_on_purchases) | Low | Fulfillment | Instance, group | |
|
||||
| [Replace namespace `add_on_purchase` GraphQL field with `add_on_purchases`](deprecations.md#replace-namespace-add_on_purchase-graphql-field-with-add_on_purchases) | Low | Fulfillment | Instance, group | |
|
||||
| [Deprecation of `name` field in `ProjectMonthlyUsageType` GraphQL API](deprecations.md#deprecation-of-name-field-in-projectmonthlyusagetype-graphql-api) | Low | Fulfillment | Project | |
|
||||
| [Deprecation of `STORAGE` enum in `NamespaceProjectSortEnum` GraphQL API](deprecations.md#deprecation-of-storage-enum-in-namespaceprojectsortenum-graphql-api) | Low | Fulfillment | Group | |
|
||||
| [DAST `dast_devtools_api_timeout` will have a lower default value](deprecations.md#dast-dast_devtools_api_timeout-will-have-a-lower-default-value) | Low | Application security testing | Project | |
|
||||
|
|
@ -40,6 +38,8 @@ This window takes place on April 28 - 30, 2025 from 09:00 UTC to 22:00 UTC.
|
|||
|
||||
| Deprecation | Impact | Stage | Scope | Check potential impact |
|
||||
|-------------|--------|-------|-------|------------------------|
|
||||
| [Replace `add_on_purchase` GraphQL field with `add_on_purchases`](deprecations.md#replace-add_on_purchase-graphql-field-with-add_on_purchases) | Low | Fulfillment | Instance, group | |
|
||||
| [Replace namespace `add_on_purchase` GraphQL field with `add_on_purchases`](deprecations.md#replace-namespace-add_on_purchase-graphql-field-with-add_on_purchases) | Low | Fulfillment | Instance, group | |
|
||||
| [Limit number of scan execution policy actions allowed per policy](deprecations.md#limit-number-of-scan-execution-policy-actions-allowed-per-policy) | Low | Security risk management | Instance, group, project | |
|
||||
| [Behavior change for Upcoming and Started milestone filters](deprecations.md#behavior-change-for-upcoming-and-started-milestone-filters) | Low | Plan | Group, project | |
|
||||
|
||||
|
|
|
|||
|
|
@ -762,6 +762,24 @@ A replacement feature is planned as part of the [Auto Remediation vision](https:
|
|||
|
||||
<div class="deprecation breaking-change" data-milestone="19.0">
|
||||
|
||||
### Resource owner password credentials grant is deprecated
|
||||
|
||||
<div class="deprecation-notes">
|
||||
|
||||
- Announced in GitLab <span class="milestone">18.0</span>
|
||||
- Removal in GitLab <span class="milestone">19.0</span> ([breaking change](https://docs.gitlab.com/update/terminology/#breaking-change))
|
||||
- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/457353).
|
||||
|
||||
</div>
|
||||
|
||||
Using the resource owner password credentials (ROPC) grant as an OAuth flow is deprecated, and support will be fully removed in GitLab 19.0. We have added a setting that can be enabled or disabled by administrators to use this grant type with client credentials only, in their instances. This allows users who would like to opt out of using ROPC without client credentials to do so prior to 19.0. ROPC will be completely removed in 19.0 and cannot be used even with client credentials after that point.
|
||||
|
||||
GitLab has [required client authentication for ROPC on GitLab.com](https://about.gitlab.com/blog/2025/04/01/improving-oauth-ropc-security-on-gitlab-com/) since April 8, 2025 for security reasons. Fully removing ROPC support keeps security in line with the OAuth RFC version 2.1.
|
||||
|
||||
</div>
|
||||
|
||||
<div class="deprecation breaking-change" data-milestone="19.0">
|
||||
|
||||
### S3 storage driver (AWS SDK v1) for the container registry
|
||||
|
||||
<div class="deprecation-notes">
|
||||
|
|
|
|||
|
|
@ -181,10 +181,13 @@ Dependency paths are supported for the following package managers only when usin
|
|||
If the [Dependency Scanning](../dependency_scanning/_index.md) CI job is configured,
|
||||
[discovered licenses](../../compliance/license_scanning_of_cyclonedx_files/_index.md) are displayed on this page.
|
||||
|
||||
## Download the dependency list
|
||||
## Export
|
||||
|
||||
You can download the full list of dependencies and their details in JSON, CSV, or CycloneDX format.
|
||||
The dependency list shows only the results of the last successful pipeline that ran on the default branch.
|
||||
You can export the dependency list in:
|
||||
|
||||
- JSON
|
||||
- CSV
|
||||
- CycloneDX format (for projects only)
|
||||
|
||||
To download the dependency list:
|
||||
|
||||
|
|
@ -192,6 +195,12 @@ To download the dependency list:
|
|||
1. Select **Secure > Dependency list**.
|
||||
1. Select **Export**.
|
||||
|
||||
{{< alert type="note" >}}
|
||||
|
||||
CycloneDX export is available on the project dependency list.
|
||||
|
||||
{{< /alert >}}
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### License appears as 'unknown'
|
||||
|
|
|
|||
|
|
@ -28244,6 +28244,9 @@ msgstr ""
|
|||
msgid "GithubImporter|Release attachments"
|
||||
msgstr ""
|
||||
|
||||
msgid "GithubImporter|Something went wrong while fetching GitHub organizations."
|
||||
msgstr ""
|
||||
|
||||
msgid "GithubImporter|Your import of GitHub gists into GitLab snippets is complete."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -35357,6 +35360,9 @@ msgstr ""
|
|||
msgid "Last Accessed On"
|
||||
msgstr ""
|
||||
|
||||
msgid "Last GitLab Duo activity"
|
||||
msgstr ""
|
||||
|
||||
msgid "Last GitLab activity"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
source 'https://rubygems.org'
|
||||
|
||||
gem 'gitlab-qa', '~> 15', '>= 15.4.0', require: 'gitlab/qa'
|
||||
gem 'gitlab_quality-test_tooling', '~> 2.9.0', require: false
|
||||
gem 'gitlab_quality-test_tooling', '~> 2.10.0', require: false
|
||||
gem 'gitlab-utils', path: '../gems/gitlab-utils'
|
||||
gem 'activesupport', '~> 7.0.8.7' # This should stay in sync with the root's Gemfile
|
||||
gem 'allure-rspec', '~> 2.26.0'
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ GEM
|
|||
rainbow (>= 3, < 4)
|
||||
table_print (= 1.5.7)
|
||||
zeitwerk (>= 2, < 3)
|
||||
gitlab_quality-test_tooling (2.9.0)
|
||||
gitlab_quality-test_tooling (2.10.0)
|
||||
activesupport (>= 7.0, < 7.3)
|
||||
amatch (~> 0.4.1)
|
||||
fog-google (~> 1.24, >= 1.24.1)
|
||||
|
|
@ -369,7 +369,7 @@ DEPENDENCIES
|
|||
gitlab-orchestrator!
|
||||
gitlab-qa (~> 15, >= 15.4.0)
|
||||
gitlab-utils!
|
||||
gitlab_quality-test_tooling (~> 2.9.0)
|
||||
gitlab_quality-test_tooling (~> 2.10.0)
|
||||
googleauth (~> 1.9.0)
|
||||
influxdb-client (~> 3.2)
|
||||
junit_merge (~> 0.1.2)
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ module RuboCop
|
|||
def on_casgn(node)
|
||||
# we want to make sure that we are running the cop only on
|
||||
# ConfigFiles::Constants::CONFIG_FILES_CONSTANTS
|
||||
return unless config_files_constants?(node.parent.parent)
|
||||
return unless config_files_constants?(node&.parent&.parent)
|
||||
|
||||
_matcher, constants_array = config_file_classes(node)
|
||||
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@ ee/spec/frontend/requirements/components/requirements_root_spec.js
|
|||
ee/spec/frontend/roadmap/components/roadmap_shell_spec.js
|
||||
ee/spec/frontend/roles_and_permissions/components/role_selector_spec.js
|
||||
ee/spec/frontend/security_configuration/components/app_spec.js
|
||||
ee/spec/frontend/security_configuration/dast_profiles/components/dast_profiles_list_spec.js
|
||||
ee/spec/frontend/security_dashboard/components/shared/vulnerability_report/vulnerability_report_tabs_spec.js
|
||||
ee/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
|
||||
ee/spec/frontend/status_checks/components/modal_create_spec.js
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
require 'spec_helper'
|
||||
require_relative './shared_context_and_examples'
|
||||
|
||||
RSpec.describe 'CI configuration validation - branch pipelines', feature_category: :tooling do
|
||||
RSpec.describe 'CI configuration validation - branch pipelines', feature_category: :tooling, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/535543' do
|
||||
include ProjectForksHelper
|
||||
include CiConfigurationValidationHelper
|
||||
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ export const mockPipelineJob = {
|
|||
};
|
||||
|
||||
// for `pipeline_stage_spec.js`
|
||||
export const mockPipelineStageJobs = {
|
||||
export const createMockPipelineStageJobs = () => ({
|
||||
data: {
|
||||
ciPipelineStage: {
|
||||
__typename: 'CiStage',
|
||||
|
|
@ -237,7 +237,9 @@ export const mockPipelineStageJobs = {
|
|||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const mockPipelineStageJobs = createMockPipelineStageJobs();
|
||||
|
||||
export const singlePipeline = {
|
||||
id: 'gid://gitlab/Ci::Pipeline/610',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import Vue from 'vue';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { GlButton, GlDisclosureDropdown, GlLoadingIcon } from '@gitlab/ui';
|
||||
import { GlButton, GlDisclosureDropdown, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
|
||||
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { createAlert } from '~/alert';
|
||||
|
|
@ -13,7 +13,12 @@ import JobDropdownItem from '~/ci/common/private/job_dropdown_item.vue';
|
|||
import PipelineStageDropdown from '~/ci/pipeline_mini_graph/pipeline_stage_dropdown.vue';
|
||||
|
||||
import getPipelineStageJobsQuery from '~/ci/pipeline_mini_graph/graphql/queries/get_pipeline_stage_jobs.query.graphql';
|
||||
import { mockPipelineStageJobs, pipelineStage, pipelineStageJobsFetchError } from './mock_data';
|
||||
import {
|
||||
createMockPipelineStageJobs,
|
||||
mockPipelineStageJobs,
|
||||
pipelineStage,
|
||||
pipelineStageJobsFetchError,
|
||||
} from './mock_data';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
jest.mock('~/alert');
|
||||
|
|
@ -57,15 +62,19 @@ describe('PipelineStageDropdown', () => {
|
|||
|
||||
const clickStageDropdown = async () => {
|
||||
await findDropdownButton().trigger('click');
|
||||
await waitForPromises;
|
||||
await waitForPromises();
|
||||
await nextTick();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
pipelineStageResponse = jest.fn();
|
||||
createComponent();
|
||||
pipelineStageResponse = jest.fn().mockResolvedValue(mockPipelineStageJobs);
|
||||
});
|
||||
|
||||
describe('when mounted', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders the dropdown', () => {
|
||||
expect(findStageDropdown().exists()).toBe(true);
|
||||
});
|
||||
|
|
@ -76,6 +85,10 @@ describe('PipelineStageDropdown', () => {
|
|||
});
|
||||
|
||||
describe('dropdown appearance', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders the icon', () => {
|
||||
expect(findCiIcon().exists()).toBe(true);
|
||||
});
|
||||
|
|
@ -94,24 +107,22 @@ describe('PipelineStageDropdown', () => {
|
|||
});
|
||||
|
||||
describe('when dropdown is clicked', () => {
|
||||
beforeEach(async () => {
|
||||
await createComponent();
|
||||
pipelineStageResponse.mockResolvedValue(mockPipelineStageJobs);
|
||||
});
|
||||
|
||||
it('has the correct header title', async () => {
|
||||
await createComponent();
|
||||
await clickStageDropdown();
|
||||
|
||||
expect(findDropdownHeader().text()).toBe('Stage: build');
|
||||
});
|
||||
|
||||
it('emits miniGraphStageClick', async () => {
|
||||
await createComponent();
|
||||
await clickStageDropdown();
|
||||
|
||||
expect(wrapper.emitted('miniGraphStageClick')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('has fired the stage query', async () => {
|
||||
await createComponent();
|
||||
await clickStageDropdown();
|
||||
const { stage } = defaultProps;
|
||||
|
||||
|
|
@ -120,16 +131,25 @@ describe('PipelineStageDropdown', () => {
|
|||
|
||||
describe('and query is loading', () => {
|
||||
it('renders a loading icon and no list', async () => {
|
||||
let res;
|
||||
pipelineStageResponse.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
res = resolve;
|
||||
}),
|
||||
);
|
||||
createComponent();
|
||||
await clickStageDropdown();
|
||||
|
||||
expect(findLoadingIcon().exists()).toBe(true);
|
||||
expect(findJobList().exists()).toBe(false);
|
||||
res();
|
||||
});
|
||||
});
|
||||
|
||||
describe('and query is successful', () => {
|
||||
beforeEach(async () => {
|
||||
await createComponent();
|
||||
await clickStageDropdown();
|
||||
});
|
||||
|
||||
|
|
@ -147,6 +167,34 @@ describe('PipelineStageDropdown', () => {
|
|||
findJobDropdownItems().at(0).vm.$emit('jobActionExecuted');
|
||||
expect(wrapper.emitted('jobActionExecuted')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('does not show search', () => {
|
||||
expect(wrapper.findComponent(GlSearchBoxByType).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with too many items', () => {
|
||||
let jobs;
|
||||
|
||||
beforeEach(async () => {
|
||||
jobs = createMockPipelineStageJobs();
|
||||
jobs.data.ciPipelineStage.jobs.nodes = new Array(13)
|
||||
.fill(jobs.data.ciPipelineStage.jobs.nodes[0])
|
||||
.map((node, i) => ({ ...node, name: node.name + i, id: node.id + i }));
|
||||
await createComponent({ pipelineStageHandler: jest.fn().mockResolvedValue(jobs) });
|
||||
await clickStageDropdown();
|
||||
});
|
||||
|
||||
it('displays search', () => {
|
||||
expect(wrapper.findComponent(GlSearchBoxByType).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('searches items', async () => {
|
||||
const { name } = jobs.data.ciPipelineStage.jobs.nodes[5];
|
||||
wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', name);
|
||||
await nextTick();
|
||||
expect(findJobDropdownItems().length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and query is not successful', () => {
|
||||
|
|
@ -155,8 +203,6 @@ describe('PipelineStageDropdown', () => {
|
|||
it('throws an error for the pipeline query', async () => {
|
||||
await createComponent({ pipelineStageHandler: failedHandler });
|
||||
await clickStageDropdown();
|
||||
await waitForPromises();
|
||||
|
||||
expect(createAlert).toHaveBeenCalledWith({ message: pipelineStageJobsFetchError });
|
||||
});
|
||||
});
|
||||
|
|
@ -164,10 +210,8 @@ describe('PipelineStageDropdown', () => {
|
|||
|
||||
describe('when there are failed jobs', () => {
|
||||
beforeEach(async () => {
|
||||
pipelineStageResponse.mockResolvedValue(mockPipelineStageJobs);
|
||||
await createComponent();
|
||||
await clickStageDropdown();
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('renders failed jobs title', () => {
|
||||
|
|
@ -189,7 +233,6 @@ describe('PipelineStageDropdown', () => {
|
|||
pipelineStageResponse.mockResolvedValue(withoutFailedJob);
|
||||
await createComponent();
|
||||
await clickStageDropdown();
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('does not render failed jobs title', () => {
|
||||
|
|
@ -206,7 +249,6 @@ describe('PipelineStageDropdown', () => {
|
|||
pipelineStageResponse.mockResolvedValue(mockPipelineStageJobs);
|
||||
await createComponent();
|
||||
await clickStageDropdown();
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('starts polling when dropdown is open', () => {
|
||||
|
|
@ -225,7 +267,6 @@ describe('PipelineStageDropdown', () => {
|
|||
expect(pipelineStageResponse).toHaveBeenCalledTimes(2);
|
||||
|
||||
await clickStageDropdown();
|
||||
await waitForPromises();
|
||||
|
||||
jest.advanceTimersByTime(8000);
|
||||
|
||||
|
|
@ -246,7 +287,6 @@ describe('PipelineStageDropdown', () => {
|
|||
props: { isMergeTrain: true },
|
||||
});
|
||||
await clickStageDropdown();
|
||||
await waitForPromises();
|
||||
|
||||
expect(findMergeTrainMessage().exists()).toBe(true);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
|
|||
import { mount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import { captureException } from '~/sentry/sentry_browser_wrapper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
|
||||
|
|
@ -13,7 +14,7 @@ import GithubOrganizationsBox from '~/import_entities/import_projects/components
|
|||
jest.mock('~/sentry/sentry_browser_wrapper');
|
||||
jest.mock('~/alert');
|
||||
|
||||
const MOCK_RESPONSE = {
|
||||
const mockResponse = {
|
||||
provider_groups: [{ name: 'alpha-1' }, { name: 'alpha-2' }, { name: 'beta-1' }],
|
||||
};
|
||||
|
||||
|
|
@ -38,27 +39,20 @@ describe('GithubOrganizationsBox component', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
mockAxios = new MockAdapter(axios);
|
||||
mockAxios.onGet(mockGithubGroupPath).reply(HTTP_STATUS_OK, MOCK_RESPONSE);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockAxios.restore();
|
||||
});
|
||||
|
||||
it('has underlying listbox as loading while loading organizations', () => {
|
||||
it('renders listbox as loading', () => {
|
||||
createComponent();
|
||||
expect(findListbox().props('loading')).toBe(true);
|
||||
});
|
||||
|
||||
it('clears underlying listbox when loading is complete', async () => {
|
||||
createComponent();
|
||||
await axios.waitForAll();
|
||||
expect(findListbox().props('loading')).toBe(false);
|
||||
});
|
||||
|
||||
it('sets toggle-text to all organizations when selection is not provided', () => {
|
||||
createComponent({ value: '' });
|
||||
expect(findListbox().props('toggleText')).toBe(GithubOrganizationsBox.i18n.allOrganizations);
|
||||
expect(findListbox().props('toggleText')).toBe('All organizations');
|
||||
});
|
||||
|
||||
it('sets toggle-text to organization name when it is provided', () => {
|
||||
|
|
@ -75,23 +69,91 @@ describe('GithubOrganizationsBox component', () => {
|
|||
expect(wrapper.emitted('input').at(-1)).toStrictEqual(['org-id']);
|
||||
});
|
||||
|
||||
it('filters list for underlying listbox', async () => {
|
||||
createComponent();
|
||||
await axios.waitForAll();
|
||||
describe('when request is successful', () => {
|
||||
beforeEach(async () => {
|
||||
mockAxios.onGet(mockGithubGroupPath).reply(HTTP_STATUS_OK, mockResponse);
|
||||
|
||||
findListbox().vm.$emit('search', 'alpha');
|
||||
await nextTick();
|
||||
createComponent();
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
// 2 matches + 'All organizations'
|
||||
expect(findListbox().props('items')).toHaveLength(3);
|
||||
it('renders listbox as loaded', () => {
|
||||
expect(findListbox().props('loading')).toBe(false);
|
||||
});
|
||||
|
||||
it('filters organizations on search', async () => {
|
||||
findListbox().vm.$emit('search', 'alpha');
|
||||
await nextTick();
|
||||
|
||||
// 2 items matching search; 1 item for 'All organizations'
|
||||
expect(findListbox().props('items')).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
it('reports error to sentry on load', async () => {
|
||||
mockAxios.onGet(mockGithubGroupPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
|
||||
createComponent();
|
||||
await axios.waitForAll();
|
||||
describe('when request fails', () => {
|
||||
it('reports error to sentry', async () => {
|
||||
mockAxios.onGet(mockGithubGroupPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
|
||||
|
||||
expect(captureException).toHaveBeenCalled();
|
||||
expect(createAlert).toHaveBeenCalled();
|
||||
createComponent();
|
||||
await waitForPromises();
|
||||
|
||||
expect(captureException).toHaveBeenCalled();
|
||||
expect(createAlert).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('infinite scroll', () => {
|
||||
describe('when less than 25 organizations are returned', () => {
|
||||
it('does not enable infinite scroll', async () => {
|
||||
createComponent();
|
||||
await waitForPromises();
|
||||
|
||||
expect(findListbox().props('infiniteScroll')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when 25 organizations are returned', () => {
|
||||
const mockResponsePage1 = {
|
||||
provider_groups: Array(25)
|
||||
.fill()
|
||||
.map((_, i) => ({ name: `org-${i}` })),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
mockAxios.onGet(mockGithubGroupPath).replyOnce(HTTP_STATUS_OK, mockResponsePage1);
|
||||
|
||||
createComponent();
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('enables infinite scroll', () => {
|
||||
expect(findListbox().props('infiniteScroll')).toBe(true);
|
||||
});
|
||||
|
||||
describe('when bottom is reached', () => {
|
||||
beforeEach(() => {
|
||||
mockAxios
|
||||
.onGet(mockGithubGroupPath, { params: { page: 2 } })
|
||||
.replyOnce(HTTP_STATUS_OK, mockResponse);
|
||||
|
||||
findListbox().vm.$emit('bottom-reached');
|
||||
});
|
||||
|
||||
it('loads more organizations', async () => {
|
||||
expect(findListbox().props('infiniteScrollLoading')).toBe(true);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findListbox().props('infiniteScrollLoading')).toBe(false);
|
||||
// 25 items on page 1; 3 items on page 2; 1 item for 'All organizations'
|
||||
expect(findListbox().props('items')).toHaveLength(29);
|
||||
});
|
||||
|
||||
it('disables infinite scroll when page 2 has less than 25 organizations', async () => {
|
||||
await waitForPromises();
|
||||
expect(findListbox().props('infiniteScroll')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -85,26 +85,6 @@ describe('GithubStatusTable', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('renders name filter disabled when tab with organization filter is selected and organization is not set', async () => {
|
||||
const NEW_ACTIVE_TAB_IDX = GithubStatusTable.relationTypes.findIndex(
|
||||
(entry) => entry.showOrganizationFilter,
|
||||
);
|
||||
await selectTab(NEW_ACTIVE_TAB_IDX);
|
||||
expect(findFilterField().props('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('enables name filter disabled when organization is set', async () => {
|
||||
const NEW_ACTIVE_TAB_IDX = GithubStatusTable.relationTypes.findIndex(
|
||||
(entry) => entry.showOrganizationFilter,
|
||||
);
|
||||
await selectTab(NEW_ACTIVE_TAB_IDX);
|
||||
|
||||
wrapper.findComponent(GithubOrganizationsBox).vm.$emit('input', 'some-org');
|
||||
await nextTick();
|
||||
|
||||
expect(findFilterField().props('disabled')).toBe(false);
|
||||
});
|
||||
|
||||
it('updates filter when search box is changed', async () => {
|
||||
const NEW_FILTER = 'test';
|
||||
findFilterField().vm.$emit('submit', NEW_FILTER);
|
||||
|
|
@ -115,12 +95,32 @@ describe('GithubStatusTable', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('updates organization_login filter when GithubOrganizationsBox emits input', () => {
|
||||
const NEW_ORG = 'some-org';
|
||||
wrapper.findComponent(GithubOrganizationsBox).vm.$emit('input', NEW_ORG);
|
||||
describe('when "Organization" tab is selected', () => {
|
||||
beforeEach(async () => {
|
||||
const organizationTabIndex = GithubStatusTable.relationTypes.findIndex(
|
||||
(entry) => entry.showOrganizationFilter,
|
||||
);
|
||||
await selectTab(organizationTabIndex);
|
||||
});
|
||||
|
||||
expect(setFilterAction).toHaveBeenCalledWith(expect.anything(), {
|
||||
organization_login: NEW_ORG,
|
||||
it('renders disabled name filter when organization is not set', () => {
|
||||
expect(findFilterField().props('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('enables name filter when organization is set', async () => {
|
||||
wrapper.findComponent(GithubOrganizationsBox).vm.$emit('input', 'some-org');
|
||||
await nextTick();
|
||||
|
||||
expect(findFilterField().props('disabled')).toBe(false);
|
||||
});
|
||||
|
||||
it('updates organization_login filter when GithubOrganizationsBox emits input', () => {
|
||||
const NEW_ORG = 'some-org';
|
||||
wrapper.findComponent(GithubOrganizationsBox).vm.$emit('input', NEW_ORG);
|
||||
|
||||
expect(setFilterAction).toHaveBeenCalledWith(expect.anything(), {
|
||||
organization_login: NEW_ORG,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -29,12 +29,16 @@ exports[`packages_list_row renders 1`] = `
|
|||
class="gl-flex gl-gap-3 gl-items-center gl-min-w-0 gl-mr-5"
|
||||
data-testid="package-name"
|
||||
>
|
||||
<a
|
||||
class="gl-break-all gl-min-w-0 gl-text-default"
|
||||
data-testid="details-link"
|
||||
<div
|
||||
class="gl-gap-2 sm:gl-flex"
|
||||
>
|
||||
@gitlab-org/package-15
|
||||
</a>
|
||||
<a
|
||||
class="gl-break-all gl-min-w-0 gl-text-default"
|
||||
data-testid="details-link"
|
||||
>
|
||||
@gitlab-org/package-15
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
|
||||
import ListItem from '~/vue_shared/components/registry/list_item.vue';
|
||||
import {
|
||||
conanMetadata,
|
||||
linksData,
|
||||
packageData,
|
||||
packagePipelines,
|
||||
|
|
@ -51,6 +52,9 @@ describe('packages_list_row', () => {
|
|||
const findListItem = () => wrapper.findComponent(ListItem);
|
||||
const findBulkDeleteAction = () => wrapper.findComponent(GlFormCheckbox);
|
||||
const findPackageName = () => wrapper.findByTestId('package-name');
|
||||
const findConanMetadata = () => wrapper.findByTestId('conan-metadata');
|
||||
const findPackageUsername = () => wrapper.findByTestId('package-username');
|
||||
const findPackageChannel = () => wrapper.findByTestId('package-channel');
|
||||
|
||||
const mountComponent = ({
|
||||
packageEntity = packageWithoutTags,
|
||||
|
|
@ -406,4 +410,26 @@ describe('packages_list_row', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('conan metadata', () => {
|
||||
it('does not render `packageUsername` and `packageChannel` for any packages other than conan', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findConanMetadata().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('renders correct `packageUsername` and `packageChannel` when package type is conan', () => {
|
||||
mountComponent({
|
||||
packageEntity: {
|
||||
...packageWithoutTags,
|
||||
packageType: 'CONAN',
|
||||
metadata: conanMetadata(),
|
||||
},
|
||||
});
|
||||
|
||||
expect(findConanMetadata().exists()).toBe(true);
|
||||
expect(findPackageUsername().text()).toBe('gitlab-org+gitlab-test');
|
||||
expect(findPackageChannel().text()).toBe('stable');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -169,6 +169,7 @@ export const packageData = (extend) => ({
|
|||
protectionRuleExists: false,
|
||||
...userPermissionsData,
|
||||
...extend,
|
||||
metadata: null,
|
||||
});
|
||||
|
||||
export const conanMetadata = () => ({
|
||||
|
|
@ -236,13 +237,7 @@ export const packageDetailsQuery = ({ extendPackage = {} } = {}) => ({
|
|||
data: {
|
||||
package: {
|
||||
...packageData(),
|
||||
metadata: {
|
||||
...conanMetadata(),
|
||||
...composerMetadata(),
|
||||
...pypiMetadata(),
|
||||
...mavenMetadata(),
|
||||
...nugetMetadata(),
|
||||
},
|
||||
metadata: packageTypeMetadataQueryMapping[extendPackage.packageType]?.() || null,
|
||||
project: {
|
||||
id: '1',
|
||||
path: 'projectPath',
|
||||
|
|
|
|||
|
|
@ -108,12 +108,7 @@ describe('WorkItemDescriptionRendered', () => {
|
|||
describe('with anchor to description item', () => {
|
||||
const anchorHash = '#description-anchor';
|
||||
|
||||
afterAll(() => {
|
||||
window.location.hash = '';
|
||||
});
|
||||
|
||||
it('scrolls matching link into view', async () => {
|
||||
window.location.hash = anchorHash;
|
||||
const setupComponent = async () => {
|
||||
createComponent({
|
||||
workItemDescription: {
|
||||
description: 'This is a long description',
|
||||
|
|
@ -128,10 +123,39 @@ describe('WorkItemDescriptionRendered', () => {
|
|||
jest.spyOn(wrapper.vm, 'truncateLongDescription');
|
||||
|
||||
await nextTick();
|
||||
};
|
||||
|
||||
afterAll(() => {
|
||||
window.location.hash = '';
|
||||
});
|
||||
|
||||
it('scrolls matching link into view when opened with hash present', async () => {
|
||||
window.location.hash = anchorHash;
|
||||
await setupComponent();
|
||||
|
||||
// Check if page loaded with hash present scrolls hash into view.
|
||||
// In order to scroll, description must not have been truncated.
|
||||
expect(handleLocationHash).toHaveBeenCalled();
|
||||
expect(wrapper.vm.truncateLongDescription).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('expands description and then scrolls to matching link into view on user navigation', async () => {
|
||||
window.location.hash = '';
|
||||
await setupComponent();
|
||||
|
||||
// Check if page loaded with no hash present shows truncated description.
|
||||
expect(handleLocationHash).not.toHaveBeenCalled();
|
||||
|
||||
// Simulate user clicking on an anchor hash within the description.
|
||||
window.location.hash = anchorHash;
|
||||
window.dispatchEvent(new Event('hashchange'));
|
||||
|
||||
await nextTick();
|
||||
|
||||
// Check if description is expanded and hash is scrolled into view.
|
||||
expect(handleLocationHash).toHaveBeenCalled();
|
||||
expect(findReadMore().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('`disableHeadingAnchors` prop', () => {
|
||||
|
|
|
|||
|
|
@ -52,6 +52,10 @@ RSpec.describe Snippets::CreateService, feature_category: :source_code_managemen
|
|||
expect(subject).to be_error
|
||||
end
|
||||
|
||||
it 'responds with a reason' do
|
||||
expect(subject.reason).to eq(described_class::SNIPPET_ACCESS_ERROR)
|
||||
end
|
||||
|
||||
it 'does not create a public snippet' do
|
||||
expect(subject.message).to match('has been restricted')
|
||||
end
|
||||
|
|
|
|||
|
|
@ -46,6 +46,10 @@ RSpec.describe Snippets::UpdateService, feature_category: :source_code_managemen
|
|||
expect(subject).to be_error
|
||||
end
|
||||
|
||||
it 'responds with a reason' do
|
||||
expect(subject.reason).to eq(described_class::SNIPPET_ACCESS_ERROR)
|
||||
end
|
||||
|
||||
it 'does not update snippet to public visibility' do
|
||||
original_visibility = snippet.visibility_level
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue