Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-12-21 09:07:17 +00:00
parent 4cd1329b80
commit 47a3dc6551
65 changed files with 960 additions and 553 deletions

View File

@ -244,6 +244,13 @@ export default {
});
}
if (this.filterParams['not[healthStatus]']) {
filteredSearchValue.push({
type: TOKEN_TYPE_HEALTH,
value: { data: this.filterParams['not[healthStatus]'], operator: '!=' },
});
}
if (search) {
filteredSearchValue.push(search);
}
@ -285,6 +292,7 @@ export default {
'not[my_reaction_emoji]': this.filterParams.not.myReactionEmoji,
'not[iteration_id]': this.filterParams.not.iterationId,
'not[release_tag]': this.filterParams.not.releaseTag,
'not[health_status]': this.filterParams.not.healthStatus,
},
undefined,
);

View File

@ -360,14 +360,17 @@ export const filters = {
},
[TOKEN_TYPE_HEALTH]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'healthStatus',
[SPECIAL_FILTER]: 'healthStatus',
[NORMAL_FILTER]: 'healthStatusFilter',
[SPECIAL_FILTER]: 'healthStatusFilter',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'health_status',
[SPECIAL_FILTER]: 'health_status',
},
[OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[health_status]',
},
},
},
[TOKEN_TYPE_CONTACT]: {

View File

@ -13,6 +13,7 @@ import {
TOKEN_TYPE_MILESTONE,
TOKEN_TYPE_RELEASE,
TOKEN_TYPE_TYPE,
TOKEN_TYPE_HEALTH,
} from '~/vue_shared/components/filtered_search_bar/constants';
import {
ALTERNATIVE_FILTER,
@ -267,8 +268,13 @@ const wildcardTokens = [TOKEN_TYPE_ITERATION, TOKEN_TYPE_MILESTONE, TOKEN_TYPE_R
const isWildcardValue = (tokenType, value) =>
wildcardTokens.includes(tokenType) && specialFilterValues.includes(value);
const isHealthStatusSpecialFilter = (tokenType, value) =>
tokenType === TOKEN_TYPE_HEALTH && specialFilterValues.includes(value);
const requiresUpperCaseValue = (tokenType, value) =>
tokenType === TOKEN_TYPE_TYPE || isWildcardValue(tokenType, value);
tokenType === TOKEN_TYPE_TYPE ||
isWildcardValue(tokenType, value) ||
isHealthStatusSpecialFilter(tokenType, value);
const formatData = (token) => {
if (requiresUpperCaseValue(token.type, token.value.data)) {

View File

@ -1,4 +1,5 @@
import mermaid from 'mermaid';
import mindmap from '@mermaid-js/mermaid-mindmap';
import { getParameterByName } from '~/lib/utils/url_utility';
const setIframeRenderedSize = (h, w) => {
@ -12,11 +13,10 @@ const drawDiagram = (source) => {
// eslint-disable-next-line no-unsanitized/property
element.innerHTML = svgCode;
const height = parseInt(element.firstElementChild.getAttribute('height'), 10);
const width = parseInt(element.firstElementChild.style.maxWidth, 10);
const { width, height } = element.firstElementChild.viewBox.baseVal;
setIframeRenderedSize(height, width);
};
mermaid.mermaidAPI.render('mermaid', source, insertSvg);
mermaid.mermaidAPI.renderAsync('mermaid', source, insertSvg);
};
const darkModeEnabled = () => getParameterByName('darkMode') === 'true';
@ -56,7 +56,13 @@ const addListener = () => {
false,
);
};
addListener();
initMermaid();
mermaid
.registerExternalDiagrams([mindmap])
.then(() => {
addListener();
initMermaid();
})
.catch((error) => {
throw error;
});
export default {};

View File

@ -49,8 +49,6 @@ export default {
:message="message"
:title="s__('Member|Deny access')"
:is-access-request="true"
icon="close"
button-category="primary"
/>
</div>
</action-button-group>

View File

@ -40,7 +40,6 @@ export default {
:title="$options.title"
:aria-label="$options.title"
icon="check"
variant="confirm"
type="submit"
/>
</gl-form>

View File

@ -41,8 +41,6 @@ export default {
<remove-member-button
:member-id="member.id"
:message="message"
icon="remove"
button-category="primary"
:title="s__('Member|Revoke invite')"
is-invite
/>

View File

@ -33,7 +33,6 @@ export default {
:title="$options.title"
:aria-label="$options.title"
icon="leave"
variant="danger"
/>
<leave-modal :member="member" />
</div>

View File

@ -32,7 +32,6 @@ export default {
<template>
<gl-button
v-gl-tooltip.hover
variant="danger"
:title="$options.i18n.buttonTitle"
:aria-label="$options.i18n.buttonTitle"
icon="remove"

View File

@ -25,23 +25,7 @@ export default {
},
title: {
type: String,
required: false,
default: null,
},
icon: {
type: String,
required: false,
default: undefined,
},
buttonText: {
type: String,
required: false,
default: '',
},
buttonCategory: {
type: String,
required: false,
default: 'secondary',
required: true,
},
isAccessRequest: {
type: Boolean,
@ -89,13 +73,10 @@ export default {
<template>
<gl-button
v-gl-tooltip
variant="danger"
:category="buttonCategory"
:title="title"
:aria-label="title"
:icon="icon"
icon="remove"
data-qa-selector="delete_member_button"
@click="showRemoveMemberModal(modalData)"
><template v-if="buttonText">{{ buttonText }}</template></gl-button
>
/>
</template>

View File

@ -1,5 +1,5 @@
<script>
import { __, s__, sprintf } from '~/locale';
import { s__, __, sprintf } from '~/locale';
import { parseUserDeletionObstacles } from '~/vue_shared/components/user_deletion_obstacles/utils';
import ActionButtonGroup from './action_button_group.vue';
import LeaveButton from './leave_button.vue';
@ -7,6 +7,9 @@ import RemoveMemberButton from './remove_member_button.vue';
export default {
name: 'UserActionButtons',
i18n: {
title: __('Remove member'),
},
components: {
ActionButtonGroup,
RemoveMemberButton,
@ -23,10 +26,6 @@ export default {
type: Boolean,
required: true,
},
isInvitedUser: {
type: Boolean,
required: true,
},
permissions: {
type: Object,
required: true,
@ -60,15 +59,6 @@ export default {
obstacles: parseUserDeletionObstacles(this.member.user),
};
},
removeMemberButtonText() {
return this.isInvitedUser ? null : __('Remove member');
},
removeMemberButtonIcon() {
return this.isInvitedUser ? 'remove' : '';
},
removeMemberButtonCategory() {
return this.isInvitedUser ? 'primary' : 'secondary';
},
},
};
</script>
@ -83,9 +73,7 @@ export default {
:member-type="member.type"
:user-deletion-obstacles="userDeletionObstaclesUserData"
:message="message"
:icon="removeMemberButtonIcon"
:button-text="removeMemberButtonText"
:button-category="removeMemberButtonCategory"
:title="$options.i18n.title"
/>
</div>
<div v-else-if="permissions.canOverride && !member.isOverridden" class="gl-px-1">

View File

@ -1,10 +1,10 @@
<script>
import { GlSprintf } from '@gitlab/ui';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import UserDate from '~/vue_shared/components/user_date.vue';
export default {
name: 'CreatedAt',
components: { GlSprintf, TimeAgoTooltip },
components: { GlSprintf, UserDate },
props: {
date: {
type: String,
@ -29,12 +29,12 @@ export default {
<span>
<gl-sprintf v-if="showCreatedBy" :message="s__('Members|%{time} by %{user}')">
<template #time>
<time-ago-tooltip :time="date" />
<user-date :date="date" />
</template>
<template #user>
<a :href="createdBy.webUrl">{{ createdBy.name }}</a>
</template>
</gl-sprintf>
<time-ago-tooltip v-else :time="date" />
<user-date v-else :date="date" />
</span>
</template>

View File

@ -32,10 +32,6 @@ export default {
type: Boolean,
required: true,
},
isInvitedUser: {
type: Boolean,
required: true,
},
},
computed: {
actionButtonComponent() {
@ -60,6 +56,5 @@ export default {
:member="member"
:permissions="permissions"
:is-current-user="isCurrentUser"
:is-invited-user="isInvitedUser"
/>
</template>

View File

@ -0,0 +1,38 @@
<script>
import UserDate from '~/vue_shared/components/user_date.vue';
export default {
components: { UserDate },
props: {
member: {
type: Object,
required: true,
},
},
computed: {
userCreated() {
return this.member.user?.createdAt;
},
lastActivity() {
return this.member.user?.lastActivityOn;
},
},
};
</script>
<template>
<div>
<div v-if="userCreated">
<strong>{{ s__('Members|User created') }}:</strong>
<user-date :date="userCreated" />
</div>
<div v-if="member.createdAt">
<strong>{{ s__('Members|Access granted') }}:</strong>
<user-date :date="member.createdAt" />
</div>
<div v-if="lastActivity">
<strong>{{ s__('Members|Last activity') }}:</strong>
<user-date :date="lastActivity" />
</div>
</div>
</template>

View File

@ -1,11 +1,19 @@
<script>
import { GlTooltipDirective } from '@gitlab/ui';
import { GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import { s__, __ } from '~/locale';
export default {
name: 'MemberSource',
i18n: {
inherited: __('Inherited'),
directMember: __('Direct member'),
directMemberWithCreatedBy: s__('Members|Direct member by %{createdBy}'),
inheritedMemberWithCreatedBy: s__('Members|%{group} by %{createdBy}'),
},
directives: {
GlTooltip: GlTooltipDirective,
},
components: { GlSprintf },
props: {
memberSource: {
type: Object,
@ -15,13 +23,40 @@ export default {
type: Boolean,
required: true,
},
createdBy: {
type: Object,
required: false,
default: null,
},
},
computed: {
showCreatedBy() {
return this.createdBy?.name && this.createdBy?.webUrl;
},
messageWithCreatedBy() {
return this.isDirectMember
? this.$options.i18n.directMemberWithCreatedBy
: this.$options.i18n.inheritedMemberWithCreatedBy;
},
},
};
</script>
<template>
<span v-if="isDirectMember">{{ __('Direct member') }}</span>
<a v-else v-gl-tooltip.hover :title="__('Inherited')" :href="memberSource.webUrl">{{
<span v-if="showCreatedBy">
<gl-sprintf :message="messageWithCreatedBy">
<template #group>
<a v-gl-tooltip.hover="$options.i18n.inherited" :href="memberSource.webUrl">{{
memberSource.fullName
}}</a>
</template>
<template #createdBy>
<a :href="createdBy.webUrl">{{ createdBy.name }}</a>
</template>
</gl-sprintf>
</span>
<span v-else-if="isDirectMember">{{ $options.i18n.directMember }}</span>
<a v-else v-gl-tooltip.hover="$options.i18n.inherited" :href="memberSource.webUrl">{{
memberSource.fullName
}}</a>
</template>

View File

@ -4,12 +4,10 @@ import { mapState } from 'vuex';
import MembersTableCell from 'ee_else_ce/members/components/table/members_table_cell.vue';
import { canUnban, canOverride, canRemove, canResend, canUpdate } from 'ee_else_ce/members/utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import UserDate from '~/vue_shared/components/user_date.vue';
import {
FIELD_KEY_ACTIONS,
FIELDS,
ACTIVE_TAB_QUERY_PARAM_NAME,
TAB_QUERY_PARAM_VALUES,
MEMBER_STATE_AWAITING,
MEMBER_STATE_ACTIVE,
USER_STATE_BLOCKED,
@ -23,6 +21,7 @@ import ExpirationDatepicker from './expiration_datepicker.vue';
import MemberActionButtons from './member_action_buttons.vue';
import MemberAvatar from './member_avatar.vue';
import MemberSource from './member_source.vue';
import MemberActivity from './member_activity.vue';
import RoleDropdown from './role_dropdown.vue';
export default {
@ -40,7 +39,7 @@ export default {
RemoveGroupLinkModal,
RemoveMemberModal,
ExpirationDatepicker,
UserDate,
MemberActivity,
LdapOverrideConfirmationModal: () =>
import('ee_component/members/components/ldap/ldap_override_confirmation_modal.vue'),
},
@ -80,9 +79,6 @@ export default {
return paramName && currentPage && perPage && totalItems;
},
isInvitedUser() {
return this.tabQueryParamValue === TAB_QUERY_PARAM_VALUES.invite;
},
},
methods: {
hasActionButtons(member) {
@ -249,7 +245,11 @@ export default {
<template #cell(source)="{ item: member }">
<members-table-cell #default="{ isDirectMember }" :member="member">
<member-source :is-direct-member="isDirectMember" :member-source="member.source" />
<member-source
:is-direct-member="isDirectMember"
:member-source="member.source"
:created-by="member.createdBy"
/>
</members-table-cell>
</template>
@ -281,12 +281,8 @@ export default {
</members-table-cell>
</template>
<template #cell(userCreatedAt)="{ item: member }">
<user-date :date="member.user.createdAt" />
</template>
<template #cell(lastActivityOn)="{ item: member }">
<user-date :date="member.user.lastActivityOn" />
<template #cell(activity)="{ item: member }">
<member-activity :member="member" />
</template>
<template #cell(actions)="{ item: member }">
@ -294,7 +290,6 @@ export default {
<member-action-buttons
:member-type="memberType"
:is-current-user="isCurrentUser"
:is-invited-user="isInvitedUser"
:permissions="permissions"
:member="member"
/>

View File

@ -20,6 +20,7 @@ export const FIELD_KEY_MAX_ROLE = 'maxRole';
export const FIELD_KEY_USER_CREATED_AT = 'userCreatedAt';
export const FIELD_KEY_LAST_ACTIVITY_ON = 'lastActivityOn';
export const FIELD_KEY_EXPIRATION = 'expiration';
export const FIELD_KEY_ACTIVITY = 'activity';
export const FIELD_KEY_LAST_SIGN_IN = 'lastSignIn';
export const FIELD_KEY_ACTIONS = 'actions';
@ -41,8 +42,6 @@ export const FIELDS = [
{
key: FIELD_KEY_GRANTED,
label: __('Access granted'),
thClass: 'col-meta',
tdClass: 'col-meta',
sort: {
asc: 'last_joined',
desc: 'oldest_joined',
@ -76,9 +75,15 @@ export const FIELDS = [
thClass: 'col-expiration',
tdClass: 'col-expiration',
},
{
key: FIELD_KEY_ACTIVITY,
label: s__('Members|Activity'),
thClass: 'col-activity',
tdClass: 'col-activity',
},
{
key: FIELD_KEY_USER_CREATED_AT,
label: __('Created on'),
label: s__('Members|User created'),
sort: {
asc: 'oldest_created_user',
desc: 'recent_created_user',

View File

@ -11,7 +11,7 @@ import { groupLinkRequestFormatter } from '~/members/utils';
const SHARED_FIELDS = ['account', 'maxRole', 'expiration', 'actions'];
const APP_OPTIONS = {
[MEMBER_TYPES.user]: {
tableFields: SHARED_FIELDS.concat(['source', 'granted', 'userCreatedAt', 'lastActivityOn']),
tableFields: SHARED_FIELDS.concat(['source', 'activity']),
tableAttrs: { tr: { 'data-qa-selector': 'member_row' } },
tableSortableFields: [
'account',

View File

@ -20,7 +20,7 @@ initImportProjectMembersTrigger();
const SHARED_FIELDS = ['account', 'maxRole', 'expiration', 'actions'];
initMembersApp(document.querySelector('.js-project-members-list-app'), {
[MEMBER_TYPES.user]: {
tableFields: SHARED_FIELDS.concat(['source', 'granted', 'userCreatedAt', 'lastActivityOn']),
tableFields: SHARED_FIELDS.concat(['source', 'activity']),
tableAttrs: { tr: { 'data-qa-selector': 'member_row' } },
tableSortableFields: [
'account',

View File

@ -9,7 +9,7 @@ const INTERVALS = {
export const FILE_SYMLINK_MODE = '120000';
export const SHORT_DATE_FORMAT = 'd mmm, yyyy';
export const SHORT_DATE_FORMAT = 'mmm dd, yyyy';
export const ISO_SHORT_FORMAT = 'yyyy-mm-dd';

View File

@ -76,6 +76,10 @@
width: px-to-rem(200px);
}
.col-activity {
width: px-to-rem(250px);
}
.col-actions {
width: px-to-rem(65px);
}

View File

@ -3,7 +3,7 @@
class Import::BulkImportsController < ApplicationController
include ActionView::Helpers::SanitizeHelper
before_action :ensure_group_import_enabled
before_action :ensure_bulk_import_enabled
before_action :verify_blocked_uri, only: :status
feature_category :importers
@ -118,8 +118,8 @@ class Import::BulkImportsController < ApplicationController
]
end
def ensure_group_import_enabled
render_404 unless ::BulkImports::Features.enabled?
def ensure_bulk_import_enabled
render_404 unless Gitlab::CurrentSettings.bulk_import_enabled?
end
def access_token_key

View File

@ -16,9 +16,8 @@
#import-group-pane.tab-pane
- if import_sources_enabled?
- if BulkImports::Features.enabled?
= render 'import_group_from_another_instance_panel'
.gl-mt-7.gl-border-b-solid.gl-border-gray-100.gl-border-1
= render 'import_group_from_another_instance_panel'
.gl-mt-7.gl-border-b-solid.gl-border-gray-100.gl-border-1
= render 'import_group_from_file_panel'
- else
.nothing-here-block

View File

@ -1,8 +0,0 @@
---
name: bulk_import
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/42704
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/255310
milestone: '13.5'
type: development
group: group::import
default_enabled: true

View File

@ -34,7 +34,8 @@ module.exports = {
'pikaday',
'@gitlab/at.js',
'jed',
'mermaid',
'mermaid/dist/mermaid.esm.mjs',
'@mermaid-js/mermaid-mindmap/dist/mermaid-mindmap.esm.mjs',
'katex',
'three',
'select2',

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class IndexMembersOnMemberNamespaceIdCompound < Gitlab::Database::Migration[2.1]
INDEX_NAME = 'index_members_on_member_namespace_id_compound'
disable_ddl_transaction!
def up
prepare_async_index(
:members,
[:member_namespace_id, :type, :requested_at, :id],
name: INDEX_NAME
)
end
def down
remove_concurrent_index_by_name :members, INDEX_NAME
end
end

View File

@ -0,0 +1 @@
8e9bb800a2eab9f5d5a3b4f3835b6c4f21ec861a5808a13bef8d496773a7799c

View File

@ -1986,7 +1986,7 @@ On each node perform the following:
{host: '10.6.0.53', port: 26379},
]
## Second cluster that will host the persistent queues, shared state, and actionable
## Second cluster that will host the persistent queues, shared state, and actioncable
gitlab_rails['redis_queues_instance'] = 'redis://:<REDIS_PRIMARY_PASSWORD_OF_SECOND_CLUSTER>@gitlab-redis-persistent'
gitlab_rails['redis_shared_state_instance'] = 'redis://:<REDIS_PRIMARY_PASSWORD_OF_SECOND_CLUSTER>@gitlab-redis-persistent'
gitlab_rails['redis_actioncable_instance'] = 'redis://:<REDIS_PRIMARY_PASSWORD_OF_SECOND_CLUSTER>@gitlab-redis-persistent'

View File

@ -330,7 +330,7 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" \
```
This action doesn't delete blobs. To delete them and recycle disk space,
[run the garbage collection](https://docs.gitlab.com/omnibus/maintenance/index.html#removing-unused-layers-not-referenced-by-manifests).
[run the garbage collection](../administration/packages/container_registry.md#container-registry-garbage-collection).
## Delete registry repository tags in bulk
@ -369,7 +369,7 @@ if successful, and performs the following operations:
These operations are executed asynchronously and can take time to get executed.
You can run this at most once an hour for a given container repository. This
action doesn't delete blobs. To delete them and recycle disk space,
[run the garbage collection](https://docs.gitlab.com/omnibus/maintenance/index.html#removing-unused-layers-not-referenced-by-manifests).
[run the garbage collection](../administration/packages/container_registry.md#container-registry-garbage-collection).
WARNING:
The number of tags deleted by this API is limited on GitLab.com

View File

@ -319,10 +319,8 @@ This operation is safe as there's no code using the table just yet.
Dropping tables can be done safely using a post-deployment migration, but only
if the application no longer uses the table.
Add the table to `DELETED_TABLES` in
[gitlab_schema.rb](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/database/gitlab_schema.rb),
along with its `gitlab_schema`. Even though the table is deleted, it is still
referenced in database migrations.
Add the table to [`db/docs/deleted_tables`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/db/docs/deleted_tables) using the process described in [database dictionary](database_dictionary.md#dropping-tables).
Even though the table is deleted, it is still referenced in database migrations.
## Renaming Tables

View File

@ -17,7 +17,7 @@ For the `geo` database, the dictionary files are stored under `ee/db/docs/`.
## Example dictionary file
```yaml
---
----
table_name: terraform_states
classes:
- Terraform::State
@ -28,45 +28,110 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26619
milestone: '13.0'
```
## Schema
| Attribute | Type | Required | Description |
|----------------------------|---------------|----------|-----------------------------------------------------------------------------------|
| `table_name` / `view_name` | String | yes | Database table name or view name |
| `classes` | Array(String) | no | List of classes that are associated to this table or view. |
| `feature_categories` | Array(String) | yes | List of feature categories using this table or view. |
| `description` | String | no | Text description of the information stored in the table or view, and its purpose. |
| `introduced_by_url` | URL | no | URL to the merge request or commit which introduced this table or view. |
| `milestone` | String | no | The milestone that introduced this table or view. |
| `gitlab_schema` | String | yes | GitLab schema name. |
## Adding tables
When adding a new table, create a new file under `db/docs/` for the `main` and `ci` databases.
For the `geo` database use `ee/db/docs/`.
Name the file as `<table_name>.yml`, containing as much information as you know about the table.
### Schema
Include this file in the commit with the migration that creates the table.
| Attribute | Type | Required | Description |
|----------------------------|---------------|----------|-------------|
| `table_name` | String | yes | Database table name. |
| `classes` | Array(String) | no | List of classes that are associated to this table. |
| `feature_categories` | Array(String) | yes | List of feature categories using this table. |
| `description` | String | no | Text description of the information stored in the table, and its purpose. |
| `introduced_by_url` | URL | no | URL to the merge request or commit which introduced this table. |
| `milestone` | String | no | The milestone that introduced this table. |
| `gitlab_schema` | String | yes | GitLab schema name. |
### Process
When adding a table, you should:
1. Create a new file for this table in the appropriate directory:
- `gitlab_main` table: `db/docs/`
- `gitlab_ci` table: `db/docs/`
- `gitlab_shared` table: `db/docs/`
- `gitlab_geo` table: `ee/db/docs/`
1. Name the file `<table_name>.yml`, and include as much information as you know about the table.
1. Include this file in the commit with the migration that creates the table.
## Dropping tables
When dropping a table, you must remove the metadata file from `db/docs/` for `main` and `ci` databases.
For the `geo` database, you must remove the file from `ee/db/docs/`.
Use the same commit with the migration that drops the table.
### Schema
| Attribute | Type | Required | Description |
|----------------------------|---------------|----------|-------------|
| `table_name` | String | yes | Database table name. |
| `classes` | Array(String) | no | List of classes that are associated to this table. |
| `feature_categories` | Array(String) | yes | List of feature categories using this table. |
| `description` | String | no | Text description of the information stored in the table, and its purpose. |
| `introduced_by_url` | URL | no | URL to the merge request or commit which introduced this table. |
| `milestone` | String | no | The milestone that introduced this table. |
| `gitlab_schema` | String | yes | GitLab schema name. |
| `removed_by_url` | String | yes | URL to the merge request or commit which removed this table. |
| `removed_in_milestone` | String | yes | The milestone that removes this table. |
### Process
When dropping a table, you should:
1. Move the dictionary file for this table to the `deleted_tables` directory:
- `gitlab_main` table: `db/docs/deleted_tables/`
- `gitlab_ci` table: `db/docs/deleted_tables/`
- `gitlab_shared` table: `db/docs/deleted_tables/`
- `gitlab_geo` table: `ee/db/docs/deleted_tables/`
1. Add the fields `removed_by_url` and `removed_in_milestone` to the dictionary file.
1. Include this change in the commit with the migration that drops the table.
## Adding views
### Schema
| Attribute | Type | Required | Description |
|----------------------------|---------------|----------|-------------|
| `table_name` | String | yes | Database view name. |
| `classes` | Array(String) | no | List of classes that are associated to this view. |
| `feature_categories` | Array(String) | yes | List of feature categories using this view. |
| `description` | String | no | Text description of the information stored in the view, and its purpose. |
| `introduced_by_url` | URL | no | URL to the merge request or commit which introduced this view. |
| `milestone` | String | no | The milestone that introduced this view. |
| `gitlab_schema` | String | yes | GitLab schema name. |
### Process
When adding a new view, you should:
1. Create a new file for this view in the appropriate directory:
- `main` database: `db/docs/views/`
- `ci` database: `db/docs/views/`
- `geo` database: `ee/db/docs/views/`
- `gitlab_main` view: `db/docs/views/`
- `gitlab_ci` view: `db/docs/views/`
- `gitlab_shared` view: `db/docs/views/`
- `gitlab_geo` view: `ee/db/docs/views/`
1. Name the file `<view_name>.yml`, and include as much information as you know about the view.
1. Include this file in the commit with the migration that creates the view.
## Dropping views
When dropping a view, you must remove the metadata file from `db/docs/views/`.
For the `geo` database, you must remove the file from `ee/db/docs/views/`.
Use the same commit with the migration that drops the view.
## Schema
| Attribute | Type | Required | Description |
|----------------------------|---------------|----------|-------------|
| `view_name` | String | yes | Database view name. |
| `classes` | Array(String) | no | List of classes that are associated to this view. |
| `feature_categories` | Array(String) | yes | List of feature categories using this view. |
| `description` | String | no | Text description of the information stored in the view, and its purpose. |
| `introduced_by_url` | URL | no | URL to the merge request or commit which introduced this view. |
| `milestone` | String | no | The milestone that introduced this view. |
| `gitlab_schema` | String | yes | GitLab schema name. |
| `removed_by_url` | String | yes | URL to the merge request or commit which removed this view. |
| `removed_in_milestone` | String | yes | The milestone that removes this view. |
### Process
When dropping a view, you should:
1. Move the dictionary file for this table to the `deleted_views` directory:
- `gitlab_main` view: `db/docs/deleted_views/`
- `gitlab_ci` view: `db/docs/deleted_views/`
- `gitlab_shared` view: `db/docs/deleted_views/`
- `gitlab_geo` view: `ee/db/docs/deleted_views/`
1. Add the fields `removed_by_url` and `removed_in_milestone` to the dictionary file.
1. Include this change in the commit with the migration that drops the view.

View File

@ -37,6 +37,7 @@ this feature, ask an administrator to [enable the feature flag](../../../adminis
Prerequisites:
- Network connection between instances or GitLab.com. Must support HTTPS.
- Both GitLab instances have migration enabled in application settings by instance administrator.
- Owner role on the top-level group to migrate.
You can import top-level groups to:

View File

@ -33,7 +33,7 @@ module API
end
before do
not_found! unless ::BulkImports::Features.enabled?
not_found! unless Gitlab::CurrentSettings.bulk_import_enabled?
authenticate!
end

View File

@ -64,67 +64,73 @@ module API
end
end
desc 'Start relations export' do
detail 'This feature was introduced in GitLab 13.12'
tags %w[group_export]
success code: 202
failure [
{ code: 401, message: 'Unauthorized' },
{ code: 403, message: 'Forbidden' },
{ code: 404, message: 'Not found' },
{ code: 503, message: 'Service unavailable' }
]
end
post ':id/export_relations' do
response = ::BulkImports::ExportService.new(portable: user_group, user: current_user).execute
if response.success?
accepted!
else
render_api_error!(message: 'Group relations export could not be started.')
resource do
before do
not_found! unless Gitlab::CurrentSettings.bulk_import_enabled?
end
end
desc 'Download relations export' do
detail 'This feature was introduced in GitLab 13.12'
produces %w[application/octet-stream application/json]
tags %w[group_export]
success code: 200
failure [
{ code: 401, message: 'Unauthorized' },
{ code: 403, message: 'Forbidden' },
{ code: 404, message: 'Not found' },
{ code: 503, message: 'Service unavailable' }
]
end
params do
requires :relation, type: String, desc: 'Group relation name'
end
get ':id/export_relations/download' do
export = user_group.bulk_import_exports.find_by_relation(params[:relation])
file = export&.upload&.export_file
if file
present_carrierwave_file!(file)
else
render_api_error!('404 Not found', 404)
desc 'Start relations export' do
detail 'This feature was introduced in GitLab 13.12'
tags %w[group_export]
success code: 202
failure [
{ code: 401, message: 'Unauthorized' },
{ code: 403, message: 'Forbidden' },
{ code: 404, message: 'Not found' },
{ code: 503, message: 'Service unavailable' }
]
end
end
post ':id/export_relations' do
response = ::BulkImports::ExportService.new(portable: user_group, user: current_user).execute
desc 'Relations export status' do
detail 'This feature was introduced in GitLab 13.12'
is_array true
tags %w[group_export]
success code: 200, model: Entities::BulkImports::ExportStatus
failure [
{ code: 401, message: 'Unauthorized' },
{ code: 403, message: 'Forbidden' },
{ code: 404, message: 'Not found' },
{ code: 503, message: 'Service unavailable' }
]
end
get ':id/export_relations/status' do
present user_group.bulk_import_exports, with: Entities::BulkImports::ExportStatus
if response.success?
accepted!
else
render_api_error!(message: 'Group relations export could not be started.')
end
end
desc 'Download relations export' do
detail 'This feature was introduced in GitLab 13.12'
produces %w[application/octet-stream application/json]
tags %w[group_export]
success code: 200
failure [
{ code: 401, message: 'Unauthorized' },
{ code: 403, message: 'Forbidden' },
{ code: 404, message: 'Not found' },
{ code: 503, message: 'Service unavailable' }
]
end
params do
requires :relation, type: String, desc: 'Group relation name'
end
get ':id/export_relations/download' do
export = user_group.bulk_import_exports.find_by_relation(params[:relation])
file = export&.upload&.export_file
if file
present_carrierwave_file!(file)
else
render_api_error!('404 Not found', 404)
end
end
desc 'Relations export status' do
detail 'This feature was introduced in GitLab 13.12'
is_array true
tags %w[group_export]
success code: 200, model: Entities::BulkImports::ExportStatus
failure [
{ code: 401, message: 'Unauthorized' },
{ code: 403, message: 'Forbidden' },
{ code: 404, message: 'Not found' },
{ code: 503, message: 'Service unavailable' }
]
end
get ':id/export_relations/status' do
present user_group.bulk_import_exports, with: Entities::BulkImports::ExportStatus
end
end
end
end

View File

@ -5,109 +5,114 @@ module API
feature_category :importers
urgency :low
before do
not_found! unless Gitlab::CurrentSettings.project_export_enabled?
authorize_admin_project
end
params do
requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project'
end
resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get export status' do
detail 'This feature was introduced in GitLab 10.6.'
success code: 200, model: Entities::ProjectExportStatus
failure [
{ code: 401, message: 'Unauthorized' },
{ code: 403, message: 'Forbidden' },
{ code: 404, message: 'Not found' },
{ code: 503, message: 'Service unavailable' }
]
tags ['project_export']
end
get ':id/export' do
present user_project, with: Entities::ProjectExportStatus
end
resource do
before do
not_found! unless Gitlab::CurrentSettings.project_export_enabled?
desc 'Download export' do
detail 'This feature was introduced in GitLab 10.6.'
success code: 200
failure [
{ code: 401, message: 'Unauthorized' },
{ code: 403, message: 'Forbidden' },
{ code: 404, message: 'Not found' },
{ code: 503, message: 'Service unavailable' }
]
tags ['project_export']
produces %w[application/octet-stream application/json]
end
get ':id/export/download' do
check_rate_limit! :project_download_export, scope: [current_user, user_project.namespace]
authorize_admin_project
end
if user_project.export_file_exists?
if user_project.export_archive_exists?
present_carrierwave_file!(user_project.export_file)
desc 'Get export status' do
detail 'This feature was introduced in GitLab 10.6.'
success code: 200, model: Entities::ProjectExportStatus
failure [
{ code: 401, message: 'Unauthorized' },
{ code: 403, message: 'Forbidden' },
{ code: 404, message: 'Not found' },
{ code: 503, message: 'Service unavailable' }
]
tags ['project_export']
end
get ':id/export' do
present user_project, with: Entities::ProjectExportStatus
end
desc 'Download export' do
detail 'This feature was introduced in GitLab 10.6.'
success code: 200
failure [
{ code: 401, message: 'Unauthorized' },
{ code: 403, message: 'Forbidden' },
{ code: 404, message: 'Not found' },
{ code: 503, message: 'Service unavailable' }
]
tags ['project_export']
produces %w[application/octet-stream application/json]
end
get ':id/export/download' do
check_rate_limit! :project_download_export, scope: [current_user, user_project.namespace]
if user_project.export_file_exists?
if user_project.export_archive_exists?
present_carrierwave_file!(user_project.export_file)
else
render_api_error!('The project export file is not available yet', 404)
end
else
render_api_error!('The project export file is not available yet', 404)
end
else
render_api_error!('404 Not found or has expired', 404)
end
end
desc 'Start export' do
detail 'This feature was introduced in GitLab 10.6.'
success code: 202
failure [
{ code: 400, message: 'Bad request' },
{ code: 401, message: 'Unauthorized' },
{ code: 403, message: 'Forbidden' },
{ code: 404, message: 'Not found' },
{ code: 429, message: 'Too many requests' },
{ code: 503, message: 'Service unavailable' }
]
tags ['project_export']
end
params do
optional :description, type: String, desc: 'Override the project description'
optional :upload, type: Hash do
optional :url, type: String, desc: 'The URL to upload the project'
optional :http_method, type: String, default: 'PUT', values: %w[PUT POST],
desc: 'HTTP method to upload the exported project'
end
end
post ':id/export' do
check_rate_limit! :project_export, scope: current_user
user_project.remove_exports
project_export_params = declared_params(include_missing: false)
after_export_params = project_export_params.delete(:upload) || {}
export_strategy = if after_export_params[:url].present?
params = after_export_params.slice(:url, :http_method).symbolize_keys
Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy.new(**params)
end
if export_strategy&.invalid?
render_validation_error!(export_strategy)
else
begin
user_project.add_export_job(current_user: current_user,
after_export_strategy: export_strategy,
params: project_export_params)
rescue Project::ExportLimitExceeded => e
render_api_error!(e.message, 400)
render_api_error!('404 Not found or has expired', 404)
end
end
accepted!
desc 'Start export' do
detail 'This feature was introduced in GitLab 10.6.'
success code: 202
failure [
{ code: 400, message: 'Bad request' },
{ code: 401, message: 'Unauthorized' },
{ code: 403, message: 'Forbidden' },
{ code: 404, message: 'Not found' },
{ code: 429, message: 'Too many requests' },
{ code: 503, message: 'Service unavailable' }
]
tags ['project_export']
end
params do
optional :description, type: String, desc: 'Override the project description'
optional :upload, type: Hash do
optional :url, type: String, desc: 'The URL to upload the project'
optional :http_method, type: String, default: 'PUT', values: %w[PUT POST],
desc: 'HTTP method to upload the exported project'
end
end
post ':id/export' do
check_rate_limit! :project_export, scope: current_user
user_project.remove_exports
project_export_params = declared_params(include_missing: false)
after_export_params = project_export_params.delete(:upload) || {}
export_strategy = if after_export_params[:url].present?
params = after_export_params.slice(:url, :http_method).symbolize_keys
Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy.new(**params)
end
if export_strategy&.invalid?
render_validation_error!(export_strategy)
else
begin
user_project.add_export_job(current_user: current_user,
after_export_strategy: export_strategy,
params: project_export_params)
rescue Project::ExportLimitExceeded => e
render_api_error!(e.message, 400)
end
end
accepted!
end
end
resource do
before do
not_found! unless ::Feature.enabled?(:bulk_import)
not_found! unless Gitlab::CurrentSettings.bulk_import_enabled?
authorize_admin_project
end
desc 'Start relations export' do

View File

@ -2,10 +2,6 @@
module BulkImports
module Features
def self.enabled?
::Feature.enabled?(:bulk_import)
end
def self.project_migration_enabled?(destination_namespace = nil)
if destination_namespace.present?
root_ancestor = Namespace.find_by_full_path(destination_namespace)&.root_ancestor

View File

@ -17,42 +17,6 @@ module Gitlab
module GitlabSchema
DICTIONARY_PATH = 'db/docs/'
# These tables are deleted/renamed, but still referenced by migrations.
# This is needed for now, but should be removed in the future
DELETED_TABLES = {
# main tables
'alerts_service_data' => :gitlab_main,
'analytics_devops_adoption_segment_selections' => :gitlab_main,
'analytics_repository_file_commits' => :gitlab_main,
'analytics_repository_file_edits' => :gitlab_main,
'analytics_repository_files' => :gitlab_main,
'audit_events_archived' => :gitlab_main,
'backup_labels' => :gitlab_main,
'clusters_applications_fluentd' => :gitlab_main,
'forked_project_links' => :gitlab_main,
'issue_milestones' => :gitlab_main,
'merge_request_milestones' => :gitlab_main,
'namespace_onboarding_actions' => :gitlab_main,
'services' => :gitlab_main,
'terraform_state_registry' => :gitlab_main,
'tmp_fingerprint_sha256_migration' => :gitlab_main, # used by lib/gitlab/background_migration/migrate_fingerprint_sha256_within_keys.rb
'web_hook_logs_archived' => :gitlab_main,
'vulnerability_export_registry' => :gitlab_main,
'vulnerability_finding_fingerprints' => :gitlab_main,
'vulnerability_export_verification_status' => :gitlab_main,
# CI tables
'ci_build_trace_sections' => :gitlab_ci,
'ci_build_trace_section_names' => :gitlab_ci,
'ci_daily_report_results' => :gitlab_ci,
'ci_test_cases' => :gitlab_ci,
'ci_test_case_failures' => :gitlab_ci,
# leftovers from early implementation of partitioning
'audit_events_part_5fc467ac26' => :gitlab_main,
'web_hook_logs_part_0c5294f417' => :gitlab_main
}.freeze
def self.table_schemas(tables)
tables.map { |table| table_schema(table) }.to_set
end
@ -69,13 +33,13 @@ module Gitlab
# strip partition number of a form `loose_foreign_keys_deleted_records_1`
table_name.gsub!(/_[0-9]+$/, '')
# Tables that are properly mapped
# Tables and views that are properly mapped
if gitlab_schema = views_and_tables_to_schema[table_name]
return gitlab_schema
end
# Tables that are deleted, but we still need to reference them
if gitlab_schema = DELETED_TABLES[table_name]
# Tables and views that are deleted, but we still need to reference them
if gitlab_schema = deleted_views_and_tables_to_schema[table_name]
return gitlab_schema
end
@ -106,29 +70,51 @@ module Gitlab
[Rails.root.join(DICTIONARY_PATH, 'views', '*.yml')]
end
def self.deleted_views_path_globs
[Rails.root.join(DICTIONARY_PATH, 'deleted_views', '*.yml')]
end
def self.deleted_tables_path_globs
[Rails.root.join(DICTIONARY_PATH, 'deleted_tables', '*.yml')]
end
def self.views_and_tables_to_schema
@views_and_tables_to_schema ||= self.tables_to_schema.merge(self.views_to_schema)
end
def self.tables_to_schema
@tables_to_schema ||= Dir.glob(self.dictionary_path_globs).each_with_object({}) do |file_path, dic|
data = YAML.load_file(file_path)
def self.deleted_views_and_tables_to_schema
@deleted_views_and_tables_to_schema ||= self.deleted_tables_to_schema.merge(self.deleted_views_to_schema)
end
dic[data['table_name']] = data['gitlab_schema'].to_sym
end
def self.deleted_tables_to_schema
@deleted_tables_to_schema ||= self.build_dictionary(self.deleted_tables_path_globs)
end
def self.deleted_views_to_schema
@deleted_views_to_schema ||= self.build_dictionary(self.deleted_views_path_globs)
end
def self.tables_to_schema
@tables_to_schema ||= self.build_dictionary(self.dictionary_path_globs)
end
def self.views_to_schema
@views_to_schema ||= Dir.glob(self.view_path_globs).each_with_object({}) do |file_path, dic|
data = YAML.load_file(file_path)
dic[data['view_name']] = data['gitlab_schema'].to_sym
end
@views_to_schema ||= self.build_dictionary(self.view_path_globs)
end
def self.schema_names
@schema_names ||= self.views_and_tables_to_schema.values.to_set
end
private_class_method def self.build_dictionary(path_globs)
Dir.glob(path_globs).each_with_object({}) do |file_path, dic|
data = YAML.load_file(file_path)
key_name = data['table_name'] || data['view_name']
dic[key_name] = data['gitlab_schema'].to_sym
end
end
end
end
end

View File

@ -42,7 +42,7 @@ module Gitlab
def should_lock_writes_on_table?(table_name)
# currently gitlab_schema represents only present existing tables, this is workaround for deleted tables
# that should be skipped as they will be removed in a future migration.
return false if Gitlab::Database::GitlabSchema::DELETED_TABLES[table_name]
return false if Gitlab::Database::GitlabSchema.deleted_tables_to_schema[table_name]
table_schema = Gitlab::Database::GitlabSchema.table_schema(table_name.to_s, undefined: false)

View File

@ -17,7 +17,8 @@ module Gitlab
HTTP_ERRORS = HTTP_TIMEOUT_ERRORS + [
EOFError, SocketError, OpenSSL::SSL::SSLError, OpenSSL::OpenSSLError,
Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ENETUNREACH,
Gitlab::HTTP::BlockedUrlError, Gitlab::HTTP::RedirectionTooDeep
Gitlab::HTTP::BlockedUrlError, Gitlab::HTTP::RedirectionTooDeep,
Net::HTTPBadResponse
].freeze
DEFAULT_TIMEOUT_OPTIONS = {

View File

@ -25797,6 +25797,9 @@ msgstr[1] ""
msgid "Membership"
msgstr ""
msgid "Members|%{group} by %{createdBy}"
msgstr ""
msgid "Members|%{time} by %{user}"
msgstr ""
@ -25806,6 +25809,12 @@ msgstr ""
msgid "Members|2FA"
msgstr ""
msgid "Members|Access granted"
msgstr ""
msgid "Members|Activity"
msgstr ""
msgid "Members|An error occurred while trying to enable LDAP override, please try again."
msgstr ""
@ -25842,6 +25851,9 @@ msgstr ""
msgid "Members|Direct"
msgstr ""
msgid "Members|Direct member by %{createdBy}"
msgstr ""
msgid "Members|Disabled"
msgstr ""
@ -25869,6 +25881,9 @@ msgstr ""
msgid "Members|LDAP override enabled."
msgstr ""
msgid "Members|Last activity"
msgstr ""
msgid "Members|Leave \"%{source}\""
msgstr ""
@ -25896,6 +25911,9 @@ msgstr ""
msgid "Members|Search invited"
msgstr ""
msgid "Members|User created"
msgstr ""
msgid "Member|Deny access"
msgstr ""

View File

@ -60,6 +60,7 @@
"@gitlab/ui": "52.6.1",
"@gitlab/visual-review-tools": "1.7.3",
"@gitlab/web-ide": "0.0.1-dev-20221217175648",
"@mermaid-js/mermaid-mindmap": "^9.3.0",
"@rails/actioncable": "6.1.4-7",
"@rails/ujs": "6.1.4-7",
"@sourcegraph/code-host-integration": "0.0.84",
@ -147,7 +148,7 @@
"marked": "^4.0.18",
"mathjax": "3",
"mdurl": "^1.0.1",
"mermaid": "^9.1.3",
"mermaid": "^9.3.0",
"micromatch": "^4.0.5",
"minimatch": "^3.0.4",
"monaco-editor": "^0.30.1",

View File

@ -2,10 +2,12 @@
require 'spec_helper'
RSpec.describe Import::BulkImportsController do
RSpec.describe Import::BulkImportsController, feature_category: :importers do
let_it_be(:user) { create(:user) }
before do
stub_application_setting(bulk_import_enabled: true)
sign_in(user)
end
@ -326,9 +328,9 @@ RSpec.describe Import::BulkImportsController do
end
end
context 'when bulk_import feature flag is disabled' do
context 'when feature is disabled' do
before do
stub_feature_flags(bulk_import: false)
stub_application_setting(bulk_import_enabled: false)
end
context 'POST configure' do

View File

@ -8,6 +8,7 @@ RSpec.shared_examples 'validate dictionary' do |objects, directory_path, require
let(:metadata_allowed_fields) do
required_fields + %i[
feature_categories
classes
description
introduced_by_url
@ -139,3 +140,19 @@ RSpec.describe 'Tables documentation', feature_category: :database do
include_examples 'validate dictionary', tables, directory_path, required_fields
end
RSpec.describe 'Deleted tables documentation', feature_category: :database do
directory_path = File.join('db', 'docs', 'deleted_tables')
tables = Dir.glob(File.join(directory_path, '*.yml')).map { |f| File.basename(f, '.yml') }.sort.uniq
required_fields = %i[table_name gitlab_schema removed_by_url removed_in_milestone]
include_examples 'validate dictionary', tables, directory_path, required_fields
end
RSpec.describe 'Deleted views documentation', feature_category: :database do
directory_path = File.join('db', 'docs', 'deleted_views')
views = Dir.glob(File.join(directory_path, '*.yml')).map { |f| File.basename(f, '.yml') }.sort.uniq
required_fields = %i[view_name gitlab_schema removed_by_url removed_in_milestone]
include_examples 'validate dictionary', views, directory_path, required_fields
end

View File

@ -28,7 +28,7 @@ RSpec.describe 'Admin::Users', feature_category: :user_management do
expect(page).to have_content(current_user.email)
expect(page).to have_content(current_user.name)
expect(page).to have_content(current_user.created_at.strftime('%e %b, %Y'))
expect(page).to have_content(current_user.created_at.strftime('%b %e, %Y'))
expect(page).to have_content(user.email)
expect(page).to have_content(user.name)
expect(page).to have_content('Projects')

View File

@ -12,6 +12,8 @@ RSpec.describe 'Import/Export - GitLab migration history', :js, feature_category
let_it_be(:failed_entity_2) { create(:bulk_import_entity, :failed, bulk_import: user_import_2) }
before do
stub_application_setting(bulk_import_enabled: true)
gitlab_sign_in(user)
visit new_group_path

View File

@ -56,7 +56,7 @@ RSpec.describe 'Groups > Members > Sort members', :js, feature_category: :subgro
expect(first_row.text).to include(owner.name)
expect(second_row.text).to include(developer.name)
expect_sort_by('Created on', :asc)
expect_sort_by('User created', :asc)
end
it 'sorts by user created on descending' do
@ -65,7 +65,7 @@ RSpec.describe 'Groups > Members > Sort members', :js, feature_category: :subgro
expect(first_row.text).to include(developer.name)
expect(second_row.text).to include(owner.name)
expect_sort_by('Created on', :desc)
expect_sort_by('User created', :desc)
end
it 'sorts by last activity ascending' do

View File

@ -48,7 +48,7 @@ RSpec.describe 'Projects > Members > Sorting', :js, feature_category: :subgroups
expect(first_row.text).to have_content(maintainer.name)
expect(second_row.text).to have_content(developer.name)
expect_sort_by('Created on', :asc)
expect_sort_by('User created', :asc)
end
it 'sorts by user created on descending' do
@ -57,7 +57,7 @@ RSpec.describe 'Projects > Members > Sorting', :js, feature_category: :subgroups
expect(first_row.text).to have_content(developer.name)
expect(second_row.text).to have_content(maintainer.name)
expect_sort_by('Created on', :desc)
expect_sort_by('User created', :desc)
end
it 'sorts by last activity ascending' do

View File

@ -24,7 +24,7 @@ describe('FormatDate component', () => {
it.each`
date | dateFormat | output
${mockDate} | ${undefined} | ${'13 Nov, 2020'}
${mockDate} | ${undefined} | ${'Nov 13, 2020'}
${null} | ${undefined} | ${'Never'}
${undefined} | ${undefined} | ${'Never'}
${mockDate} | ${ISO_SHORT_FORMAT} | ${'2020-11-13'}

View File

@ -139,6 +139,7 @@ describe('BoardFilteredSearch', () => {
{ type: TOKEN_TYPE_ITERATION, value: { data: 'Any&3', operator: '=' } },
{ type: TOKEN_TYPE_RELEASE, value: { data: 'v1.0.0', operator: '=' } },
{ type: TOKEN_TYPE_HEALTH, value: { data: 'onTrack', operator: '=' } },
{ type: TOKEN_TYPE_HEALTH, value: { data: 'atRisk', operator: '!=' } },
];
jest.spyOn(urlUtility, 'updateHistory');
findFilteredSearch().vm.$emit('onFilter', mockFilters);
@ -147,7 +148,7 @@ describe('BoardFilteredSearch', () => {
title: '',
replace: true,
url:
'http://test.host/?author_username=root&label_name[]=label&label_name[]=label%262&assignee_username=root&milestone_title=New%20Milestone&iteration_id=Any&iteration_cadence_id=3&types=INCIDENT&weight=2&release_tag=v1.0.0&health_status=onTrack',
'http://test.host/?not[health_status]=atRisk&author_username=root&label_name[]=label&label_name[]=label%262&assignee_username=root&milestone_title=New%20Milestone&iteration_id=Any&iteration_cadence_id=3&types=INCIDENT&weight=2&release_tag=v1.0.0&health_status=onTrack',
});
});

View File

@ -16,6 +16,7 @@ import {
TOKEN_TYPE_RELEASE,
TOKEN_TYPE_TYPE,
TOKEN_TYPE_WEIGHT,
TOKEN_TYPE_HEALTH,
} from '~/vue_shared/components/filtered_search_bar/constants';
export const getIssuesQueryResponse = {
@ -170,6 +171,8 @@ export const locationSearch = [
'not[weight]=3',
'crm_contact_id=123',
'crm_organization_id=456',
'health_status=atRisk',
'not[health_status]=onTrack',
].join('&');
export const locationSearchWithSpecialValues = [
@ -182,6 +185,7 @@ export const locationSearchWithSpecialValues = [
'milestone_title=Upcoming',
'epic_id=None',
'weight=None',
'health_status=None',
].join('&');
export const filteredTokens = [
@ -225,6 +229,8 @@ export const filteredTokens = [
{ type: TOKEN_TYPE_WEIGHT, value: { data: '3', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_CONTACT, value: { data: '123', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_ORGANIZATION, value: { data: '456', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_HEALTH, value: { data: 'atRisk', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_HEALTH, value: { data: 'onTrack', operator: OPERATOR_NOT } },
{ type: FILTERED_SEARCH_TERM, value: { data: 'find' } },
{ type: FILTERED_SEARCH_TERM, value: { data: 'issues' } },
];
@ -239,6 +245,7 @@ export const filteredTokensWithSpecialValues = [
{ type: TOKEN_TYPE_MILESTONE, value: { data: 'Upcoming', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_EPIC, value: { data: 'None', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_WEIGHT, value: { data: 'None', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_HEALTH, value: { data: 'None', operator: OPERATOR_IS } },
];
export const apiParams = {
@ -255,6 +262,7 @@ export const apiParams = {
weight: '1',
crmContactId: '123',
crmOrganizationId: '456',
healthStatusFilter: 'atRisk',
not: {
authorUsername: 'marge',
assigneeUsernames: ['patty', 'selma'],
@ -266,6 +274,7 @@ export const apiParams = {
iterationId: ['20', '42'],
epicId: '34',
weight: '3',
healthStatusFilter: 'onTrack',
},
or: {
authorUsernames: ['burns', 'smithers'],
@ -283,6 +292,7 @@ export const apiParamsWithSpecialValues = {
milestoneWildcardId: 'UPCOMING',
epicId: 'None',
weight: 'None',
healthStatusFilter: 'NONE',
};
export const urlParams = {
@ -311,6 +321,8 @@ export const urlParams = {
'not[weight]': '3',
crm_contact_id: '123',
crm_organization_id: '456',
health_status: 'atRisk',
'not[health_status]': 'onTrack',
};
export const urlParamsWithSpecialValues = {
@ -323,6 +335,7 @@ export const urlParamsWithSpecialValues = {
milestone_title: 'Upcoming',
epic_id: 'None',
weight: 'None',
health_status: 'None',
};
export const project1 = {

View File

@ -38,7 +38,6 @@ describe('AccessRequestActionButtons', () => {
title: 'Deny access',
isAccessRequest: true,
isInvite: false,
icon: 'close',
});
});

View File

@ -44,7 +44,6 @@ describe('InviteActionButtons', () => {
title: 'Revoke invite',
isAccessRequest: false,
isInvite: true,
icon: 'remove',
});
});
});

View File

@ -79,18 +79,4 @@ describe('RemoveMemberButton', () => {
expect(actions.showRemoveMemberModal).toHaveBeenCalledWith(expect.any(Object), modalData);
});
describe('button optional properties', () => {
it('has default value for category and text', () => {
createComponent();
expect(findButton().props('category')).toBe('secondary');
expect(findButton().text()).toBe('');
});
it('allow changing value of button category and text', () => {
createComponent({ buttonCategory: 'primary', buttonText: 'Decline request' });
expect(findButton().props('category')).toBe('primary');
expect(findButton().text()).toBe('Decline request');
});
});
});

View File

@ -43,12 +43,9 @@ describe('UserActionButtons', () => {
memberId: member.id,
memberType: 'GroupMember',
message: `Are you sure you want to remove ${member.user.name} from "${member.source.fullName}"?`,
title: null,
title: UserActionButtons.i18n.title,
isAccessRequest: false,
isInvite: false,
icon: '',
buttonCategory: 'secondary',
buttonText: 'Remove member',
userDeletionObstacles: {
name: member.user.name,
obstacles: parseUserDeletionObstacles(member.user),
@ -132,30 +129,4 @@ describe('UserActionButtons', () => {
expect(findRemoveMemberButton().props().memberType).toBe('ProjectMember');
});
});
describe('isInvitedUser', () => {
it.each`
isInvitedUser | icon | buttonText | buttonCategory
${true} | ${'remove'} | ${null} | ${'primary'}
${false} | ${''} | ${'Remove member'} | ${'secondary'}
`(
'passes the correct props to remove-member-button when isInvitedUser is $isInvitedUser',
({ isInvitedUser, icon, buttonText, buttonCategory }) => {
createComponent({
isInvitedUser,
permissions: {
canRemove: true,
},
});
expect(findRemoveMemberButton().props()).toEqual(
expect.objectContaining({
icon,
buttonText,
buttonCategory,
}),
);
},
);
});
});

View File

@ -0,0 +1,61 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MemberActivity with a member that does not have all of the fields renders \`User created\` field 1`] = `
<div>
<!---->
<div>
<strong>
Access granted:
</strong>
<span>
Aug 06, 2020
</span>
</div>
<!---->
</div>
`;
exports[`MemberActivity with a member that has all fields renders \`User created\`, \`Access granted\`, and \`Last activity\` fields 1`] = `
<div>
<div>
<strong>
User created:
</strong>
<span>
Mar 10, 2022
</span>
</div>
<div>
<strong>
Access granted:
</strong>
<span>
Jul 17, 2020
</span>
</div>
<div>
<strong>
Last activity:
</strong>
<span>
Mar 15, 2022
</span>
</div>
</div>
`;

View File

@ -1,20 +1,18 @@
import { within } from '@testing-library/dom';
import { mount, createWrapper } from '@vue/test-utils';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { useFakeDate } from 'helpers/fake_date';
import CreatedAt from '~/members/components/table/created_at.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
describe('CreatedAt', () => {
// March 15th, 2020
useFakeDate(2020, 2, 15);
const date = '2020-03-01T00:00:00.000';
const dateTimeAgo = '2 weeks ago';
const formattedDate = 'Mar 01, 2020';
let wrapper;
const createComponent = (propsData) => {
wrapper = mount(CreatedAt, {
wrapper = mountExtended(CreatedAt, {
propsData: {
date,
...propsData,
@ -22,9 +20,6 @@ describe('CreatedAt', () => {
});
};
const getByText = (text, options) =>
createWrapper(within(wrapper.element).getByText(text, options));
afterEach(() => {
wrapper.destroy();
});
@ -35,11 +30,7 @@ describe('CreatedAt', () => {
});
it('displays created at text', () => {
expect(getByText(dateTimeAgo).exists()).toBe(true);
});
it('uses `TimeAgoTooltip` component to display tooltip', () => {
expect(wrapper.findComponent(TimeAgoTooltip).exists()).toBe(true);
expect(wrapper.findByText(formattedDate).exists()).toBe(true);
});
});
@ -52,7 +43,7 @@ describe('CreatedAt', () => {
},
});
const link = getByText('Administrator');
const link = wrapper.findByRole('link', { name: 'Administrator' });
expect(link.exists()).toBe(true);
expect(link.attributes('href')).toBe('https://gitlab.com/root');

View File

@ -0,0 +1,40 @@
import { mountExtended } from 'helpers/vue_test_utils_helper';
import MemberActivity from '~/members/components/table/member_activity.vue';
import { member as memberMock, group as groupLinkMock } from '../../mock_data';
describe('MemberActivity', () => {
let wrapper;
const defaultPropsData = {
member: memberMock,
};
const createComponent = ({ propsData = {} } = {}) => {
wrapper = mountExtended(MemberActivity, {
propsData: {
...defaultPropsData,
...propsData,
},
});
};
describe('with a member that has all fields', () => {
beforeEach(() => {
createComponent();
});
it('renders `User created`, `Access granted`, and `Last activity` fields', () => {
expect(wrapper.element).toMatchSnapshot();
});
});
describe('with a member that does not have all of the fields', () => {
beforeEach(() => {
createComponent({ propsData: { member: groupLinkMock } });
});
it('renders `User created` field', () => {
expect(wrapper.element).toMatchSnapshot();
});
});
});

View File

@ -1,19 +1,25 @@
import { getByText as getByTextHelper } from '@testing-library/dom';
import { mount, createWrapper } from '@vue/test-utils';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import MemberSource from '~/members/components/table/member_source.vue';
describe('MemberSource', () => {
let wrapper;
const memberSource = {
id: 102,
fullName: 'Foo bar',
webUrl: 'https://gitlab.com/groups/foo-bar',
};
const createdBy = {
name: 'Administrator',
webUrl: 'https://gitlab.com/root',
};
const createComponent = (propsData) => {
wrapper = mount(MemberSource, {
wrapper = mountExtended(MemberSource, {
propsData: {
memberSource: {
id: 102,
fullName: 'Foo bar',
webUrl: 'https://gitlab.com/groups/foo-bar',
},
memberSource,
...propsData,
},
directives: {
@ -22,9 +28,6 @@ describe('MemberSource', () => {
});
};
const getByText = (text, options) =>
createWrapper(getByTextHelper(wrapper.element, text, options));
const getTooltipDirective = (elementWrapper) => getBinding(elementWrapper.element, 'gl-tooltip');
afterEach(() => {
@ -32,40 +35,69 @@ describe('MemberSource', () => {
});
describe('direct member', () => {
it('displays "Direct member"', () => {
createComponent({
isDirectMember: true,
});
describe('when created by is available', () => {
it('displays "Direct member by <user name>"', () => {
createComponent({
isDirectMember: true,
createdBy,
});
expect(getByText('Direct member').exists()).toBe(true);
expect(wrapper.text()).toBe('Direct member by Administrator');
expect(wrapper.findByRole('link', { name: createdBy.name }).attributes('href')).toBe(
createdBy.webUrl,
);
});
});
describe('when created by is not available', () => {
it('displays "Direct member"', () => {
createComponent({
isDirectMember: true,
});
expect(wrapper.text()).toBe('Direct member');
});
});
});
describe('inherited member', () => {
let sourceGroupLink;
beforeEach(() => {
createComponent({
isDirectMember: false,
describe('when created by is available', () => {
beforeEach(() => {
createComponent({
isDirectMember: false,
createdBy,
});
});
sourceGroupLink = getByText('Foo bar');
it('displays "<group name> by <user name>"', () => {
expect(wrapper.text()).toBe('Foo bar by Administrator');
expect(wrapper.findByRole('link', { name: memberSource.fullName }).attributes('href')).toBe(
memberSource.webUrl,
);
expect(wrapper.findByRole('link', { name: createdBy.name }).attributes('href')).toBe(
createdBy.webUrl,
);
});
});
it('displays a link to source group', () => {
createComponent({
isDirectMember: false,
describe('when created by is not available', () => {
beforeEach(() => {
createComponent({
isDirectMember: false,
});
});
expect(sourceGroupLink.exists()).toBe(true);
expect(sourceGroupLink.attributes('href')).toBe('https://gitlab.com/groups/foo-bar');
});
it('displays a link to source group', () => {
expect(wrapper.text()).toBe(memberSource.fullName);
expect(wrapper.attributes('href')).toBe(memberSource.webUrl);
});
it('displays tooltip with "Inherited"', () => {
const tooltipDirective = getTooltipDirective(sourceGroupLink);
it('displays tooltip with "Inherited"', () => {
const tooltipDirective = getTooltipDirective(wrapper);
expect(tooltipDirective).not.toBeUndefined();
expect(sourceGroupLink.attributes('title')).toBe('Inherited');
expect(tooltipDirective).not.toBeUndefined();
expect(tooltipDirective.value).toBe('Inherited');
});
});
});
});

View File

@ -8,9 +8,9 @@ import ExpirationDatepicker from '~/members/components/table/expiration_datepick
import MemberActionButtons from '~/members/components/table/member_action_buttons.vue';
import MemberAvatar from '~/members/components/table/member_avatar.vue';
import MemberSource from '~/members/components/table/member_source.vue';
import MemberActivity from '~/members/components/table/member_activity.vue';
import MembersTable from '~/members/components/table/members_table.vue';
import RoleDropdown from '~/members/components/table/role_dropdown.vue';
import UserDate from '~/vue_shared/components/user_date.vue';
import {
MEMBER_TYPES,
MEMBER_STATE_CREATED,
@ -106,16 +106,14 @@ describe('MembersTable', () => {
};
it.each`
field | label | member | expectedComponent
${'account'} | ${'Account'} | ${memberMock} | ${MemberAvatar}
${'source'} | ${'Source'} | ${memberMock} | ${MemberSource}
${'granted'} | ${'Access granted'} | ${memberMock} | ${CreatedAt}
${'invited'} | ${'Invited'} | ${invite} | ${CreatedAt}
${'requested'} | ${'Requested'} | ${accessRequest} | ${CreatedAt}
${'maxRole'} | ${'Max role'} | ${memberCanUpdate} | ${RoleDropdown}
${'expiration'} | ${'Expiration'} | ${memberMock} | ${ExpirationDatepicker}
${'userCreatedAt'} | ${'Created on'} | ${memberMock} | ${UserDate}
${'lastActivityOn'} | ${'Last activity'} | ${memberMock} | ${UserDate}
field | label | member | expectedComponent
${'account'} | ${'Account'} | ${memberMock} | ${MemberAvatar}
${'source'} | ${'Source'} | ${memberMock} | ${MemberSource}
${'invited'} | ${'Invited'} | ${invite} | ${CreatedAt}
${'requested'} | ${'Requested'} | ${accessRequest} | ${CreatedAt}
${'maxRole'} | ${'Max role'} | ${memberCanUpdate} | ${RoleDropdown}
${'expiration'} | ${'Expiration'} | ${memberMock} | ${ExpirationDatepicker}
${'activity'} | ${'Activity'} | ${memberMock} | ${MemberActivity}
`('renders the $label field', ({ field, label, member, expectedComponent }) => {
createComponent({
members: [member],

View File

@ -8,13 +8,21 @@ RSpec.shared_examples 'validate path globs' do |path_globs|
end
end
RSpec.shared_examples 'validate schema data' do |tables_and_views|
it 'all tables and views have assigned a known gitlab_schema' do
expect(tables_and_views).to all(
match([be_a(String), be_in(Gitlab::Database.schemas_to_base_models.keys.map(&:to_sym))])
)
end
end
RSpec.describe Gitlab::Database::GitlabSchema do
describe '.deleted_views_and_tables_to_schema' do
include_examples 'validate schema data', described_class.deleted_views_and_tables_to_schema
end
describe '.views_and_tables_to_schema' do
it 'all tables and views have assigned a known gitlab_schema' do
expect(described_class.views_and_tables_to_schema).to all(
match([be_a(String), be_in(Gitlab::Database.schemas_to_base_models.keys.map(&:to_sym))])
)
end
include_examples 'validate schema data', described_class.views_and_tables_to_schema
# This being run across different databases indirectly also tests
# a general consistency of structure across databases
@ -55,6 +63,14 @@ RSpec.describe Gitlab::Database::GitlabSchema do
include_examples 'validate path globs', described_class.view_path_globs
end
describe '.deleted_tables_path_globs' do
include_examples 'validate path globs', described_class.deleted_tables_path_globs
end
describe '.deleted_views_path_globs' do
include_examples 'validate path globs', described_class.deleted_views_path_globs
end
describe '.tables_to_schema' do
let(:database_models) { Gitlab::Database.database_base_models.except(:geo) }
let(:views) { database_models.flat_map { |_, m| m.connection.views }.sort.uniq }

View File

@ -95,7 +95,9 @@ RSpec.describe Gitlab::Database::MigrationHelpers::AutomaticLockWritesOnTables,
context 'when table listed as a deleted table' do
before do
stub_const("Gitlab::Database::GitlabSchema::DELETED_TABLES", { table_name.to_s => :gitlab_main })
allow(Gitlab::Database::GitlabSchema).to receive(:deleted_tables_to_schema).and_return(
{ table_name.to_s => :gitlab_main }
)
end
it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:ci]
@ -132,7 +134,9 @@ RSpec.describe Gitlab::Database::MigrationHelpers::AutomaticLockWritesOnTables,
context 'when table listed as a deleted table' do
before do
stub_const("Gitlab::Database::GitlabSchema::DELETED_TABLES", { table_name.to_s => :gitlab_ci })
allow(Gitlab::Database::GitlabSchema).to receive(:deleted_tables_to_schema).and_return(
{ table_name.to_s => :gitlab_ci }
)
end
it_behaves_like 'does not lock writes on table', Gitlab::Database.database_base_models[:main]

View File

@ -11,9 +11,26 @@ RSpec.describe API::BulkImports, feature_category: :importers do
let_it_be(:entity_3) { create(:bulk_import_entity, bulk_import: import_2) }
let_it_be(:failure_3) { create(:bulk_import_failure, entity: entity_3) }
before do
stub_application_setting(bulk_import_enabled: true)
end
shared_examples 'disabled feature' do
it 'returns 404' do
stub_application_setting(bulk_import_enabled: false)
request
expect(response).to have_gitlab_http_status(:not_found)
end
end
describe 'GET /bulk_imports' do
let(:request) { get api('/bulk_imports', user), params: params }
let(:params) { {} }
it 'returns a list of bulk imports authored by the user' do
get api('/bulk_imports', user)
request
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.pluck('id')).to contain_exactly(import_1.id, import_2.id)
@ -21,26 +38,38 @@ RSpec.describe API::BulkImports, feature_category: :importers do
context 'sort parameter' do
it 'sorts by created_at descending by default' do
get api('/bulk_imports', user)
request
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.pluck('id')).to eq([import_2.id, import_1.id])
end
it 'sorts by created_at descending when explicitly specified' do
get api('/bulk_imports', user), params: { sort: 'desc' }
context 'when explicitly specified' do
context 'when descending' do
let(:params) { { sort: 'desc' } }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.pluck('id')).to eq([import_2.id, import_1.id])
end
it 'sorts by created_at descending' do
request
it 'sorts by created_at ascending when explicitly specified' do
get api('/bulk_imports', user), params: { sort: 'asc' }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.pluck('id')).to match_array([import_2.id, import_1.id])
end
end
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.pluck('id')).to eq([import_1.id, import_2.id])
context 'when ascending' do
let(:params) { { sort: 'asc' } }
it 'sorts by created_at ascending when explicitly specified' do
request
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.pluck('id')).to match_array([import_1.id, import_2.id])
end
end
end
end
include_examples 'disabled feature'
end
describe 'POST /bulk_imports' do
@ -56,21 +85,10 @@ RSpec.describe API::BulkImports, feature_category: :importers do
end
end
context 'when bulk_import feature flag is disabled' do
before do
stub_feature_flags(bulk_import: false)
end
it 'returns 404' do
post api('/bulk_imports', user), params: {}
expect(response).to have_gitlab_http_status(:not_found)
end
end
shared_examples 'starting a new migration' do
it 'starts a new migration' do
post api('/bulk_imports', user), params: {
let(:request) { post api('/bulk_imports', user), params: params }
let(:params) do
{
configuration: {
url: 'http://gitlab.example',
access_token: 'access_token'
@ -83,6 +101,10 @@ RSpec.describe API::BulkImports, feature_category: :importers do
}.merge(destination_param)
]
}
end
it 'starts a new migration' do
request
expect(response).to have_gitlab_http_status(:created)
@ -99,8 +121,8 @@ RSpec.describe API::BulkImports, feature_category: :importers do
end
context 'when both destination_name & destination_slug are provided' do
it 'returns a mutually exclusive error' do
post api('/bulk_imports', user), params: {
let(:params) do
{
configuration: {
url: 'http://gitlab.example',
access_token: 'access_token'
@ -115,6 +137,10 @@ RSpec.describe API::BulkImports, feature_category: :importers do
}
]
}
end
it 'returns a mutually exclusive error' do
request
expect(response).to have_gitlab_http_status(:bad_request)
@ -123,8 +149,8 @@ RSpec.describe API::BulkImports, feature_category: :importers do
end
context 'when neither destination_name nor destination_slug is provided' do
it 'returns at_least_one_of error' do
post api('/bulk_imports', user), params: {
let(:params) do
{
configuration: {
url: 'http://gitlab.example',
access_token: 'access_token'
@ -137,6 +163,10 @@ RSpec.describe API::BulkImports, feature_category: :importers do
}
]
}
end
it 'returns at_least_one_of error' do
request
expect(response).to have_gitlab_http_status(:bad_request)
@ -145,8 +175,8 @@ RSpec.describe API::BulkImports, feature_category: :importers do
end
context 'when provided url is blocked' do
it 'returns blocked url error' do
post api('/bulk_imports', user), params: {
let(:params) do
{
configuration: {
url: 'url',
access_token: 'access_token'
@ -158,49 +188,71 @@ RSpec.describe API::BulkImports, feature_category: :importers do
destination_namespace: 'destination_namespace'
]
}
end
it 'returns blocked url error' do
request
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response['message']).to eq('Validation failed: Url is blocked: Only allowed schemes are http, https')
end
end
include_examples 'disabled feature'
end
describe 'GET /bulk_imports/entities' do
let(:request) { get api('/bulk_imports/entities', user) }
it 'returns a list of all import entities authored by the user' do
get api('/bulk_imports/entities', user)
request
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.pluck('id')).to contain_exactly(entity_1.id, entity_2.id, entity_3.id)
end
include_examples 'disabled feature'
end
describe 'GET /bulk_imports/:id' do
let(:request) { get api("/bulk_imports/#{import_1.id}", user) }
it 'returns specified bulk import' do
get api("/bulk_imports/#{import_1.id}", user)
request
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq(import_1.id)
end
include_examples 'disabled feature'
end
describe 'GET /bulk_imports/:id/entities' do
let(:request) { get api("/bulk_imports/#{import_2.id}/entities", user) }
it 'returns specified bulk import entities with failures' do
get api("/bulk_imports/#{import_2.id}/entities", user)
request
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.pluck('id')).to contain_exactly(entity_3.id)
expect(json_response.first['failures'].first['exception_class']).to eq(failure_3.exception_class)
end
include_examples 'disabled feature'
end
describe 'GET /bulk_imports/:id/entities/:entity_id' do
let(:request) { get api("/bulk_imports/#{import_1.id}/entities/#{entity_2.id}", user) }
it 'returns specified bulk import entity' do
get api("/bulk_imports/#{import_1.id}/entities/#{entity_2.id}", user)
request
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq(entity_2.id)
end
include_examples 'disabled feature'
end
context 'when user is unauthenticated' do

View File

@ -173,6 +173,8 @@ RSpec.describe API::GroupExport, feature_category: :importers do
let(:status_path) { "/groups/#{group.id}/export_relations/status" }
before do
stub_application_setting(bulk_import_enabled: true)
group.add_owner(user)
end
@ -212,11 +214,12 @@ RSpec.describe API::GroupExport, feature_category: :importers do
context 'when export_file.file does not exist' do
it 'returns 404' do
allow(upload).to receive(:export_file).and_return(nil)
allow(export).to receive(:upload).and_return(nil)
get api(download_path, user)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Not found')
end
end
end
@ -234,5 +237,11 @@ RSpec.describe API::GroupExport, feature_category: :importers do
expect(json_response.pluck('status')).to contain_exactly(-1, 0, 1)
end
end
context 'when bulk import is disabled' do
it_behaves_like '404 response' do
let(:request) { get api(path, user) }
end
end
end
end

View File

@ -511,6 +511,10 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category:
let_it_be(:status_path) { "/projects/#{project.id}/export_relations/status" }
before do
stub_application_setting(bulk_import_enabled: true)
end
context 'when user is a maintainer' do
before do
project.add_maintainer(user)
@ -584,9 +588,9 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category:
end
end
context 'with bulk_import FF disabled' do
context 'with bulk_import is disabled' do
before do
stub_feature_flags(bulk_import: false)
stub_application_setting(bulk_import_enabled: false)
end
describe 'POST /projects/:id/export_relations' do
@ -641,5 +645,11 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category:
end
end
end
context 'when bulk import is disabled' do
it_behaves_like '404 response' do
let(:request) { get api(path, user) }
end
end
end
end

172
yarn.lock
View File

@ -1627,6 +1627,19 @@
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.3.tgz#0300943770e04231041a51bd39f0439b5c7ab4f0"
integrity sha512-nkalE/f1RvRGChwBnEIoBfSEYOXnCRdleKuv6+lePbMDrMZXeDQnqak5XDOeBgrPPyPfAdcCu/B5z+v3VhplGg==
"@mermaid-js/mermaid-mindmap@^9.3.0":
version "9.3.0"
resolved "https://registry.yarnpkg.com/@mermaid-js/mermaid-mindmap/-/mermaid-mindmap-9.3.0.tgz#cfe10329198a0f37e27eef1dcc4a1cf21f187e2b"
integrity sha512-IhtYSVBBRYviH1Ehu8gk69pMDF8DSRqXBRDMWrEfHoaMruHeaP2DXA3PBnuwsMaCdPQhlUUcy/7DBLAEIXvCAw==
dependencies:
"@braintree/sanitize-url" "^6.0.0"
cytoscape "^3.23.0"
cytoscape-cose-bilkent "^4.1.0"
cytoscape-fcose "^2.1.0"
d3 "^7.0.0"
khroma "^2.0.0"
non-layered-tidy-tree-layout "^2.0.2"
"@miragejs/pretender-node-polyfill@^0.1.0":
version "0.1.2"
resolved "https://registry.yarnpkg.com/@miragejs/pretender-node-polyfill/-/pretender-node-polyfill-0.1.2.tgz#d26b6b7483fb70cd62189d05c95d2f67153e43f2"
@ -4007,6 +4020,20 @@ core-util-is@~1.0.0:
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
cose-base@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/cose-base/-/cose-base-1.0.3.tgz#650334b41b869578a543358b80cda7e0abe0a60a"
integrity sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==
dependencies:
layout-base "^1.0.0"
cose-base@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cose-base/-/cose-base-2.1.0.tgz#89b2d4a59d7bd0cde3138a4689825f3e8a5abd6a"
integrity sha512-HTMm07dhxq1dIPGWwpiVrIk9n+DH7KYmqWA786mLe8jDS+1ZjGtJGIIsJVKoseZXS6/FxiUWCJ2B7XzqUCuhPw==
dependencies:
layout-base "^2.0.0"
cosmiconfig-toml-loader@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/cosmiconfig-toml-loader/-/cosmiconfig-toml-loader-1.0.0.tgz#0681383651cceff918177debe9084c0d3769509b"
@ -4245,15 +4272,37 @@ cyclist@~0.2.2:
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=
cytoscape-cose-bilkent@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz#762fa121df9930ffeb51a495d87917c570ac209b"
integrity sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==
dependencies:
cose-base "^1.0.0"
cytoscape-fcose@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cytoscape-fcose/-/cytoscape-fcose-2.1.0.tgz#04c3093776ea6b71787009de641607db7d4edf55"
integrity sha512-Q3apPl66jf8/2sMsrCjNP247nbDkyIPjA9g5iPMMWNLZgP3/mn9aryF7EFY/oRPEpv7bKJ4jYmCoU5r5/qAc1Q==
dependencies:
cose-base "^2.0.0"
cytoscape@^3.23.0:
version "3.23.0"
resolved "https://registry.yarnpkg.com/cytoscape/-/cytoscape-3.23.0.tgz#054ee05a6d0aa3b4f139382bbf2f4e5226df3c6d"
integrity sha512-gRZqJj/1kiAVPkrVFvz/GccxsXhF3Qwpptl32gKKypO4IlqnKBjTOu+HbXtEggSGzC5KCaHp3/F7GgENrtsFkA==
dependencies:
heap "^0.2.6"
lodash "^4.17.21"
d3-array@1, "d3-array@1 - 2", d3-array@^1.1.1, d3-array@^1.2.0:
version "1.2.4"
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f"
integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==
"d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3:
version "3.0.4"
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.0.4.tgz#60550bcc9818be9ace88d269ccd97038fc399b55"
integrity sha512-ShFl90cxNqDaSynDF/Bik/kTzISqePqU3qo2fv6kSJEvF7y7tDCDpcU6WiT01rPO6zngZnrvJ/0j4q6Qg+5EQg==
"d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3, d3-array@^3.2.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.1.tgz#39331ea706f5709417d31bbb6ec152e0328b39b3"
integrity sha512-gUY/qeHq/yNqqoCKNq4vtpFLdoCdvyNpWoC/KNjhGbhDuQpAM9sIQQKkXSNpXa9h5KySs/gzm7R88WkUutgwWQ==
dependencies:
internmap "1 - 2"
@ -4326,12 +4375,12 @@ d3-contour@1:
dependencies:
d3-array "^1.1.1"
d3-contour@3:
version "3.0.1"
resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-3.0.1.tgz#2c64255d43059599cd0dba8fe4cc3d51ccdd9bbd"
integrity sha512-0Oc4D0KyhwhM7ZL0RMnfGycLN7hxHB8CMmwZ3+H26PWAG0ozNuYG5hXSDNgmP1SgJkQMrlG6cP20HoaSbvcJTQ==
d3-contour@4:
version "4.0.0"
resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-4.0.0.tgz#5a1337c6da0d528479acdb5db54bc81a0ff2ec6b"
integrity sha512-7aQo0QHUTu/Ko3cP9YK9yUTxtoDEiDGwnBHyLxG5M4vqlBkO/uixMRele3nfsfj6UXOcuReVpVXzAboGraYIJw==
dependencies:
d3-array "2 - 3"
d3-array "^3.2.0"
d3-delaunay@6:
version "6.0.2"
@ -4672,7 +4721,7 @@ d3-zoom@3:
d3-selection "2 - 3"
d3-transition "2 - 3"
d3@^5.14, d3@^5.16.0:
d3@^5.16.0:
version "5.16.0"
resolved "https://registry.yarnpkg.com/d3/-/d3-5.16.0.tgz#9c5e8d3b56403c79d4ed42fbd62f6113f199c877"
integrity sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw==
@ -4709,17 +4758,17 @@ d3@^5.14, d3@^5.16.0:
d3-voronoi "1"
d3-zoom "1"
d3@^7.0.0:
version "7.0.4"
resolved "https://registry.yarnpkg.com/d3/-/d3-7.0.4.tgz#37dfeb3b526f64a0de2ddb705ea61649325207bd"
integrity sha512-ruRiyPYZEGeJBOOjVS5pHliNUZM2HAllEY7HKB2ff+9ENxOti4N+S+WZqo9ggUMr8tSPMm+riqKpJd1oYEDN5Q==
d3@^7.0.0, d3@^7.7.0:
version "7.7.0"
resolved "https://registry.yarnpkg.com/d3/-/d3-7.7.0.tgz#e7779a74ea7c807b432fdfd8128de062b19c62eb"
integrity sha512-VEwHCMgMjD2WBsxeRGUE18RmzxT9Bn7ghDpzvTEvkLSBAKgTMydJjouZTjspgQfRHpPt/PB3EHWBa6SSyFQq4g==
dependencies:
d3-array "3"
d3-axis "3"
d3-brush "3"
d3-chord "3"
d3-color "3"
d3-contour "3"
d3-contour "4"
d3-delaunay "6"
d3-dispatch "3"
d3-drag "3"
@ -4745,23 +4794,13 @@ d3@^7.0.0:
d3-transition "3"
d3-zoom "3"
dagre-d3@^0.6.4:
version "0.6.4"
resolved "https://registry.yarnpkg.com/dagre-d3/-/dagre-d3-0.6.4.tgz#0728d5ce7f177ca2337df141ceb60fbe6eeb7b29"
integrity sha512-e/6jXeCP7/ptlAM48clmX4xTZc5Ek6T6kagS7Oz2HrYSdqcLZFLqpAfh7ldbZRFfxCZVyh61NEPR08UQRVxJzQ==
dagre-d3-es@7.0.6:
version "7.0.6"
resolved "https://registry.yarnpkg.com/dagre-d3-es/-/dagre-d3-es-7.0.6.tgz#8cab465ff95aca8a1ca2292d07e1fb31b5db83f2"
integrity sha512-CaaE/nZh205ix+Up4xsnlGmpog5GGm81Upi2+/SBHxwNwrccBb3K51LzjZ1U6hgvOlAEUsVWf1xSTzCyKpJ6+Q==
dependencies:
d3 "^5.14"
dagre "^0.8.5"
graphlib "^2.1.8"
lodash "^4.17.15"
dagre@^0.8.5:
version "0.8.5"
resolved "https://registry.yarnpkg.com/dagre/-/dagre-0.8.5.tgz#ba30b0055dac12b6c1fcc247817442777d06afee"
integrity sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==
dependencies:
graphlib "^2.1.8"
lodash "^4.17.15"
d3 "^7.7.0"
lodash-es "^4.17.21"
data-urls@^3.0.1:
version "3.0.2"
@ -5090,12 +5129,7 @@ dommatrix@^1.0.3:
resolved "https://registry.yarnpkg.com/dommatrix/-/dommatrix-1.0.3.tgz#e7c18e8d6f3abdd1fef3dd4aa74c4d2e620a0525"
integrity sha512-l32Xp/TLgWb8ReqbVJAFIvXmY7go4nTxxlWiAFyhoQw9RKEOHBZNnyGvJWqDVSPmq3Y9HlM4npqF/T6VMOXhww==
dompurify@2.3.8:
version "2.3.8"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.8.tgz#224fe9ae57d7ebd9a1ae1ac18c1c1ca3f532226f"
integrity sha512-eVhaWoVibIzqdGYjwsBWodIQIaXFSB+cKDf4cfxLMsK0xiud6SE+/WCVx/Xw/UwQsa4cS3T2eITcdtmTg2UKcw==
dompurify@^2.4.1:
dompurify@2.4.1, dompurify@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.4.1.tgz#f9cb1a275fde9af6f2d0a2644ef648dd6847b631"
integrity sha512-ewwFzHzrrneRjxzmK6oVz/rZn9VWspGFRDb4/rRtIsM1n36t9AKma/ye8syCpcw+XJ25kOK/hOG7t1j2I2yBqA==
@ -6445,13 +6479,6 @@ grapheme-splitter@^1.0.4:
resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==
graphlib@^2.1.8:
version "2.1.8"
resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.8.tgz#5761d414737870084c92ec7b5dbcb0592c9d35da"
integrity sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==
dependencies:
lodash "^4.17.15"
graphql-config@^4.3.6:
version "4.3.6"
resolved "https://registry.yarnpkg.com/graphql-config/-/graphql-config-4.3.6.tgz#908ef03d6670c3068e51fe2e84e10e3e0af220b6"
@ -6711,6 +6738,11 @@ he@^1.1.0:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
heap@^0.2.6:
version "0.2.7"
resolved "https://registry.yarnpkg.com/heap/-/heap-0.2.7.tgz#1e6adf711d3f27ce35a81fe3b7bd576c2260a8fc"
integrity sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==
highlight.js@^11.5.1, highlight.js@~11.5.0:
version "11.5.1"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.5.1.tgz#027c24e4509e2f4dcd00b4a6dda542ce0a1f7aea"
@ -8055,6 +8087,16 @@ known-css-properties@^0.25.0:
resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.25.0.tgz#6ebc4d4b412f602e5cfbeb4086bd544e34c0a776"
integrity sha512-b0/9J1O9Jcyik1GC6KC42hJ41jKwdO/Mq8Mdo5sYN+IuRTXs2YFHZC3kZSx6ueusqa95x3wLYe/ytKjbAfGixA==
layout-base@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/layout-base/-/layout-base-1.0.2.tgz#1291e296883c322a9dd4c5dd82063721b53e26e2"
integrity sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==
layout-base@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/layout-base/-/layout-base-2.0.1.tgz#d0337913586c90f9c2c075292069f5c2da5dd285"
integrity sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==
leven@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
@ -8153,6 +8195,11 @@ locate-path@^6.0.0:
dependencies:
p-locate "^5.0.0"
lodash-es@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
lodash.assign@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7"
@ -8674,20 +8721,21 @@ merge2@^1.3.0, merge2@^1.4.1:
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
mermaid@^9.1.3:
version "9.1.3"
resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-9.1.3.tgz#15d08662c66250124ce31106a4620285061ac59c"
integrity sha512-jTIYiqKwsUXVCoxHUVkK8t0QN3zSKIdJlb9thT0J5jCnzXyc+gqTbZE2QmjRfavFTPPn5eRy5zaFp7V+6RhxYg==
mermaid@^9.3.0:
version "9.3.0"
resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-9.3.0.tgz#8bd7c4a44b53e4e85c53a0a474442e9c273494ae"
integrity sha512-mGl0BM19TD/HbU/LmlaZbjBi//tojelg8P/mxD6pPZTAYaI+VawcyBdqRsoUHSc7j71PrMdJ3HBadoQNdvP5cg==
dependencies:
"@braintree/sanitize-url" "^6.0.0"
d3 "^7.0.0"
dagre "^0.8.5"
dagre-d3 "^0.6.4"
dompurify "2.3.8"
graphlib "^2.1.8"
dagre-d3-es "7.0.6"
dompurify "2.4.1"
khroma "^2.0.0"
lodash-es "^4.17.21"
moment-mini "^2.24.0"
stylis "^4.0.10"
non-layered-tidy-tree-layout "^2.0.2"
stylis "^4.1.2"
uuid "^9.0.0"
meros@^1.1.4:
version "1.2.0"
@ -9428,6 +9476,11 @@ nomnom@^1.5.x:
chalk "~0.4.0"
underscore "~1.6.0"
non-layered-tidy-tree-layout@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz#57d35d13c356643fc296a55fb11ac15e74da7804"
integrity sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==
nopt@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48"
@ -11558,10 +11611,10 @@ stylelint@^14.9.1:
v8-compile-cache "^2.3.0"
write-file-atomic "^4.0.1"
stylis@^4.0.10:
version "4.0.10"
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.10.tgz#446512d1097197ab3f02fb3c258358c3f7a14240"
integrity sha512-m3k+dk7QeJw660eIKRRn3xPF6uuvHs/FFzjX3HQ5ove0qYsiygoAhwn5a3IYKaZPo5LrYD0rfVmtv1gNY1uYwg==
stylis@^4.1.2:
version "4.1.3"
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.1.3.tgz#fd2fbe79f5fed17c55269e16ed8da14c84d069f7"
integrity sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==
subscriptions-transport-ws@^0.11.0:
version "0.11.0"
@ -12287,6 +12340,11 @@ uuid@^8.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
uuid@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5"
integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==
uvu@^0.5.0:
version "0.5.3"
resolved "https://registry.yarnpkg.com/uvu/-/uvu-0.5.3.tgz#3d83c5bc1230f153451877bfc7f4aea2392219ae"