Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
23dda8d4ed
commit
484a245a95
|
@ -0,0 +1,14 @@
|
|||
include:
|
||||
- template: DAST-API.gitlab-ci.yml
|
||||
|
||||
dast_api:
|
||||
variables:
|
||||
DAST_API_PROFILE: Passive
|
||||
DAST_API_GRAPHQL: /api/graphql
|
||||
DAST_API_TARGET_URL: ${CI_ENVIRONMENT_URL}
|
||||
DAST_API_OVERRIDES_ENV: "{\"headers\":{\"Authorization\":\"Bearer $REVIEW_APPS_ROOT_TOKEN\"}}"
|
||||
needs: ["review-deploy"]
|
||||
# Uncomment resource_group if DAST_API_PROFILE is changed to an active scan
|
||||
# resource_group: dast_api_scan
|
||||
rules:
|
||||
- !reference [".reports:rules:schedule-dast", rules]
|
|
@ -14,6 +14,7 @@ include:
|
|||
- local: .gitlab/ci/review-apps/rules.gitlab-ci.yml
|
||||
- local: .gitlab/ci/review-apps/qa.gitlab-ci.yml
|
||||
- local: .gitlab/ci/review-apps/dast.gitlab-ci.yml
|
||||
- local: .gitlab/ci/review-apps/dast-api.gitlab-ci.yml
|
||||
|
||||
.base-before_script: &base-before_script
|
||||
- source ./scripts/utils.sh
|
||||
|
|
2
Gemfile
2
Gemfile
|
@ -357,7 +357,7 @@ gem 'warning', '~> 1.3.0'
|
|||
group :development do
|
||||
gem 'lefthook', '~> 1.1.1', require: false
|
||||
gem 'rubocop'
|
||||
gem 'solargraph', '~> 0.46.0', require: false
|
||||
gem 'solargraph', '~> 0.47.2', require: false
|
||||
|
||||
gem 'letter_opener_web', '~> 2.0.0'
|
||||
gem 'lookbook', '~> 1.0', '>= 1.0.8'
|
||||
|
|
|
@ -458,7 +458,7 @@
|
|||
{"name":"redis-namespace","version":"1.9.0","platform":"ruby","checksum":"0923961f38cf15b86cb57d92507e0a3b32480729eb5033249f5de8b12e0d8612"},
|
||||
{"name":"redis-rack","version":"2.1.4","platform":"ruby","checksum":"0872eecb303e483c3863d6bd0d47323d230640d41c1a4ac4a2c7596ec0b1774c"},
|
||||
{"name":"redis-store","version":"1.9.1","platform":"ruby","checksum":"7b4c7438d46f7b7ce8f67fc0eda3a04fc67d32d28cf606cc98a5df4d2b77071d"},
|
||||
{"name":"regexp_parser","version":"2.5.0","platform":"ruby","checksum":"a076d2d35ab8d11feab5fecf8aa09ec6df68c2429810748cba079f7b021ecde5"},
|
||||
{"name":"regexp_parser","version":"2.6.0","platform":"ruby","checksum":"f163ba463a45ca2f2730e0902f2475bb0eefcd536dfc2f900a86d1e5a7d7a556"},
|
||||
{"name":"regexp_property_values","version":"1.0.0","platform":"java","checksum":"5e26782b01241616855c4ee7bb8a62fce9387e484f2d3eaf04f2a0633708222e"},
|
||||
{"name":"regexp_property_values","version":"1.0.0","platform":"ruby","checksum":"162499dc0bba1e66d334273a059f207a61981cc8cc69d2ca743594e7886d080f"},
|
||||
{"name":"representable","version":"3.0.4","platform":"ruby","checksum":"07d43917dea4712ecebd19c1909e769deed863ad444d23ceb6461519e2cba962"},
|
||||
|
@ -544,7 +544,7 @@
|
|||
{"name":"slack-messenger","version":"2.3.4","platform":"ruby","checksum":"49c611d2be5b0f9c250a3a957b9cc09b9c07b81dacb9843642d87b6fa35609c1"},
|
||||
{"name":"snaky_hash","version":"2.0.0","platform":"ruby","checksum":"fe8b2e39e8ff69320f7812af73ea06401579e29ff1734a7009567391600687de"},
|
||||
{"name":"snowplow-tracker","version":"0.6.1","platform":"ruby","checksum":"9cec52fd060619f4974b3dc1f7d9a2776c5e31b668a6ead53145b9780e312314"},
|
||||
{"name":"solargraph","version":"0.46.0","platform":"ruby","checksum":"1da9fd8c364501f18b0454e54506e7098bc38dae719219713fe5f246dfc91465"},
|
||||
{"name":"solargraph","version":"0.47.2","platform":"ruby","checksum":"87ca4b799b9155c2c31c15954c483e952fdacd800f52d6709b901dd447bcac6a"},
|
||||
{"name":"sorted_set","version":"1.0.3","platform":"java","checksum":"996283f2e5c6e838825bcdcee31d6306515ae5f24bcb0ee4ce09dfff32919b8c"},
|
||||
{"name":"sorted_set","version":"1.0.3","platform":"ruby","checksum":"4f2b8bee6e8c59cbd296228c0f1f81679357177a8b6859dcc2a99e86cce6372f"},
|
||||
{"name":"spamcheck","version":"1.0.0","platform":"ruby","checksum":"dfeea085184091353e17d729d2f3d714b07cba36aaf64c32dfc35ce9b466fc9c"},
|
||||
|
@ -576,7 +576,7 @@
|
|||
{"name":"text","version":"1.3.1","platform":"ruby","checksum":"2fbbbc82c1ce79c4195b13018a87cbb00d762bda39241bb3cdc32792759dd3f4"},
|
||||
{"name":"thor","version":"1.2.1","platform":"ruby","checksum":"b1752153dc9c6b8d3fcaa665e9e1a00a3e73f28da5e238b81c404502e539d446"},
|
||||
{"name":"thrift","version":"0.16.0","platform":"ruby","checksum":"d023286ea89e30444c9f1c28dd76107f87d8aaf85fe1742da1d8cd3b5417dcce"},
|
||||
{"name":"tilt","version":"2.0.10","platform":"ruby","checksum":"9b664f0e9ae2b500cfa00f9c65c34abc6ff1799cf0034a8c0a0412d520fac866"},
|
||||
{"name":"tilt","version":"2.0.11","platform":"ruby","checksum":"7b180fc472cbdeb186c85d31c0f2d1e61a2c0d77e1d9fd0ca28482a9d972d6a0"},
|
||||
{"name":"timecop","version":"0.9.1","platform":"ruby","checksum":"374b543f0961dbd487e96d09ac812d4fdfeb603ec705bbff241ba060d0a9f534"},
|
||||
{"name":"timeliness","version":"0.3.10","platform":"ruby","checksum":"c357233ce19dc53148e8b29dfddde134689f18f52b32928e9dfe12ebcf4a773f"},
|
||||
{"name":"timfel-krb5-auth","version":"0.8.3","platform":"ruby","checksum":"ab388c9d747fa3cd95baf2cc1c03253e372d8c680adcc543670f4f099854bb80"},
|
||||
|
|
|
@ -1144,7 +1144,7 @@ GEM
|
|||
redis-store (>= 1.2, < 2)
|
||||
redis-store (1.9.1)
|
||||
redis (>= 4, < 5)
|
||||
regexp_parser (2.5.0)
|
||||
regexp_parser (2.6.0)
|
||||
regexp_property_values (1.0.0)
|
||||
representable (3.0.4)
|
||||
declarative (< 0.1.0)
|
||||
|
@ -1333,7 +1333,7 @@ GEM
|
|||
version_gem (~> 1.1)
|
||||
snowplow-tracker (0.6.1)
|
||||
contracts (~> 0.7, <= 0.11)
|
||||
solargraph (0.46.0)
|
||||
solargraph (0.47.2)
|
||||
backport (~> 1.2)
|
||||
benchmark
|
||||
bundler (>= 1.17.2)
|
||||
|
@ -1402,7 +1402,7 @@ GEM
|
|||
text (1.3.1)
|
||||
thor (1.2.1)
|
||||
thrift (0.16.0)
|
||||
tilt (2.0.10)
|
||||
tilt (2.0.11)
|
||||
timecop (0.9.1)
|
||||
timeliness (0.3.10)
|
||||
timfel-krb5-auth (0.8.3)
|
||||
|
@ -1783,7 +1783,7 @@ DEPENDENCIES
|
|||
simplecov-lcov (~> 0.8.0)
|
||||
slack-messenger (~> 2.3.4)
|
||||
snowplow-tracker (~> 0.6.1)
|
||||
solargraph (~> 0.46.0)
|
||||
solargraph (~> 0.47.2)
|
||||
spamcheck (~> 1.0.0)
|
||||
spring (~> 2.1.0)
|
||||
spring-commands-rspec (~> 1.0.4)
|
||||
|
|
|
@ -397,7 +397,6 @@ export default {
|
|||
:img-size="avatarSize"
|
||||
class="js-no-trigger user-avatar-link"
|
||||
tooltip-placement="bottom"
|
||||
:enforce-gl-avatar="true"
|
||||
>
|
||||
<span class="js-assignee-tooltip">
|
||||
<span class="gl-font-weight-bold gl-display-block">{{ __('Assignee') }}</span>
|
||||
|
|
|
@ -97,6 +97,7 @@ export default {
|
|||
eventHub.$on(`${this.action}showLeaveGroupModal`, this.showLeaveGroupModal);
|
||||
eventHub.$on(`${this.action}updatePagination`, this.updatePagination);
|
||||
eventHub.$on(`${this.action}updateGroups`, this.updateGroups);
|
||||
eventHub.$on(`${this.action}fetchFilteredAndSortedGroups`, this.fetchFilteredAndSortedGroups);
|
||||
},
|
||||
mounted() {
|
||||
this.fetchAllGroups();
|
||||
|
@ -111,6 +112,7 @@ export default {
|
|||
eventHub.$off(`${this.action}showLeaveGroupModal`, this.showLeaveGroupModal);
|
||||
eventHub.$off(`${this.action}updatePagination`, this.updatePagination);
|
||||
eventHub.$off(`${this.action}updateGroups`, this.updateGroups);
|
||||
eventHub.$off(`${this.action}fetchFilteredAndSortedGroups`, this.fetchFilteredAndSortedGroups);
|
||||
},
|
||||
methods: {
|
||||
hideModal() {
|
||||
|
@ -153,6 +155,18 @@ export default {
|
|||
this.updateGroups(res, Boolean(this.filterGroupsBy));
|
||||
});
|
||||
},
|
||||
fetchFilteredAndSortedGroups({ filterGroupsBy, sortBy }) {
|
||||
this.isLoading = true;
|
||||
|
||||
return this.fetchGroups({
|
||||
filterGroupsBy,
|
||||
sortBy,
|
||||
updatePagination: true,
|
||||
}).then((res) => {
|
||||
this.isLoading = false;
|
||||
this.updateGroups(res, Boolean(filterGroupsBy));
|
||||
});
|
||||
},
|
||||
fetchPage({ page, filterGroupsBy, sortBy, archived }) {
|
||||
this.isLoading = true;
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { GlTabs, GlTab } from '@gitlab/ui';
|
||||
import { isString } from 'lodash';
|
||||
import { GlTabs, GlTab, GlSearchBoxByType, GlSorting, GlSortingItem } from '@gitlab/ui';
|
||||
import { isString, debounce } from 'lodash';
|
||||
import { __ } from '~/locale';
|
||||
import GroupsStore from '../store/groups_store';
|
||||
import GroupsService from '../service/groups_service';
|
||||
|
@ -8,12 +8,16 @@ import {
|
|||
ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
|
||||
ACTIVE_TAB_SHARED,
|
||||
ACTIVE_TAB_ARCHIVED,
|
||||
OVERVIEW_TABS_SORTING_ITEMS,
|
||||
} from '../constants';
|
||||
import eventHub from '../event_hub';
|
||||
import GroupsApp from './app.vue';
|
||||
|
||||
const [SORTING_ITEM_NAME] = OVERVIEW_TABS_SORTING_ITEMS;
|
||||
|
||||
export default {
|
||||
components: { GlTabs, GlTab, GroupsApp },
|
||||
inject: ['endpoints'],
|
||||
components: { GlTabs, GlTab, GroupsApp, GlSearchBoxByType, GlSorting, GlSortingItem },
|
||||
inject: ['endpoints', 'initialSort'],
|
||||
data() {
|
||||
return {
|
||||
tabs: [
|
||||
|
@ -43,9 +47,35 @@ export default {
|
|||
},
|
||||
],
|
||||
activeTabIndex: 0,
|
||||
sort: SORTING_ITEM_NAME,
|
||||
isAscending: true,
|
||||
search: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
activeTab() {
|
||||
return this.tabs[this.activeTabIndex];
|
||||
},
|
||||
sortQueryStringValue() {
|
||||
return this.isAscending ? this.sort.asc : this.sort.desc;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
search: debounce(async function debouncedSearch() {
|
||||
this.handleSearchOrSortChange();
|
||||
}, 250),
|
||||
},
|
||||
mounted() {
|
||||
this.search = this.$route.query?.filter || '';
|
||||
|
||||
const sortQueryStringValue = this.$route.query?.sort || this.initialSort;
|
||||
const sort =
|
||||
OVERVIEW_TABS_SORTING_ITEMS.find((sortOption) =>
|
||||
[sortOption.asc, sortOption.desc].includes(sortQueryStringValue),
|
||||
) || SORTING_ITEM_NAME;
|
||||
this.sort = sort;
|
||||
this.isAscending = sort.asc === sortQueryStringValue;
|
||||
|
||||
const activeTabIndex = this.tabs.findIndex((tab) => tab.key === this.$route.name);
|
||||
|
||||
if (activeTabIndex === -1) {
|
||||
|
@ -72,14 +102,56 @@ export default {
|
|||
? this.$route.params.group.split('/')
|
||||
: this.$route.params.group;
|
||||
|
||||
this.$router.push({ name: tab.key, params: { group: groupParam } });
|
||||
this.$router.push({ name: tab.key, params: { group: groupParam }, query: this.$route.query });
|
||||
},
|
||||
handleSearchOrSortChange() {
|
||||
// Update query string
|
||||
const query = {};
|
||||
if (this.sortQueryStringValue !== this.initialSort) {
|
||||
query.sort = this.isAscending ? this.sort.asc : this.sort.desc;
|
||||
}
|
||||
if (this.search) {
|
||||
query.filter = this.search;
|
||||
}
|
||||
this.$router.push({ query });
|
||||
|
||||
// Reset `lazy` prop so that groups/projects are fetched with updated `sort` and `filter` params when switching tabs
|
||||
this.tabs.forEach((tab, index) => {
|
||||
if (index === this.activeTabIndex) {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
tab.lazy = true;
|
||||
});
|
||||
|
||||
// Update data
|
||||
eventHub.$emit(`${this.activeTab.key}fetchFilteredAndSortedGroups`, {
|
||||
filterGroupsBy: this.search,
|
||||
sortBy: this.sortQueryStringValue,
|
||||
});
|
||||
},
|
||||
handleSortDirectionChange() {
|
||||
this.isAscending = !this.isAscending;
|
||||
|
||||
this.handleSearchOrSortChange();
|
||||
},
|
||||
handleSortingItemClick(sortingItem) {
|
||||
if (sortingItem === this.sort) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.sort = sortingItem;
|
||||
|
||||
this.handleSearchOrSortChange();
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]: __('Subgroups and projects'),
|
||||
[ACTIVE_TAB_SHARED]: __('Shared projects'),
|
||||
[ACTIVE_TAB_ARCHIVED]: __('Archived projects'),
|
||||
searchPlaceholder: __('Search'),
|
||||
},
|
||||
OVERVIEW_TABS_SORTING_ITEMS,
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -99,5 +171,36 @@ export default {
|
|||
:render-empty-state="renderEmptyState"
|
||||
/>
|
||||
</gl-tab>
|
||||
<template #tabs-end>
|
||||
<li class="gl-flex-grow-1 gl-align-self-center gl-w-full gl-lg-w-auto gl-py-2">
|
||||
<div class="gl-lg-display-flex gl-justify-content-end gl-mx-n2 gl-my-n2">
|
||||
<div class="gl-p-2 gl-lg-form-input-md gl-w-full">
|
||||
<gl-search-box-by-type
|
||||
v-model="search"
|
||||
:placeholder="$options.i18n.searchPlaceholder"
|
||||
data-qa-selector="groups_filter_field"
|
||||
/>
|
||||
</div>
|
||||
<div class="gl-p-2 gl-w-full gl-lg-w-auto">
|
||||
<gl-sorting
|
||||
class="gl-w-full"
|
||||
dropdown-class="gl-w-full"
|
||||
data-testid="group_sort_by_dropdown"
|
||||
:text="sort.label"
|
||||
:is-ascending="isAscending"
|
||||
@sortDirectionChange="handleSortDirectionChange"
|
||||
>
|
||||
<gl-sorting-item
|
||||
v-for="sortingItem in $options.OVERVIEW_TABS_SORTING_ITEMS"
|
||||
:key="sortingItem.label"
|
||||
:active="sortingItem === sort"
|
||||
@click="handleSortingItemClick(sortingItem)"
|
||||
>{{ sortingItem.label }}</gl-sorting-item
|
||||
>
|
||||
</gl-sorting>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</gl-tabs>
|
||||
</template>
|
||||
|
|
|
@ -62,3 +62,26 @@ export const VISIBILITY_TYPE_ICON = {
|
|||
[VISIBILITY_LEVEL_INTERNAL_STRING]: 'shield',
|
||||
[VISIBILITY_LEVEL_PRIVATE_STRING]: 'lock',
|
||||
};
|
||||
|
||||
export const OVERVIEW_TABS_SORTING_ITEMS = [
|
||||
{
|
||||
label: __('Name'),
|
||||
asc: 'name_asc',
|
||||
desc: 'name_desc',
|
||||
},
|
||||
{
|
||||
label: __('Created'),
|
||||
asc: 'created_asc',
|
||||
desc: 'created_desc',
|
||||
},
|
||||
{
|
||||
label: __('Updated'),
|
||||
asc: 'latest_activity_asc',
|
||||
desc: 'latest_activity_desc',
|
||||
},
|
||||
{
|
||||
label: __('Stars'),
|
||||
asc: 'stars_asc',
|
||||
desc: 'stars_desc',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -51,6 +51,7 @@ export const initGroupOverviewTabs = () => {
|
|||
subgroupsAndProjectsEndpoint,
|
||||
sharedProjectsEndpoint,
|
||||
archivedProjectsEndpoint,
|
||||
initialSort,
|
||||
} = el.dataset;
|
||||
|
||||
return new Vue({
|
||||
|
@ -70,6 +71,7 @@ export const initGroupOverviewTabs = () => {
|
|||
[ACTIVE_TAB_SHARED]: sharedProjectsEndpoint,
|
||||
[ACTIVE_TAB_ARCHIVED]: archivedProjectsEndpoint,
|
||||
},
|
||||
initialSort,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(OverviewTabs);
|
||||
|
|
|
@ -122,14 +122,12 @@ export default {
|
|||
:link-href="commit.author.webPath"
|
||||
:img-src="commit.author.avatarUrl"
|
||||
:img-size="32"
|
||||
:img-css-classes="'gl-mr-0!' /* NOTE: this is needed only while we migrate user-avatar-image to GlAvatar (7731 epics) */"
|
||||
class="gl-my-2 gl-mr-4"
|
||||
/>
|
||||
<user-avatar-image
|
||||
v-else
|
||||
class="gl-my-2 gl-mr-4"
|
||||
:img-src="commit.authorGravatar || $options.defaultAvatarUrl"
|
||||
:css-classes="'gl-mr-0!' /* NOTE: this is needed only while we migrate user-avatar-image to GlAvatar (7731 epics) */"
|
||||
:size="32"
|
||||
/>
|
||||
<div
|
||||
|
|
|
@ -5,29 +5,29 @@
|
|||
|
||||
Sample configuration:
|
||||
|
||||
<user-avatar-image
|
||||
<user-avatar
|
||||
lazy
|
||||
:img-src="userAvatarSrc"
|
||||
:img-alt="tooltipText"
|
||||
:tooltip-text="tooltipText"
|
||||
tooltip-placement="top"
|
||||
:size="24"
|
||||
/>
|
||||
|
||||
*/
|
||||
|
||||
import { GlTooltip, GlAvatar } from '@gitlab/ui';
|
||||
import { isObject } from 'lodash';
|
||||
import defaultAvatarUrl from 'images/no_avatar.png';
|
||||
import { __ } from '~/locale';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import UserAvatarImageNew from './user_avatar_image_new.vue';
|
||||
import UserAvatarImageOld from './user_avatar_image_old.vue';
|
||||
import { placeholderImage } from '~/lazy_loader';
|
||||
|
||||
export default {
|
||||
name: 'UserAvatarImage',
|
||||
components: {
|
||||
UserAvatarImageNew,
|
||||
UserAvatarImageOld,
|
||||
GlTooltip,
|
||||
GlAvatar,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
props: {
|
||||
lazy: {
|
||||
type: Boolean,
|
||||
|
@ -51,8 +51,7 @@ export default {
|
|||
},
|
||||
size: {
|
||||
type: [Number, Object],
|
||||
required: false,
|
||||
default: 20,
|
||||
required: true,
|
||||
},
|
||||
tooltipText: {
|
||||
type: String,
|
||||
|
@ -64,22 +63,52 @@ export default {
|
|||
required: false,
|
||||
default: 'top',
|
||||
},
|
||||
enforceGlAvatar: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
computed: {
|
||||
// API response sends null when gravatar is disabled and
|
||||
// we provide an empty string when we use it inside user avatar link.
|
||||
// In both cases we should render the defaultAvatarUrl
|
||||
sanitizedSource() {
|
||||
let baseSrc = this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
|
||||
// Only adds the width to the URL if its not a base64 data image
|
||||
if (!(baseSrc.indexOf('data:') === 0) && !baseSrc.includes('?'))
|
||||
baseSrc += `?width=${this.maximumSize}`;
|
||||
return baseSrc;
|
||||
},
|
||||
maximumSize() {
|
||||
if (isObject(this.size)) {
|
||||
return Math.max(...Object.values(this.size));
|
||||
}
|
||||
|
||||
return this.size;
|
||||
},
|
||||
resultantSrcAttribute() {
|
||||
return this.lazy ? placeholderImage : this.sanitizedSource;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<user-avatar-image-new
|
||||
v-if="glFeatures.glAvatarForAllUserAvatars || enforceGlAvatar"
|
||||
v-bind="$props"
|
||||
>
|
||||
<slot></slot>
|
||||
</user-avatar-image-new>
|
||||
<user-avatar-image-old v-else v-bind="$props">
|
||||
<slot></slot>
|
||||
</user-avatar-image-old>
|
||||
<span ref="userAvatar">
|
||||
<gl-avatar
|
||||
:class="{
|
||||
lazy: lazy,
|
||||
[cssClasses]: true,
|
||||
}"
|
||||
:src="resultantSrcAttribute"
|
||||
:data-src="sanitizedSource"
|
||||
:size="size"
|
||||
:alt="imgAlt"
|
||||
/>
|
||||
|
||||
<gl-tooltip
|
||||
v-if="tooltipText || $scopedSlots.default"
|
||||
:target="() => $refs.userAvatar"
|
||||
:placement="tooltipPlacement"
|
||||
boundary="window"
|
||||
>
|
||||
<slot>{{ tooltipText }}</slot>
|
||||
</gl-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
|
|
|
@ -1,117 +0,0 @@
|
|||
<script>
|
||||
/* This is a re-usable vue component for rendering a user avatar that
|
||||
does not need to link to the user's profile. The image and an optional
|
||||
tooltip can be configured by props passed to this component.
|
||||
|
||||
Sample configuration:
|
||||
|
||||
<user-avatar
|
||||
lazy
|
||||
:img-src="userAvatarSrc"
|
||||
:img-alt="tooltipText"
|
||||
:tooltip-text="tooltipText"
|
||||
tooltip-placement="top"
|
||||
/>
|
||||
|
||||
*/
|
||||
|
||||
import { GlTooltip, GlAvatar } from '@gitlab/ui';
|
||||
import { isObject } from 'lodash';
|
||||
import defaultAvatarUrl from 'images/no_avatar.png';
|
||||
import { __ } from '~/locale';
|
||||
import { placeholderImage } from '~/lazy_loader';
|
||||
|
||||
export default {
|
||||
name: 'UserAvatarImageNew',
|
||||
components: {
|
||||
GlTooltip,
|
||||
GlAvatar,
|
||||
},
|
||||
props: {
|
||||
lazy: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
imgSrc: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: defaultAvatarUrl,
|
||||
},
|
||||
cssClasses: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
imgAlt: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: __('user avatar'),
|
||||
},
|
||||
size: {
|
||||
type: [Number, Object],
|
||||
required: false,
|
||||
default: 20,
|
||||
},
|
||||
tooltipText: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
tooltipPlacement: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'top',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
// API response sends null when gravatar is disabled and
|
||||
// we provide an empty string when we use it inside user avatar link.
|
||||
// In both cases we should render the defaultAvatarUrl
|
||||
sanitizedSource() {
|
||||
let baseSrc = this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
|
||||
// Only adds the width to the URL if its not a base64 data image
|
||||
if (!(baseSrc.indexOf('data:') === 0) && !baseSrc.includes('?'))
|
||||
baseSrc += `?width=${this.maximumSize}`;
|
||||
return baseSrc;
|
||||
},
|
||||
maximumSize() {
|
||||
if (isObject(this.size)) {
|
||||
return Math.max(...Object.values(this.size));
|
||||
}
|
||||
|
||||
return this.size;
|
||||
},
|
||||
resultantSrcAttribute() {
|
||||
return this.lazy ? placeholderImage : this.sanitizedSource;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span ref="userAvatar">
|
||||
<gl-avatar
|
||||
:class="{
|
||||
lazy: lazy,
|
||||
[cssClasses]: true,
|
||||
}"
|
||||
:src="resultantSrcAttribute"
|
||||
:data-src="sanitizedSource"
|
||||
:size="size"
|
||||
:alt="imgAlt"
|
||||
/>
|
||||
|
||||
<gl-tooltip
|
||||
v-if="
|
||||
tooltipText ||
|
||||
$slots.default /* eslint-disable-line @gitlab/vue-prefer-dollar-scopedslots */
|
||||
"
|
||||
:target="() => $refs.userAvatar"
|
||||
:placement="tooltipPlacement"
|
||||
boundary="window"
|
||||
>
|
||||
<slot>{{ tooltipText }}</slot>
|
||||
</gl-tooltip>
|
||||
</span>
|
||||
</template>
|
|
@ -1,114 +0,0 @@
|
|||
<script>
|
||||
/* This is a re-usable vue component for rendering a user avatar that
|
||||
does not need to link to the user's profile. The image and an optional
|
||||
tooltip can be configured by props passed to this component.
|
||||
|
||||
Sample configuration:
|
||||
|
||||
<user-avatar-image
|
||||
lazy
|
||||
:img-src="userAvatarSrc"
|
||||
:img-alt="tooltipText"
|
||||
:tooltip-text="tooltipText"
|
||||
tooltip-placement="top"
|
||||
/>
|
||||
|
||||
*/
|
||||
|
||||
import { GlTooltip } from '@gitlab/ui';
|
||||
import defaultAvatarUrl from 'images/no_avatar.png';
|
||||
import { __ } from '~/locale';
|
||||
import { placeholderImage } from '~/lazy_loader';
|
||||
|
||||
export default {
|
||||
name: 'UserAvatarImageOld',
|
||||
components: {
|
||||
GlTooltip,
|
||||
},
|
||||
props: {
|
||||
lazy: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
imgSrc: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: defaultAvatarUrl,
|
||||
},
|
||||
cssClasses: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
imgAlt: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: __('user avatar'),
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 20,
|
||||
},
|
||||
tooltipText: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
tooltipPlacement: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'top',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
// API response sends null when gravatar is disabled and
|
||||
// we provide an empty string when we use it inside user avatar link.
|
||||
// In both cases we should render the defaultAvatarUrl
|
||||
sanitizedSource() {
|
||||
let baseSrc = this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
|
||||
// Only adds the width to the URL if its not a base64 data image
|
||||
if (!(baseSrc.indexOf('data:') === 0) && !baseSrc.includes('?'))
|
||||
baseSrc += `?width=${this.size}`;
|
||||
return baseSrc;
|
||||
},
|
||||
resultantSrcAttribute() {
|
||||
return this.lazy ? placeholderImage : this.sanitizedSource;
|
||||
},
|
||||
avatarSizeClass() {
|
||||
return `s${this.size}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span>
|
||||
<img
|
||||
ref="userAvatarImage"
|
||||
:class="{
|
||||
lazy: lazy,
|
||||
[avatarSizeClass]: true,
|
||||
[cssClasses]: true,
|
||||
}"
|
||||
:src="resultantSrcAttribute"
|
||||
:width="size"
|
||||
:height="size"
|
||||
:alt="imgAlt"
|
||||
:data-src="sanitizedSource"
|
||||
class="avatar"
|
||||
/>
|
||||
<gl-tooltip
|
||||
v-if="
|
||||
tooltipText ||
|
||||
$slots.default /* eslint-disable-line @gitlab/vue-prefer-dollar-scopedslots */
|
||||
"
|
||||
:target="() => $refs.userAvatarImage"
|
||||
:placement="tooltipPlacement"
|
||||
boundary="window"
|
||||
>
|
||||
<slot>{{ tooltipText }}</slot>
|
||||
</gl-tooltip>
|
||||
</span>
|
||||
</template>
|
|
@ -9,7 +9,7 @@
|
|||
:link-href="userProfileUrl"
|
||||
:img-src="userAvatarSrc"
|
||||
:img-alt="tooltipText"
|
||||
:img-size="20"
|
||||
:img-size="32"
|
||||
:tooltip-text="tooltipText"
|
||||
:tooltip-placement="top"
|
||||
:username="username"
|
||||
|
@ -17,17 +17,18 @@
|
|||
|
||||
*/
|
||||
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import UserAvatarLinkNew from './user_avatar_link_new.vue';
|
||||
import UserAvatarLinkOld from './user_avatar_link_old.vue';
|
||||
import { GlAvatarLink, GlTooltipDirective } from '@gitlab/ui';
|
||||
import UserAvatarImage from './user_avatar_image.vue';
|
||||
|
||||
export default {
|
||||
name: 'UserAvatarLink',
|
||||
name: 'UserAvatarLinkNew',
|
||||
components: {
|
||||
UserAvatarLinkNew,
|
||||
UserAvatarLinkOld,
|
||||
UserAvatarImage,
|
||||
GlAvatarLink,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
props: {
|
||||
lazy: {
|
||||
type: Boolean,
|
||||
|
@ -56,8 +57,7 @@ export default {
|
|||
},
|
||||
imgSize: {
|
||||
type: [Number, Object],
|
||||
required: false,
|
||||
default: 20,
|
||||
required: true,
|
||||
},
|
||||
tooltipText: {
|
||||
type: String,
|
||||
|
@ -74,29 +74,43 @@ export default {
|
|||
required: false,
|
||||
default: '',
|
||||
},
|
||||
enforceGlAvatar: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
computed: {
|
||||
shouldShowUsername() {
|
||||
return this.username.length > 0;
|
||||
},
|
||||
avatarTooltipText() {
|
||||
return this.shouldShowUsername ? '' : this.tooltipText;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<user-avatar-link-new
|
||||
v-if="glFeatures.glAvatarForAllUserAvatars || enforceGlAvatar"
|
||||
v-bind="$props"
|
||||
>
|
||||
<slot></slot>
|
||||
<template #avatar-badge>
|
||||
<slot name="avatar-badge"></slot>
|
||||
</template>
|
||||
</user-avatar-link-new>
|
||||
<gl-avatar-link :href="linkHref" class="user-avatar-link">
|
||||
<user-avatar-image
|
||||
:img-src="imgSrc"
|
||||
:img-alt="imgAlt"
|
||||
:css-classes="imgCssClasses"
|
||||
:size="imgSize"
|
||||
:tooltip-text="avatarTooltipText"
|
||||
:tooltip-placement="tooltipPlacement"
|
||||
:lazy="lazy"
|
||||
>
|
||||
<slot></slot>
|
||||
</user-avatar-image>
|
||||
|
||||
<user-avatar-link-old v-else v-bind="$props">
|
||||
<slot></slot>
|
||||
<template #avatar-badge>
|
||||
<slot name="avatar-badge"></slot>
|
||||
</template>
|
||||
</user-avatar-link-old>
|
||||
<span
|
||||
v-if="shouldShowUsername"
|
||||
v-gl-tooltip
|
||||
:title="tooltipText"
|
||||
:tooltip-placement="tooltipPlacement"
|
||||
class="gl-ml-3"
|
||||
data-testid="user-avatar-link-username"
|
||||
>
|
||||
{{ username }}
|
||||
</span>
|
||||
|
||||
<slot name="avatar-badge"></slot>
|
||||
</gl-avatar-link>
|
||||
</template>
|
||||
|
|
|
@ -1,122 +0,0 @@
|
|||
<script>
|
||||
/* This is a re-usable vue component for rendering a user avatar wrapped in
|
||||
a clickable link (likely to the user's profile). The link, image, and
|
||||
tooltip can be configured by props passed to this component.
|
||||
|
||||
Sample configuration:
|
||||
|
||||
<user-avatar-link
|
||||
:link-href="userProfileUrl"
|
||||
:img-src="userAvatarSrc"
|
||||
:img-alt="tooltipText"
|
||||
:img-size="20"
|
||||
:tooltip-text="tooltipText"
|
||||
:tooltip-placement="top"
|
||||
:username="username"
|
||||
/>
|
||||
|
||||
*/
|
||||
|
||||
import { GlAvatarLink, GlTooltipDirective } from '@gitlab/ui';
|
||||
import UserAvatarImage from './user_avatar_image.vue';
|
||||
|
||||
export default {
|
||||
name: 'UserAvatarLinkNew',
|
||||
components: {
|
||||
UserAvatarImage,
|
||||
GlAvatarLink,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
props: {
|
||||
lazy: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
linkHref: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
imgSrc: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
imgAlt: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
imgCssClasses: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
imgSize: {
|
||||
type: [Number, Object],
|
||||
required: false,
|
||||
default: 20,
|
||||
},
|
||||
tooltipText: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
tooltipPlacement: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'top',
|
||||
},
|
||||
username: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
enforceGlAvatar: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
shouldShowUsername() {
|
||||
return this.username.length > 0;
|
||||
},
|
||||
avatarTooltipText() {
|
||||
return this.shouldShowUsername ? '' : this.tooltipText;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-avatar-link :href="linkHref" class="user-avatar-link">
|
||||
<user-avatar-image
|
||||
:img-src="imgSrc"
|
||||
:img-alt="imgAlt"
|
||||
:css-classes="imgCssClasses"
|
||||
:size="imgSize"
|
||||
:tooltip-text="avatarTooltipText"
|
||||
:tooltip-placement="tooltipPlacement"
|
||||
:lazy="lazy"
|
||||
:enforce-gl-avatar="enforceGlAvatar"
|
||||
>
|
||||
<slot></slot>
|
||||
</user-avatar-image>
|
||||
|
||||
<span
|
||||
v-if="shouldShowUsername"
|
||||
v-gl-tooltip
|
||||
:title="tooltipText"
|
||||
:tooltip-placement="tooltipPlacement"
|
||||
class="gl-ml-3"
|
||||
data-testid="user-avatar-link-username"
|
||||
>
|
||||
{{ username }}
|
||||
</span>
|
||||
|
||||
<slot name="avatar-badge"></slot>
|
||||
</gl-avatar-link>
|
||||
</template>
|
|
@ -1,117 +0,0 @@
|
|||
<script>
|
||||
/* This is a re-usable vue component for rendering a user avatar wrapped in
|
||||
a clickable link (likely to the user's profile). The link, image, and
|
||||
tooltip can be configured by props passed to this component.
|
||||
|
||||
Sample configuration:
|
||||
|
||||
<user-avatar-link
|
||||
:link-href="userProfileUrl"
|
||||
:img-src="userAvatarSrc"
|
||||
:img-alt="tooltipText"
|
||||
:img-size="20"
|
||||
:tooltip-text="tooltipText"
|
||||
:tooltip-placement="top"
|
||||
:username="username"
|
||||
/>
|
||||
|
||||
*/
|
||||
|
||||
import { GlLink, GlTooltipDirective } from '@gitlab/ui';
|
||||
import UserAvatarImage from './user_avatar_image.vue';
|
||||
|
||||
export default {
|
||||
name: 'UserAvatarLinkOld',
|
||||
components: {
|
||||
GlLink,
|
||||
UserAvatarImage,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
props: {
|
||||
lazy: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
linkHref: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
imgSrc: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
imgAlt: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
imgCssClasses: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
imgSize: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 20,
|
||||
},
|
||||
tooltipText: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
tooltipPlacement: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'top',
|
||||
},
|
||||
username: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
shouldShowUsername() {
|
||||
return this.username.length > 0;
|
||||
},
|
||||
avatarTooltipText() {
|
||||
return this.shouldShowUsername ? '' : this.tooltipText;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span>
|
||||
<gl-link :href="linkHref" class="user-avatar-link">
|
||||
<user-avatar-image
|
||||
:img-src="imgSrc"
|
||||
:img-alt="imgAlt"
|
||||
:css-classes="imgCssClasses"
|
||||
:size="imgSize"
|
||||
:tooltip-text="avatarTooltipText"
|
||||
:tooltip-placement="tooltipPlacement"
|
||||
:lazy="lazy"
|
||||
>
|
||||
<slot></slot>
|
||||
</user-avatar-image>
|
||||
|
||||
<span
|
||||
v-if="shouldShowUsername"
|
||||
v-gl-tooltip
|
||||
:title="tooltipText"
|
||||
:tooltip-placement="tooltipPlacement"
|
||||
data-testid="user-avatar-link-username"
|
||||
>
|
||||
{{ username }}
|
||||
</span>
|
||||
<slot name="avatar-badge"></slot>
|
||||
</gl-link>
|
||||
</span>
|
||||
</template>
|
|
@ -1,6 +1,5 @@
|
|||
<script>
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import { sprintf, __ } from '~/locale';
|
||||
import UserAvatarLink from './user_avatar_link.vue';
|
||||
|
||||
|
@ -9,7 +8,6 @@ export default {
|
|||
UserAvatarLink,
|
||||
GlButton,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
|
@ -22,8 +20,7 @@ export default {
|
|||
},
|
||||
imgSize: {
|
||||
type: [Number, Object],
|
||||
required: false,
|
||||
default: 20,
|
||||
required: true,
|
||||
},
|
||||
emptyText: {
|
||||
type: String,
|
||||
|
@ -59,9 +56,6 @@ export default {
|
|||
|
||||
return sprintf(__('%{count} more'), { count });
|
||||
},
|
||||
imgCssClasses() {
|
||||
return this.glFeatures.glAvatarForAllUserAvatars ? 'gl-mr-3' : '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
expand() {
|
||||
|
@ -85,7 +79,7 @@ export default {
|
|||
:img-alt="item.name"
|
||||
:tooltip-text="item.name"
|
||||
:img-size="imgSize"
|
||||
:img-css-classes="imgCssClasses"
|
||||
img-css-classes="gl-mr-3"
|
||||
/>
|
||||
<template v-if="hasBreakpoint">
|
||||
<gl-button v-if="hasHiddenItems" variant="link" @click="expand">
|
||||
|
|
|
@ -177,7 +177,8 @@ module GroupsHelper
|
|||
subgroups_and_projects_endpoint: group_children_path(group, format: :json),
|
||||
shared_projects_endpoint: group_shared_projects_path(group, format: :json),
|
||||
archived_projects_endpoint: group_children_path(group, format: :json, archived: 'only'),
|
||||
current_group_visibility: group.visibility
|
||||
current_group_visibility: group.visibility,
|
||||
initial_sort: project_list_sort_by
|
||||
}.merge(subgroups_and_projects_list_app_data(group))
|
||||
end
|
||||
|
||||
|
|
|
@ -19,7 +19,9 @@
|
|||
%p.gl-text-center= html_escape(_('%{gitlab_experience_text}. Don\'t worry, this information isn\'t shared outside of your self-managed GitLab instance.')) % { gitlab_experience_text: gitlab_experience_text }
|
||||
= gitlab_ui_form_for(current_user,
|
||||
url: users_sign_up_welcome_path(glm_tracking_params),
|
||||
html: { class: 'card gl-w-full! gl-p-5 js-users-signup-welcome', 'aria-live' => 'assertive' }) do |f|
|
||||
html: { class: 'card gl-w-full! gl-p-5 js-users-signup-welcome',
|
||||
'aria-live' => 'assertive',
|
||||
data: { testid: 'welcome-form' } }) do |f|
|
||||
.devise-errors
|
||||
= render 'devise/shared/error_messages', resource: current_user
|
||||
.row
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: gl_avatar_for_all_user_avatars
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81437
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353477
|
||||
milestone: '14.9'
|
||||
type: development
|
||||
group: group::foundations
|
||||
default_enabled: false
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
table_name: project_wiki_repository_states
|
||||
classes:
|
||||
- ProjectWikiRepositoryState
|
||||
feature_categories:
|
||||
- geo_replication
|
||||
description: Separate table for project wikis containing Geo verification metadata.
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/99168
|
||||
milestone: '15.5'
|
|
@ -0,0 +1,44 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateProjectWikiRepositoryStates < Gitlab::Database::Migration[2.0]
|
||||
VERIFICATION_STATE_INDEX_NAME = "index_project_wiki_repository_states_on_verification_state"
|
||||
PENDING_VERIFICATION_INDEX_NAME = "index_project_wiki_repository_states_pending_verification"
|
||||
FAILED_VERIFICATION_INDEX_NAME = "index_project_wiki_repository_states_failed_verification"
|
||||
NEEDS_VERIFICATION_INDEX_NAME = "index_project_wiki_repository_states_needs_verification"
|
||||
|
||||
enable_lock_retries!
|
||||
|
||||
def up
|
||||
create_table :project_wiki_repository_states, id: false do |t|
|
||||
t.datetime_with_timezone :verification_started_at
|
||||
t.datetime_with_timezone :verification_retry_at
|
||||
t.datetime_with_timezone :verified_at
|
||||
t.references :project, primary_key: true, default: nil, index: false, foreign_key: { on_delete: :cascade }
|
||||
t.integer :verification_state, default: 0, limit: 2, null: false
|
||||
t.integer :verification_retry_count, limit: 2
|
||||
t.binary :verification_checksum, using: 'verification_checksum::bytea'
|
||||
t.text :verification_failure, limit: 255
|
||||
|
||||
t.index :verification_state,
|
||||
name: VERIFICATION_STATE_INDEX_NAME
|
||||
|
||||
t.index :verified_at,
|
||||
where: "(verification_state = 0)",
|
||||
order: { verified_at: 'ASC NULLS FIRST' },
|
||||
name: PENDING_VERIFICATION_INDEX_NAME
|
||||
|
||||
t.index :verification_retry_at,
|
||||
where: "(verification_state = 3)",
|
||||
order: { verification_retry_at: 'ASC NULLS FIRST' },
|
||||
name: FAILED_VERIFICATION_INDEX_NAME
|
||||
|
||||
t.index :verification_state,
|
||||
where: "(verification_state = 0 OR verification_state = 3)",
|
||||
name: NEEDS_VERIFICATION_INDEX_NAME
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
drop_table :project_wiki_repository_states
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
b2492ebefc3738dfe706379ef664d3f28315102acc1c0681ba67e6aae62861d7
|
|
@ -20153,6 +20153,18 @@ CREATE SEQUENCE project_topics_id_seq
|
|||
|
||||
ALTER SEQUENCE project_topics_id_seq OWNED BY project_topics.id;
|
||||
|
||||
CREATE TABLE project_wiki_repository_states (
|
||||
verification_started_at timestamp with time zone,
|
||||
verification_retry_at timestamp with time zone,
|
||||
verified_at timestamp with time zone,
|
||||
project_id bigint NOT NULL,
|
||||
verification_state smallint DEFAULT 0 NOT NULL,
|
||||
verification_retry_count smallint,
|
||||
verification_checksum bytea,
|
||||
verification_failure text,
|
||||
CONSTRAINT check_119f134b68 CHECK ((char_length(verification_failure) <= 255))
|
||||
);
|
||||
|
||||
CREATE TABLE projects (
|
||||
id integer NOT NULL,
|
||||
name character varying,
|
||||
|
@ -26184,6 +26196,9 @@ ALTER TABLE ONLY project_statistics
|
|||
ALTER TABLE ONLY project_topics
|
||||
ADD CONSTRAINT project_topics_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY project_wiki_repository_states
|
||||
ADD CONSTRAINT project_wiki_repository_states_pkey PRIMARY KEY (project_id);
|
||||
|
||||
ALTER TABLE ONLY projects
|
||||
ADD CONSTRAINT projects_pkey PRIMARY KEY (id);
|
||||
|
||||
|
@ -29957,6 +29972,14 @@ CREATE INDEX index_project_topics_on_topic_id ON project_topics USING btree (top
|
|||
|
||||
CREATE UNIQUE INDEX index_project_user_callouts_feature ON user_project_callouts USING btree (user_id, feature_name, project_id);
|
||||
|
||||
CREATE INDEX index_project_wiki_repository_states_failed_verification ON project_wiki_repository_states USING btree (verification_retry_at NULLS FIRST) WHERE (verification_state = 3);
|
||||
|
||||
CREATE INDEX index_project_wiki_repository_states_needs_verification ON project_wiki_repository_states USING btree (verification_state) WHERE ((verification_state = 0) OR (verification_state = 3));
|
||||
|
||||
CREATE INDEX index_project_wiki_repository_states_on_verification_state ON project_wiki_repository_states USING btree (verification_state);
|
||||
|
||||
CREATE INDEX index_project_wiki_repository_states_pending_verification ON project_wiki_repository_states USING btree (verified_at NULLS FIRST) WHERE (verification_state = 0);
|
||||
|
||||
CREATE INDEX index_projects_aimed_for_deletion ON projects USING btree (marked_for_deletion_at) WHERE ((marked_for_deletion_at IS NOT NULL) AND (pending_delete = false));
|
||||
|
||||
CREATE INDEX index_projects_api_created_at_id_desc ON projects USING btree (created_at, id DESC);
|
||||
|
@ -34240,6 +34263,9 @@ ALTER TABLE ONLY packages_debian_project_distributions
|
|||
ALTER TABLE ONLY packages_rubygems_metadata
|
||||
ADD CONSTRAINT fk_rails_95a3f5ce78 FOREIGN KEY (package_id) REFERENCES packages_packages(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY project_wiki_repository_states
|
||||
ADD CONSTRAINT fk_rails_9647227ce1 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY packages_pypi_metadata
|
||||
ADD CONSTRAINT fk_rails_9698717cdd FOREIGN KEY (package_id) REFERENCES packages_packages(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
@ -247,8 +247,8 @@ The connection settings match those provided by [fog-aws](https://github.com/fog
|
|||
| `aws_signature_version` | AWS signature version to use. `2` or `4` are valid options. Digital Ocean Spaces and other providers may need `2`. | `4` |
|
||||
| `enable_signature_v4_streaming` | Set to `true` to enable HTTP chunked transfers with [AWS v4 signatures](https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html). Oracle Cloud S3 needs this to be `false`. | `true` |
|
||||
| `region` | AWS region. | |
|
||||
| `host` | S3 compatible host for when not using AWS. For example, `localhost` or `storage.example.com`. HTTPS and port 443 is assumed. | `s3.amazonaws.com` |
|
||||
| `endpoint` | Can be used when configuring an S3 compatible service such as [MinIO](https://min.io), by entering a URL such as `http://127.0.0.1:9000`. This takes precedence over `host`. | (optional) |
|
||||
| `host` | DEPRECATED: Use `endpoint` instead. S3 compatible host for when not using AWS. For example, `localhost` or `storage.example.com`. HTTPS and port 443 is assumed. | `s3.amazonaws.com` |
|
||||
| `endpoint` | Can be used when configuring an S3 compatible service such as [MinIO](https://min.io), by entering a URL such as `http://127.0.0.1:9000`. This takes precedence over `host`. Always use `endpoint` for consolidated form. | (optional) |
|
||||
| `path_style` | Set to `true` to use `host/bucket_name/object` style paths instead of `bucket_name.host/object`. Set to `true` for using [MinIO](https://min.io). Leave as `false` for AWS S3. | `false`. |
|
||||
| `use_iam_profile` | Set to `true` to use IAM profile instead of access keys. | `false` |
|
||||
| `aws_credentials_refresh_threshold_seconds` | Sets the [automatic refresh threshold](https://github.com/fog/fog-aws#controlling-credential-refresh-time-with-iam-authentication) when using temporary credentials in IAM. | `15` |
|
||||
|
|
|
@ -287,7 +287,7 @@ Parameters:
|
|||
| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, `similarity` (1), or `last_activity_at` fields. Default is `created_at` |
|
||||
| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
|
||||
| `search` | string | no | Return list of authorized projects matching the search criteria |
|
||||
| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
|
||||
| `simple` | boolean | no | Return only limited fields for each project. This is a no-op without authentication where only simple fields are returned. |
|
||||
| `owned` | boolean | no | Limit by projects owned by the current user |
|
||||
| `starred` | boolean | no | Limit by projects starred by the current user |
|
||||
| `with_issues_enabled` | boolean | no | Limit by projects with issues feature enabled. Default is `false` |
|
||||
|
@ -370,7 +370,7 @@ Parameters:
|
|||
| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
|
||||
| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
|
||||
| `search` | string | no | Return list of authorized projects matching the search criteria |
|
||||
| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
|
||||
| `simple` | boolean | no | Return only limited fields for each project. This is a no-op without authentication where only simple fields are returned. |
|
||||
| `starred` | boolean | no | Limit by projects starred by the current user |
|
||||
| `with_issues_enabled` | boolean | no | Limit by projects with issues feature enabled. Default is `false` |
|
||||
| `with_merge_requests_enabled` | boolean | no | Limit by projects with merge requests feature enabled. Default is `false` |
|
||||
|
|
|
@ -61,7 +61,7 @@ GET /projects
|
|||
| `repository_storage` | string | **{dotted-circle}** No | Limit results to projects stored on `repository_storage`. _(administrators only)_ |
|
||||
| `search_namespaces` | boolean | **{dotted-circle}** No | Include ancestor namespaces when matching search criteria. Default is `false`. |
|
||||
| `search` | string | **{dotted-circle}** No | Return list of projects matching the search criteria. |
|
||||
| `simple` | boolean | **{dotted-circle}** No | Return only limited fields for each project. This is a no-op without authentication as then _only_ simple fields are returned. |
|
||||
| `simple` | boolean | **{dotted-circle}** No | Return only limited fields for each project. This is a no-op without authentication where only simple fields are returned. |
|
||||
| `sort` | string | **{dotted-circle}** No | Return projects sorted in `asc` or `desc` order. Default is `desc`. |
|
||||
| `starred` | boolean | **{dotted-circle}** No | Limit by projects starred by the current user. |
|
||||
| `statistics` | boolean | **{dotted-circle}** No | Include project statistics. Only available to Reporter or higher level role members. |
|
||||
|
@ -336,7 +336,7 @@ GET /users/:user_id/projects
|
|||
| `order_by` | string | **{dotted-circle}** No | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at`. |
|
||||
| `owned` | boolean | **{dotted-circle}** No | Limit by projects explicitly owned by the current user. |
|
||||
| `search` | string | **{dotted-circle}** No | Return list of projects matching the search criteria. |
|
||||
| `simple` | boolean | **{dotted-circle}** No | Return only limited fields for each project. This is a no-op without authentication as then _only_ simple fields are returned. |
|
||||
| `simple` | boolean | **{dotted-circle}** No | Return only limited fields for each project. This is a no-op without authentication where only simple fields are returned. |
|
||||
| `sort` | string | **{dotted-circle}** No | Return projects sorted in `asc` or `desc` order. Default is `desc`. |
|
||||
| `starred` | boolean | **{dotted-circle}** No | Limit by projects starred by the current user. |
|
||||
| `statistics` | boolean | **{dotted-circle}** No | Include project statistics. Only available to Reporter or higher level role members. |
|
||||
|
@ -591,7 +591,7 @@ GET /users/:user_id/starred_projects
|
|||
| `order_by` | string | **{dotted-circle}** No | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at`. |
|
||||
| `owned` | boolean | **{dotted-circle}** No | Limit by projects explicitly owned by the current user. |
|
||||
| `search` | string | **{dotted-circle}** No | Return list of projects matching the search criteria. |
|
||||
| `simple` | boolean | **{dotted-circle}** No | Return only limited fields for each project. This is a no-op without authentication as then _only_ simple fields are returned. |
|
||||
| `simple` | boolean | **{dotted-circle}** No | Return only limited fields for each project. This is a no-op without authentication where only simple fields are returned. |
|
||||
| `sort` | string | **{dotted-circle}** No | Return projects sorted in `asc` or `desc` order. Default is `desc`. |
|
||||
| `starred` | boolean | **{dotted-circle}** No | Limit by projects starred by the current user. |
|
||||
| `statistics` | boolean | **{dotted-circle}** No | Include project statistics. Only available to Reporter or higher level role members. |
|
||||
|
@ -1476,7 +1476,7 @@ GET /projects/:id/forks
|
|||
| `order_by` | string | **{dotted-circle}** No | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at`. |
|
||||
| `owned` | boolean | **{dotted-circle}** No | Limit by projects explicitly owned by the current user. |
|
||||
| `search` | string | **{dotted-circle}** No | Return list of projects matching the search criteria. |
|
||||
| `simple` | boolean | **{dotted-circle}** No | Return only limited fields for each project. This is a no-op without authentication as then _only_ simple fields are returned. |
|
||||
| `simple` | boolean | **{dotted-circle}** No | Return only limited fields for each project. This is a no-op without authentication where only simple fields are returned. |
|
||||
| `sort` | string | **{dotted-circle}** No | Return projects sorted in `asc` or `desc` order. Default is `desc`. |
|
||||
| `starred` | boolean | **{dotted-circle}** No | Limit by projects starred by the current user. |
|
||||
| `statistics` | boolean | **{dotted-circle}** No | Include project statistics. Only available to Reporter or higher level role members. |
|
||||
|
|
|
@ -282,8 +282,8 @@ Use one of the following methods to determine the value for `DOCKER_AUTH_CONFIG`
|
|||
configuration JSON manually. Open a terminal and execute the following command:
|
||||
|
||||
```shell
|
||||
# The use of "-n" - prevents encoding a newline in the password.
|
||||
echo -n "my_username:my_password" | base64
|
||||
# The use of printf (as opposed to echo) prevents encoding a newline in the password.
|
||||
printf "my_username:my_password" | openssl base64 -A
|
||||
|
||||
# Example output to copy
|
||||
bXlfdXNlcm5hbWU6bXlfcGFzc3dvcmQ=
|
||||
|
|
|
@ -7,131 +7,151 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
# Vault Authentication with GitLab OpenID Connect **(FREE)**
|
||||
|
||||
[Vault](https://www.vaultproject.io/) is a secrets management application offered by HashiCorp.
|
||||
It allows you to store and manage sensitive information such as secret environment variables, encryption keys, and authentication tokens.
|
||||
Vault offers Identity-based Access, which means Vault users can authenticate through several of their preferred cloud providers.
|
||||
It allows you to store and manage sensitive information such as secret environment
|
||||
variables, encryption keys, and authentication tokens.
|
||||
|
||||
This document explains how Vault users can authenticate themselves through GitLab by utilizing our OpenID authentication feature.
|
||||
The following assumes you already have Vault installed and running.
|
||||
Vault offers Identity-based Access, which means Vault users can authenticate
|
||||
through several of their preferred cloud providers.
|
||||
|
||||
1. **Get the OpenID Connect client ID and secret from GitLab:**
|
||||
The following content explains how Vault users can authenticate themselves through
|
||||
GitLab by using our OpenID authentication feature.
|
||||
|
||||
First you must create a GitLab application to obtain an application ID and secret for authenticating into Vault.
|
||||
To do this, sign in to GitLab and follow these steps:
|
||||
## Prerequisites
|
||||
|
||||
1. In the top-right corner, select your avatar.
|
||||
1. Select **Edit profile**.
|
||||
1. On the left sidebar, select **Applications**.
|
||||
1. Fill out the application **Name** and [**Redirect URI**](https://www.vaultproject.io/docs/auth/jwt#redirect-uris).
|
||||
1. Select the **OpenID** scope.
|
||||
1. Select **Save application**.
|
||||
1. Copy client ID and secret, or keep the page open for reference.
|
||||
1. [Install Vault](https://www.vaultproject.io/docs/install).
|
||||
1. Run Vault.
|
||||
|
||||
![GitLab OAuth provider](img/gitlab_oauth_vault_v12_6.png)
|
||||
## Get the OpenID Connect client ID and secret from GitLab
|
||||
|
||||
1. **Enable OIDC auth on Vault:**
|
||||
First you must create a GitLab application to obtain an application ID and secret
|
||||
for authenticating into Vault. To do this, sign in to GitLab and follow these steps:
|
||||
|
||||
OpenID Connect is not enabled in Vault by default. This needs to be enabled in the terminal.
|
||||
1. In the top-right corner, select your avatar.
|
||||
1. Select **Edit profile**.
|
||||
1. On the left sidebar, select **Applications**.
|
||||
1. Fill out the application **Name** and [**Redirect URI**](https://www.vaultproject.io/docs/auth/jwt#redirect-uris).
|
||||
1. Select the **OpenID** scope.
|
||||
1. Select **Save application**.
|
||||
1. Copy the **Client ID** and **Client Secret**, or keep the page open for reference.
|
||||
|
||||
Open a terminal session and run the following command to enable the OpenID Connect authentication provider in Vault:
|
||||
![GitLab OAuth provider](img/gitlab_oauth_vault_v12_6.png)
|
||||
|
||||
```shell
|
||||
vault auth enable oidc
|
||||
```
|
||||
## Enable OpenID Connect on Vault
|
||||
|
||||
You should see the following output in the terminal:
|
||||
OpenID Connect (OIDC) is not enabled in Vault by default.
|
||||
|
||||
```plaintext
|
||||
Success! Enabled oidc auth method at: oidc/
|
||||
```
|
||||
To enable the OIDC authentication provider in Vault, open a terminal session
|
||||
and run the following command:
|
||||
|
||||
1. **Write the OIDC configuration:**
|
||||
```shell
|
||||
vault auth enable oidc
|
||||
```
|
||||
|
||||
Next, Vault needs to be given the application ID and secret generated by GitLab.
|
||||
You should see the following output in the terminal:
|
||||
|
||||
In the terminal session, run the following command to give Vault access to the GitLab application you've just created with an OpenID scope. This allows Vault to authenticate through GitLab.
|
||||
```plaintext
|
||||
Success! Enabled oidc auth method at: oidc/
|
||||
```
|
||||
|
||||
Replace `your_application_id` and `your_secret` in the example below with the application ID and secret generated for your app:
|
||||
## Write the OIDC configuration
|
||||
|
||||
```shell
|
||||
$ vault write auth/oidc/config \
|
||||
oidc_discovery_url="https://gitlab.com" \
|
||||
oidc_client_id="your_application_id" \
|
||||
oidc_client_secret="your_secret" \
|
||||
default_role="demo" \
|
||||
bound_issuer="localhost"
|
||||
```
|
||||
To give Vault the application ID and secret generated by GitLab and allow
|
||||
Vault to authenticate through GitLab, run the following command in the terminal:
|
||||
|
||||
You should see the following output in the terminal:
|
||||
```shell
|
||||
$ vault write auth/oidc/config \
|
||||
oidc_discovery_url="https://gitlab.com" \
|
||||
oidc_client_id="<your_application_id>" \
|
||||
oidc_client_secret="<your_secret>" \
|
||||
default_role="demo" \
|
||||
bound_issuer="localhost"
|
||||
```
|
||||
|
||||
```shell
|
||||
Success! Data written to: auth/oidc/config
|
||||
```
|
||||
Replace `<your_application_id>` and `<your_secret>` with the application ID
|
||||
and secret generated for your app.
|
||||
|
||||
1. **Write the OIDC Role Configuration:**
|
||||
You should see the following output in the terminal:
|
||||
|
||||
Now that Vault has a GitLab application ID and secret, it needs to know the [**Redirect URIs**](https://www.vaultproject.io/docs/auth/jwt#redirect-uris) and scopes given to GitLab during the application creation process. The redirect URIs need to match where your Vault instance is running. The `oidc_scopes` field needs to include the `openid`. Similarly to the previous step, replace `your_application_id` with the generated application ID from GitLab:
|
||||
```shell
|
||||
Success! Data written to: auth/oidc/config
|
||||
```
|
||||
|
||||
This configuration is saved under the name of the role you are creating. In this case, we are creating a `demo` role. Later, we show how you can access this role through the Vault CLI.
|
||||
## Write the OIDC role configuration
|
||||
|
||||
WARNING:
|
||||
If you're using a public GitLab instance (GitLab.com or any other instance publicly
|
||||
accessible), it's paramount to specify the `bound_claims` to allow access only to
|
||||
members of your group/project. Otherwise, anyone with a public account can access
|
||||
your Vault instance.
|
||||
You must tell Vault the [**Redirect URIs**](https://www.vaultproject.io/docs/auth/jwt#redirect-uris)
|
||||
and scopes given to GitLab when you created the application.
|
||||
|
||||
```shell
|
||||
vault write auth/oidc/role/demo -<<EOF
|
||||
{
|
||||
"user_claim": "sub",
|
||||
"allowed_redirect_uris": "your_vault_instance_redirect_uris",
|
||||
"bound_audiences": "your_application_id",
|
||||
"oidc_scopes": "openid",
|
||||
"role_type": "oidc",
|
||||
"policies": "demo",
|
||||
"ttl": "1h",
|
||||
"bound_claims": { "groups": ["yourGroup/yourSubgrup"] }
|
||||
}
|
||||
EOF
|
||||
```
|
||||
Run the following command in the terminal:
|
||||
|
||||
1. **Sign in to Vault:**
|
||||
```shell
|
||||
vault write auth/oidc/role/demo -<<EOF
|
||||
{
|
||||
"user_claim": "sub",
|
||||
"allowed_redirect_uris": "<your_vault_instance_redirect_uris>",
|
||||
"bound_audiences": "<your_application_id>",
|
||||
"oidc_scopes": "<openid>",
|
||||
"role_type": "oidc",
|
||||
"policies": "demo",
|
||||
"ttl": "1h",
|
||||
"bound_claims": { "groups": ["<yourGroup/yourSubgrup>"] }
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
1. Go to your Vault UI (example: [http://127.0.0.1:8200/ui/vault/auth?with=oidc](http://127.0.0.1:8200/ui/vault/auth?with=oidc)).
|
||||
1. If the `OIDC` method is not currently selected, open the dropdown and select it.
|
||||
1. Select **Sign in With GitLab**, which opens a modal window:
|
||||
Replace:
|
||||
|
||||
![Sign into Vault with GitLab](img/sign_into_vault_with_gitlab_v12_6.png)
|
||||
- `<your_vault_instance_redirect_uris>` with redirect URIs that match where your
|
||||
Vault instance is running.
|
||||
- `<your_application_id>` with the application ID generated for your app.
|
||||
|
||||
1. Select **Authorize** to allow Vault to sign in through GitLab. This redirects you back to your Vault UI as a signed-in user.
|
||||
The `oidc_scopes` field must include `openid`.
|
||||
|
||||
![Authorize Vault to connect with GitLab](img/authorize_vault_with_gitlab_v12_6.png)
|
||||
This configuration is saved under the name of the role you are creating. In this
|
||||
example, we are creating a `demo` role.
|
||||
|
||||
1. **Sign in using the Vault CLI** (optional):
|
||||
WARNING:
|
||||
If you're using a public GitLab instance, such as GitLab.com, you must specify
|
||||
the `bound_claims` to allow access only to members of your group or project.
|
||||
Otherwise, anyone with a public account can access your Vault instance.
|
||||
|
||||
Vault also allows you to sign in via their CLI.
|
||||
## Sign in to Vault
|
||||
|
||||
After writing the same configurations from above, you can run the command below in your terminal to sign in with the role configuration created in step 4 above:
|
||||
1. Go to your Vault UI. For example: [http://127.0.0.1:8200/ui/vault/auth?with=oidc](http://127.0.0.1:8200/ui/vault/auth?with=oidc).
|
||||
1. If the `OIDC` method is not selected, open the dropdown list and select it.
|
||||
1. Select **Sign in With GitLab**, which opens a modal window:
|
||||
|
||||
![Sign into Vault with GitLab](img/sign_into_vault_with_gitlab_v12_6.png)
|
||||
|
||||
1. To allow Vault to sign in through GitLab, select **Authorize**. This redirects you back to your Vault UI as a signed-in user.
|
||||
|
||||
![Authorize Vault to connect with GitLab](img/authorize_vault_with_gitlab_v12_6.png)
|
||||
|
||||
## Sign in using the Vault CLI (optional)
|
||||
|
||||
You can also sign into Vault using the [Vault CLI](https://www.vaultproject.io/docs/commands).
|
||||
|
||||
1. To sign in with the role configuration you created in the previous example,
|
||||
run the following command in your terminal:
|
||||
|
||||
```shell
|
||||
vault login -method=oidc port=8250 role=demo
|
||||
```
|
||||
|
||||
Here's a short explanation of what this command does:
|
||||
This command sets:
|
||||
|
||||
1. In the **Write the OIDC Role Configuration** (step 4), we created a role called
|
||||
`demo`. We set `role=demo` so Vault knows which configuration we'd like to
|
||||
sign in with.
|
||||
1. To set Vault to use the `OIDC` sign-in method, we set `-method=oidc`.
|
||||
1. To set the port that GitLab should redirect to, we set `port=8250` or
|
||||
another port number that matches the port given to GitLab when listing
|
||||
[Redirect URIs](https://www.vaultproject.io/docs/auth/jwt#redirect-uris).
|
||||
- `role=demo` so Vault knows which configuration we'd like to sign in with.
|
||||
- `-method=oidc` to set Vault to use the `OIDC` sign-in method.
|
||||
- `port=8250` to set the port that GitLab should redirect to. This port
|
||||
number must match the port given to GitLab when listing
|
||||
[Redirect URIs](https://www.vaultproject.io/docs/auth/jwt#redirect-uris).
|
||||
|
||||
After running the command, it presents a link in the terminal.
|
||||
Select the link in the terminal and a browser tab opens that confirms you're signed into Vault via OIDC:
|
||||
After running this command, you should see a link in the terminal.
|
||||
|
||||
1. Open this link in a web browser:
|
||||
|
||||
![Signed into Vault via OIDC](img/signed_into_vault_via_oidc_v12_6.png)
|
||||
|
||||
The terminal outputs:
|
||||
You should see in the terminal:
|
||||
|
||||
```plaintext
|
||||
Success! You are now authenticated. The token information displayed below
|
||||
|
|
|
@ -392,6 +392,8 @@ The following table, while not exhaustive, shows some examples of the supported
|
|||
upgrade paths.
|
||||
Additional steps between the mentioned versions are possible. We list the minimally necessary steps only.
|
||||
|
||||
For a dynamic view of examples of supported upgrade paths, try the [Upgrade Path tool](https://gitlab-com.gitlab.io/support/toolbox/upgrade-path/). The Upgrade Path tool is maintained by the [GitLab Support team](https://about.gitlab.com/handbook/support/#about-the-support-team). Share feedback and help improve the tool by raising an issue or MR in the [upgrade-path project](https://gitlab.com/gitlab-com/support/toolbox/upgrade-path).
|
||||
|
||||
| Target version | Your version | Supported upgrade path | Note |
|
||||
| -------------- | ------------ | ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `15.1.0` | `14.6.2` | `14.6.2` -> `14.9.5` -> `14.10.5` -> `15.0.2` -> `15.1.0` | Three intermediate versions are required: `14.9` and `14.10`, `15.0`, then `15.1.0`. |
|
||||
|
|
|
@ -180,8 +180,9 @@ Prerequisites:
|
|||
To promote a project milestone:
|
||||
|
||||
1. On the top bar, select **Main menu > Projects** and find your project.
|
||||
1. On the left sidebar, select **Issues > Milestones**.
|
||||
1. Either:
|
||||
- Select **Promote to Group Milestone** (**{level-up}**).
|
||||
- Select **Promote to Group Milestone** (**{level-up}**) next to the milestone you want to promote.
|
||||
- Select the milestone title, and then select **Promote**.
|
||||
1. Select **Promote Milestone**.
|
||||
|
||||
|
|
|
@ -118,7 +118,8 @@ threads. Some quick actions might not be available to all subscription tiers.
|
|||
| `/unapprove` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Unapprove the merge request. ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/8103) in GitLab 14.3 |
|
||||
| `/unassign @user1 @user2` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No | Remove specific assignees. |
|
||||
| `/unassign` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Remove all assignees. |
|
||||
| `/unassign_reviewer @user1 @user2` or `/remove_reviewer @user1 @user2` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Remove specific reviewers. |
|
||||
| `/unassign_reviewer @user1 @user2` or `/remove_reviewer @user1 @user2` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Remove specific reviewers.
|
||||
| `/unassign_reviewer me` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Remove yourself as a reviewer. |
|
||||
| `/unassign_reviewer` or `/remove_reviewer` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Remove all reviewers. |
|
||||
| `/unlabel ~label1 ~label2` or `/remove_label ~label1 ~label2` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Remove specified labels. |
|
||||
| `/unlabel` or `/remove_label` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Remove all labels. |
|
||||
|
|
|
@ -56,3 +56,5 @@ module Banzai
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
Banzai::Filter::MarkdownEngines::CommonMark.prepend_mod
|
||||
|
|
|
@ -449,6 +449,7 @@ projects: :gitlab_main
|
|||
projects_sync_events: :gitlab_main
|
||||
project_statistics: :gitlab_main
|
||||
project_topics: :gitlab_main
|
||||
project_wiki_repository_states: :gitlab_main
|
||||
prometheus_alert_events: :gitlab_main
|
||||
prometheus_alerts: :gitlab_main
|
||||
prometheus_metrics: :gitlab_main
|
||||
|
|
|
@ -55,7 +55,6 @@ module Gitlab
|
|||
push_frontend_feature_flag(:security_auto_fix)
|
||||
push_frontend_feature_flag(:new_header_search)
|
||||
push_frontend_feature_flag(:source_editor_toolbar)
|
||||
push_frontend_feature_flag(:gl_avatar_for_all_user_avatars)
|
||||
push_frontend_feature_flag(:gl_listbox_for_sort_dropdowns)
|
||||
end
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ module QA
|
|||
def self.included(base)
|
||||
super
|
||||
|
||||
base.view 'app/views/shared/groups/_search_form.html.haml' do
|
||||
base.view 'app/assets/javascripts/groups/components/overview_tabs.vue' do
|
||||
element :groups_filter_field
|
||||
end
|
||||
|
||||
|
|
|
@ -23,7 +23,6 @@ RSpec.describe 'Project issue boards', :js do
|
|||
project.add_maintainer(user2)
|
||||
|
||||
sign_in(user)
|
||||
stub_feature_flags(gl_avatar_for_all_user_avatars: false)
|
||||
|
||||
set_cookie('sidebar_collapsed', 'true')
|
||||
end
|
||||
|
|
|
@ -10,7 +10,6 @@ RSpec.describe 'User scrolls to deep-linked note' do
|
|||
|
||||
context 'on issue page', :js do
|
||||
it 'on comment' do
|
||||
stub_feature_flags(gl_avatar_for_all_user_avatars: false)
|
||||
visit project_issue_path(project, issue, anchor: "note_#{comment_1.id}")
|
||||
|
||||
wait_for_requests
|
||||
|
|
|
@ -25,7 +25,6 @@ RSpec.describe 'Merge request > User sees avatars on diff notes', :js do
|
|||
before do
|
||||
project.add_maintainer(user)
|
||||
sign_in user
|
||||
stub_feature_flags(gl_avatar_for_all_user_avatars: false)
|
||||
|
||||
set_cookie('sidebar_collapsed', 'true')
|
||||
end
|
||||
|
|
|
@ -24,7 +24,6 @@ RSpec.describe 'User sorts projects and order persists' do
|
|||
end
|
||||
|
||||
it "is set on the group_canonical_path" do
|
||||
stub_feature_flags(group_overview_tabs_vue: false)
|
||||
visit(group_canonical_path(group))
|
||||
|
||||
within '[data-testid=group_sort_by_dropdown]' do
|
||||
|
@ -33,7 +32,6 @@ RSpec.describe 'User sorts projects and order persists' do
|
|||
end
|
||||
|
||||
it "is set on the details_group_path" do
|
||||
stub_feature_flags(group_overview_tabs_vue: false)
|
||||
visit(details_group_path(group))
|
||||
|
||||
within '[data-testid=group_sort_by_dropdown]' do
|
||||
|
@ -42,7 +40,7 @@ RSpec.describe 'User sorts projects and order persists' do
|
|||
end
|
||||
end
|
||||
|
||||
context "from explore projects" do
|
||||
context "from explore projects", :js do
|
||||
before do
|
||||
stub_feature_flags(gl_listbox_for_sort_dropdowns: false)
|
||||
sign_in(user)
|
||||
|
@ -51,10 +49,10 @@ RSpec.describe 'User sorts projects and order persists' do
|
|||
first(:link, 'Updated date').click
|
||||
end
|
||||
|
||||
it_behaves_like "sort order persists across all views", 'Updated date', 'Updated date'
|
||||
it_behaves_like "sort order persists across all views", 'Updated date', 'Updated'
|
||||
end
|
||||
|
||||
context 'from dashboard projects' do
|
||||
context 'from dashboard projects', :js do
|
||||
before do
|
||||
stub_feature_flags(gl_listbox_for_sort_dropdowns: false)
|
||||
sign_in(user)
|
||||
|
@ -68,31 +66,29 @@ RSpec.describe 'User sorts projects and order persists' do
|
|||
|
||||
context 'from group homepage', :js do
|
||||
before do
|
||||
stub_feature_flags(gl_listbox_for_sort_dropdowns: false)
|
||||
stub_feature_flags(group_overview_tabs_vue: false)
|
||||
sign_in(user)
|
||||
visit(group_canonical_path(group))
|
||||
within '[data-testid=group_sort_by_dropdown]' do
|
||||
find('button.gl-dropdown-toggle').click
|
||||
first(:button, 'Last created').click
|
||||
first(:button, 'Created').click
|
||||
wait_for_requests
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like "sort order persists across all views", "Created date", "Last created"
|
||||
it_behaves_like "sort order persists across all views", "Created date", "Created"
|
||||
end
|
||||
|
||||
context 'from group details', :js do
|
||||
before do
|
||||
stub_feature_flags(gl_listbox_for_sort_dropdowns: false)
|
||||
stub_feature_flags(group_overview_tabs_vue: false)
|
||||
sign_in(user)
|
||||
visit(details_group_path(group))
|
||||
within '[data-testid=group_sort_by_dropdown]' do
|
||||
find('button.gl-dropdown-toggle').click
|
||||
first(:button, 'Most stars').click
|
||||
first(:button, 'Stars').click
|
||||
wait_for_requests
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like "sort order persists across all views", "Stars", "Most stars"
|
||||
it_behaves_like "sort order persists across all views", "Stars", "Stars"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -82,7 +82,7 @@ describe('diffs/components/commit_item', () => {
|
|||
const imgElement = avatarElement.find('img');
|
||||
|
||||
expect(avatarElement.attributes('href')).toBe(commit.author.web_url);
|
||||
expect(imgElement.classes()).toContain('s32');
|
||||
expect(imgElement.classes()).toContain('gl-avatar-s32');
|
||||
expect(imgElement.attributes('alt')).toBe(commit.author.name);
|
||||
expect(imgElement.attributes('src')).toBe(commit.author.avatar_url);
|
||||
});
|
||||
|
|
|
@ -440,6 +440,10 @@ describe('AppComponent', () => {
|
|||
expect(eventHub.$on).toHaveBeenCalledWith('showLeaveGroupModal', expect.any(Function));
|
||||
expect(eventHub.$on).toHaveBeenCalledWith('updatePagination', expect.any(Function));
|
||||
expect(eventHub.$on).toHaveBeenCalledWith('updateGroups', expect.any(Function));
|
||||
expect(eventHub.$on).toHaveBeenCalledWith(
|
||||
'fetchFilteredAndSortedGroups',
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `false`', async () => {
|
||||
|
@ -468,6 +472,46 @@ describe('AppComponent', () => {
|
|||
expect(eventHub.$off).toHaveBeenCalledWith('showLeaveGroupModal', expect.any(Function));
|
||||
expect(eventHub.$off).toHaveBeenCalledWith('updatePagination', expect.any(Function));
|
||||
expect(eventHub.$off).toHaveBeenCalledWith('updateGroups', expect.any(Function));
|
||||
expect(eventHub.$off).toHaveBeenCalledWith(
|
||||
'fetchFilteredAndSortedGroups',
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when `fetchFilteredAndSortedGroups` event is emitted', () => {
|
||||
const search = 'Foo bar';
|
||||
const sort = 'created_asc';
|
||||
const emitFetchFilteredAndSortedGroups = () => {
|
||||
eventHub.$emit('fetchFilteredAndSortedGroups', {
|
||||
filterGroupsBy: search,
|
||||
sortBy: sort,
|
||||
});
|
||||
};
|
||||
let setPaginationInfoSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
setPaginationInfoSpy = jest.spyOn(GroupsStore.prototype, 'setPaginationInfo');
|
||||
createShallowComponent();
|
||||
});
|
||||
|
||||
it('renders loading icon', async () => {
|
||||
emitFetchFilteredAndSortedGroups();
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('calls API with expected params', () => {
|
||||
emitFetchFilteredAndSortedGroups();
|
||||
|
||||
expect(getGroupsSpy).toHaveBeenCalledWith(undefined, undefined, search, sort, undefined);
|
||||
});
|
||||
|
||||
it('updates pagination', () => {
|
||||
emitFetchFilteredAndSortedGroups();
|
||||
|
||||
expect(setPaginationInfoSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { GlTab } from '@gitlab/ui';
|
||||
import { GlSorting, GlSortingItem, GlTab } from '@gitlab/ui';
|
||||
import { nextTick } from 'vue';
|
||||
import { createLocalVue } from '@vue/test-utils';
|
||||
import AxiosMockAdapter from 'axios-mock-adapter';
|
||||
|
@ -9,25 +9,38 @@ import GroupFolderComponent from '~/groups/components/group_folder.vue';
|
|||
import GroupsStore from '~/groups/store/groups_store';
|
||||
import GroupsService from '~/groups/service/groups_service';
|
||||
import { createRouter } from '~/groups/init_overview_tabs';
|
||||
import eventHub from '~/groups/event_hub';
|
||||
import {
|
||||
ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
|
||||
ACTIVE_TAB_SHARED,
|
||||
ACTIVE_TAB_ARCHIVED,
|
||||
OVERVIEW_TABS_SORTING_ITEMS,
|
||||
} from '~/groups/constants';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.component('GroupFolder', GroupFolderComponent);
|
||||
const router = createRouter();
|
||||
const [SORTING_ITEM_NAME, , SORTING_ITEM_UPDATED] = OVERVIEW_TABS_SORTING_ITEMS;
|
||||
|
||||
describe('OverviewTabs', () => {
|
||||
let wrapper;
|
||||
let axiosMock;
|
||||
|
||||
const endpoints = {
|
||||
subgroups_and_projects: '/groups/foobar/-/children.json',
|
||||
shared: '/groups/foobar/-/shared_projects.json',
|
||||
archived: '/groups/foobar/-/children.json?archived=only',
|
||||
const defaultProvide = {
|
||||
endpoints: {
|
||||
subgroups_and_projects: '/groups/foobar/-/children.json',
|
||||
shared: '/groups/foobar/-/shared_projects.json',
|
||||
archived: '/groups/foobar/-/children.json?archived=only',
|
||||
},
|
||||
newSubgroupPath: '/groups/new',
|
||||
newProjectPath: 'projects/new',
|
||||
newSubgroupIllustration: '',
|
||||
newProjectIllustration: '',
|
||||
emptySubgroupIllustration: '',
|
||||
canCreateSubgroups: false,
|
||||
canCreateProjects: false,
|
||||
initialSort: 'name_asc',
|
||||
};
|
||||
|
||||
const routerMock = {
|
||||
|
@ -36,18 +49,13 @@ describe('OverviewTabs', () => {
|
|||
|
||||
const createComponent = async ({
|
||||
route = { name: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, params: { group: 'foo/bar/baz' } },
|
||||
provide = {},
|
||||
} = {}) => {
|
||||
wrapper = mountExtended(OverviewTabs, {
|
||||
router,
|
||||
provide: {
|
||||
endpoints,
|
||||
newSubgroupPath: '/groups/new',
|
||||
newProjectPath: 'projects/new',
|
||||
newSubgroupIllustration: '',
|
||||
newProjectIllustration: '',
|
||||
emptySubgroupIllustration: '',
|
||||
canCreateSubgroups: false,
|
||||
canCreateProjects: false,
|
||||
...defaultProvide,
|
||||
...provide,
|
||||
},
|
||||
localVue,
|
||||
mocks: { $route: route, $router: routerMock },
|
||||
|
@ -81,7 +89,7 @@ describe('OverviewTabs', () => {
|
|||
expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({
|
||||
action: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
|
||||
store: new GroupsStore({ showSchemaMarkup: true }),
|
||||
service: new GroupsService(endpoints[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]),
|
||||
service: new GroupsService(defaultProvide.endpoints[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]),
|
||||
hideProjects: false,
|
||||
renderEmptyState: true,
|
||||
});
|
||||
|
@ -102,7 +110,7 @@ describe('OverviewTabs', () => {
|
|||
expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({
|
||||
action: ACTIVE_TAB_SHARED,
|
||||
store: new GroupsStore(),
|
||||
service: new GroupsService(endpoints[ACTIVE_TAB_SHARED]),
|
||||
service: new GroupsService(defaultProvide.endpoints[ACTIVE_TAB_SHARED]),
|
||||
hideProjects: false,
|
||||
renderEmptyState: false,
|
||||
});
|
||||
|
@ -125,7 +133,7 @@ describe('OverviewTabs', () => {
|
|||
expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({
|
||||
action: ACTIVE_TAB_ARCHIVED,
|
||||
store: new GroupsStore(),
|
||||
service: new GroupsService(endpoints[ACTIVE_TAB_ARCHIVED]),
|
||||
service: new GroupsService(defaultProvide.endpoints[ACTIVE_TAB_ARCHIVED]),
|
||||
hideProjects: false,
|
||||
renderEmptyState: false,
|
||||
});
|
||||
|
@ -197,4 +205,109 @@ describe('OverviewTabs', () => {
|
|||
expect(routerMock.push).toHaveBeenCalledWith(expectedRoute);
|
||||
});
|
||||
});
|
||||
|
||||
describe('searching and sorting', () => {
|
||||
const setup = async () => {
|
||||
jest.spyOn(eventHub, '$emit');
|
||||
await createComponent();
|
||||
|
||||
// Click through tabs so they are all loaded
|
||||
await findTab(OverviewTabs.i18n[ACTIVE_TAB_SHARED]).trigger('click');
|
||||
await findTab(OverviewTabs.i18n[ACTIVE_TAB_ARCHIVED]).trigger('click');
|
||||
await findTab(OverviewTabs.i18n[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]).trigger('click');
|
||||
};
|
||||
|
||||
const sharedAssertions = ({ search, sort }) => {
|
||||
it('sets `lazy` prop to `true` for all of the non-active tabs so they are reloaded after sort or search is applied', () => {
|
||||
expect(findTabPanels().at(0).vm.$attrs.lazy).toBe(false);
|
||||
expect(findTabPanels().at(1).vm.$attrs.lazy).toBe(true);
|
||||
expect(findTabPanels().at(2).vm.$attrs.lazy).toBe(true);
|
||||
});
|
||||
|
||||
it('emits `fetchFilteredAndSortedGroups` event from `eventHub`', () => {
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith(
|
||||
`${ACTIVE_TAB_SUBGROUPS_AND_PROJECTS}fetchFilteredAndSortedGroups`,
|
||||
{
|
||||
filterGroupsBy: search,
|
||||
sortBy: sort,
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
describe('when search is typed in', () => {
|
||||
const search = 'Foo bar';
|
||||
|
||||
beforeEach(async () => {
|
||||
await setup();
|
||||
await wrapper.findByPlaceholderText(OverviewTabs.i18n.searchPlaceholder).setValue(search);
|
||||
});
|
||||
|
||||
it('updates query string with `filter` key', () => {
|
||||
expect(routerMock.push).toHaveBeenCalledWith({ query: { filter: search } });
|
||||
});
|
||||
|
||||
sharedAssertions({ search, sort: defaultProvide.initialSort });
|
||||
});
|
||||
|
||||
describe('when sort is changed', () => {
|
||||
beforeEach(async () => {
|
||||
await setup();
|
||||
wrapper.findAllComponents(GlSortingItem).at(2).vm.$emit('click');
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
it('updates query string with `sort` key', () => {
|
||||
expect(routerMock.push).toHaveBeenCalledWith({
|
||||
query: { sort: SORTING_ITEM_UPDATED.asc },
|
||||
});
|
||||
});
|
||||
|
||||
sharedAssertions({ search: '', sort: SORTING_ITEM_UPDATED.asc });
|
||||
});
|
||||
|
||||
describe('when sort direction is changed', () => {
|
||||
beforeEach(async () => {
|
||||
await setup();
|
||||
await wrapper
|
||||
.findByRole('button', { name: 'Sorting Direction: Ascending' })
|
||||
.trigger('click');
|
||||
});
|
||||
|
||||
it('updates query string with `sort` key', () => {
|
||||
expect(routerMock.push).toHaveBeenCalledWith({
|
||||
query: { sort: SORTING_ITEM_NAME.desc },
|
||||
});
|
||||
});
|
||||
|
||||
sharedAssertions({ search: '', sort: SORTING_ITEM_NAME.desc });
|
||||
});
|
||||
|
||||
describe('when `filter` and `sort` query strings are set', () => {
|
||||
beforeEach(async () => {
|
||||
await createComponent({
|
||||
route: {
|
||||
name: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
|
||||
params: { group: 'foo/bar/baz' },
|
||||
query: { filter: 'Foo bar', sort: SORTING_ITEM_UPDATED.desc },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('sets value of search input', () => {
|
||||
expect(
|
||||
wrapper.findByPlaceholderText(OverviewTabs.i18n.searchPlaceholder).element.value,
|
||||
).toBe('Foo bar');
|
||||
});
|
||||
|
||||
it('sets sort dropdown', () => {
|
||||
expect(wrapper.findComponent(GlSorting).props()).toMatchObject({
|
||||
text: SORTING_ITEM_UPDATED.label,
|
||||
isAscending: false,
|
||||
});
|
||||
|
||||
expect(wrapper.findAllComponents(GlSortingItem).at(2).vm.$attrs.active).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@ exports[`Repository last commit component renders commit widget 1`] = `
|
|||
<user-avatar-link-stub
|
||||
class="gl-my-2 gl-mr-4"
|
||||
imgalt=""
|
||||
imgcssclasses="gl-mr-0!"
|
||||
imgcssclasses=""
|
||||
imgsize="32"
|
||||
imgsrc="https://test.com"
|
||||
linkhref="/test"
|
||||
|
|
|
@ -1,134 +0,0 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlAvatar, GlTooltip } from '@gitlab/ui';
|
||||
import defaultAvatarUrl from 'images/no_avatar.png';
|
||||
import { placeholderImage } from '~/lazy_loader';
|
||||
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image_new.vue';
|
||||
|
||||
jest.mock('images/no_avatar.png', () => 'default-avatar-url');
|
||||
|
||||
const PROVIDED_PROPS = {
|
||||
size: 32,
|
||||
imgSrc: 'myavatarurl.com',
|
||||
imgAlt: 'mydisplayname',
|
||||
cssClasses: 'myextraavatarclass',
|
||||
tooltipText: 'tooltip text',
|
||||
tooltipPlacement: 'bottom',
|
||||
};
|
||||
|
||||
describe('User Avatar Image Component', () => {
|
||||
let wrapper;
|
||||
|
||||
const findAvatar = () => wrapper.findComponent(GlAvatar);
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('Initialization', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(UserAvatarImage, {
|
||||
propsData: {
|
||||
...PROVIDED_PROPS,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should render `GlAvatar` and provide correct properties to it', () => {
|
||||
expect(findAvatar().attributes('data-src')).toBe(
|
||||
`${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
|
||||
);
|
||||
expect(findAvatar().props()).toMatchObject({
|
||||
src: `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
|
||||
alt: PROVIDED_PROPS.imgAlt,
|
||||
size: PROVIDED_PROPS.size,
|
||||
});
|
||||
});
|
||||
|
||||
it('should add correct CSS classes', () => {
|
||||
const classes = wrapper.findComponent(GlAvatar).classes();
|
||||
expect(classes).toContain(PROVIDED_PROPS.cssClasses);
|
||||
expect(classes).not.toContain('lazy');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Initialization when lazy', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(UserAvatarImage, {
|
||||
propsData: {
|
||||
...PROVIDED_PROPS,
|
||||
lazy: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should add lazy attributes', () => {
|
||||
expect(findAvatar().classes()).toContain('lazy');
|
||||
expect(findAvatar().attributes()).toMatchObject({
|
||||
src: placeholderImage,
|
||||
'data-src': `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
|
||||
});
|
||||
});
|
||||
|
||||
it('should use maximum number when size is provided as an object', () => {
|
||||
wrapper = shallowMount(UserAvatarImage, {
|
||||
propsData: {
|
||||
...PROVIDED_PROPS,
|
||||
size: { default: 16, md: 64, lg: 24 },
|
||||
lazy: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(findAvatar().attributes('data-src')).toBe(`${PROVIDED_PROPS.imgSrc}?width=${64}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Initialization without src', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(UserAvatarImage, {
|
||||
propsData: {
|
||||
...PROVIDED_PROPS,
|
||||
imgSrc: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should have default avatar image', () => {
|
||||
expect(findAvatar().props('src')).toBe(`${defaultAvatarUrl}?width=${PROVIDED_PROPS.size}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dynamic tooltip content', () => {
|
||||
const slots = {
|
||||
default: ['Action!'],
|
||||
};
|
||||
|
||||
describe('when `tooltipText` is provided and no default slot', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(UserAvatarImage, {
|
||||
propsData: { ...PROVIDED_PROPS },
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the tooltip with `tooltipText` as content', () => {
|
||||
expect(wrapper.findComponent(GlTooltip).text()).toBe(PROVIDED_PROPS.tooltipText);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when `tooltipText` and default slot is provided', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(UserAvatarImage, {
|
||||
propsData: { ...PROVIDED_PROPS },
|
||||
slots,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not render `tooltipText` inside the tooltip', () => {
|
||||
expect(wrapper.findComponent(GlTooltip).text()).not.toBe(PROVIDED_PROPS.tooltipText);
|
||||
});
|
||||
|
||||
it('renders the content provided via default slot', () => {
|
||||
expect(wrapper.findComponent(GlTooltip).text()).toContain(slots.default[0]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,127 +0,0 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlTooltip } from '@gitlab/ui';
|
||||
import defaultAvatarUrl from 'images/no_avatar.png';
|
||||
import { placeholderImage } from '~/lazy_loader';
|
||||
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image_old.vue';
|
||||
|
||||
jest.mock('images/no_avatar.png', () => 'default-avatar-url');
|
||||
|
||||
const PROVIDED_PROPS = {
|
||||
size: 32,
|
||||
imgSrc: 'myavatarurl.com',
|
||||
imgAlt: 'mydisplayname',
|
||||
cssClasses: 'myextraavatarclass',
|
||||
tooltipText: 'tooltip text',
|
||||
tooltipPlacement: 'bottom',
|
||||
};
|
||||
|
||||
const DEFAULT_PROPS = {
|
||||
size: 20,
|
||||
};
|
||||
|
||||
describe('User Avatar Image Component', () => {
|
||||
let wrapper;
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('Initialization', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(UserAvatarImage, {
|
||||
propsData: {
|
||||
...PROVIDED_PROPS,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should have <img> as a child element', () => {
|
||||
const imageElement = wrapper.find('img');
|
||||
|
||||
expect(imageElement.exists()).toBe(true);
|
||||
expect(imageElement.attributes('src')).toBe(
|
||||
`${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
|
||||
);
|
||||
expect(imageElement.attributes('data-src')).toBe(
|
||||
`${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
|
||||
);
|
||||
expect(imageElement.attributes('alt')).toBe(PROVIDED_PROPS.imgAlt);
|
||||
});
|
||||
|
||||
it('should properly render img css', () => {
|
||||
const classes = wrapper.find('img').classes();
|
||||
expect(classes).toEqual(['avatar', 's32', PROVIDED_PROPS.cssClasses]);
|
||||
expect(classes).not.toContain('lazy');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Initialization when lazy', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(UserAvatarImage, {
|
||||
propsData: {
|
||||
...PROVIDED_PROPS,
|
||||
lazy: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should add lazy attributes', () => {
|
||||
const imageElement = wrapper.find('img');
|
||||
|
||||
expect(imageElement.classes()).toContain('lazy');
|
||||
expect(imageElement.attributes('src')).toBe(placeholderImage);
|
||||
expect(imageElement.attributes('data-src')).toBe(
|
||||
`${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Initialization without src', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(UserAvatarImage);
|
||||
});
|
||||
|
||||
it('should have default avatar image', () => {
|
||||
const imageElement = wrapper.find('img');
|
||||
|
||||
expect(imageElement.attributes('src')).toBe(
|
||||
`${defaultAvatarUrl}?width=${DEFAULT_PROPS.size}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dynamic tooltip content', () => {
|
||||
const slots = {
|
||||
default: ['Action!'],
|
||||
};
|
||||
|
||||
describe('when `tooltipText` is provided and no default slot', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(UserAvatarImage, {
|
||||
propsData: { ...PROVIDED_PROPS },
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the tooltip with `tooltipText` as content', () => {
|
||||
expect(wrapper.findComponent(GlTooltip).text()).toBe(PROVIDED_PROPS.tooltipText);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when `tooltipText` and default slot is provided', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(UserAvatarImage, {
|
||||
propsData: { ...PROVIDED_PROPS },
|
||||
slots,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not render `tooltipText` inside the tooltip', () => {
|
||||
expect(wrapper.findComponent(GlTooltip).text()).not.toBe(PROVIDED_PROPS.tooltipText);
|
||||
});
|
||||
|
||||
it('renders the content provided via default slot', () => {
|
||||
expect(wrapper.findComponent(GlTooltip).text()).toContain(slots.default[0]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,7 +1,10 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlAvatar, GlTooltip } from '@gitlab/ui';
|
||||
import defaultAvatarUrl from 'images/no_avatar.png';
|
||||
import { placeholderImage } from '~/lazy_loader';
|
||||
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
|
||||
import UserAvatarImageNew from '~/vue_shared/components/user_avatar/user_avatar_image_new.vue';
|
||||
import UserAvatarImageOld from '~/vue_shared/components/user_avatar/user_avatar_image_old.vue';
|
||||
|
||||
jest.mock('images/no_avatar.png', () => 'default-avatar-url');
|
||||
|
||||
const PROVIDED_PROPS = {
|
||||
size: 32,
|
||||
|
@ -15,37 +18,117 @@ const PROVIDED_PROPS = {
|
|||
describe('User Avatar Image Component', () => {
|
||||
let wrapper;
|
||||
|
||||
const createWrapper = (props = {}, { glAvatarForAllUserAvatars } = {}) => {
|
||||
wrapper = shallowMount(UserAvatarImage, {
|
||||
propsData: {
|
||||
...PROVIDED_PROPS,
|
||||
...props,
|
||||
},
|
||||
provide: {
|
||||
glFeatures: {
|
||||
glAvatarForAllUserAvatars,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
const findAvatar = () => wrapper.findComponent(GlAvatar);
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe.each([
|
||||
[false, true, true],
|
||||
[true, false, true],
|
||||
[true, true, true],
|
||||
[false, false, false],
|
||||
])(
|
||||
'when glAvatarForAllUserAvatars=%s and enforceGlAvatar=%s',
|
||||
(glAvatarForAllUserAvatars, enforceGlAvatar, isUsingNewVersion) => {
|
||||
it(`will render ${isUsingNewVersion ? 'new' : 'old'} version`, () => {
|
||||
createWrapper({ enforceGlAvatar }, { glAvatarForAllUserAvatars });
|
||||
expect(wrapper.findComponent(UserAvatarImageNew).exists()).toBe(isUsingNewVersion);
|
||||
expect(wrapper.findComponent(UserAvatarImageOld).exists()).toBe(!isUsingNewVersion);
|
||||
describe('Initialization', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(UserAvatarImage, {
|
||||
propsData: {
|
||||
...PROVIDED_PROPS,
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should render `GlAvatar` and provide correct properties to it', () => {
|
||||
expect(findAvatar().attributes('data-src')).toBe(
|
||||
`${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
|
||||
);
|
||||
expect(findAvatar().props()).toMatchObject({
|
||||
src: `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
|
||||
alt: PROVIDED_PROPS.imgAlt,
|
||||
size: PROVIDED_PROPS.size,
|
||||
});
|
||||
});
|
||||
|
||||
it('should add correct CSS classes', () => {
|
||||
const classes = wrapper.findComponent(GlAvatar).classes();
|
||||
expect(classes).toContain(PROVIDED_PROPS.cssClasses);
|
||||
expect(classes).not.toContain('lazy');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Initialization when lazy', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(UserAvatarImage, {
|
||||
propsData: {
|
||||
...PROVIDED_PROPS,
|
||||
lazy: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should add lazy attributes', () => {
|
||||
expect(findAvatar().classes()).toContain('lazy');
|
||||
expect(findAvatar().attributes()).toMatchObject({
|
||||
src: placeholderImage,
|
||||
'data-src': `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
|
||||
});
|
||||
});
|
||||
|
||||
it('should use maximum number when size is provided as an object', () => {
|
||||
wrapper = shallowMount(UserAvatarImage, {
|
||||
propsData: {
|
||||
...PROVIDED_PROPS,
|
||||
size: { default: 16, md: 64, lg: 24 },
|
||||
lazy: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(findAvatar().attributes('data-src')).toBe(`${PROVIDED_PROPS.imgSrc}?width=${64}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Initialization without src', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(UserAvatarImage, {
|
||||
propsData: {
|
||||
...PROVIDED_PROPS,
|
||||
imgSrc: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should have default avatar image', () => {
|
||||
expect(findAvatar().props('src')).toBe(`${defaultAvatarUrl}?width=${PROVIDED_PROPS.size}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dynamic tooltip content', () => {
|
||||
const slots = {
|
||||
default: ['Action!'],
|
||||
};
|
||||
|
||||
describe('when `tooltipText` is provided and no default slot', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(UserAvatarImage, {
|
||||
propsData: { ...PROVIDED_PROPS },
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the tooltip with `tooltipText` as content', () => {
|
||||
expect(wrapper.findComponent(GlTooltip).text()).toBe(PROVIDED_PROPS.tooltipText);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when `tooltipText` and default slot is provided', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(UserAvatarImage, {
|
||||
propsData: { ...PROVIDED_PROPS },
|
||||
slots,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not render `tooltipText` inside the tooltip', () => {
|
||||
expect(wrapper.findComponent(GlTooltip).text()).not.toBe(PROVIDED_PROPS.tooltipText);
|
||||
});
|
||||
|
||||
it('renders the content provided via default slot', () => {
|
||||
expect(wrapper.findComponent(GlTooltip).text()).toContain(slots.default[0]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,103 +0,0 @@
|
|||
import { GlAvatarLink } from '@gitlab/ui';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { TEST_HOST } from 'spec/test_constants';
|
||||
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
|
||||
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link_new.vue';
|
||||
|
||||
describe('User Avatar Link Component', () => {
|
||||
let wrapper;
|
||||
|
||||
const findUserName = () => wrapper.findByTestId('user-avatar-link-username');
|
||||
|
||||
const defaultProps = {
|
||||
linkHref: `${TEST_HOST}/myavatarurl.com`,
|
||||
imgSize: 32,
|
||||
imgSrc: `${TEST_HOST}/myavatarurl.com`,
|
||||
imgAlt: 'mydisplayname',
|
||||
imgCssClasses: 'myextraavatarclass',
|
||||
tooltipText: 'tooltip text',
|
||||
tooltipPlacement: 'bottom',
|
||||
username: 'username',
|
||||
};
|
||||
|
||||
const createWrapper = (props, slots) => {
|
||||
wrapper = shallowMountExtended(UserAvatarLink, {
|
||||
propsData: {
|
||||
...defaultProps,
|
||||
...props,
|
||||
...slots,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
createWrapper();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('should render GlLink with correct props', () => {
|
||||
const link = wrapper.findComponent(GlAvatarLink);
|
||||
expect(link.exists()).toBe(true);
|
||||
expect(link.attributes('href')).toBe(defaultProps.linkHref);
|
||||
});
|
||||
|
||||
it('should render UserAvatarImage and provide correct props to it', () => {
|
||||
expect(wrapper.findComponent(UserAvatarImage).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(UserAvatarImage).props()).toEqual({
|
||||
cssClasses: defaultProps.imgCssClasses,
|
||||
imgAlt: defaultProps.imgAlt,
|
||||
imgSrc: defaultProps.imgSrc,
|
||||
lazy: false,
|
||||
size: defaultProps.imgSize,
|
||||
tooltipPlacement: defaultProps.tooltipPlacement,
|
||||
tooltipText: '',
|
||||
enforceGlAvatar: false,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when username provided', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper({ username: defaultProps.username });
|
||||
});
|
||||
|
||||
it('should render provided username', () => {
|
||||
expect(findUserName().text()).toBe(defaultProps.username);
|
||||
});
|
||||
|
||||
it('should provide the tooltip data for the username', () => {
|
||||
expect(findUserName().attributes()).toEqual(
|
||||
expect.objectContaining({
|
||||
title: defaultProps.tooltipText,
|
||||
'tooltip-placement': defaultProps.tooltipPlacement,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when username is NOT provided', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper({ username: '' });
|
||||
});
|
||||
|
||||
it('should NOT render username', () => {
|
||||
expect(findUserName().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('avatar-badge slot', () => {
|
||||
const badge = '<span>User badge</span>';
|
||||
|
||||
beforeEach(() => {
|
||||
createWrapper(defaultProps, {
|
||||
'avatar-badge': badge,
|
||||
});
|
||||
});
|
||||
|
||||
it('should render provided `avatar-badge` slot content', () => {
|
||||
expect(wrapper.html()).toContain(badge);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,103 +0,0 @@
|
|||
import { GlLink } from '@gitlab/ui';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { TEST_HOST } from 'spec/test_constants';
|
||||
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
|
||||
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link_old.vue';
|
||||
|
||||
describe('User Avatar Link Component', () => {
|
||||
let wrapper;
|
||||
|
||||
const findUserName = () => wrapper.find('[data-testid="user-avatar-link-username"]');
|
||||
|
||||
const defaultProps = {
|
||||
linkHref: `${TEST_HOST}/myavatarurl.com`,
|
||||
imgSize: 32,
|
||||
imgSrc: `${TEST_HOST}/myavatarurl.com`,
|
||||
imgAlt: 'mydisplayname',
|
||||
imgCssClasses: 'myextraavatarclass',
|
||||
tooltipText: 'tooltip text',
|
||||
tooltipPlacement: 'bottom',
|
||||
username: 'username',
|
||||
};
|
||||
|
||||
const createWrapper = (props, slots) => {
|
||||
wrapper = shallowMountExtended(UserAvatarLink, {
|
||||
propsData: {
|
||||
...defaultProps,
|
||||
...props,
|
||||
...slots,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
createWrapper();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('should render GlLink with correct props', () => {
|
||||
const link = wrapper.findComponent(GlLink);
|
||||
expect(link.exists()).toBe(true);
|
||||
expect(link.attributes('href')).toBe(defaultProps.linkHref);
|
||||
});
|
||||
|
||||
it('should render UserAvatarImage and povide correct props to it', () => {
|
||||
expect(wrapper.findComponent(UserAvatarImage).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(UserAvatarImage).props()).toEqual({
|
||||
cssClasses: defaultProps.imgCssClasses,
|
||||
imgAlt: defaultProps.imgAlt,
|
||||
imgSrc: defaultProps.imgSrc,
|
||||
lazy: false,
|
||||
size: defaultProps.imgSize,
|
||||
tooltipPlacement: defaultProps.tooltipPlacement,
|
||||
tooltipText: '',
|
||||
enforceGlAvatar: false,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when username provided', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper({ username: defaultProps.username });
|
||||
});
|
||||
|
||||
it('should render provided username', () => {
|
||||
expect(findUserName().text()).toBe(defaultProps.username);
|
||||
});
|
||||
|
||||
it('should provide the tooltip data for the username', () => {
|
||||
expect(findUserName().attributes()).toEqual(
|
||||
expect.objectContaining({
|
||||
title: defaultProps.tooltipText,
|
||||
'tooltip-placement': defaultProps.tooltipPlacement,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when username is NOT provided', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper({ username: '' });
|
||||
});
|
||||
|
||||
it('should NOT render username', () => {
|
||||
expect(findUserName().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('avatar-badge slot', () => {
|
||||
const badge = '<span>User badge</span>';
|
||||
|
||||
beforeEach(() => {
|
||||
createWrapper(defaultProps, {
|
||||
'avatar-badge': badge,
|
||||
});
|
||||
});
|
||||
|
||||
it('should render provided `avatar-badge` slot content', () => {
|
||||
expect(wrapper.html()).toContain(badge);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,51 +1,102 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlAvatarLink } from '@gitlab/ui';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { TEST_HOST } from 'spec/test_constants';
|
||||
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
|
||||
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
|
||||
import UserAvatarLinkNew from '~/vue_shared/components/user_avatar/user_avatar_link_new.vue';
|
||||
import UserAvatarLinkOld from '~/vue_shared/components/user_avatar/user_avatar_link_old.vue';
|
||||
|
||||
const PROVIDED_PROPS = {
|
||||
size: 32,
|
||||
imgSrc: 'myavatarurl.com',
|
||||
imgAlt: 'mydisplayname',
|
||||
cssClasses: 'myextraavatarclass',
|
||||
tooltipText: 'tooltip text',
|
||||
tooltipPlacement: 'bottom',
|
||||
};
|
||||
|
||||
describe('User Avatar Link Component', () => {
|
||||
let wrapper;
|
||||
|
||||
const createWrapper = (props = {}, { glAvatarForAllUserAvatars } = {}) => {
|
||||
wrapper = shallowMount(UserAvatarLink, {
|
||||
const findUserName = () => wrapper.findByTestId('user-avatar-link-username');
|
||||
|
||||
const defaultProps = {
|
||||
linkHref: `${TEST_HOST}/myavatarurl.com`,
|
||||
imgSize: 32,
|
||||
imgSrc: `${TEST_HOST}/myavatarurl.com`,
|
||||
imgAlt: 'mydisplayname',
|
||||
imgCssClasses: 'myextraavatarclass',
|
||||
tooltipText: 'tooltip text',
|
||||
tooltipPlacement: 'bottom',
|
||||
username: 'username',
|
||||
};
|
||||
|
||||
const createWrapper = (props, slots) => {
|
||||
wrapper = shallowMountExtended(UserAvatarLink, {
|
||||
propsData: {
|
||||
...PROVIDED_PROPS,
|
||||
...defaultProps,
|
||||
...props,
|
||||
},
|
||||
provide: {
|
||||
glFeatures: {
|
||||
glAvatarForAllUserAvatars,
|
||||
},
|
||||
...slots,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
createWrapper();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe.each([
|
||||
[false, true, true],
|
||||
[true, false, true],
|
||||
[true, true, true],
|
||||
[false, false, false],
|
||||
])(
|
||||
'when glAvatarForAllUserAvatars=%s and enforceGlAvatar=%s',
|
||||
(glAvatarForAllUserAvatars, enforceGlAvatar, isUsingNewVersion) => {
|
||||
it(`will render ${isUsingNewVersion ? 'new' : 'old'} version`, () => {
|
||||
createWrapper({ enforceGlAvatar }, { glAvatarForAllUserAvatars });
|
||||
expect(wrapper.findComponent(UserAvatarLinkNew).exists()).toBe(isUsingNewVersion);
|
||||
expect(wrapper.findComponent(UserAvatarLinkOld).exists()).toBe(!isUsingNewVersion);
|
||||
it('should render GlLink with correct props', () => {
|
||||
const link = wrapper.findComponent(GlAvatarLink);
|
||||
expect(link.exists()).toBe(true);
|
||||
expect(link.attributes('href')).toBe(defaultProps.linkHref);
|
||||
});
|
||||
|
||||
it('should render UserAvatarImage and provide correct props to it', () => {
|
||||
expect(wrapper.findComponent(UserAvatarImage).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(UserAvatarImage).props()).toEqual({
|
||||
cssClasses: defaultProps.imgCssClasses,
|
||||
imgAlt: defaultProps.imgAlt,
|
||||
imgSrc: defaultProps.imgSrc,
|
||||
lazy: false,
|
||||
size: defaultProps.imgSize,
|
||||
tooltipPlacement: defaultProps.tooltipPlacement,
|
||||
tooltipText: '',
|
||||
});
|
||||
});
|
||||
|
||||
describe('when username provided', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper({ username: defaultProps.username });
|
||||
});
|
||||
|
||||
it('should render provided username', () => {
|
||||
expect(findUserName().text()).toBe(defaultProps.username);
|
||||
});
|
||||
|
||||
it('should provide the tooltip data for the username', () => {
|
||||
expect(findUserName().attributes()).toEqual(
|
||||
expect.objectContaining({
|
||||
title: defaultProps.tooltipText,
|
||||
'tooltip-placement': defaultProps.tooltipPlacement,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when username is NOT provided', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper({ username: '' });
|
||||
});
|
||||
|
||||
it('should NOT render username', () => {
|
||||
expect(findUserName().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('avatar-badge slot', () => {
|
||||
const badge = '<span>User badge</span>';
|
||||
|
||||
beforeEach(() => {
|
||||
createWrapper(defaultProps, {
|
||||
'avatar-badge': badge,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should render provided `avatar-badge` slot content', () => {
|
||||
expect(wrapper.html()).toContain(badge);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -153,29 +153,4 @@ describe('UserAvatarList', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('additional styling for the image', () => {
|
||||
it('should not add CSS class when feature flag `glAvatarForAllUserAvatars` is disabled', () => {
|
||||
factory({
|
||||
propsData: { items: createList(1) },
|
||||
});
|
||||
|
||||
const link = wrapper.findComponent(UserAvatarLink);
|
||||
expect(link.props('imgCssClasses')).not.toBe('gl-mr-3');
|
||||
});
|
||||
|
||||
it('should add CSS class when feature flag `glAvatarForAllUserAvatars` is enabled', () => {
|
||||
factory({
|
||||
propsData: { items: createList(1) },
|
||||
provide: {
|
||||
glFeatures: {
|
||||
glAvatarForAllUserAvatars: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const link = wrapper.findComponent(UserAvatarLink);
|
||||
expect(link.props('imgCssClasses')).toBe('gl-mr-3');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -531,12 +531,14 @@ RSpec.describe GroupsHelper do
|
|||
describe '#group_overview_tabs_app_data' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:initial_sort) { 'created_asc' }
|
||||
|
||||
before do
|
||||
allow(helper).to receive(:current_user).and_return(user)
|
||||
|
||||
allow(helper).to receive(:can?).with(user, :create_subgroup, group) { true }
|
||||
allow(helper).to receive(:can?).with(user, :create_projects, group) { true }
|
||||
allow(helper).to receive(:project_list_sort_by).and_return(initial_sort)
|
||||
end
|
||||
|
||||
it 'returns expected hash' do
|
||||
|
@ -545,7 +547,8 @@ RSpec.describe GroupsHelper do
|
|||
subgroups_and_projects_endpoint: including("/groups/#{group.path}/-/children.json"),
|
||||
shared_projects_endpoint: including("/groups/#{group.path}/-/shared_projects.json"),
|
||||
archived_projects_endpoint: including("/groups/#{group.path}/-/children.json?archived=only"),
|
||||
current_group_visibility: group.visibility
|
||||
current_group_visibility: group.visibility,
|
||||
initial_sort: initial_sort
|
||||
}.merge(helper.group_overview_tabs_app_data(group))
|
||||
)
|
||||
end
|
||||
|
|
|
@ -96,9 +96,9 @@ module StubGitlabCalls
|
|||
def stub_commonmark_sourcepos_disabled
|
||||
render_options = Banzai::Filter::MarkdownEngines::CommonMark::RENDER_OPTIONS
|
||||
|
||||
allow_any_instance_of(Banzai::Filter::MarkdownEngines::CommonMark)
|
||||
.to receive(:render_options)
|
||||
.and_return(render_options)
|
||||
allow_next_instance_of(Banzai::Filter::MarkdownEngines::CommonMark) do |instance|
|
||||
allow(instance).to receive(:render_options).and_return(render_options)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
|
Loading…
Reference in New Issue