diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-10-13 12:12:20 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-10-13 12:12:20 +0000 |
commit | 429a0c87c1f36ff9ebfe131ebb6c71a83d9f917c (patch) | |
tree | ffe9ecb8fc29c6fd9aaeef295bd92d3da3d0a312 /app | |
parent | bc2f7ab125361e4180018b1b933f42a8709df356 (diff) | |
download | gitlab-ce-429a0c87c1f36ff9ebfe131ebb6c71a83d9f917c.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
45 files changed, 512 insertions, 132 deletions
diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue index e0105d63d99..9bbb8a1a1b2 100644 --- a/app/assets/javascripts/boards/components/board_content_sidebar.vue +++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue @@ -3,15 +3,18 @@ import { GlDrawer } from '@gitlab/ui'; import { MountingPortal } from 'portal-vue'; import { mapState, mapActions, mapGetters } from 'vuex'; import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdown_widget.vue'; +import { __, sprintf } from '~/locale'; import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue'; import BoardSidebarTimeTracker from '~/boards/components/sidebar/board_sidebar_time_tracker.vue'; import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue'; import { ISSUABLE } from '~/boards/constants'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue'; import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue'; import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue'; import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue'; import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue'; +import SidebarLabelsWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { @@ -23,6 +26,7 @@ export default { SidebarConfidentialityWidget, BoardSidebarTimeTracker, BoardSidebarLabelsSelect, + SidebarLabelsWidget, SidebarSubscriptionsWidget, SidebarDropdownWidget, SidebarTodoWidget, @@ -46,16 +50,20 @@ export default { weightFeatureAvailable: { default: false, }, + allowLabelEdit: { + default: false, + }, }, inheritAttrs: false, computed: { ...mapGetters([ + 'isGroupBoard', 'isSidebarOpen', 'activeBoardItem', 'groupPathForActiveIssue', 'projectPathForActiveIssue', ]), - ...mapState(['sidebarType', 'issuableType']), + ...mapState(['sidebarType', 'issuableType', 'isSettingLabels']), isIssuableSidebar() { return this.sidebarType === ISSUABLE; }, @@ -65,17 +73,48 @@ export default { fullPath() { return this.activeBoardItem?.referencePath?.split('#')[0] || ''; }, + createLabelTitle() { + return sprintf(__('Create %{workspace} label'), { + workspace: this.isGroupBoard ? 'group' : 'project', + }); + }, + manageLabelTitle() { + return sprintf(__('Manage %{workspace} labels'), { + workspace: this.isGroupBoard ? 'group' : 'project', + }); + }, + attrWorkspacePath() { + return this.isGroupBoard ? this.groupPathForActiveIssue : undefined; + }, }, methods: { ...mapActions([ 'toggleBoardItem', 'setAssignees', 'setActiveItemConfidential', + 'setActiveBoardItemLabels', 'setActiveItemWeight', ]), handleClose() { this.toggleBoardItem({ boardItem: this.activeBoardItem, sidebarType: this.sidebarType }); }, + handleUpdateSelectedLabels(input) { + this.setActiveBoardItemLabels({ + iid: this.activeBoardItem.iid, + projectPath: this.projectPathForActiveIssue, + addLabelIds: input.map((label) => getIdFromGraphQLId(label.id)), + removeLabelIds: this.activeBoardItem.labels + .filter((label) => !input.find((selected) => selected.id === label.id)) + .map((label) => label.id), + }); + }, + handleLabelRemove(input) { + this.setActiveBoardItemLabels({ + iid: this.activeBoardItem.iid, + projectPath: this.projectPathForActiveIssue, + removeLabelIds: [input], + }); + }, }, }; </script> @@ -160,7 +199,28 @@ export default { :issuable-type="issuableType" data-testid="sidebar-due-date" /> - <board-sidebar-labels-select class="block labels" /> + <sidebar-labels-widget + v-if="glFeatures.labelsWidget" + class="block labels" + data-testid="sidebar-labels" + :iid="activeBoardItem.iid" + :full-path="projectPathForActiveIssue" + :allow-label-remove="allowLabelEdit" + :allow-multiselect="true" + :selected-labels="activeBoardItem.labels" + :labels-select-in-progress="isSettingLabels" + :footer-create-label-title="createLabelTitle" + :footer-manage-label-title="manageLabelTitle" + :labels-create-title="createLabelTitle" + :labels-filter-base-path="projectPathForActiveIssue" + :attr-workspace-path="attrWorkspacePath" + :issuable-type="issuableType" + @onLabelRemove="handleLabelRemove" + @updateSelectedLabels="handleUpdateSelectedLabels" + > + {{ __('None') }} + </sidebar-labels-widget> + <board-sidebar-labels-select v-else class="block labels" /> <sidebar-weight-widget v-if="weightFeatureAvailable" :iid="activeBoardItem.iid" diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index 1c8b1f9b009..b6b1094fb3a 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -87,6 +87,9 @@ function mountBoardApp(el) { iterationListsAvailable: parseBoolean(el.dataset.iterationListsAvailable), issuableType: issuableTypes.issue, emailsDisabled: parseBoolean(el.dataset.emailsDisabled), + allowLabelCreate: parseBoolean(el.dataset.canUpdate), + allowLabelEdit: parseBoolean(el.dataset.canUpdate), + allowScopedLabels: parseBoolean(el.dataset.scopedLabels), }, render: (createComponent) => createComponent(BoardApp), }); diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js index 270f2ff085b..ca993e75cf9 100644 --- a/app/assets/javascripts/boards/stores/actions.js +++ b/app/assets/javascripts/boards/stores/actions.js @@ -656,6 +656,7 @@ export default { }, setActiveIssueLabels: async ({ commit, getters }, input) => { + commit(types.SET_LABELS_LOADING, true); const { activeBoardItem } = getters; const { data } = await gqlClient.mutate({ mutation: issueSetLabelsMutation, @@ -669,6 +670,8 @@ export default { }, }); + commit(types.SET_LABELS_LOADING, false); + if (data.updateIssue?.errors?.length > 0) { throw new Error(data.updateIssue.errors); } diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js index 928cece19f7..26b785932bb 100644 --- a/app/assets/javascripts/boards/stores/mutation_types.js +++ b/app/assets/javascripts/boards/stores/mutation_types.js @@ -28,6 +28,7 @@ export const ADD_BOARD_ITEM_TO_LIST = 'ADD_BOARD_ITEM_TO_LIST'; export const REMOVE_BOARD_ITEM_FROM_LIST = 'REMOVE_BOARD_ITEM_FROM_LIST'; export const SET_ACTIVE_ID = 'SET_ACTIVE_ID'; export const UPDATE_BOARD_ITEM_BY_ID = 'UPDATE_BOARD_ITEM_BY_ID'; +export const SET_LABELS_LOADING = 'SET_LABELS_LOADING'; export const SET_ASSIGNEE_LOADING = 'SET_ASSIGNEE_LOADING'; export const RESET_ISSUES = 'RESET_ISSUES'; export const REQUEST_GROUP_PROJECTS = 'REQUEST_GROUP_PROJECTS'; diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js index ef5b84b4575..d381c076c19 100644 --- a/app/assets/javascripts/boards/stores/mutations.js +++ b/app/assets/javascripts/boards/stores/mutations.js @@ -195,6 +195,10 @@ export default { Vue.set(state.boardItems[itemId], prop, value); }, + [mutationTypes.SET_LABELS_LOADING](state, isLoading) { + state.isSettingLabels = isLoading; + }, + [mutationTypes.SET_ASSIGNEE_LOADING](state, isLoading) { state.isSettingAssignees = isLoading; }, diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js index 80c51c966d2..2a6605e687b 100644 --- a/app/assets/javascripts/boards/stores/state.js +++ b/app/assets/javascripts/boards/stores/state.js @@ -12,6 +12,7 @@ export default () => ({ listsFlags: {}, boardItemsByListId: {}, backupItemsList: [], + isSettingLabels: false, isSettingAssignees: false, pageInfoByListId: {}, boardItems: {}, diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue index 4aee02e45c8..9d4eddc510a 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue +++ b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue @@ -293,7 +293,7 @@ export default { :items="roles" :loading="isLoadingRoles" :loading-text="s__('ClusterIntegration|Loading IAM Roles')" - :placeholder="s__('ClusterIntergation|Select service role')" + :placeholder="s__('ClusterIntegration|Select service role')" :search-field-placeholder="s__('ClusterIntegration|Search IAM Roles')" :empty-text="s__('ClusterIntegration|No IAM Roles found')" :has-errors="Boolean(loadingRolesError)" @@ -330,7 +330,7 @@ export default { :disabled-text="s__('ClusterIntegration|Select a region to choose a Key Pair')" :loading="isLoadingKeyPairs" :loading-text="s__('ClusterIntegration|Loading Key Pairs')" - :placeholder="s__('ClusterIntergation|Select key pair')" + :placeholder="s__('ClusterIntegration|Select key pair')" :search-field-placeholder="s__('ClusterIntegration|Search Key Pairs')" :empty-text="s__('ClusterIntegration|No Key Pairs found')" :has-errors="Boolean(loadingKeyPairsError)" @@ -359,7 +359,7 @@ export default { :disabled="vpcDropdownDisabled" :disabled-text="s__('ClusterIntegration|Select a region to choose a VPC')" :loading-text="s__('ClusterIntegration|Loading VPCs')" - :placeholder="s__('ClusterIntergation|Select a VPC')" + :placeholder="s__('ClusterIntegration|Select a VPC')" :search-field-placeholder="s__('ClusterIntegration|Search VPCs')" :empty-text="s__('ClusterIntegration|No VPCs found')" :has-errors="Boolean(loadingVpcsError)" @@ -389,7 +389,7 @@ export default { :disabled="subnetDropdownDisabled" :disabled-text="s__('ClusterIntegration|Select a VPC to choose a subnet')" :loading-text="s__('ClusterIntegration|Loading subnets')" - :placeholder="s__('ClusterIntergation|Select a subnet')" + :placeholder="s__('ClusterIntegration|Select a subnet')" :search-field-placeholder="s__('ClusterIntegration|Search subnets')" :empty-text="s__('ClusterIntegration|No subnet found')" :has-errors="displaySubnetError" @@ -420,7 +420,7 @@ export default { :disabled="securityGroupDropdownDisabled" :disabled-text="s__('ClusterIntegration|Select a VPC to choose a security group')" :loading-text="s__('ClusterIntegration|Loading security groups')" - :placeholder="s__('ClusterIntergation|Select a security group')" + :placeholder="s__('ClusterIntegration|Select a security group')" :search-field-placeholder="s__('ClusterIntegration|Search security groups')" :empty-text="s__('ClusterIntegration|No security group found')" :has-errors="Boolean(loadingSecurityGroupsError)" @@ -451,7 +451,7 @@ export default { :items="instanceTypes" :loading="isLoadingInstanceTypes" :loading-text="s__('ClusterIntegration|Loading instance types')" - :placeholder="s__('ClusterIntergation|Select an instance type')" + :placeholder="s__('ClusterIntegration|Select an instance type')" :search-field-placeholder="s__('ClusterIntegration|Search instance types')" :empty-text="s__('ClusterIntegration|No instance type found')" :has-errors="Boolean(loadingInstanceTypesError)" diff --git a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_network_dropdown.vue b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_network_dropdown.vue index 12b6070a79a..8f18ac29c0f 100644 --- a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_network_dropdown.vue +++ b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_network_dropdown.vue @@ -43,7 +43,7 @@ export default { :loading="isLoadingItems" :has-errors="Boolean(loadingItemsError)" :loading-text="s__('ClusterIntegration|Loading networks')" - :placeholder="s__('ClusterIntergation|Select a network')" + :placeholder="s__('ClusterIntegration|Select a network')" :search-field-placeholder="s__('ClusterIntegration|Search networks')" :empty-text="s__('ClusterIntegration|No networks found')" :error-message="s__('ClusterIntegration|Could not load networks')" diff --git a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_subnetwork_dropdown.vue b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_subnetwork_dropdown.vue index ec7889e2907..dab4adc3789 100644 --- a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_subnetwork_dropdown.vue +++ b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_subnetwork_dropdown.vue @@ -34,7 +34,7 @@ export default { :loading="isLoadingItems" :has-errors="Boolean(loadingItemsError)" :loading-text="s__('ClusterIntegration|Loading subnetworks')" - :placeholder="s__('ClusterIntergation|Select a subnetwork')" + :placeholder="s__('ClusterIntegration|Select a subnetwork')" :search-field-placeholder="s__('ClusterIntegration|Search subnetworks')" :empty-text="s__('ClusterIntegration|No subnetworks found')" :error-message="s__('ClusterIntegration|Could not load subnetworks')" diff --git a/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue b/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue index 858c30649bb..1a470d74b59 100644 --- a/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue +++ b/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue @@ -17,7 +17,7 @@ export default { }, }, i18n: { - percentageDescription: __('Enter an integer number number between 0 and 100'), + percentageDescription: __('Enter an integer number between 0 and 100'), percentageInvalid: __('Percent rollout must be an integer number between 0 and 100'), percentageLabel: __('Percentage'), stickinessDescription: __('Consistency guarantee method'), diff --git a/app/assets/javascripts/issuable_suggestions/components/item.vue b/app/assets/javascripts/issuable_suggestions/components/item.vue index dea7608685a..a01f4f747b9 100644 --- a/app/assets/javascripts/issuable_suggestions/components/item.vue +++ b/app/assets/javascripts/issuable_suggestions/components/item.vue @@ -1,5 +1,4 @@ <script> -/* eslint-disable @gitlab/vue-require-i18n-strings */ import { GlLink, GlTooltip, GlTooltipDirective, GlIcon } from '@gitlab/ui'; import { uniqueId } from 'lodash'; import { __ } from '~/locale'; @@ -26,12 +25,6 @@ export default { }, }, computed: { - isOpen() { - return this.suggestion.state === 'opened'; - }, - isClosed() { - return this.suggestion.state === 'closed'; - }, counts() { return [ { @@ -48,7 +41,13 @@ export default { }, ].filter(({ count }) => count); }, - stateIcon() { + isClosed() { + return this.suggestion.state === 'closed'; + }, + stateIconClass() { + return this.isClosed ? 'gl-text-blue-500' : 'gl-text-green-500'; + }, + stateIconName() { return this.isClosed ? 'issue-close' : 'issue-open-m'; }, stateTitle() { @@ -72,7 +71,7 @@ export default { v-gl-tooltip.bottom :title="__('Confidential')" name="eye-slash" - class="suggestion-help-hover mr-1 suggestion-confidential" + class="gl-cursor-help gl-mr-2 gl-text-orange-500" /> <gl-link :href="suggestion.webUrl" @@ -83,15 +82,7 @@ export default { </gl-link> </div> <div class="text-secondary suggestion-footer"> - <gl-icon - ref="state" - :name="stateIcon" - :class="{ - 'suggestion-state-open': isOpen, - 'suggestion-state-closed': isClosed, - }" - class="suggestion-help-hover" - /> + <gl-icon ref="state" :name="stateIconName" :class="stateIconClass" class="gl-cursor-help" /> <gl-tooltip :target="() => $refs.state" placement="bottom"> <span class="d-block"> <span class="bold"> {{ stateTitle }} </span> {{ timeFormatted(closedOrCreatedDate) }} @@ -102,9 +93,9 @@ export default { <timeago-tooltip :time="suggestion.createdAt" tooltip-placement="bottom" - class="suggestion-help-hover" + class="gl-cursor-help" /> - by + {{ __('by') }} <gl-link :href="suggestion.author.webUrl"> <user-avatar-image :img-src="suggestion.author.avatarUrl" @@ -122,7 +113,7 @@ export default { <timeago-tooltip :time="suggestion.updatedAt" tooltip-placement="bottom" - class="suggestion-help-hover" + class="gl-cursor-help" /> </template> <span class="suggestion-counts"> @@ -131,7 +122,7 @@ export default { :key="id" v-gl-tooltip.bottom :title="tooltipTitle" - class="suggestion-help-hover gl-ml-3 text-tertiary" + class="gl-cursor-help gl-ml-3 text-tertiary" > <gl-icon :name="icon" /> {{ count }} </span> diff --git a/app/assets/javascripts/members/components/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue index 3caecdd1d4b..202f3aa89e1 100644 --- a/app/assets/javascripts/members/components/table/members_table.vue +++ b/app/assets/javascripts/members/components/table/members_table.vue @@ -5,7 +5,13 @@ import MembersTableCell from 'ee_else_ce/members/components/table/members_table_ import { canOverride, canRemove, canResend, canUpdate } from 'ee_else_ce/members/utils'; import { mergeUrlParams } from '~/lib/utils/url_utility'; import initUserPopovers from '~/user_popovers'; -import { FIELDS, ACTIVE_TAB_QUERY_PARAM_NAME } from '../../constants'; +import { + FIELDS, + ACTIVE_TAB_QUERY_PARAM_NAME, + MEMBER_STATE_AWAITING, + USER_STATE_BLOCKED_PENDING_APPROVAL, + BADGE_LABELS_PENDING_OWNER_APPROVAL, +} from '../../constants'; import RemoveGroupLinkModal from '../modals/remove_group_link_modal.vue'; import RemoveMemberModal from '../modals/remove_member_modal.vue'; import CreatedAt from './created_at.vue'; @@ -129,6 +135,74 @@ export default { window.location.href, ); }, + /** + * Returns whether it's a new or existing user + * + * If memberInviteMetadata doesn't exist, it means we're adding an existing user + * to the Group/Project, so `isNewUser` should be false. + * If memberInviteMetadata exists but `userState` has content, + * the user has registered but is awaiting root approval + * + * @param {object} memberInviteMetadata - MemberEntity.invite + * @see {@link ~/app/serializers/member_entity.rb} + * @returns {boolean} + */ + isNewUser(memberInviteMetadata) { + return memberInviteMetadata && !memberInviteMetadata.userState; + }, + /** + * Returns whether the user is awaiting root approval + * + * This checks User.state exposed via MemberEntity + * + * @param {object} memberInviteMetadata - MemberEntity.invite + * @see {@link ~/app/serializers/member_entity.rb} + * @returns {boolean} + */ + isUserPendingRootApproval(memberInviteMetadata) { + return memberInviteMetadata?.userState === USER_STATE_BLOCKED_PENDING_APPROVAL; + }, + /** + * Returns whether the member is awaiting owner approval + * + * This checks Member.state exposed via MemberEntity + * + * @param {Number} memberState - Member.state exposed via MemberEntity.state + * @see {@link ~/ee/app/models/ee/member.rb} + * @see {@link ~/app/serializers/member_entity.rb} + * @returns {boolean} + */ + isMemberPendingOwnerApproval(memberState) { + return memberState === MEMBER_STATE_AWAITING; + }, + isUserAwaiting(memberInviteMetadata, memberState) { + return ( + this.isUserPendingRootApproval(memberInviteMetadata) || + this.isMemberPendingOwnerApproval(memberState) + ); + }, + shouldAddPendingOwnerApprovalBadge(memberInviteMetadata, memberState) { + return ( + this.isUserAwaiting(memberInviteMetadata, memberState) && + !this.isNewUser(memberInviteMetadata) + ); + }, + /** + * Returns the string to be used in the invite badge + * + * @param {object} memberInviteMetadata - MemberEntity.invite + * @see {@link ~/app/serializers/member_entity.rb} + * @param {Number} memberState - Member.state exposed via MemberEntity.state + * @see {@link ~/ee/app/models/ee/member.rb} + * @returns {string} + */ + inviteBadge(memberInviteMetadata, memberState) { + if (this.shouldAddPendingOwnerApprovalBadge(memberInviteMetadata, memberState)) { + return BADGE_LABELS_PENDING_OWNER_APPROVAL; + } + + return ''; + }, }, }; </script> @@ -172,8 +246,11 @@ export default { <created-at :date="createdAt" :created-by="createdBy" /> </template> - <template #cell(invited)="{ item: { createdAt, createdBy } }"> + <template #cell(invited)="{ item: { createdAt, createdBy, invite, state } }"> <created-at :date="createdAt" :created-by="createdBy" /> + <gl-badge v-if="inviteBadge(invite, state)" data-testid="invited-badge">{{ + inviteBadge(invite, state) + }}</gl-badge> </template> <template #cell(requested)="{ item: { createdAt } }"> diff --git a/app/assets/javascripts/members/constants.js b/app/assets/javascripts/members/constants.js index 54d4442d5ce..f5ca881ab0d 100644 --- a/app/assets/javascripts/members/constants.js +++ b/app/assets/javascripts/members/constants.js @@ -89,6 +89,22 @@ export const TAB_QUERY_PARAM_VALUES = { accessRequest: 'access_requests', }; +/** + * This user state value comes from the User model + * see the state machine in app/models/user.rb + */ +export const USER_STATE_BLOCKED_PENDING_APPROVAL = 'blocked_pending_approval'; + +/** + * This and following member state constants' values + * come from ee/app/models/ee/member.rb + */ +export const MEMBER_STATE_CREATED = 0; +export const MEMBER_STATE_AWAITING = 1; +export const MEMBER_STATE_ACTIVE = 2; + +export const BADGE_LABELS_PENDING_OWNER_APPROVAL = __('Pending owner approval'); + export const DAYS_TO_EXPIRE_SOON = 7; export const LEAVE_MODAL_ID = 'member-leave-modal'; diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue index f0da7db6c91..1360b03856f 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue @@ -24,7 +24,13 @@ export default { <template> <div> - <details-row icon="project" padding="gl-p-4" dashed data-testid="nuget-source"> + <details-row + v-if="packageEntity.metadata.projectUrl" + icon="project" + padding="gl-p-4" + dashed + data-testid="nuget-source" + > <gl-sprintf :message="$options.i18n.sourceText"> <template #link> <gl-link :href="packageEntity.metadata.projectUrl" target="_blank">{{ @@ -33,7 +39,12 @@ export default { </template> </gl-sprintf> </details-row> - <details-row icon="license" padding="gl-p-4" data-testid="nuget-license"> + <details-row + v-if="packageEntity.metadata.licenseUrl" + icon="license" + padding="gl-p-4" + data-testid="nuget-license" + > <gl-sprintf :message="$options.i18n.licenseText"> <template #link> <gl-link :href="packageEntity.metadata.licenseUrl" target="_blank">{{ diff --git a/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue b/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue new file mode 100644 index 00000000000..75b1398a3c2 --- /dev/null +++ b/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue @@ -0,0 +1,49 @@ +<script> +import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue'; + +export default { + components: { + PipelineMiniGraph, + }, + props: { + pipeline: { + type: Object, + required: true, + }, + }, + computed: { + pipelinePath() { + return this.pipeline.detailedStatus?.detailsPath || ''; + }, + pipelineStages() { + const stages = this.pipeline.stages?.edges; + if (!stages) { + return []; + } + + return stages.map(({ node }) => { + const { name, detailedStatus } = node; + return { + // TODO: fetch dropdown_path from graphql when available + // see https://gitlab.com/gitlab-org/gitlab/-/issues/342585 + dropdown_path: `${this.pipelinePath}/stage.json?stage=${name}`, + name, + path: `${this.pipelinePath}#${name}`, + status: { + details_path: `${this.pipelinePath}#${name}`, + has_details: detailedStatus.hasDetails, + ...detailedStatus, + }, + title: `${name}: ${detailedStatus.text}`, + }; + }); + }, + }, +}; +</script> + +<template> + <div v-if="pipelineStages.length > 0" class="stage-cell gl-mr-5"> + <pipeline-mini-graph class="gl-display-inline" :stages="pipelineStages" /> + </div> +</template> diff --git a/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue b/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue index ec240854be5..a1fa2147994 100644 --- a/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue +++ b/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue @@ -10,6 +10,8 @@ import { toggleQueryPollingByVisibility, } from '~/pipelines/components/graph/utils'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import PipelineEditorMiniGraph from './pipeline_editor_mini_graph.vue'; const POLL_INTERVAL = 10000; export const i18n = { @@ -30,7 +32,9 @@ export default { GlLink, GlLoadingIcon, GlSprintf, + PipelineEditorMiniGraph, }, + mixins: [glFeatureFlagMixin()], inject: ['projectFullPath'], props: { commitSha: { @@ -55,12 +59,15 @@ export default { }; }, update(data) { - const { id, commitPath = '', detailedStatus = {} } = data.project?.pipeline || {}; + const { id, commitPath = '', detailedStatus = {}, stages, status } = + data.project?.pipeline || {}; return { id, commitPath, detailedStatus, + stages, + status, }; }, result(res) { @@ -111,9 +118,7 @@ export default { </script> <template> - <div - class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-white-space-nowrap gl-max-w-full" - > + <div class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-flex-wrap"> <template v-if="showLoadingState"> <div> <gl-loading-icon class="gl-mr-auto gl-display-inline-block" size="sm" /> @@ -129,19 +134,12 @@ export default { <template v-else> <div> <a :href="status.detailsPath" class="gl-mr-auto"> - <ci-icon :status="status" :size="16" /> + <ci-icon :status="status" :size="16" data-testid="pipeline-status-icon" /> </a> <span class="gl-font-weight-bold"> <gl-sprintf :message="$options.i18n.pipelineInfo"> <template #id="{ content }"> - <gl-link - :href="status.detailsPath" - class="pipeline-id gl-font-weight-normal pipeline-number" - target="_blank" - data-testid="pipeline-id" - > - {{ content }}{{ pipelineId }}</gl-link - > + <span data-testid="pipeline-id"> {{ content }}{{ pipelineId }} </span> </template> <template #status>{{ status.text }}</template> <template #commit> @@ -157,8 +155,13 @@ export default { </gl-sprintf> </span> </div> - <div> + <div class="gl-display-flex gl-flex-wrap"> + <pipeline-editor-mini-graph + v-if="glFeatures.pipelineEditorMiniGraph" + :pipeline="pipeline" + /> <gl-button + class="gl-mt-2 gl-md-mt-0" target="_blank" category="secondary" variant="confirm" diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline.graphql index d3a7387ad2d..0c3653a2880 100644 --- a/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline.graphql +++ b/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline.graphql @@ -11,6 +11,25 @@ query getPipeline($fullPath: ID!, $sha: String!) { group text } + stages { + edges { + node { + id + name + status + detailedStatus { + detailsPath + group + hasDetails + icon + id + label + text + tooltip + } + } + } + } } } } diff --git a/app/assets/javascripts/related_issues/components/add_issuable_form.vue b/app/assets/javascripts/related_issues/components/add_issuable_form.vue index 545263a9e37..f936c03c5d3 100644 --- a/app/assets/javascripts/related_issues/components/add_issuable_form.vue +++ b/app/assets/javascripts/related_issues/components/add_issuable_form.vue @@ -201,7 +201,7 @@ export default { <gl-button ref="addButton" category="primary" - variant="success" + variant="confirm" :disabled="isSubmitButtonDisabled" :loading="isSubmitting" type="submit" diff --git a/app/assets/javascripts/search_settings/components/search_settings.vue b/app/assets/javascripts/search_settings/components/search_settings.vue index 116967a62c8..3e23b8a3435 100644 --- a/app/assets/javascripts/search_settings/components/search_settings.vue +++ b/app/assets/javascripts/search_settings/components/search_settings.vue @@ -1,7 +1,13 @@ <script> import { GlSearchBoxByType } from '@gitlab/ui'; -import { uniq } from 'lodash'; -import { EXCLUDED_NODES, HIDE_CLASS, HIGHLIGHT_CLASS, TYPING_DELAY } from '../constants'; +import { uniq, escapeRegExp } from 'lodash'; +import { + EXCLUDED_NODES, + HIDE_CLASS, + HIGHLIGHT_CLASS, + NONE_PADDING_CLASS, + TYPING_DELAY, +} from '../constants'; const origExpansions = new Map(); @@ -37,9 +43,13 @@ const resetSections = ({ sectionSelector }) => { }; const clearHighlights = () => { - document - .querySelectorAll(`.${HIGHLIGHT_CLASS}`) - .forEach((element) => element.classList.remove(HIGHLIGHT_CLASS)); + document.querySelectorAll(`.${HIGHLIGHT_CLASS}`).forEach((element) => { + const { parentNode } = element; + const textNode = document.createTextNode(element.textContent); + parentNode.replaceChild(textNode, element); + + parentNode.normalize(); + }); }; const hideSectionsExcept = (sectionSelector, visibleSections) => { @@ -50,17 +60,41 @@ const hideSectionsExcept = (sectionSelector, visibleSections) => { }); }; -const highlightElements = (elements = []) => { - elements.forEach((element) => element.classList.add(HIGHLIGHT_CLASS)); +const transformMatchElement = (element, searchTerm) => { + const textStr = element.textContent; + const escapedSearchTerm = new RegExp(`(${escapeRegExp(searchTerm)})`, 'gi'); + + const textList = textStr.split(escapedSearchTerm); + const replaceFragment = document.createDocumentFragment(); + textList.forEach((text) => { + let addElement = document.createTextNode(text); + if (escapedSearchTerm.test(text)) { + addElement = document.createElement('mark'); + addElement.className = `${HIGHLIGHT_CLASS} ${NONE_PADDING_CLASS}`; + addElement.textContent = text; + escapedSearchTerm.lastIndex = 0; + } + replaceFragment.appendChild(addElement); + }); + + return replaceFragment; +}; + +const highlightElements = (elements = [], searchTerm) => { + elements.forEach((element) => { + const replaceFragment = transformMatchElement(element, searchTerm); + element.innerHTML = ''; + element.appendChild(replaceFragment); + }); }; -const displayResults = ({ sectionSelector, expandSection }, matches) => { +const displayResults = ({ sectionSelector, expandSection, searchTerm }, matches) => { const elements = matches.map((match) => match.parentElement); const sections = uniq(elements.map((element) => findSettingsSection(sectionSelector, element))); hideSectionsExcept(sectionSelector, sections); sections.forEach(expandSection); - highlightElements(elements); + highlightElements(elements, searchTerm); }; const clearResults = (params) => { @@ -116,21 +150,21 @@ export default { }, methods: { search(value) { + this.searchTerm = value; const displayOptions = { sectionSelector: this.sectionSelector, expandSection: this.expandSection, collapseSection: this.collapseSection, isExpanded: this.isExpandedFn, + searchTerm: this.searchTerm, }; - this.searchTerm = value; - clearResults(displayOptions); if (value.length) { saveExpansionState(document.querySelectorAll(this.sectionSelector), displayOptions); - displayResults(displayOptions, search(this.searchRoot, value)); + displayResults(displayOptions, search(this.searchRoot, this.searchTerm)); } else { restoreExpansionState(displayOptions); } diff --git a/app/assets/javascripts/search_settings/constants.js b/app/assets/javascripts/search_settings/constants.js index 9452d149122..a49351dc7b0 100644 --- a/app/assets/javascripts/search_settings/constants.js +++ b/app/assets/javascripts/search_settings/constants.js @@ -7,5 +7,8 @@ export const HIDE_CLASS = 'gl-display-none'; // used to highlight the text that matches the * search term export const HIGHLIGHT_CLASS = 'gl-bg-orange-100'; +// used to remove padding for text that matches the * search term +export const NONE_PADDING_CLASS = 'gl-p-0'; + // How many seconds to wait until the user * stops typing export const TYPING_DELAY = 400; diff --git a/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue index af426584f4f..d5647619ea3 100644 --- a/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue +++ b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue @@ -36,6 +36,7 @@ export default { 'allowLabelEdit', 'allowScopedLabels', 'iid', + 'fullPath', 'initiallySelectedLabels', 'issuableType', 'labelsFetchPath', @@ -145,6 +146,8 @@ export default { <labels-select-widget v-if="glFeatures.labelsWidget" class="block labels js-labels-block" + :iid="iid" + :full-path="fullPath" :allow-label-remove="allowLabelEdit" :allow-multiselect="true" :footer-create-label-title="__('Create project label')" diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue index 13701cbbd64..3ee0baf8812 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue @@ -57,6 +57,15 @@ export default { required: false, default: false, }, + fullPath: { + type: String, + required: true, + }, + attrWorkspacePath: { + type: String, + required: false, + default: undefined, + }, }, data() { return { @@ -182,6 +191,8 @@ export default { :selected-labels="selectedLabels" :allow-multiselect="allowMultiselect" :issuable-type="issuableType" + :full-path="fullPath" + :attr-workspace-path="attrWorkspacePath" @hideCreateView="toggleDropdownContentsCreateView" /> </template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue index 0f660c92e7c..a2ed08e6b28 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue @@ -19,16 +19,20 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, - inject: { - fullPath: { - default: '', - }, - }, props: { issuableType: { type: String, required: true, }, + fullPath: { + type: String, + required: true, + }, + attrWorkspacePath: { + type: String, + required: false, + default: undefined, + }, }, data() { return { @@ -46,11 +50,19 @@ export default { return Object.keys(colorsMap).map((color) => ({ [color]: colorsMap[color] })); }, mutationVariables() { - return this.issuableType === IssuableType.Epic + if (this.issuableType === IssuableType.Epic) { + return { + title: this.labelTitle, + color: this.selectedColor, + groupPath: this.fullPath, + }; + } + + return this.attrWorkspacePath !== undefined ? { title: this.labelTitle, color: this.selectedColor, - groupPath: this.fullPath, + groupPath: this.attrWorkspacePath, } : { title: this.labelTitle, diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue index 2215f2cba91..e6a25362ff0 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue @@ -24,7 +24,6 @@ export default { GlIntersectionObserver, LabelItem, }, - inject: ['fullPath'], model: { prop: 'localSelectedLabels', }, @@ -45,6 +44,10 @@ export default { type: Array, required: true, }, + fullPath: { + type: String, + required: true, + }, }, data() { return { @@ -84,7 +87,7 @@ export default { return this.$apollo.queries.labels.loading; }, localSelectedLabelsIds() { - return this.localSelectedLabels.map((label) => label.id); + return this.localSelectedLabels.map((label) => getIdFromGraphQLId(label.id)); }, visibleLabels() { if (this.searchKey) { @@ -130,7 +133,9 @@ export default { updateSelectedLabels(label) { let labels; if (this.isLabelSelected(label)) { - labels = this.localSelectedLabels.filter(({ id }) => id !== getIdFromGraphQLId(label.id)); + labels = this.localSelectedLabels.filter( + ({ id }) => id !== getIdFromGraphQLId(label.id) && id !== label.id, + ); } else { labels = [ ...this.localSelectedLabels, diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue index e4ec7909bdd..6bd43da2203 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue @@ -21,15 +21,20 @@ export default { SidebarEditableItem, }, inject: { - iid: { - default: '', - }, allowLabelEdit: { default: false, }, - fullPath: {}, }, props: { + iid: { + type: String, + required: false, + default: '', + }, + fullPath: { + type: String, + required: true, + }, allowLabelRemove: { type: Boolean, required: false, @@ -99,6 +104,11 @@ export default { type: String, required: true, }, + attrWorkspacePath: { + type: String, + required: false, + default: undefined, + }, }, data() { return { @@ -206,6 +216,8 @@ export default { :variant="variant" :issuable-type="issuableType" :is-visible="edit" + :full-path="fullPath" + :attr-workspace-path="attrWorkspacePath" @setLabels="handleDropdownClose" @closeDropdown="collapseEditableItem" /> @@ -224,6 +236,7 @@ export default { :selected-labels="selectedLabels" :variant="variant" :issuable-type="issuableType" + :full-path="fullPath" @setLabels="handleDropdownClose" /> </div> diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 6296b023e90..a2d822bb5de 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -877,18 +877,6 @@ vertical-align: sub; } -.suggestion-confidential { - color: $orange-500; -} - -.suggestion-state-open { - color: $green-500; -} - -.suggestion-state-closed { - color: $blue-500; -} - .suggestion-help-hover { cursor: help; } diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb index 60708c13b85..e8e6a7e5c1a 100644 --- a/app/controllers/groups/boards_controller.rb +++ b/app/controllers/groups/boards_controller.rb @@ -11,6 +11,7 @@ class Groups::BoardsController < Groups::ApplicationController push_frontend_feature_flag(:board_multi_select, group, default_enabled: :yaml) push_frontend_feature_flag(:swimlanes_buffered_rendering, group, default_enabled: :yaml) push_frontend_feature_flag(:iteration_cadences, group, default_enabled: :yaml) + push_frontend_feature_flag(:labels_widget, group, default_enabled: :yaml) end feature_category :boards diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 558a4bbfd45..6e59f159636 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -54,7 +54,7 @@ class Groups::GroupMembersController < Groups::ApplicationController end def invited_members - group_members.invite + group_members.invite.with_invited_user_state end def non_invited_members diff --git a/app/controllers/import/bulk_imports_controller.rb b/app/controllers/import/bulk_imports_controller.rb index 2dca9385da6..bec26cb547d 100644 --- a/app/controllers/import/bulk_imports_controller.rb +++ b/app/controllers/import/bulk_imports_controller.rb @@ -22,13 +22,16 @@ class Import::BulkImportsController < ApplicationController def status respond_to do |format| format.json do - data = importable_data + data = ::BulkImports::GetImportableDataService.new(params, query_params, credentials).execute pagination_headers.each do |header| - response.set_header(header, data.headers[header]) + response.set_header(header, data[:response].headers[header]) end - render json: { importable_data: serialized_data(data.parsed_response) } + json_response = { importable_data: serialized_data(data[:response].parsed_response) } + json_response[:version_validation] = data[:version_validation] + + render json: json_response end format.html do @source_url = session[url_key] @@ -66,10 +69,6 @@ class Import::BulkImportsController < ApplicationController @serializer ||= BaseSerializer.new(current_user: current_user) end - def importable_data - client.get('groups', query_params) - end - # Default query string params used to fetch groups from GitLab source instance # # top_level_only: fetch only top level groups (subgroups are fetched during import itself) @@ -85,15 +84,6 @@ class Import::BulkImportsController < ApplicationController query_params end - def client - @client ||= BulkImports::Clients::HTTP.new( - url: session[url_key], - token: session[access_token_key], - per_page: params[:per_page], - page: params[:page] - ) - end - def configure_params params.permit(access_token_key, url_key) end diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb index 316582f3994..834e4baa7dd 100644 --- a/app/controllers/projects/boards_controller.rb +++ b/app/controllers/projects/boards_controller.rb @@ -11,6 +11,7 @@ class Projects::BoardsController < Projects::ApplicationController push_frontend_feature_flag(:issue_boards_filtered_search, project, default_enabled: :yaml) push_frontend_feature_flag(:board_multi_select, project, default_enabled: :yaml) push_frontend_feature_flag(:iteration_cadences, project&.group, default_enabled: :yaml) + push_frontend_feature_flag(:labels_widget, project, default_enabled: :yaml) end feature_category :boards diff --git a/app/controllers/projects/ci/pipeline_editor_controller.rb b/app/controllers/projects/ci/pipeline_editor_controller.rb index 953b9d83a66..e925bbea5e9 100644 --- a/app/controllers/projects/ci/pipeline_editor_controller.rb +++ b/app/controllers/projects/ci/pipeline_editor_controller.rb @@ -4,6 +4,7 @@ class Projects::Ci::PipelineEditorController < Projects::ApplicationController before_action :check_can_collaborate! before_action do push_frontend_feature_flag(:pipeline_editor_drawer, @project, default_enabled: :yaml) + push_frontend_feature_flag(:pipeline_editor_mini_graph, @project, default_enabled: :yaml) push_frontend_feature_flag(:schema_linting, @project, default_enabled: :yaml) end diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index cce2497ae25..e8074f7d793 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -58,7 +58,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController end def invited_members - members.invite + members.invite.with_invited_user_state end def non_invited_members diff --git a/app/controllers/repositories/git_http_controller.rb b/app/controllers/repositories/git_http_controller.rb index e51bfe6a37e..c3c6a51239d 100644 --- a/app/controllers/repositories/git_http_controller.rb +++ b/app/controllers/repositories/git_http_controller.rb @@ -11,6 +11,9 @@ module Repositories rescue_from Gitlab::GitAccess::NotFoundError, with: :render_404_with_exception rescue_from Gitlab::GitAccessProject::CreationError, with: :render_422_with_exception rescue_from Gitlab::GitAccess::TimeoutError, with: :render_503_with_exception + rescue_from GRPC::Unavailable do |e| + render_503_with_exception(e, message: 'The git server, Gitaly, is not available at this time. Please contact your administrator.') + end # GET /foo/bar.git/info/refs?service=git-upload-pack (git pull) # GET /foo/bar.git/info/refs?service=git-receive-pack (git push) @@ -71,8 +74,8 @@ module Repositories render plain: exception.message, status: :unprocessable_entity end - def render_503_with_exception(exception) - render plain: exception.message, status: :service_unavailable + def render_503_with_exception(exception, message: nil) + render plain: message || exception.message, status: :service_unavailable end def update_fetch_statistics diff --git a/app/graphql/types/packages/nuget/metadatum_type.rb b/app/graphql/types/packages/nuget/metadatum_type.rb index ed9d97724af..b58fd954a74 100644 --- a/app/graphql/types/packages/nuget/metadatum_type.rb +++ b/app/graphql/types/packages/nuget/metadatum_type.rb @@ -10,9 +10,9 @@ module Types authorize :read_package field :id, ::Types::GlobalIDType[::Packages::Nuget::Metadatum], null: false, description: 'ID of the metadatum.' - field :license_url, GraphQL::Types::String, null: false, description: 'License URL of the Nuget package.' - field :project_url, GraphQL::Types::String, null: false, description: 'Project URL of the Nuget package.' - field :icon_url, GraphQL::Types::String, null: false, description: 'Icon URL of the Nuget package.' + field :license_url, GraphQL::Types::String, null: true, description: 'License URL of the Nuget package.' + field :project_url, GraphQL::Types::String, null: true, description: 'Project URL of the Nuget package.' + field :icon_url, GraphQL::Types::String, null: true, description: 'Icon URL of the Nuget package.' end end end diff --git a/app/models/bulk_import.rb b/app/models/bulk_import.rb index dee55675304..818ae04ba29 100644 --- a/app/models/bulk_import.rb +++ b/app/models/bulk_import.rb @@ -4,7 +4,8 @@ # projects to a GitLab instance. It associates the import with the responsible # user. class BulkImport < ApplicationRecord - MINIMUM_GITLAB_MAJOR_VERSION = 14 + MIN_MAJOR_VERSION = 14 + MIN_MINOR_VERSION_FOR_PROJECT = 4 belongs_to :user, optional: false @@ -34,6 +35,14 @@ class BulkImport < ApplicationRecord end end + def source_version_info + Gitlab::VersionInfo.parse(source_version) + end + + def self.min_gl_version_for_project_migration + Gitlab::VersionInfo.new(MIN_MAJOR_VERSION, MIN_MINOR_VERSION_FOR_PROJECT) + end + def self.all_human_statuses state_machine.states.map(&:human_name) end diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb index d2b9e1f567a..ecac4ab95f4 100644 --- a/app/models/bulk_imports/entity.rb +++ b/app/models/bulk_imports/entity.rb @@ -83,9 +83,9 @@ class BulkImports::Entity < ApplicationRecord def pipelines @pipelines ||= case source_type when 'group_entity' - BulkImports::Groups::Stage.pipelines + BulkImports::Groups::Stage.new(bulk_import).pipelines when 'project_entity' - BulkImports::Projects::Stage.pipelines + BulkImports::Projects::Stage.new(bulk_import).pipelines end end diff --git a/app/models/member.rb b/app/models/member.rb index 45af34a3e6b..7e0b4705217 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -50,6 +50,11 @@ class Member < ApplicationRecord }, if: :project_bot? + scope :with_invited_user_state, -> do + joins('LEFT JOIN users as invited_user ON invited_user.email = members.invite_email') + .select('members.*', 'invited_user.state as invited_user_state') + end + scope :in_hierarchy, ->(source) do groups = source.root_ancestor.self_and_descendants group_members = Member.default_scoped.where(source: groups) diff --git a/app/models/packages/helm/file_metadatum.rb b/app/models/packages/helm/file_metadatum.rb index 1771003d1f9..dfa4ab6df82 100644 --- a/app/models/packages/helm/file_metadatum.rb +++ b/app/models/packages/helm/file_metadatum.rb @@ -12,7 +12,7 @@ module Packages validates :channel, presence: true, - length: { maximum: 63 }, + length: { maximum: 255 }, format: { with: Gitlab::Regex.helm_channel_regex } validates :metadata, diff --git a/app/serializers/member_entity.rb b/app/serializers/member_entity.rb index 5100a41638e..d7221109ecb 100644 --- a/app/serializers/member_entity.rb +++ b/app/serializers/member_entity.rb @@ -44,6 +44,8 @@ class MemberEntity < Grape::Entity MemberUserEntity.represent(member.user, source: options[:source]) end + expose :state + expose :invite, if: -> (member) { member.invite? } do expose :email do |member| member.invite_email @@ -56,6 +58,10 @@ class MemberEntity < Grape::Entity expose :can_resend do |member| member.can_resend_invite? end + + expose :user_state do |member| + member.respond_to?(:invited_user_state) ? member.invited_user_state : "" + end end end diff --git a/app/services/bulk_imports/create_service.rb b/app/services/bulk_imports/create_service.rb index 1cea7632aa1..c1becbb5609 100644 --- a/app/services/bulk_imports/create_service.rb +++ b/app/services/bulk_imports/create_service.rb @@ -52,7 +52,11 @@ module BulkImports def create_bulk_import BulkImport.transaction do - bulk_import = BulkImport.create!(user: current_user, source_type: 'gitlab') + bulk_import = BulkImport.create!( + user: current_user, + source_type: 'gitlab', + source_version: client.instance_version + ) bulk_import.create_configuration!(credentials.slice(:url, :access_token)) params.each do |entity| @@ -68,5 +72,12 @@ module BulkImports bulk_import end end + + def client + @client ||= BulkImports::Clients::HTTP.new( + url: @credentials[:url], + token: @credentials[:access_token] + ) + end end end diff --git a/app/services/bulk_imports/get_importable_data_service.rb b/app/services/bulk_imports/get_importable_data_service.rb new file mode 100644 index 00000000000..07e0b3976a1 --- /dev/null +++ b/app/services/bulk_imports/get_importable_data_service.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module BulkImports + class GetImportableDataService + def initialize(params, query_params, credentials) + @params = params + @query_params = query_params + @credentials = credentials + end + + def execute + { + version_validation: version_validation, + response: importables + } + end + + private + + def importables + client.get('groups', @query_params) + end + + def version_validation + { + features: { + project_migration: { + available: client.compatible_for_project_migration?, + min_version: BulkImport.min_gl_version_for_project_migration.to_s + }, + source_instance_version: client.instance_version.to_s + } + } + end + + def client + @client ||= BulkImports::Clients::HTTP.new( + url: @credentials[:url], + token: @credentials[:access_token], + per_page: @params[:per_page], + page: @params[:page] + ) + end + end +end diff --git a/app/views/admin/users/_access_levels.html.haml b/app/views/admin/users/_access_levels.html.haml index aeb274fe2cb..6a5f07dd2db 100644 --- a/app/views/admin/users/_access_levels.html.haml +++ b/app/views/admin/users/_access_levels.html.haml @@ -19,22 +19,20 @@ .col-sm-10 - editing_current_user = (current_user == @user) - = f.radio_button :access_level, :regular, disabled: editing_current_user - = f.label :access_level_regular, class: 'font-weight-bold' do - = s_('AdminUsers|Regular') - %p.light - = s_('AdminUsers|Regular users have access to their groups and projects') + = f.gitlab_ui_radio_component :access_level, :regular, + s_('AdminUsers|Regular'), + radio_options: { disabled: editing_current_user }, + help_text: s_('AdminUsers|Regular users have access to their groups and projects.') = render_if_exists 'admin/users/auditor_access_level_radio', f: f, disabled: editing_current_user - = f.radio_button :access_level, :admin, disabled: editing_current_user - = f.label :access_level_admin, class: 'font-weight-bold' do - = s_('AdminUsers|Admin') - %p.light - = s_('AdminUsers|Administrators have access to all groups, projects and users and can manage all features in this installation') - - if editing_current_user - %p.light - = s_('AdminUsers|You cannot remove your own admin rights.') + - help_text = s_('AdminUsers|Administrators have access to all groups, projects and users and can manage all features in this installation.') + - help_text += ' ' + s_('AdminUsers|You cannot remove your own admin rights.') if editing_current_user + = f.gitlab_ui_radio_component :access_level, :admin, + s_('AdminUsers|Admin'), + radio_options: { disabled: editing_current_user }, + help_text: help_text + .form-group.row .col-sm-2.col-form-label.gl-pt-0 diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml index 9d62c19e2fc..96fd0f5aac1 100644 --- a/app/views/admin/users/_form.html.haml +++ b/app/views/admin/users/_form.html.haml @@ -1,5 +1,5 @@ .user_new - = form_for [:admin, @user], html: { class: 'fieldset-form' } do |f| + = gitlab_ui_form_for [:admin, @user], html: { class: 'fieldset-form' } do |f| = form_errors(@user) %fieldset diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index ec2904245d3..dff1b5e3d04 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -17,6 +17,7 @@ = render_two_factor_auth_recovery_settings_check = render_if_exists "layouts/header/ee_subscribable_banner" = render_if_exists "shared/namespace_storage_limit_alert" + = render_if_exists "shared/namespace_user_cap_reached_alert" = render_if_exists "shared/new_user_signups_cap_reached_alert" = yield :page_level_alert = yield :customize_homepage_banner diff --git a/app/views/projects/ci/pipeline_editor/show.html.haml b/app/views/projects/ci/pipeline_editor/show.html.haml index 674765e9f89..ce6f7553ab4 100644 --- a/app/views/projects/ci/pipeline_editor/show.html.haml +++ b/app/views/projects/ci/pipeline_editor/show.html.haml @@ -1,3 +1,5 @@ +- add_page_specific_style 'page_bundles/pipelines' + - page_title s_('Pipelines|Pipeline Editor') - content_for :prefetch_asset_tags do - webpack_preload_asset_tag('monaco') |