diff options
65 files changed, 948 insertions, 541 deletions
diff --git a/app/assets/javascripts/boards/components/board_filtered_search.vue b/app/assets/javascripts/boards/components/board_filtered_search.vue index 97f52f21e7f..ce86a4d3123 100644 --- a/app/assets/javascripts/boards/components/board_filtered_search.vue +++ b/app/assets/javascripts/boards/components/board_filtered_search.vue @@ -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, ); diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js index 49a953cad43..33892f5386d 100644 --- a/app/assets/javascripts/issues/list/constants.js +++ b/app/assets/javascripts/issues/list/constants.js @@ -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]: { diff --git a/app/assets/javascripts/issues/list/utils.js b/app/assets/javascripts/issues/list/utils.js index b566e08731c..03b0b3367fc 100644 --- a/app/assets/javascripts/issues/list/utils.js +++ b/app/assets/javascripts/issues/list/utils.js @@ -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)) { diff --git a/app/assets/javascripts/lib/mermaid.js b/app/assets/javascripts/lib/mermaid.js index c72561ce69d..a119a33b0d7 100644 --- a/app/assets/javascripts/lib/mermaid.js +++ b/app/assets/javascripts/lib/mermaid.js @@ -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 {}; diff --git a/app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue b/app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue index f4893721b9e..164fed308ff 100644 --- a/app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue +++ b/app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue @@ -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> diff --git a/app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue b/app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue index 112f722c632..90034f46e7c 100644 --- a/app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue +++ b/app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue @@ -40,7 +40,6 @@ export default { :title="$options.title" :aria-label="$options.title" icon="check" - variant="confirm" type="submit" /> </gl-form> diff --git a/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue b/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue index ab9abfd38c6..91062c222f4 100644 --- a/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue +++ b/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue @@ -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 /> diff --git a/app/assets/javascripts/members/components/action_buttons/leave_button.vue b/app/assets/javascripts/members/components/action_buttons/leave_button.vue index f600a207b8d..6713819c382 100644 --- a/app/assets/javascripts/members/components/action_buttons/leave_button.vue +++ b/app/assets/javascripts/members/components/action_buttons/leave_button.vue @@ -33,7 +33,6 @@ export default { :title="$options.title" :aria-label="$options.title" icon="leave" - variant="danger" /> <leave-modal :member="member" /> </div> diff --git a/app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue b/app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue index fef7940eaa2..24500fbe44d 100644 --- a/app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue +++ b/app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue @@ -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" diff --git a/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue b/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue index 27c67e84675..e7d813279a2 100644 --- a/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue +++ b/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue @@ -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> diff --git a/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue b/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue index 122e0a142a9..66b5ced1fa9 100644 --- a/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue +++ b/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue @@ -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"> diff --git a/app/assets/javascripts/members/components/table/created_at.vue b/app/assets/javascripts/members/components/table/created_at.vue index 0bad70894f9..44d124ad0db 100644 --- a/app/assets/javascripts/members/components/table/created_at.vue +++ b/app/assets/javascripts/members/components/table/created_at.vue @@ -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> diff --git a/app/assets/javascripts/members/components/table/member_action_buttons.vue b/app/assets/javascripts/members/components/table/member_action_buttons.vue index ecc2ed82ad0..81981bfc2ca 100644 --- a/app/assets/javascripts/members/components/table/member_action_buttons.vue +++ b/app/assets/javascripts/members/components/table/member_action_buttons.vue @@ -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> diff --git a/app/assets/javascripts/members/components/table/member_activity.vue b/app/assets/javascripts/members/components/table/member_activity.vue new file mode 100644 index 00000000000..3b223cb1afa --- /dev/null +++ b/app/assets/javascripts/members/components/table/member_activity.vue @@ -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> diff --git a/app/assets/javascripts/members/components/table/member_source.vue b/app/assets/javascripts/members/components/table/member_source.vue index 30fcbfcd3f8..ed1971d020b 100644 --- a/app/assets/javascripts/members/components/table/member_source.vue +++ b/app/assets/javascripts/members/components/table/member_source.vue @@ -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> diff --git a/app/assets/javascripts/members/components/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue index 0512bc04085..c847f9c8311 100644 --- a/app/assets/javascripts/members/components/table/members_table.vue +++ b/app/assets/javascripts/members/components/table/members_table.vue @@ -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" /> diff --git a/app/assets/javascripts/members/constants.js b/app/assets/javascripts/members/constants.js index dab544c7cbc..5560348b803 100644 --- a/app/assets/javascripts/members/constants.js +++ b/app/assets/javascripts/members/constants.js @@ -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', @@ -77,8 +76,14 @@ export const FIELDS = [ 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', diff --git a/app/assets/javascripts/pages/groups/group_members/index.js b/app/assets/javascripts/pages/groups/group_members/index.js index 62d47cb49b8..ceda2c8fa17 100644 --- a/app/assets/javascripts/pages/groups/group_members/index.js +++ b/app/assets/javascripts/pages/groups/group_members/index.js @@ -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', diff --git a/app/assets/javascripts/pages/projects/project_members/index.js b/app/assets/javascripts/pages/projects/project_members/index.js index 9a7fd74fd8c..2fd372a45b8 100644 --- a/app/assets/javascripts/pages/projects/project_members/index.js +++ b/app/assets/javascripts/pages/projects/project_members/index.js @@ -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', diff --git a/app/assets/javascripts/vue_shared/constants.js b/app/assets/javascripts/vue_shared/constants.js index 2f85a29fb84..c93dd95a886 100644 --- a/app/assets/javascripts/vue_shared/constants.js +++ b/app/assets/javascripts/vue_shared/constants.js @@ -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'; diff --git a/app/assets/stylesheets/page_bundles/members.scss b/app/assets/stylesheets/page_bundles/members.scss index 8d2c0a8ca22..826921be8f0 100644 --- a/app/assets/stylesheets/page_bundles/members.scss +++ b/app/assets/stylesheets/page_bundles/members.scss @@ -76,6 +76,10 @@ width: px-to-rem(200px); } + .col-activity { + width: px-to-rem(250px); + } + .col-actions { width: px-to-rem(65px); } diff --git a/app/controllers/import/bulk_imports_controller.rb b/app/controllers/import/bulk_imports_controller.rb index 9a7118ce498..c51cb454ba1 100644 --- a/app/controllers/import/bulk_imports_controller.rb +++ b/app/controllers/import/bulk_imports_controller.rb @@ -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 diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index 657a582bdc5..b75fda2f344 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -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 diff --git a/config/feature_flags/development/bulk_import.yml b/config/feature_flags/development/bulk_import.yml deleted file mode 100644 index 5a654b3f6d9..00000000000 --- a/config/feature_flags/development/bulk_import.yml +++ /dev/null @@ -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 diff --git a/config/webpack.vendor.config.js b/config/webpack.vendor.config.js index 1300bf16e56..7648c865797 100644 --- a/config/webpack.vendor.config.js +++ b/config/webpack.vendor.config.js @@ -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', diff --git a/db/post_migrate/20221216232658_index_members_on_member_namespace_id_compound.rb b/db/post_migrate/20221216232658_index_members_on_member_namespace_id_compound.rb new file mode 100644 index 00000000000..56adad1e4a5 --- /dev/null +++ b/db/post_migrate/20221216232658_index_members_on_member_namespace_id_compound.rb @@ -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 diff --git a/db/schema_migrations/20221216232658 b/db/schema_migrations/20221216232658 new file mode 100644 index 00000000000..18a3e5e18f2 --- /dev/null +++ b/db/schema_migrations/20221216232658 @@ -0,0 +1 @@ +8e9bb800a2eab9f5d5a3b4f3835b6c4f21ec861a5808a13bef8d496773a7799c
\ No newline at end of file diff --git a/doc/administration/reference_architectures/10k_users.md b/doc/administration/reference_architectures/10k_users.md index 88913eb1f7f..2485f44d173 100644 --- a/doc/administration/reference_architectures/10k_users.md +++ b/doc/administration/reference_architectures/10k_users.md @@ -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' diff --git a/doc/api/container_registry.md b/doc/api/container_registry.md index a2e4d9f37f5..b94bee210e4 100644 --- a/doc/api/container_registry.md +++ b/doc/api/container_registry.md @@ -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 diff --git a/doc/development/database/avoiding_downtime_in_migrations.md b/doc/development/database/avoiding_downtime_in_migrations.md index b34c0bbf728..2afbb238948 100644 --- a/doc/development/database/avoiding_downtime_in_migrations.md +++ b/doc/development/database/avoiding_downtime_in_migrations.md @@ -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 diff --git a/doc/development/database/database_dictionary.md b/doc/development/database/database_dictionary.md index d74d7e77edb..b7e6fa4b5b3 100644 --- a/doc/development/database/database_dictionary.md +++ b/doc/development/database/database_dictionary.md @@ -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 +## Adding tables -| 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. | +### Schema -## Adding tables +| 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. | -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. +### Process -Include this file in the commit with the migration that creates the table. +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. diff --git a/doc/user/group/import/index.md b/doc/user/group/import/index.md index 9a671ff6679..6050307401d 100644 --- a/doc/user/group/import/index.md +++ b/doc/user/group/import/index.md @@ -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: diff --git a/lib/api/bulk_imports.rb b/lib/api/bulk_imports.rb index a28db321348..3f861249612 100644 --- a/lib/api/bulk_imports.rb +++ b/lib/api/bulk_imports.rb @@ -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 diff --git a/lib/api/group_export.rb b/lib/api/group_export.rb index eb0a01e0d3d..37dfbfdb925 100644 --- a/lib/api/group_export.rb +++ b/lib/api/group_export.rb @@ -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 + resource do + before do + not_found! unless Gitlab::CurrentSettings.bulk_import_enabled? + end - if response.success? - accepted! - else - render_api_error!(message: 'Group relations export could not be started.') + 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 '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 response.success? + accepted! + else + render_api_error!(message: 'Group relations export could not be started.') + end + end - if file - present_carrierwave_file!(file) - else - render_api_error!('404 Not found', 404) + 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 - 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 - 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 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 diff --git a/lib/api/project_export.rb b/lib/api/project_export.rb index e4e950fb603..19e5ed3f9e0 100644 --- a/lib/api/project_export.rb +++ b/lib/api/project_export.rb @@ -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 + + 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 - if user_project.export_file_exists? - if user_project.export_archive_exists? - present_carrierwave_file!(user_project.export_file) + 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) + render_api_error!('404 Not found or has expired', 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' + 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 - end - post ':id/export' do - check_rate_limit! :project_export, scope: current_user + 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 + user_project.remove_exports - project_export_params = declared_params(include_missing: false) - after_export_params = project_export_params.delete(:upload) || {} + 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 + 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 + 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) + 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 - end - accepted! + 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 diff --git a/lib/bulk_imports/features.rb b/lib/bulk_imports/features.rb index 952e8e62d71..9fdceb03655 100644 --- a/lib/bulk_imports/features.rb +++ b/lib/bulk_imports/features.rb @@ -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 diff --git a/lib/gitlab/database/gitlab_schema.rb b/lib/gitlab/database/gitlab_schema.rb index 0f848ed40fb..ac6c7a4bef9 100644 --- a/lib/gitlab/database/gitlab_schema.rb +++ b/lib/gitlab/database/gitlab_schema.rb @@ -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.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) + def self.deleted_views_to_schema + @deleted_views_to_schema ||= self.build_dictionary(self.deleted_views_path_globs) + end - dic[data['view_name']] = data['gitlab_schema'].to_sym - 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 ||= 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 diff --git a/lib/gitlab/database/migration_helpers/automatic_lock_writes_on_tables.rb b/lib/gitlab/database/migration_helpers/automatic_lock_writes_on_tables.rb index 0aa4b0d01c4..38c9208d39a 100644 --- a/lib/gitlab/database/migration_helpers/automatic_lock_writes_on_tables.rb +++ b/lib/gitlab/database/migration_helpers/automatic_lock_writes_on_tables.rb @@ -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) diff --git a/lib/gitlab/http.rb b/lib/gitlab/http.rb index b05767c7ed4..c6cd5fbfced 100644 --- a/lib/gitlab/http.rb +++ b/lib/gitlab/http.rb @@ -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 = { diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 6b4a352c8d7..fff1c2c6834 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/package.json b/package.json index f5c010e0d0b..47e94ce962a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/spec/controllers/import/bulk_imports_controller_spec.rb b/spec/controllers/import/bulk_imports_controller_spec.rb index a0bb39f3e98..fb0a07a4019 100644 --- a/spec/controllers/import/bulk_imports_controller_spec.rb +++ b/spec/controllers/import/bulk_imports_controller_spec.rb @@ -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 diff --git a/spec/db/docs_spec.rb b/spec/db/docs_spec.rb index 6cfff725988..b2a713e8b51 100644 --- a/spec/db/docs_spec.rb +++ b/spec/db/docs_spec.rb @@ -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 diff --git a/spec/features/admin/users/users_spec.rb b/spec/features/admin/users/users_spec.rb index 4b49e8f4bc6..6f7fe312fa5 100644 --- a/spec/features/admin/users/users_spec.rb +++ b/spec/features/admin/users/users_spec.rb @@ -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') diff --git a/spec/features/groups/import_export/migration_history_spec.rb b/spec/features/groups/import_export/migration_history_spec.rb index f851c5e2ec5..4f3dba89c61 100644 --- a/spec/features/groups/import_export/migration_history_spec.rb +++ b/spec/features/groups/import_export/migration_history_spec.rb @@ -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 diff --git a/spec/features/groups/members/sort_members_spec.rb b/spec/features/groups/members/sort_members_spec.rb index 4e9adda5f2b..5634122ec16 100644 --- a/spec/features/groups/members/sort_members_spec.rb +++ b/spec/features/groups/members/sort_members_spec.rb @@ -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 diff --git a/spec/features/projects/members/sorting_spec.rb b/spec/features/projects/members/sorting_spec.rb index 5c72d9efeb3..6df1e974f42 100644 --- a/spec/features/projects/members/sorting_spec.rb +++ b/spec/features/projects/members/sorting_spec.rb @@ -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 diff --git a/spec/frontend/admin/users/components/user_date_spec.js b/spec/frontend/admin/users/components/user_date_spec.js index af262c6d3f0..73be33d5a9d 100644 --- a/spec/frontend/admin/users/components/user_date_spec.js +++ b/spec/frontend/admin/users/components/user_date_spec.js @@ -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'} diff --git a/spec/frontend/boards/components/board_filtered_search_spec.js b/spec/frontend/boards/components/board_filtered_search_spec.js index e80c66f7fb8..4c0cc36889c 100644 --- a/spec/frontend/boards/components/board_filtered_search_spec.js +++ b/spec/frontend/boards/components/board_filtered_search_spec.js @@ -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', }); }); diff --git a/spec/frontend/issues/list/mock_data.js b/spec/frontend/issues/list/mock_data.js index 0690501dee9..d9d45ca4e30 100644 --- a/spec/frontend/issues/list/mock_data.js +++ b/spec/frontend/issues/list/mock_data.js @@ -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 = { diff --git a/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js index df5c884f42e..b94964dc482 100644 --- a/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js +++ b/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js @@ -38,7 +38,6 @@ describe('AccessRequestActionButtons', () => { title: 'Deny access', isAccessRequest: true, isInvite: false, - icon: 'close', }); }); diff --git a/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js index ea819b4fb83..d81e6635827 100644 --- a/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js +++ b/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js @@ -44,7 +44,6 @@ describe('InviteActionButtons', () => { title: 'Revoke invite', isAccessRequest: false, isInvite: true, - icon: 'remove', }); }); }); diff --git a/spec/frontend/members/components/action_buttons/remove_member_button_spec.js b/spec/frontend/members/components/action_buttons/remove_member_button_spec.js index 0e5b667eb9b..d22655857c9 100644 --- a/spec/frontend/members/components/action_buttons/remove_member_button_spec.js +++ b/spec/frontend/members/components/action_buttons/remove_member_button_spec.js @@ -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'); - }); - }); }); diff --git a/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js index 6ac46619bc9..08132df6fd9 100644 --- a/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js +++ b/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js @@ -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, - }), - ); - }, - ); - }); }); diff --git a/spec/frontend/members/components/table/__snapshots__/member_activity_spec.js.snap b/spec/frontend/members/components/table/__snapshots__/member_activity_spec.js.snap new file mode 100644 index 00000000000..a0d9bae8a0b --- /dev/null +++ b/spec/frontend/members/components/table/__snapshots__/member_activity_spec.js.snap @@ -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> +`; diff --git a/spec/frontend/members/components/table/created_at_spec.js b/spec/frontend/members/components/table/created_at_spec.js index 793c122587d..fa31177564b 100644 --- a/spec/frontend/members/components/table/created_at_spec.js +++ b/spec/frontend/members/components/table/created_at_spec.js @@ -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'); diff --git a/spec/frontend/members/components/table/member_activity_spec.js b/spec/frontend/members/components/table/member_activity_spec.js new file mode 100644 index 00000000000..a372b40fd1f --- /dev/null +++ b/spec/frontend/members/components/table/member_activity_spec.js @@ -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(); + }); + }); +}); diff --git a/spec/frontend/members/components/table/member_source_spec.js b/spec/frontend/members/components/table/member_source_spec.js index 2cd888207b1..fbfd0ca7ae7 100644 --- a/spec/frontend/members/components/table/member_source_spec.js +++ b/spec/frontend/members/components/table/member_source_spec.js @@ -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(wrapper.text()).toBe('Direct member by Administrator'); + expect(wrapper.findByRole('link', { name: createdBy.name }).attributes('href')).toBe( + createdBy.webUrl, + ); }); + }); - expect(getByText('Direct member').exists()).toBe(true); + 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'); + }); }); }); }); diff --git a/spec/frontend/members/components/table/members_table_spec.js b/spec/frontend/members/components/table/members_table_spec.js index 0ed01396fcb..3165173ce21 100644 --- a/spec/frontend/members/components/table/members_table_spec.js +++ b/spec/frontend/members/components/table/members_table_spec.js @@ -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], diff --git a/spec/lib/gitlab/database/gitlab_schema_spec.rb b/spec/lib/gitlab/database/gitlab_schema_spec.rb index 4b37cbda047..3f52f0ae1e9 100644 --- a/spec/lib/gitlab/database/gitlab_schema_spec.rb +++ b/spec/lib/gitlab/database/gitlab_schema_spec.rb @@ -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 } diff --git a/spec/lib/gitlab/database/migration_helpers/automatic_lock_writes_on_tables_spec.rb b/spec/lib/gitlab/database/migration_helpers/automatic_lock_writes_on_tables_spec.rb index 9fd49b312eb..a7b5205d2dc 100644 --- a/spec/lib/gitlab/database/migration_helpers/automatic_lock_writes_on_tables_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers/automatic_lock_writes_on_tables_spec.rb @@ -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] diff --git a/spec/requests/api/bulk_imports_spec.rb b/spec/requests/api/bulk_imports_spec.rb index 13f079c69e7..378adf1211e 100644 --- a/spec/requests/api/bulk_imports_spec.rb +++ b/spec/requests/api/bulk_imports_spec.rb @@ -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 diff --git a/spec/requests/api/group_export_spec.rb b/spec/requests/api/group_export_spec.rb index 565365506a7..9dd5fe6f7c4 100644 --- a/spec/requests/api/group_export_spec.rb +++ b/spec/requests/api/group_export_spec.rb @@ -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 diff --git a/spec/requests/api/project_export_spec.rb b/spec/requests/api/project_export_spec.rb index fdd76c63069..096f0b73b4c 100644 --- a/spec/requests/api/project_export_spec.rb +++ b/spec/requests/api/project_export_spec.rb @@ -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 diff --git a/yarn.lock b/yarn.lock index 0184cfbdfc8..8fc039355fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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== - 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== +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: - 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" |