Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
51d5328e82
commit
efbd20fd1e
|
@ -53,7 +53,6 @@ rules:
|
|||
- '^$'
|
||||
- '^variables$'
|
||||
- 'attrs?$'
|
||||
'@gitlab/vue-prefer-dollar-scopedslots': error
|
||||
no-param-reassign:
|
||||
- error
|
||||
- props: true
|
||||
|
|
|
@ -559,8 +559,6 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
|
|||
/doc/development/ee_features.md @fneill
|
||||
/doc/development/logging.md @msedlakjakubowski
|
||||
/doc/development/maintenance_mode.md @axil
|
||||
/doc/development/new_fe_guide/modules/widget_extensions.md @aqualls
|
||||
/doc/development/new_fe_guide/tips.md @sselhorn
|
||||
/doc/development/omnibus.md @axil
|
||||
/doc/development/packages/ @claytoncornell
|
||||
/doc/development/permissions.md @eread
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import katex from 'katex';
|
||||
import marked from 'marked';
|
||||
import { marked } from 'marked';
|
||||
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
|
||||
import { sanitize } from '~/lib/dompurify';
|
||||
import { hasContent, markdownConfig } from '~/lib/utils/text_utility';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/* eslint-disable @gitlab/require-i18n-strings */
|
||||
import marked from 'marked';
|
||||
import { marked } from 'marked';
|
||||
import { sanitize } from '~/lib/dompurify';
|
||||
import { markdownConfig } from '~/lib/utils/text_utility';
|
||||
|
||||
|
|
|
@ -29,6 +29,10 @@ export default {
|
|||
lfsLabel: s__('ProjectSettings|Git Large File Storage (LFS)'),
|
||||
mergeRequestsLabel: s__('ProjectSettings|Merge requests'),
|
||||
operationsLabel: s__('ProjectSettings|Operations'),
|
||||
environmentsLabel: s__('ProjectSettings|Environments'),
|
||||
environmentsHelpText: s__(
|
||||
'ProjectSettings|Every project can make deployments to environments either via CI/CD or API calls. Non-project members have read-only access.',
|
||||
),
|
||||
packagesHelpText: s__(
|
||||
'ProjectSettings|Every project can have its own space to store its packages. Note: The Package Registry is always visible when a project is public.',
|
||||
),
|
||||
|
@ -209,6 +213,7 @@ export default {
|
|||
requirementsAccessLevel: featureAccessLevel.EVERYONE,
|
||||
securityAndComplianceAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
|
||||
operationsAccessLevel: featureAccessLevel.EVERYONE,
|
||||
environmentsAccessLevel: featureAccessLevel.EVERYONE,
|
||||
containerRegistryAccessLevel: featureAccessLevel.EVERYONE,
|
||||
warnAboutPotentiallyUnwantedCharacters: true,
|
||||
lfsEnabled: true,
|
||||
|
@ -282,6 +287,9 @@ export default {
|
|||
return this.operationsAccessLevel > featureAccessLevel.NOT_ENABLED;
|
||||
},
|
||||
|
||||
environmentsEnabled() {
|
||||
return this.environmentsAccessLevel > featureAccessLevel.NOT_ENABLED;
|
||||
},
|
||||
repositoryEnabled() {
|
||||
return this.repositoryAccessLevel > featureAccessLevel.NOT_ENABLED;
|
||||
},
|
||||
|
@ -318,6 +326,9 @@ export default {
|
|||
packageRegistryAccessLevelEnabled() {
|
||||
return this.glFeatures.packageRegistryAccessLevel;
|
||||
},
|
||||
splitOperationsEnabled() {
|
||||
return this.glFeatures.splitOperationsVisibilityPermissions;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
@ -374,6 +385,10 @@ export default {
|
|||
featureAccessLevel.PROJECT_MEMBERS,
|
||||
this.operationsAccessLevel,
|
||||
);
|
||||
this.environmentsAccessLevel = Math.min(
|
||||
featureAccessLevel.PROJECT_MEMBERS,
|
||||
this.environmentsAccessLevel,
|
||||
);
|
||||
this.containerRegistryAccessLevel = Math.min(
|
||||
featureAccessLevel.PROJECT_MEMBERS,
|
||||
this.containerRegistryAccessLevel,
|
||||
|
@ -415,6 +430,8 @@ export default {
|
|||
this.requirementsAccessLevel = featureAccessLevel.EVERYONE;
|
||||
if (this.operationsAccessLevel === featureAccessLevel.PROJECT_MEMBERS)
|
||||
this.operationsAccessLevel = featureAccessLevel.EVERYONE;
|
||||
if (this.environmentsAccessLevel === featureAccessLevel.PROJECT_MEMBERS)
|
||||
this.environmentsAccessLevel = featureAccessLevel.EVERYONE;
|
||||
if (this.containerRegistryAccessLevel === featureAccessLevel.PROJECT_MEMBERS)
|
||||
this.containerRegistryAccessLevel = featureAccessLevel.EVERYONE;
|
||||
|
||||
|
@ -857,6 +874,20 @@ export default {
|
|||
/>
|
||||
</project-setting-row>
|
||||
</div>
|
||||
<template v-if="splitOperationsEnabled">
|
||||
<project-setting-row
|
||||
ref="environments-settings"
|
||||
:label="$options.i18n.environmentsLabel"
|
||||
:help-text="$options.i18n.environmentsHelpText"
|
||||
>
|
||||
<project-feature-setting
|
||||
v-model="environmentsAccessLevel"
|
||||
:label="$options.i18n.environmentsLabel"
|
||||
:options="featureAccessLevelOptions"
|
||||
name="project[project_feature_attributes][environments_access_level]"
|
||||
/>
|
||||
</project-setting-row>
|
||||
</template>
|
||||
</div>
|
||||
<project-setting-row v-if="canDisableEmails" ref="email-settings" class="mb-3">
|
||||
<label class="js-emails-disabled">
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlButton, GlBadge, GlIcon, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { GlButton, GlBadge, GlIcon, GlAlert, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { produce } from 'immer';
|
||||
import { s__ } from '~/locale';
|
||||
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
|
@ -25,6 +25,7 @@ export default {
|
|||
GlButton,
|
||||
GlBadge,
|
||||
GlIcon,
|
||||
GlAlert,
|
||||
GlLoadingIcon,
|
||||
WorkItemLinksForm,
|
||||
WorkItemLinksMenu,
|
||||
|
@ -57,6 +58,9 @@ export default {
|
|||
skip() {
|
||||
return !this.issuableId;
|
||||
},
|
||||
error(e) {
|
||||
this.error = e.message || this.$options.i18n.fetchError;
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
|
@ -66,6 +70,7 @@ export default {
|
|||
activeChildId: null,
|
||||
activeToast: null,
|
||||
prefetchedWorkItem: null,
|
||||
error: undefined,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -227,6 +232,9 @@ export default {
|
|||
},
|
||||
i18n: {
|
||||
title: s__('WorkItem|Child items'),
|
||||
fetchError: s__(
|
||||
'WorkItem|Something went wrong when fetching the items list. Please refresh this page.',
|
||||
),
|
||||
emptyStateMessage: s__(
|
||||
'WorkItem|No child items are currently assigned. Use child items to prioritize tasks that your team should complete in order to accomplish your goals!',
|
||||
),
|
||||
|
@ -273,15 +281,19 @@ export default {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<gl-alert v-if="error && !isLoading" variant="danger" @dismiss="error = undefined">
|
||||
{{ error }}
|
||||
</gl-alert>
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="gl-bg-gray-10 gl-p-5 gl-pb-3 gl-rounded-bottom-left-base gl-rounded-bottom-right-base"
|
||||
class="gl-bg-gray-10 gl-rounded-bottom-left-base gl-rounded-bottom-right-base"
|
||||
:class="{ 'gl-p-5 gl-pb-3': !error }"
|
||||
data-testid="links-body"
|
||||
>
|
||||
<gl-loading-icon v-if="isLoading" color="dark" class="gl-my-3" />
|
||||
|
||||
<template v-else>
|
||||
<div v-if="isChildrenEmpty && !isShownAddForm" data-testid="links-empty">
|
||||
<div v-if="isChildrenEmpty && !isShownAddForm && !error" data-testid="links-empty">
|
||||
<p class="gl-mt-3 gl-mb-4">
|
||||
{{ $options.i18n.emptyStateMessage }}
|
||||
</p>
|
||||
|
|
|
@ -16,6 +16,11 @@ module Crm
|
|||
|
||||
attr_reader :params, :current_user
|
||||
|
||||
def self.counts_by_state(current_user, params = {})
|
||||
params = params.merge(sort: nil)
|
||||
new(current_user, params).execute.counts_by_state
|
||||
end
|
||||
|
||||
def initialize(current_user, params = {})
|
||||
@current_user = current_user
|
||||
@params = params
|
||||
|
@ -28,11 +33,25 @@ module Crm
|
|||
contacts = by_ids(contacts)
|
||||
contacts = by_state(contacts)
|
||||
contacts = by_search(contacts)
|
||||
contacts.sort_by_name
|
||||
sort_contacts(contacts)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def sort_contacts(contacts)
|
||||
return contacts.sort_by_name unless @params.key?(:sort)
|
||||
return contacts if @params[:sort].nil?
|
||||
|
||||
field = @params[:sort][:field]
|
||||
direction = @params[:sort][:direction]
|
||||
|
||||
if field == 'organization'
|
||||
contacts.sort_by_organization(direction)
|
||||
else
|
||||
contacts.sort_by_field(field, direction)
|
||||
end
|
||||
end
|
||||
|
||||
def root_group
|
||||
strong_memoize(:root_group) do
|
||||
group = params[:group]&.root_ancestor
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Resolvers
|
||||
module Crm
|
||||
class ContactStateCountsResolver < BaseResolver
|
||||
include Gitlab::Graphql::Authorize::AuthorizeResource
|
||||
|
||||
authorize :read_crm_contact
|
||||
|
||||
type Types::CustomerRelations::ContactStateCountsType, null: true
|
||||
|
||||
argument :search, GraphQL::Types::String,
|
||||
required: false,
|
||||
description: 'Search term to find contacts with.'
|
||||
|
||||
argument :state, Types::CustomerRelations::ContactStateEnum,
|
||||
required: false,
|
||||
description: 'State of the contacts to search for.'
|
||||
|
||||
def resolve(**args)
|
||||
CustomerRelations::ContactStateCounts.new(context[:current_user], object, args)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -10,6 +10,11 @@ module Resolvers
|
|||
|
||||
type Types::CustomerRelations::ContactType, null: true
|
||||
|
||||
argument :sort, Types::CustomerRelations::ContactSortEnum,
|
||||
description: 'Criteria to sort issues by.',
|
||||
required: false,
|
||||
default_value: { field: 'last_name', direction: :asc }
|
||||
|
||||
argument :search, GraphQL::Types::String,
|
||||
required: false,
|
||||
description: 'Search term to find contacts with.'
|
||||
|
@ -24,13 +29,25 @@ module Resolvers
|
|||
|
||||
def resolve(**args)
|
||||
args[:ids] = resolve_ids(args.delete(:ids))
|
||||
args.delete(:state) if args[:state] == :all
|
||||
|
||||
::Crm::ContactsFinder.new(current_user, { group: group }.merge(args)).execute
|
||||
contacts = ::Crm::ContactsFinder.new(current_user, { group: group }.merge(args)).execute
|
||||
if needs_offset?(args)
|
||||
offset_pagination(contacts)
|
||||
else
|
||||
contacts
|
||||
end
|
||||
end
|
||||
|
||||
def group
|
||||
object.respond_to?(:sync) ? object.sync : object
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def needs_offset?(args)
|
||||
args.key?(:sort) && args[:sort][:field] == 'organization'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Types
|
||||
module CustomerRelations
|
||||
class ContactSortEnum < SortEnum
|
||||
graphql_name 'ContactSort'
|
||||
description 'Values for sorting contacts'
|
||||
|
||||
sortable_fields = ['First name', 'Last name', 'Email', 'Phone', 'Description', 'Organization']
|
||||
|
||||
sortable_fields.each do |field|
|
||||
value "#{field.upcase.tr(' ', '_')}_ASC",
|
||||
value: { field: field.downcase.tr(' ', '_'), direction: :asc },
|
||||
description: "#{field} by ascending order."
|
||||
value "#{field.upcase.tr(' ', '_')}_DESC",
|
||||
value: { field: field.downcase.tr(' ', '_'), direction: :desc },
|
||||
description: "#{field} by descending order."
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Types
|
||||
module CustomerRelations
|
||||
class ContactStateCountsType < Types::BaseObject
|
||||
graphql_name 'ContactStateCounts'
|
||||
description 'Represents the total number of contacts for the represented statuses.'
|
||||
|
||||
authorize :read_crm_contact
|
||||
|
||||
def self.available_contact_states
|
||||
@available_contact_states ||= ::CustomerRelations::Contact.states.keys.push('all')
|
||||
end
|
||||
|
||||
available_contact_states.each do |state|
|
||||
field state,
|
||||
GraphQL::Types::Int,
|
||||
null: true,
|
||||
description: "Number of contacts with state `#{state.upcase}`"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,12 +5,16 @@ module Types
|
|||
class ContactStateEnum < BaseEnum
|
||||
graphql_name 'CustomerRelationsContactState'
|
||||
|
||||
value 'all',
|
||||
description: "All available contacts.",
|
||||
value: :all
|
||||
|
||||
value 'active',
|
||||
description: "Active contact.",
|
||||
description: "Active contacts.",
|
||||
value: :active
|
||||
|
||||
value 'inactive',
|
||||
description: "Inactive contact.",
|
||||
description: "Inactive contacts.",
|
||||
value: :inactive
|
||||
end
|
||||
end
|
||||
|
|
|
@ -217,6 +217,12 @@ module Types
|
|||
description: "Find contacts of this group.",
|
||||
resolver: Resolvers::Crm::ContactsResolver
|
||||
|
||||
field :contact_state_counts,
|
||||
Types::CustomerRelations::ContactStateCountsType,
|
||||
null: true,
|
||||
description: 'Counts of contacts by status for the group.',
|
||||
resolver: Resolvers::Crm::ContactStateCountsResolver
|
||||
|
||||
field :work_item_types, Types::WorkItems::TypeType.connection_type,
|
||||
resolver: Resolvers::WorkItems::TypesResolver,
|
||||
description: 'Work item types available to the group.' \
|
||||
|
|
|
@ -29,6 +29,12 @@ class CustomerRelations::Contact < ApplicationRecord
|
|||
validate :validate_email_format
|
||||
validate :validate_root_group
|
||||
|
||||
scope :order_scope_asc, ->(field) { order(arel_table[field].asc.nulls_last) }
|
||||
scope :order_scope_desc, ->(field) { order(arel_table[field].desc.nulls_last) }
|
||||
|
||||
scope :order_by_organization_asc, -> { includes(:organization).order("customer_relations_organizations.name ASC NULLS LAST") }
|
||||
scope :order_by_organization_desc, -> { includes(:organization).order("customer_relations_organizations.name DESC NULLS LAST") }
|
||||
|
||||
def self.reference_prefix
|
||||
'[contact:'
|
||||
end
|
||||
|
@ -56,6 +62,22 @@ class CustomerRelations::Contact < ApplicationRecord
|
|||
where(state: state)
|
||||
end
|
||||
|
||||
def self.sort_by_field(field, direction)
|
||||
if direction == :asc
|
||||
order_scope_asc(field)
|
||||
else
|
||||
order_scope_desc(field)
|
||||
end
|
||||
end
|
||||
|
||||
def self.sort_by_organization(direction)
|
||||
if direction == :asc
|
||||
order_by_organization_asc
|
||||
else
|
||||
order_by_organization_desc
|
||||
end
|
||||
end
|
||||
|
||||
def self.sort_by_name
|
||||
order(Gitlab::Pagination::Keyset::Order.build([
|
||||
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
||||
|
@ -115,6 +137,10 @@ class CustomerRelations::Contact < ApplicationRecord
|
|||
where(group: group).update_all(group_id: group.root_ancestor.id)
|
||||
end
|
||||
|
||||
def self.counts_by_state
|
||||
group(:state).count
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_email_format
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module CustomerRelations
|
||||
# Represents counts of each status or category of statuses
|
||||
class ContactStateCounts
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
attr_reader :group
|
||||
|
||||
def self.declarative_policy_class
|
||||
'CustomerRelations::ContactPolicy'
|
||||
end
|
||||
|
||||
def initialize(current_user, group, params)
|
||||
@current_user = current_user
|
||||
@group = group
|
||||
@params = params
|
||||
end
|
||||
|
||||
# Define method for each state
|
||||
::CustomerRelations::Contact.states.each_key do |state|
|
||||
define_method(state) { counts[state] }
|
||||
end
|
||||
|
||||
def all
|
||||
counts.values.sum
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :current_user, :params
|
||||
|
||||
def counts
|
||||
strong_memoize(:counts) do
|
||||
Hash.new(0).merge(counts_by_state)
|
||||
end
|
||||
end
|
||||
|
||||
def counts_by_state
|
||||
::Crm::ContactsFinder.counts_by_state(current_user, params.merge({ group: group }))
|
||||
end
|
||||
end
|
||||
end
|
|
@ -30,6 +30,7 @@ class User < ApplicationRecord
|
|||
include Gitlab::Auth::Otp::Fortinet
|
||||
include RestrictedSignup
|
||||
include StripAttribute
|
||||
include EachBatch
|
||||
|
||||
DEFAULT_NOTIFICATION_LEVEL = :participating
|
||||
|
||||
|
|
|
@ -10,43 +10,23 @@ module Users
|
|||
|
||||
feature_category :utilization
|
||||
|
||||
NUMBER_OF_BATCHES = 50
|
||||
BATCH_SIZE = 200
|
||||
PAUSE_SECONDS = 0.25
|
||||
|
||||
def perform
|
||||
return if Gitlab.com?
|
||||
|
||||
return unless ::Gitlab::CurrentSettings.current_application_settings.deactivate_dormant_users
|
||||
|
||||
with_context(caller_id: self.class.name.to_s) do
|
||||
NUMBER_OF_BATCHES.times do
|
||||
result = User.connection.execute(update_query)
|
||||
|
||||
break if result.cmd_tuples == 0
|
||||
|
||||
sleep(PAUSE_SECONDS)
|
||||
end
|
||||
end
|
||||
deactivate_users(User.dormant)
|
||||
deactivate_users(User.with_no_activity)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_query
|
||||
<<~SQL
|
||||
UPDATE "users"
|
||||
SET "state" = 'deactivated'
|
||||
WHERE "users"."id" IN (
|
||||
(#{users.dormant.to_sql})
|
||||
UNION
|
||||
(#{users.with_no_activity.to_sql})
|
||||
LIMIT #{BATCH_SIZE}
|
||||
)
|
||||
SQL
|
||||
end
|
||||
|
||||
def users
|
||||
User.select(:id).limit(BATCH_SIZE)
|
||||
def deactivate_users(scope)
|
||||
with_context(caller_id: self.class.name.to_s) do
|
||||
scope.each_batch do |batch|
|
||||
batch.each(&:deactivate)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveDeactivatedUserHighestRoleStats < Gitlab::Database::Migration[2.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
|
||||
def up
|
||||
# This migration is applicable to self-managed instances that may utilize the
|
||||
# dormant user deactivation feature. This feature is not enabled on Gitlab.com.
|
||||
return if Gitlab.com?
|
||||
|
||||
users_table = define_batchable_model('users')
|
||||
user_highest_roles_table = define_batchable_model('user_highest_roles')
|
||||
|
||||
users_table.where(state: 'deactivated').each_batch do |users_batch|
|
||||
user_ids = users_batch.pluck(:id)
|
||||
user_highest_roles_table.where(user_id: user_ids).delete_all
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
# no-op
|
||||
|
||||
# This migration removes entries from the UserHighestRole table and cannot be reversed
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
4de7fddbc2f44cf1450af25bd55a5f2586c3daf79b1443ec26ba9d47002707d7
|
|
@ -10558,6 +10558,18 @@ Connection details for an Agent.
|
|||
| <a id="connectedagentconnectionid"></a>`connectionId` | [`BigInt`](#bigint) | ID of the connection. |
|
||||
| <a id="connectedagentmetadata"></a>`metadata` | [`AgentMetadata`](#agentmetadata) | Information about the Agent. |
|
||||
|
||||
### `ContactStateCounts`
|
||||
|
||||
Represents the total number of contacts for the represented statuses.
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="contactstatecountsactive"></a>`active` | [`Int`](#int) | Number of contacts with state `ACTIVE`. |
|
||||
| <a id="contactstatecountsall"></a>`all` | [`Int`](#int) | Number of contacts with state `ALL`. |
|
||||
| <a id="contactstatecountsinactive"></a>`inactive` | [`Int`](#int) | Number of contacts with state `INACTIVE`. |
|
||||
|
||||
### `ContainerExpirationPolicy`
|
||||
|
||||
A tag expiration policy designed to keep only the images that matter most.
|
||||
|
@ -12270,6 +12282,19 @@ four standard [pagination arguments](#connection-pagination-arguments):
|
|||
| ---- | ---- | ----------- |
|
||||
| <a id="groupcomplianceframeworksid"></a>`id` | [`ComplianceManagementFrameworkID`](#compliancemanagementframeworkid) | Global ID of a specific compliance framework to return. |
|
||||
|
||||
##### `Group.contactStateCounts`
|
||||
|
||||
Counts of contacts by status for the group.
|
||||
|
||||
Returns [`ContactStateCounts`](#contactstatecounts).
|
||||
|
||||
###### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="groupcontactstatecountssearch"></a>`search` | [`String`](#string) | Search term to find contacts with. |
|
||||
| <a id="groupcontactstatecountsstate"></a>`state` | [`CustomerRelationsContactState`](#customerrelationscontactstate) | State of the contacts to search for. |
|
||||
|
||||
##### `Group.contacts`
|
||||
|
||||
Find contacts of this group.
|
||||
|
@ -12286,6 +12311,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
|
|||
| ---- | ---- | ----------- |
|
||||
| <a id="groupcontactsids"></a>`ids` | [`[CustomerRelationsContactID!]`](#customerrelationscontactid) | Filter contacts by IDs. |
|
||||
| <a id="groupcontactssearch"></a>`search` | [`String`](#string) | Search term to find contacts with. |
|
||||
| <a id="groupcontactssort"></a>`sort` | [`ContactSort`](#contactsort) | Criteria to sort issues by. |
|
||||
| <a id="groupcontactsstate"></a>`state` | [`CustomerRelationsContactState`](#customerrelationscontactstate) | State of the contacts to search for. |
|
||||
|
||||
##### `Group.containerRepositories`
|
||||
|
@ -19284,6 +19310,33 @@ Conan file types.
|
|||
| <a id="conanmetadatumfiletypeenumpackage_file"></a>`PACKAGE_FILE` | A package file type. |
|
||||
| <a id="conanmetadatumfiletypeenumrecipe_file"></a>`RECIPE_FILE` | A recipe file type. |
|
||||
|
||||
### `ContactSort`
|
||||
|
||||
Values for sorting contacts.
|
||||
|
||||
| Value | Description |
|
||||
| ----- | ----------- |
|
||||
| <a id="contactsortcreated_asc"></a>`CREATED_ASC` | Created at ascending order. |
|
||||
| <a id="contactsortcreated_desc"></a>`CREATED_DESC` | Created at descending order. |
|
||||
| <a id="contactsortdescription_asc"></a>`DESCRIPTION_ASC` | Description by ascending order. |
|
||||
| <a id="contactsortdescription_desc"></a>`DESCRIPTION_DESC` | Description by descending order. |
|
||||
| <a id="contactsortemail_asc"></a>`EMAIL_ASC` | Email by ascending order. |
|
||||
| <a id="contactsortemail_desc"></a>`EMAIL_DESC` | Email by descending order. |
|
||||
| <a id="contactsortfirst_name_asc"></a>`FIRST_NAME_ASC` | First name by ascending order. |
|
||||
| <a id="contactsortfirst_name_desc"></a>`FIRST_NAME_DESC` | First name by descending order. |
|
||||
| <a id="contactsortlast_name_asc"></a>`LAST_NAME_ASC` | Last name by ascending order. |
|
||||
| <a id="contactsortlast_name_desc"></a>`LAST_NAME_DESC` | Last name by descending order. |
|
||||
| <a id="contactsortorganization_asc"></a>`ORGANIZATION_ASC` | Organization by ascending order. |
|
||||
| <a id="contactsortorganization_desc"></a>`ORGANIZATION_DESC` | Organization by descending order. |
|
||||
| <a id="contactsortphone_asc"></a>`PHONE_ASC` | Phone by ascending order. |
|
||||
| <a id="contactsortphone_desc"></a>`PHONE_DESC` | Phone by descending order. |
|
||||
| <a id="contactsortupdated_asc"></a>`UPDATED_ASC` | Updated at ascending order. |
|
||||
| <a id="contactsortupdated_desc"></a>`UPDATED_DESC` | Updated at descending order. |
|
||||
| <a id="contactsortcreated_asc"></a>`created_asc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `CREATED_ASC`. |
|
||||
| <a id="contactsortcreated_desc"></a>`created_desc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `CREATED_DESC`. |
|
||||
| <a id="contactsortupdated_asc"></a>`updated_asc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `UPDATED_ASC`. |
|
||||
| <a id="contactsortupdated_desc"></a>`updated_desc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `UPDATED_DESC`. |
|
||||
|
||||
### `ContainerExpirationPolicyCadenceEnum`
|
||||
|
||||
| Value | Description |
|
||||
|
@ -19365,8 +19418,9 @@ Values for sorting tags.
|
|||
|
||||
| Value | Description |
|
||||
| ----- | ----------- |
|
||||
| <a id="customerrelationscontactstateactive"></a>`active` | Active contact. |
|
||||
| <a id="customerrelationscontactstateinactive"></a>`inactive` | Inactive contact. |
|
||||
| <a id="customerrelationscontactstateactive"></a>`active` | Active contacts. |
|
||||
| <a id="customerrelationscontactstateall"></a>`all` | All available contacts. |
|
||||
| <a id="customerrelationscontactstateinactive"></a>`inactive` | Inactive contacts. |
|
||||
|
||||
### `CustomerRelationsOrganizationState`
|
||||
|
||||
|
|
|
@ -0,0 +1,230 @@
|
|||
---
|
||||
stage: Package
|
||||
group: Package
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Terraform Registry API **(FREE)**
|
||||
|
||||
This is the API documentation for [Terraform Modules](../../user/packages/terraform_module_registry/index.md).
|
||||
|
||||
WARNING:
|
||||
This API is used by the [terraform cli](https://www.terraform.io/)
|
||||
and is generally not meant for manual consumption.
|
||||
|
||||
For instructions on how to upload and install Maven packages from the GitLab
|
||||
package registry, see the [Terraform modules registry documentation](../../user/packages/terraform_module_registry/index.md).
|
||||
|
||||
## List available versions for a specific module
|
||||
|
||||
Get a list of available versions for a specific module.
|
||||
|
||||
```plaintext
|
||||
GET packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/versions
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `module_namespace` | string | yes | The group to which Terraform module's project belongs. |
|
||||
| `module_name` | string | yes | The module name. |
|
||||
| `module_system` | string | yes | The name of the module system or [provider](https://www.terraform.io/registry/providers). |
|
||||
|
||||
```shell
|
||||
curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local/versions"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```shell
|
||||
{
|
||||
"modules": [
|
||||
{
|
||||
"versions": [
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"submodules": [],
|
||||
"root": {
|
||||
"dependencies": [],
|
||||
"providers": [
|
||||
{
|
||||
"name": "local",
|
||||
"version":""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.9.3",
|
||||
"submodules": [],
|
||||
"root": {
|
||||
"dependencies": [],
|
||||
"providers": [
|
||||
{
|
||||
"name": "local",
|
||||
"version":""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"source": "https://gitlab.example.com/group/hello-world"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Latest version for a specific module
|
||||
|
||||
Get information about the latest version for a given module.
|
||||
|
||||
```plaintext
|
||||
GET packages/terraform/modules/v1/:module_namespace/:module_name/:module_system
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `module_namespace` | string | yes | The group to which Terraform module's project belongs. |
|
||||
| `module_name` | string | yes | The module name. |
|
||||
| `module_system` | string | yes | The name of the module system or [provider](https://www.terraform.io/registry/providers). |
|
||||
|
||||
```shell
|
||||
curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```shell
|
||||
{
|
||||
"name": "hellow-world/local",
|
||||
"provider": "local",
|
||||
"providers": [
|
||||
"local"
|
||||
],
|
||||
"root": {
|
||||
"dependencies": []
|
||||
},
|
||||
"source": "https://gitlab.example.com/group/hello-world",
|
||||
"submodules": [],
|
||||
"version": "1.0.0",
|
||||
"versions": [
|
||||
"1.0.0"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Get specific version for a specific module
|
||||
|
||||
Get information about the latest version for a given module.
|
||||
|
||||
```plaintext
|
||||
GET packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/1.0.0
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `module_namespace` | string | yes | The group to which Terraform module's project belongs. |
|
||||
| `module_name` | string | yes | The module name. |
|
||||
| `module_system` | string | yes | The name of the module system or [provider](https://www.terraform.io/registry/providers). |
|
||||
|
||||
```shell
|
||||
curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local/1.0.0"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```shell
|
||||
{
|
||||
"name": "hellow-world/local",
|
||||
"provider": "local",
|
||||
"providers": [
|
||||
"local"
|
||||
],
|
||||
"root": {
|
||||
"dependencies": []
|
||||
},
|
||||
"source": "https://gitlab.example.com/group/hello-world",
|
||||
"submodules": [],
|
||||
"version": "1.0.0",
|
||||
"versions": [
|
||||
"1.0.0"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Get URL for downloading latest module version
|
||||
|
||||
Get the download URL for latest module version in `X-Terraform-Get` header
|
||||
|
||||
```plaintext
|
||||
GET packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/download
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `module_namespace` | string | yes | The group to which Terraform module's project belongs. |
|
||||
| `module_name` | string | yes | The module name. |
|
||||
| `module_system` | string | yes | The name of the module system or [provider](https://www.terraform.io/registry/providers). |
|
||||
|
||||
```shell
|
||||
curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local/download"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```shell
|
||||
HTTP/1.1 204 No Content
|
||||
Content-Length: 0
|
||||
X-Terraform-Get: /api/v4/packages/terraform/modules/v1/group/hello-world/local/1.0.0/file?token=&archive=tgz
|
||||
```
|
||||
|
||||
Under the hood, this API endpoint redirects to `packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/:module_version/download`
|
||||
|
||||
## Get URL for downloading specific module version
|
||||
|
||||
Get the download URL for a specific module version in `X-Terraform-Get` header
|
||||
|
||||
```plaintext
|
||||
GET packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/:module_version/download
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `module_namespace` | string | yes | The group to which Terraform module's project belongs. |
|
||||
| `module_name` | string | yes | The module name. |
|
||||
| `module_system` | string | yes | The name of the module system or [provider](https://www.terraform.io/registry/providers). |
|
||||
| `module_version` | string | yes | Specific module version to download. |
|
||||
|
||||
```shell
|
||||
curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local/1.0.0/download"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```shell
|
||||
HTTP/1.1 204 No Content
|
||||
Content-Length: 0
|
||||
X-Terraform-Get: /api/v4/packages/terraform/modules/v1/group/hello-world/local/1.0.0/file?token=&archive=tgz
|
||||
```
|
||||
|
||||
## Download module
|
||||
|
||||
```plaintext
|
||||
GET packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/:module_version/file
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `module_namespace` | string | yes | The group to which Terraform module's project belongs. |
|
||||
| `module_name` | string | yes | The module name. |
|
||||
| `module_system` | string | yes | The name of the module system or [provider](https://www.terraform.io/registry/providers). |
|
||||
| `module_version` | string | yes | Specific module version to download. |
|
||||
|
||||
```shell
|
||||
curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local/1.0.0/file"
|
||||
```
|
||||
|
||||
To write the output to file:
|
||||
|
||||
```shell
|
||||
curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/packages/terraform/modules/v1/group/hello-world/local/1.0.0/file" --output hello-world-local.tgz
|
||||
```
|
|
@ -124,6 +124,7 @@ as it can cause the pipeline to behave unexpectedly.
|
|||
| `CI_SERVER_VERSION` | all | all | The full version of the GitLab instance. |
|
||||
| `CI_SERVER` | all | all | Available for all jobs executed in CI/CD. `yes` when available. |
|
||||
| `CI_SHARED_ENVIRONMENT` | all | 10.1 | Only available if the job is executed in a shared environment (something that is persisted across CI/CD invocations, like the `shell` or `ssh` executor). `true` when available. |
|
||||
| `CI_TEMPLATE_REGISTRY_HOST` | 15.3 | all | The host of the registry used by CI/CD templates. Defaults to `registry.gitlab.com`. |
|
||||
| `GITLAB_CI` | all | all | Available for all jobs executed in CI/CD. `true` when available. |
|
||||
| `GITLAB_FEATURES` | 10.6 | all | The comma-separated list of licensed features available for the GitLab instance and license. |
|
||||
| `GITLAB_USER_EMAIL` | 8.12 | all | The email of the user who started the job. |
|
||||
|
|
|
@ -0,0 +1,437 @@
|
|||
---
|
||||
stage: Create
|
||||
group: Code Review
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Merge request widget extensions **(FREE)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44616) in GitLab 13.6.
|
||||
|
||||
Extensions in the merge request widget enable you to add new features
|
||||
into the merge request widget that match the design framework.
|
||||
With extensions we get a lot of benefits out of the box without much effort required, like:
|
||||
|
||||
- A consistent look and feel.
|
||||
- Tracking when the extension is opened.
|
||||
- Virtual scrolling for performance.
|
||||
|
||||
## Usage
|
||||
|
||||
To use extensions you must first create a new extension object to fetch the
|
||||
data to render in the extension. For a working example, refer to the example file in
|
||||
`app/assets/javascripts/vue_merge_request_widget/extensions/issues.js`.
|
||||
|
||||
The basic object structure:
|
||||
|
||||
```javascript
|
||||
export default {
|
||||
name: '', // Required: This helps identify the widget
|
||||
props: [], // Required: Props passed from the widget state
|
||||
i18n: { // Required: Object to hold i18n text
|
||||
label: '', // Required: Used for tooltips and aria-labels
|
||||
loading: '', // Required: Loading text for when data is loading
|
||||
},
|
||||
expandEvent: '', // Optional: RedisHLL event name to track expanding content
|
||||
enablePolling: false, // Optional: Tells extension to poll for data
|
||||
modalComponent: null, // Optional: The component to use for the modal
|
||||
telemetry: true, // Optional: Reports basic telemetry for the extension. Set to false to disable telemetry
|
||||
computed: {
|
||||
summary(data) {}, // Required: Level 1 summary text
|
||||
statusIcon(data) {}, // Required: Level 1 status icon
|
||||
tertiaryButtons() {}, // Optional: Level 1 action buttons
|
||||
shouldCollapse() {}, // Optional: Add logic to determine if the widget can expand or not
|
||||
},
|
||||
methods: {
|
||||
fetchCollapsedData(props) {}, // Required: Fetches data required for collapsed state
|
||||
fetchFullData(props) {}, // Required: Fetches data for the full expanded content
|
||||
fetchMultiData() {}, // Optional: Works in conjunction with `enablePolling` and allows polling multiple endpoints
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
By following the same data structure, each extension can follow the same registering structure,
|
||||
but each extension can manage its data sources.
|
||||
|
||||
After creating this structure, you must register it. You can register the extension at any
|
||||
point _after_ the widget has been created. To register a extension:
|
||||
|
||||
```javascript
|
||||
// Import the register method
|
||||
import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
|
||||
|
||||
// Import the new extension
|
||||
import issueExtension from '~/vue_merge_request_widget/extensions/issues';
|
||||
|
||||
// Register the imported extension
|
||||
registerExtension(issueExtension);
|
||||
```
|
||||
|
||||
## Data fetching
|
||||
|
||||
Each extension must fetch data. Fetching is handled when registering the extension,
|
||||
not by the core component itself. This approach allows for various different
|
||||
data fetching methods to be used, such as GraphQL or REST API calls.
|
||||
|
||||
### API calls
|
||||
|
||||
For performance reasons, it is best if the collapsed state fetches only the data required to
|
||||
render the collapsed state. This fetching happens in the `fetchCollapsedData` method.
|
||||
This method is called with the props as an argument, so you can easily access
|
||||
any paths set in the state.
|
||||
|
||||
To allow the extension to set the data, this method **must** return the data. No
|
||||
special formatting is required. When the extension receives this data,
|
||||
it is set to `collapsedData`. You can access `collapsedData` in any computed property or
|
||||
method.
|
||||
|
||||
When the user clicks **Expand**, the `fetchFullData` method is called. This method
|
||||
also gets called with the props as an argument. This method **must** also return
|
||||
the full data. However, this data must be correctly formatted to match the format
|
||||
mentioned in the data structure section.
|
||||
|
||||
#### Technical debt
|
||||
|
||||
For some of the current extensions, there is no split in data fetching. All the data
|
||||
is fetched through the `fetchCollapsedData` method. While less performant,
|
||||
it allows for faster iteration.
|
||||
|
||||
To handle this the `fetchFullData` returns the data set through
|
||||
the `fetchCollapsedData` method call. In these cases, the `fetchFullData` must
|
||||
return a promise:
|
||||
|
||||
```javascript
|
||||
fetchCollapsedData() {
|
||||
return ['Some data'];
|
||||
},
|
||||
fetchFullData() {
|
||||
return Promise.resolve(this.collapsedData)
|
||||
},
|
||||
```
|
||||
|
||||
### Data structure
|
||||
|
||||
The data returned from `fetchFullData` must match the format below. This format
|
||||
allows the core component to render the data in a way that matches
|
||||
the design framework. Any text properties can use the styling placeholders
|
||||
mentioned below:
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: data.id, // Required: ID used as a key for each row
|
||||
header: 'Header' || ['Header', 'sub-header'], // Required: String or array can be used for the header text
|
||||
text: '', // Required: Main text for the row
|
||||
subtext: '', // Optional: Smaller sub-text to be displayed below the main text
|
||||
icon: { // Optional: Icon object
|
||||
name: EXTENSION_ICONS.success, // Required: The icon name for the row
|
||||
},
|
||||
badge: { // Optional: Badge displayed after text
|
||||
text: '', // Required: Text to be displayed inside badge
|
||||
variant: '', // Optional: GitLab UI badge variant, defaults to info
|
||||
},
|
||||
link: { // Optional: Link to a URL displayed after text
|
||||
text: '', // Required: Text of the link
|
||||
href: '', // Optional: URL for the link
|
||||
},
|
||||
modal: { // Optional: Link to open a modal displayed after text
|
||||
text: '', // Required: Text of the link
|
||||
onClick: () => {} // Optional: Function to run when link is clicked, i.e. to set this.modalData
|
||||
}
|
||||
actions: [], // Optional: Action button for row
|
||||
children: [], // Optional: Child content to render, structure matches the same structure
|
||||
}
|
||||
```
|
||||
|
||||
### Polling
|
||||
|
||||
To enable polling for an extension, an options flag must be present in the extension:
|
||||
|
||||
```javascript
|
||||
export default {
|
||||
//...
|
||||
enablePolling: true
|
||||
};
|
||||
```
|
||||
|
||||
This flag tells the base component we should poll the `fetchCollapsedData()`
|
||||
defined in the extension. Polling stops if the response has data, or if an error is present.
|
||||
|
||||
When writing the logic for `fetchCollapsedData()`, a complete Axios response must be returned
|
||||
from the method. The polling utility needs data like polling headers to work correctly:
|
||||
|
||||
```javascript
|
||||
export default {
|
||||
//...
|
||||
enablePolling: true
|
||||
methods: {
|
||||
fetchCollapsedData() {
|
||||
return axios.get(this.reportPath)
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
Most of the time the data returned from the extension's endpoint is not in the format
|
||||
the UI needs. We must format the data before setting the collapsed data in the base component.
|
||||
|
||||
If the computed property `summary` can rely on `collapsedData`, you can format the data
|
||||
when `fetchFullData` is invoked:
|
||||
|
||||
```javascript
|
||||
export default {
|
||||
//...
|
||||
enablePolling: true
|
||||
methods: {
|
||||
fetchCollapsedData() {
|
||||
return axios.get(this.reportPath)
|
||||
},
|
||||
fetchFullData() {
|
||||
return Promise.resolve(this.prepareReports());
|
||||
},
|
||||
// custom method
|
||||
prepareReports() {
|
||||
// unpack values from collapsedData
|
||||
const { new_errors, existing_errors, resolved_errors } = this.collapsedData;
|
||||
|
||||
// perform data formatting
|
||||
|
||||
return [...newErrors, ...existingErrors, ...resolvedErrors]
|
||||
}
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
If the extension relies on `collapsedData` being formatted before invoking `fetchFullData()`,
|
||||
then `fetchCollapsedData()` must return the Axios response as well as the formatted data:
|
||||
|
||||
```javascript
|
||||
export default {
|
||||
//...
|
||||
enablePolling: true
|
||||
methods: {
|
||||
fetchCollapsedData() {
|
||||
return axios.get(this.reportPath).then(res => {
|
||||
const formattedData = this.prepareReports(res.data)
|
||||
|
||||
return {
|
||||
...res,
|
||||
data: formattedData,
|
||||
}
|
||||
})
|
||||
},
|
||||
// Custom method
|
||||
prepareReports() {
|
||||
// Unpack values from collapsedData
|
||||
const { new_errors, existing_errors, resolved_errors } = this.collapsedData;
|
||||
|
||||
// Perform data formatting
|
||||
|
||||
return [...newErrors, ...existingErrors, ...resolvedErrors]
|
||||
}
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
If the extension must poll multiple endpoints at the same time, then `fetchMultiData`
|
||||
can be used to return an array of functions. A new `poll` object is created for each
|
||||
endpoint and they are polled separately. After all endpoints are resolved, polling is
|
||||
stopped and `setCollapsedData` is called with an array of `response.data`.
|
||||
|
||||
```javascript
|
||||
export default {
|
||||
//...
|
||||
enablePolling: true
|
||||
methods: {
|
||||
fetchMultiData() {
|
||||
return [
|
||||
() => axios.get(this.reportPath1),
|
||||
() => axios.get(this.reportPath2),
|
||||
() => axios.get(this.reportPath3)
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
WARNING:
|
||||
The function must return a `Promise` that resolves the `response` object.
|
||||
The implementation relies on the `POLL-INTERVAL` header to keep polling, therefore it is
|
||||
important not to alter the status code and headers.
|
||||
|
||||
### Errors
|
||||
|
||||
If `fetchCollapsedData()` or `fetchFullData()` methods throw an error:
|
||||
|
||||
- The loading state of the extension is updated to `LOADING_STATES.collapsedError`
|
||||
and `LOADING_STATES.expandedError` respectively.
|
||||
- The extensions header displays an error icon and updates the text to be either:
|
||||
- The text defined in `$options.i18n.error`.
|
||||
- "Failed to load" if `$options.i18n.error` is not defined.
|
||||
- The error is sent to Sentry to log that it occurred.
|
||||
|
||||
To customise the error text, add it to the `i18n` object in your extension:
|
||||
|
||||
```javascript
|
||||
export default {
|
||||
//...
|
||||
i18n: {
|
||||
//...
|
||||
error: __('Your error text'),
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Telemetry
|
||||
|
||||
The base implementation of the widget extension framework includes some telemetry events.
|
||||
Each widget reports:
|
||||
|
||||
- `view`: When it is rendered to the screen.
|
||||
- `expand`: When it is expanded.
|
||||
- `full_report_clicked`: When an (optional) input is clicked to view the full report.
|
||||
- Outcome (`expand_success`, `expand_warning`, or `expand_failed`): One of three
|
||||
additional events relating to the status of the widget when it was expanded.
|
||||
|
||||
### Add new widgets
|
||||
|
||||
When adding new widgets, the above events must be marked as `known`, and have metrics
|
||||
created, to be reportable.
|
||||
|
||||
NOTE:
|
||||
Events that are only for EE should include `--ee` at the end of both shell commands below.
|
||||
|
||||
To generate these known events for a single widget:
|
||||
|
||||
1. Widgets should be named `Widget${CamelName}`.
|
||||
- For example: a widget for **Test Reports** should be `WidgetTestReports`.
|
||||
1. Compute the widget name slug by converting the `${CamelName}` to lower-, snake-case.
|
||||
- The previous example would be `test_reports`.
|
||||
1. Add the new widget name slug to `lib/gitlab/usage_data_counters/merge_request_widget_extension_counter.rb`
|
||||
in the `WIDGETS` list.
|
||||
1. Ensure the GDK is running (`gdk start`).
|
||||
1. Generate known events on the command line with the following command.
|
||||
Replace `test_reports` with your appropriate name slug:
|
||||
|
||||
```shell
|
||||
bundle exec rails generate gitlab:usage_metric_definition \
|
||||
counts.i_code_review_merge_request_widget_test_reports_count_view \
|
||||
counts.i_code_review_merge_request_widget_test_reports_count_full_report_clicked \
|
||||
counts.i_code_review_merge_request_widget_test_reports_count_expand \
|
||||
counts.i_code_review_merge_request_widget_test_reports_count_expand_success \
|
||||
counts.i_code_review_merge_request_widget_test_reports_count_expand_warning \
|
||||
counts.i_code_review_merge_request_widget_test_reports_count_expand_failed \
|
||||
--dir=all
|
||||
```
|
||||
|
||||
1. Modify each newly generated file to match the existing files for the merge request widget extension telemetry.
|
||||
- Find existing examples by doing a glob search, like: `metrics/**/*_i_code_review_merge_request_widget_*`
|
||||
- Roughly speaking, each file should have these values:
|
||||
1. `description` = A plain English description of this value. Review existing widget extension telemetry files for examples.
|
||||
1. `product_section` = `dev`
|
||||
1. `product_stage` = `create`
|
||||
1. `product_group` = `code_review`
|
||||
1. `product_category` = `code_review`
|
||||
1. `introduced_by_url` = `'[your MR]'`
|
||||
1. `options.events` = (the event in the command from above that generated this file, like `i_code_review_merge_request_widget_test_reports_count_view`)
|
||||
- This value is how the telemetry events are linked to "metrics" so this is probably one of the more important values.
|
||||
1. `data_source` = `redis`
|
||||
1. `data_category` = `optional`
|
||||
1. Generate known HLL events on the command line with the following command.
|
||||
Replace `test_reports` with your appropriate name slug.
|
||||
|
||||
```shell
|
||||
bundle exec rails generate gitlab:usage_metric_definition:redis_hll code_review \
|
||||
i_code_review_merge_request_widget_test_reports_view \
|
||||
i_code_review_merge_request_widget_test_reports_full_report_clicked \
|
||||
i_code_review_merge_request_widget_test_reports_expand \
|
||||
i_code_review_merge_request_widget_test_reports_expand_success \
|
||||
i_code_review_merge_request_widget_test_reports_expand_warning \
|
||||
i_code_review_merge_request_widget_test_reports_expand_failed \
|
||||
--class_name=RedisHLLMetric
|
||||
```
|
||||
|
||||
1. Repeat step 6, but change the `data_source` to `redis_hll`.
|
||||
1. Add each of the HLL metrics to `lib/gitlab/usage_data_counters/known_events/code_review_events.yml`:
|
||||
1. `name` = (the event)
|
||||
1. `redis_slot` = `code_review`
|
||||
1. `category` = `code_review`
|
||||
1. `aggregation` = `weekly`
|
||||
1. Add each event to the appropriate aggregates in `config/metrics/aggregates/code_review.yml`
|
||||
|
||||
### Add new events
|
||||
|
||||
If you are adding a new event to our known events, include the new event in the
|
||||
`KNOWN_EVENTS` list in `lib/gitlab/usage_data_counters/merge_request_widget_extension_counter.rb`.
|
||||
|
||||
## Icons
|
||||
|
||||
Level 1 and all subsequent levels can have their own status icons. To keep with
|
||||
the design framework, import the `EXTENSION_ICONS` constant
|
||||
from the `constants.js` file:
|
||||
|
||||
```javascript
|
||||
import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants.js';
|
||||
```
|
||||
|
||||
This constant has the below icons available for use. Per the design framework,
|
||||
only some of these icons should be used on level 1:
|
||||
|
||||
- `failed`
|
||||
- `warning`
|
||||
- `success`
|
||||
- `neutral`
|
||||
- `error`
|
||||
- `notice`
|
||||
- `severityCritical`
|
||||
- `severityHigh`
|
||||
- `severityMedium`
|
||||
- `severityLow`
|
||||
- `severityInfo`
|
||||
- `severityUnknown`
|
||||
|
||||
## Text styling
|
||||
|
||||
Any area that has text can be styled with the placeholders below. This
|
||||
technique follows the same technique as `sprintf`. However, instead of specifying
|
||||
these through `sprintf`, the extension does this automatically.
|
||||
|
||||
Every placeholder contains starting and ending tags. For example, `success` uses
|
||||
`Hello %{success_start}world%{success_end}`. The extension then
|
||||
adds the start and end tags with the correct styling classes.
|
||||
|
||||
| Placeholder | Style |
|
||||
|-------------|-----------------------------------------|
|
||||
| success | `gl-font-weight-bold gl-text-green-500` |
|
||||
| danger | `gl-font-weight-bold gl-text-red-500` |
|
||||
| critical | `gl-font-weight-bold gl-text-red-800` |
|
||||
| same | `gl-font-weight-bold gl-text-gray-700` |
|
||||
| strong | `gl-font-weight-bold` |
|
||||
| small | `gl-font-sm` |
|
||||
|
||||
## Action buttons
|
||||
|
||||
You can add action buttons to all level 1 and 2 in each extension. These buttons
|
||||
are meant as a way to provide links or actions for each row:
|
||||
|
||||
- Action buttons for level 1 can be set through the `tertiaryButtons` computed property.
|
||||
This property should return an array of objects for each action button.
|
||||
- Action buttons for level 2 can be set by adding the `actions` key to the level 2 rows object.
|
||||
The value for this key must also be an array of objects for each action button.
|
||||
|
||||
Links must follow this structure:
|
||||
|
||||
```javascript
|
||||
{
|
||||
text: 'Click me',
|
||||
href: this.someLinkHref,
|
||||
target: '_blank', // Optional
|
||||
}
|
||||
```
|
||||
|
||||
For internal action buttons, follow this structure:
|
||||
|
||||
```javascript
|
||||
{
|
||||
text: 'Click me',
|
||||
onClick() {}
|
||||
}
|
||||
```
|
|
@ -8,6 +8,21 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
Performance is an essential part and one of the main areas of concern for any modern application.
|
||||
|
||||
## Monitoring
|
||||
|
||||
We have a performance dashboard available in one of our [Grafana instances](https://dashboards.gitlab.net/d/000000043/sitespeed-page-summary?orgId=1). This dashboard automatically aggregates metric data from [sitespeed.io](https://www.sitespeed.io/) every 4 hours. These changes are displayed after a set number of pages are aggregated.
|
||||
|
||||
These pages can be found inside text files in the [`sitespeed-measurement-setup` repository](https://gitlab.com/gitlab-org/frontend/sitespeed-measurement-setup) called [`gitlab`](https://gitlab.com/gitlab-org/frontend/sitespeed-measurement-setup/-/tree/master/gitlab)
|
||||
Any frontend engineer can contribute to this dashboard. They can contribute by adding or removing URLs of pages to the text files. The changes are pushed live on the next scheduled run after the changes are merged into `main`.
|
||||
|
||||
There are 3 recommended high impact metrics (core web vitals) to review on each page:
|
||||
|
||||
- [Largest Contentful Paint](https://web.dev/lcp/)
|
||||
- [First Input Delay](https://web.dev/fid/)
|
||||
- [Cumulative Layout Shift](https://web.dev/cls/)
|
||||
|
||||
For these metrics, lower numbers are better as it means that the website is more performant.
|
||||
|
||||
## User Timing API
|
||||
|
||||
[User Timing API](https://developer.mozilla.org/en-US/docs/Web/API/User_Timing_API) is a web API
|
||||
|
|
|
@ -144,4 +144,4 @@ methods: {
|
|||
|
||||
## Merge request widgets
|
||||
|
||||
Refer to the documentation specific to the [merge request widget extension framework](../new_fe_guide/modules/widget_extensions.md).
|
||||
Refer to the documentation specific to the [merge request widget extension framework](merge_request_widget_extensions.md).
|
||||
|
|
|
@ -18,7 +18,7 @@ The merge request is made up of several different key components and ideas that
|
|||
1. Approval rules
|
||||
|
||||
When developing new merge request widgets, read the
|
||||
[merge request widget extension framework](../new_fe_guide/modules/widget_extensions.md)
|
||||
[merge request widget extension framework](../fe_guide/merge_request_widget_extensions.md)
|
||||
documentation. All new widgets should use this framework, and older widgets should
|
||||
be ported to use it.
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
---
|
||||
redirect_to: '../new_fe_guide/modules/widget_extensions.md'
|
||||
redirect_to: '../fe_guide/merge_request_widget_extensions.md'
|
||||
remove_date: '2022-10-27'
|
||||
---
|
||||
|
||||
This document was moved to [another location](../new_fe_guide/modules/widget_extensions.md).
|
||||
This document was moved to [another location](../fe_guide/merge_request_widget_extensions.md).
|
||||
|
||||
<!-- This redirect file can be deleted after <2022-10-27>. -->
|
||||
<!-- Redirects that point to other docs in the same project expire in three months. -->
|
||||
|
|
|
@ -1,52 +1,11 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
redirect_to: '../../fe_guide/accessibility.md'
|
||||
remove_date: '2022-11-15'
|
||||
---
|
||||
|
||||
# Accessibility
|
||||
This document was moved to [another location](../../fe_guide/accessibility.md).
|
||||
|
||||
Using semantic HTML plays a key role when it comes to accessibility.
|
||||
|
||||
## Accessible Rich Internet Applications - ARIA
|
||||
|
||||
WAI-ARIA (the Accessible Rich Internet Applications specification) defines a way to make Web content and Web applications more accessible to people with disabilities.
|
||||
|
||||
The W3C recommends [using semantic elements](https://www.w3.org/TR/using-aria/#notes2) as the primary method to achieve accessibility rather than adding aria attributes. Adding aria attributes should be seen as a secondary method for creating accessible elements.
|
||||
|
||||
### Role
|
||||
|
||||
The `role` attribute describes the role the element plays in the context of the document.
|
||||
|
||||
Review the list of [WAI-ARIA roles](https://www.w3.org/TR/wai-aria-1.1/#landmark_roles).
|
||||
|
||||
## Icons
|
||||
|
||||
When using icons or images that aren't absolutely needed to understand the context, we should use `aria-hidden="true"`.
|
||||
|
||||
On the other hand, if an icon is crucial to understand the context we should do one of the following:
|
||||
|
||||
1. Use `aria-label` in the element with a meaningful description
|
||||
1. Use `aria-labelledby` to point to an element that contains the explanation for that icon
|
||||
|
||||
## Form inputs
|
||||
|
||||
In forms we should use the `for` attribute in the label statement:
|
||||
|
||||
```html
|
||||
<div>
|
||||
<label for="name">Fill in your name:</label>
|
||||
<input type="text" id="name" name="name">
|
||||
</div>
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
1. On MacOS you can use [VoiceOver](https://www.apple.com/accessibility/vision/) by pressing `cmd+F5`.
|
||||
1. On Windows you can use [Narrator](https://www.microsoft.com/en-us/accessibility/windows) by pressing Windows logo key + Control + Enter.
|
||||
|
||||
## Online resources
|
||||
|
||||
- [Chrome Accessibility Developer Tools](https://github.com/GoogleChrome/accessibility-developer-tools) for testing accessibility
|
||||
- [Audit Rules Page](https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules) for best practices
|
||||
- [Lighthouse Accessibility Score](https://web.dev/performance-scoring/) for accessibility audits
|
||||
<!-- This redirect file can be deleted after <2022-11-15>. -->
|
||||
<!-- Redirects that point to other docs in the same project expire in three months. -->
|
||||
<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
|
||||
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->
|
||||
|
|
|
@ -1,27 +1,11 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
redirect_to: '../../fe_guide/index.md'
|
||||
remove_date: '2022-11-15'
|
||||
---
|
||||
|
||||
# Components
|
||||
This document was moved to [another location](../../fe_guide/index.md).
|
||||
|
||||
## Graphs
|
||||
|
||||
We have a lot of graphing libraries in our codebase to render graphs. In an effort to improve maintainability, new graphs should use [D3.js](https://d3js.org/). If a new graph is fairly simple, consider implementing it in SVGs or HTML5 canvas.
|
||||
|
||||
We chose D3 as our library going forward because of the following features:
|
||||
|
||||
- [Tree shaking webpack capabilities](https://github.com/d3/d3/blob/master/CHANGES.md#changes-in-d3-40).
|
||||
- [Compatible with vue.js as well as vanilla JavaScript](https://github.com/d3/d3/blob/master/CHANGES.md#changes-in-d3-40).
|
||||
|
||||
D3 is very popular across many projects outside of GitLab:
|
||||
|
||||
- [The New York Times](https://archive.nytimes.com/www.nytimes.com/interactive/2012/02/13/us/politics/2013-budget-proposal-graphic.html)
|
||||
- [plot.ly](https://plotly.com/)
|
||||
- [Ayoa](https://www.ayoa.com/previously-droptask/)
|
||||
|
||||
Within GitLab, D3 has been used for the following notable features
|
||||
|
||||
- [Prometheus graphs](../../../user/project/integrations/prometheus.md)
|
||||
- Contribution calendars
|
||||
<!-- This redirect file can be deleted after <2022-11-15>. -->
|
||||
<!-- Redirects that point to other docs in the same project expire in three months. -->
|
||||
<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
|
||||
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->
|
||||
|
|
|
@ -1,23 +1,11 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
redirect_to: '../../fe_guide/index.md'
|
||||
remove_date: '2022-11-15'
|
||||
---
|
||||
|
||||
# Development
|
||||
This document was moved to [another location](../../fe_guide/index.md).
|
||||
|
||||
## [Components](components.md)
|
||||
|
||||
Documentation on existing components and how to best create a new component.
|
||||
|
||||
## [Accessibility](accessibility.md)
|
||||
|
||||
Learn how to implement an accessible frontend.
|
||||
|
||||
## [Performance](performance.md)
|
||||
|
||||
Learn how to keep our frontend performant.
|
||||
|
||||
## [Testing](../../testing_guide/frontend_testing.md)
|
||||
|
||||
Learn how to keep our frontend tested.
|
||||
<!-- This redirect file can be deleted after <2022-11-15>. -->
|
||||
<!-- Redirects that point to other docs in the same project expire in three months. -->
|
||||
<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
|
||||
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->
|
||||
|
|
|
@ -1,22 +1,11 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
redirect_to: '../../fe_guide/performance.md'
|
||||
remove_date: '2022-11-15'
|
||||
---
|
||||
|
||||
# Performance
|
||||
This document was moved to [another location](../../fe_guide/performance.md).
|
||||
|
||||
## Monitoring
|
||||
|
||||
We have a performance dashboard available in one of our [Grafana instances](https://dashboards.gitlab.net/d/000000043/sitespeed-page-summary?orgId=1). This dashboard automatically aggregates metric data from [sitespeed.io](https://www.sitespeed.io/) every 4 hours. These changes are displayed after a set number of pages are aggregated.
|
||||
|
||||
These pages can be found inside text files in the [`sitespeed-measurement-setup` repository](https://gitlab.com/gitlab-org/frontend/sitespeed-measurement-setup) called [`gitlab`](https://gitlab.com/gitlab-org/frontend/sitespeed-measurement-setup/-/tree/master/gitlab)
|
||||
Any frontend engineer can contribute to this dashboard. They can contribute by adding or removing URLs of pages to the text files. The changes are pushed live on the next scheduled run after the changes are merged into `main`.
|
||||
|
||||
There are 3 recommended high impact metrics (core web vitals) to review on each page:
|
||||
|
||||
- [Largest Contentful Paint](https://web.dev/lcp/)
|
||||
- [First Input Delay](https://web.dev/fid/)
|
||||
- [Cumulative Layout Shift](https://web.dev/cls/)
|
||||
|
||||
For these metrics, lower numbers are better as it means that the website is more performant.
|
||||
<!-- This redirect file can be deleted after <2022-11-15>. -->
|
||||
<!-- Redirects that point to other docs in the same project expire in three months. -->
|
||||
<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
|
||||
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->
|
||||
|
|
|
@ -1,22 +1,11 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
redirect_to: '../fe_guide/index.md'
|
||||
remove_date: '2022-11-15'
|
||||
---
|
||||
|
||||
# Frontend Development Guidelines
|
||||
This document was moved to [another location](../fe_guide/index.md).
|
||||
|
||||
This guide contains all the information to successfully contribute to the GitLab frontend.
|
||||
This is a living document, and we welcome contributions, feedback, and suggestions.
|
||||
|
||||
## [Development](development/index.md)
|
||||
|
||||
Guidance on topics related to development.
|
||||
|
||||
## [Modules](modules/index.md)
|
||||
|
||||
Learn about all the internal JavaScript modules that make up our frontend.
|
||||
|
||||
## [Tips](tips.md)
|
||||
|
||||
Tips from our frontend team to develop more efficiently and effectively.
|
||||
<!-- This redirect file can be deleted after <2022-11-15>. -->
|
||||
<!-- Redirects that point to other docs in the same project expire in three months. -->
|
||||
<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
|
||||
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->
|
||||
|
|
|
@ -1,28 +1,11 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
redirect_to: '../../fe_guide/index.md'
|
||||
remove_date: '2022-11-15'
|
||||
---
|
||||
|
||||
# Dirty Submit
|
||||
This document was moved to [another location](../../fe_guide/index.md).
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/21115) in GitLab 11.3.
|
||||
|
||||
## Summary
|
||||
|
||||
Prevent submitting forms with no changes.
|
||||
|
||||
Currently handles `input`, `textarea` and `select` elements.
|
||||
|
||||
Also, see [the code](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/dirty_submit/)
|
||||
within the GitLab project.
|
||||
|
||||
## Usage
|
||||
|
||||
```javascript
|
||||
import dirtySubmitFactory from './dirty_submit/dirty_submit_form';
|
||||
|
||||
new DirtySubmitForm(document.querySelector('form'));
|
||||
// or
|
||||
new DirtySubmitForm(document.querySelectorAll('form'));
|
||||
```
|
||||
<!-- This redirect file can be deleted after <2022-11-15>. -->
|
||||
<!-- Redirects that point to other docs in the same project expire in three months. -->
|
||||
<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
|
||||
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->
|
||||
|
|
|
@ -1,15 +1,11 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
redirect_to: '../../fe_guide/index.md'
|
||||
remove_date: '2022-11-15'
|
||||
---
|
||||
|
||||
# Modules
|
||||
This document was moved to [another location](../../fe_guide/index.md).
|
||||
|
||||
- [DirtySubmit](dirty_submit.md)
|
||||
|
||||
Disable form submits until there are unsaved changes.
|
||||
|
||||
- [Merge Request widget extensions](widget_extensions.md)
|
||||
|
||||
Easily add extensions into the merge request widget
|
||||
<!-- This redirect file can be deleted after <2022-11-15>. -->
|
||||
<!-- Redirects that point to other docs in the same project expire in three months. -->
|
||||
<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
|
||||
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->
|
||||
|
|
|
@ -1,437 +1,11 @@
|
|||
---
|
||||
stage: Create
|
||||
group: Code Review
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
redirect_to: '../../fe_guide/merge_request_widget_extensions.md'
|
||||
remove_date: '2022-11-15'
|
||||
---
|
||||
|
||||
# Merge request widget extensions **(FREE)**
|
||||
This document was moved to [another location](../../fe_guide/merge_request_widget_extensions.md).
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44616) in GitLab 13.6.
|
||||
|
||||
Extensions in the merge request widget enable you to add new features
|
||||
into the merge request widget that match the design framework.
|
||||
With extensions we get a lot of benefits out of the box without much effort required, like:
|
||||
|
||||
- A consistent look and feel.
|
||||
- Tracking when the extension is opened.
|
||||
- Virtual scrolling for performance.
|
||||
|
||||
## Usage
|
||||
|
||||
To use extensions you must first create a new extension object to fetch the
|
||||
data to render in the extension. For a working example, refer to the example file in
|
||||
`app/assets/javascripts/vue_merge_request_widget/extensions/issues.js`.
|
||||
|
||||
The basic object structure:
|
||||
|
||||
```javascript
|
||||
export default {
|
||||
name: '', // Required: This helps identify the widget
|
||||
props: [], // Required: Props passed from the widget state
|
||||
i18n: { // Required: Object to hold i18n text
|
||||
label: '', // Required: Used for tooltips and aria-labels
|
||||
loading: '', // Required: Loading text for when data is loading
|
||||
},
|
||||
expandEvent: '', // Optional: RedisHLL event name to track expanding content
|
||||
enablePolling: false, // Optional: Tells extension to poll for data
|
||||
modalComponent: null, // Optional: The component to use for the modal
|
||||
telemetry: true, // Optional: Reports basic telemetry for the extension. Set to false to disable telemetry
|
||||
computed: {
|
||||
summary(data) {}, // Required: Level 1 summary text
|
||||
statusIcon(data) {}, // Required: Level 1 status icon
|
||||
tertiaryButtons() {}, // Optional: Level 1 action buttons
|
||||
shouldCollapse() {}, // Optional: Add logic to determine if the widget can expand or not
|
||||
},
|
||||
methods: {
|
||||
fetchCollapsedData(props) {}, // Required: Fetches data required for collapsed state
|
||||
fetchFullData(props) {}, // Required: Fetches data for the full expanded content
|
||||
fetchMultiData() {}, // Optional: Works in conjunction with `enablePolling` and allows polling multiple endpoints
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
By following the same data structure, each extension can follow the same registering structure,
|
||||
but each extension can manage its data sources.
|
||||
|
||||
After creating this structure, you must register it. You can register the extension at any
|
||||
point _after_ the widget has been created. To register a extension:
|
||||
|
||||
```javascript
|
||||
// Import the register method
|
||||
import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
|
||||
|
||||
// Import the new extension
|
||||
import issueExtension from '~/vue_merge_request_widget/extensions/issues';
|
||||
|
||||
// Register the imported extension
|
||||
registerExtension(issueExtension);
|
||||
```
|
||||
|
||||
## Data fetching
|
||||
|
||||
Each extension must fetch data. Fetching is handled when registering the extension,
|
||||
not by the core component itself. This approach allows for various different
|
||||
data fetching methods to be used, such as GraphQL or REST API calls.
|
||||
|
||||
### API calls
|
||||
|
||||
For performance reasons, it is best if the collapsed state fetches only the data required to
|
||||
render the collapsed state. This fetching happens in the `fetchCollapsedData` method.
|
||||
This method is called with the props as an argument, so you can easily access
|
||||
any paths set in the state.
|
||||
|
||||
To allow the extension to set the data, this method **must** return the data. No
|
||||
special formatting is required. When the extension receives this data,
|
||||
it is set to `collapsedData`. You can access `collapsedData` in any computed property or
|
||||
method.
|
||||
|
||||
When the user clicks **Expand**, the `fetchFullData` method is called. This method
|
||||
also gets called with the props as an argument. This method **must** also return
|
||||
the full data. However, this data must be correctly formatted to match the format
|
||||
mentioned in the data structure section.
|
||||
|
||||
#### Technical debt
|
||||
|
||||
For some of the current extensions, there is no split in data fetching. All the data
|
||||
is fetched through the `fetchCollapsedData` method. While less performant,
|
||||
it allows for faster iteration.
|
||||
|
||||
To handle this the `fetchFullData` returns the data set through
|
||||
the `fetchCollapsedData` method call. In these cases, the `fetchFullData` must
|
||||
return a promise:
|
||||
|
||||
```javascript
|
||||
fetchCollapsedData() {
|
||||
return ['Some data'];
|
||||
},
|
||||
fetchFullData() {
|
||||
return Promise.resolve(this.collapsedData)
|
||||
},
|
||||
```
|
||||
|
||||
### Data structure
|
||||
|
||||
The data returned from `fetchFullData` must match the format below. This format
|
||||
allows the core component to render the data in a way that matches
|
||||
the design framework. Any text properties can use the styling placeholders
|
||||
mentioned below:
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: data.id, // Required: ID used as a key for each row
|
||||
header: 'Header' || ['Header', 'sub-header'], // Required: String or array can be used for the header text
|
||||
text: '', // Required: Main text for the row
|
||||
subtext: '', // Optional: Smaller sub-text to be displayed below the main text
|
||||
icon: { // Optional: Icon object
|
||||
name: EXTENSION_ICONS.success, // Required: The icon name for the row
|
||||
},
|
||||
badge: { // Optional: Badge displayed after text
|
||||
text: '', // Required: Text to be displayed inside badge
|
||||
variant: '', // Optional: GitLab UI badge variant, defaults to info
|
||||
},
|
||||
link: { // Optional: Link to a URL displayed after text
|
||||
text: '', // Required: Text of the link
|
||||
href: '', // Optional: URL for the link
|
||||
},
|
||||
modal: { // Optional: Link to open a modal displayed after text
|
||||
text: '', // Required: Text of the link
|
||||
onClick: () => {} // Optional: Function to run when link is clicked, i.e. to set this.modalData
|
||||
}
|
||||
actions: [], // Optional: Action button for row
|
||||
children: [], // Optional: Child content to render, structure matches the same structure
|
||||
}
|
||||
```
|
||||
|
||||
### Polling
|
||||
|
||||
To enable polling for an extension, an options flag must be present in the extension:
|
||||
|
||||
```javascript
|
||||
export default {
|
||||
//...
|
||||
enablePolling: true
|
||||
};
|
||||
```
|
||||
|
||||
This flag tells the base component we should poll the `fetchCollapsedData()`
|
||||
defined in the extension. Polling stops if the response has data, or if an error is present.
|
||||
|
||||
When writing the logic for `fetchCollapsedData()`, a complete Axios response must be returned
|
||||
from the method. The polling utility needs data like polling headers to work correctly:
|
||||
|
||||
```javascript
|
||||
export default {
|
||||
//...
|
||||
enablePolling: true
|
||||
methods: {
|
||||
fetchCollapsedData() {
|
||||
return axios.get(this.reportPath)
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
Most of the time the data returned from the extension's endpoint is not in the format
|
||||
the UI needs. We must format the data before setting the collapsed data in the base component.
|
||||
|
||||
If the computed property `summary` can rely on `collapsedData`, you can format the data
|
||||
when `fetchFullData` is invoked:
|
||||
|
||||
```javascript
|
||||
export default {
|
||||
//...
|
||||
enablePolling: true
|
||||
methods: {
|
||||
fetchCollapsedData() {
|
||||
return axios.get(this.reportPath)
|
||||
},
|
||||
fetchFullData() {
|
||||
return Promise.resolve(this.prepareReports());
|
||||
},
|
||||
// custom method
|
||||
prepareReports() {
|
||||
// unpack values from collapsedData
|
||||
const { new_errors, existing_errors, resolved_errors } = this.collapsedData;
|
||||
|
||||
// perform data formatting
|
||||
|
||||
return [...newErrors, ...existingErrors, ...resolvedErrors]
|
||||
}
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
If the extension relies on `collapsedData` being formatted before invoking `fetchFullData()`,
|
||||
then `fetchCollapsedData()` must return the Axios response as well as the formatted data:
|
||||
|
||||
```javascript
|
||||
export default {
|
||||
//...
|
||||
enablePolling: true
|
||||
methods: {
|
||||
fetchCollapsedData() {
|
||||
return axios.get(this.reportPath).then(res => {
|
||||
const formattedData = this.prepareReports(res.data)
|
||||
|
||||
return {
|
||||
...res,
|
||||
data: formattedData,
|
||||
}
|
||||
})
|
||||
},
|
||||
// Custom method
|
||||
prepareReports() {
|
||||
// Unpack values from collapsedData
|
||||
const { new_errors, existing_errors, resolved_errors } = this.collapsedData;
|
||||
|
||||
// Perform data formatting
|
||||
|
||||
return [...newErrors, ...existingErrors, ...resolvedErrors]
|
||||
}
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
If the extension must poll multiple endpoints at the same time, then `fetchMultiData`
|
||||
can be used to return an array of functions. A new `poll` object is created for each
|
||||
endpoint and they are polled separately. After all endpoints are resolved, polling is
|
||||
stopped and `setCollapsedData` is called with an array of `response.data`.
|
||||
|
||||
```javascript
|
||||
export default {
|
||||
//...
|
||||
enablePolling: true
|
||||
methods: {
|
||||
fetchMultiData() {
|
||||
return [
|
||||
() => axios.get(this.reportPath1),
|
||||
() => axios.get(this.reportPath2),
|
||||
() => axios.get(this.reportPath3)
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
WARNING:
|
||||
The function must return a `Promise` that resolves the `response` object.
|
||||
The implementation relies on the `POLL-INTERVAL` header to keep polling, therefore it is
|
||||
important not to alter the status code and headers.
|
||||
|
||||
### Errors
|
||||
|
||||
If `fetchCollapsedData()` or `fetchFullData()` methods throw an error:
|
||||
|
||||
- The loading state of the extension is updated to `LOADING_STATES.collapsedError`
|
||||
and `LOADING_STATES.expandedError` respectively.
|
||||
- The extensions header displays an error icon and updates the text to be either:
|
||||
- The text defined in `$options.i18n.error`.
|
||||
- "Failed to load" if `$options.i18n.error` is not defined.
|
||||
- The error is sent to Sentry to log that it occurred.
|
||||
|
||||
To customise the error text, add it to the `i18n` object in your extension:
|
||||
|
||||
```javascript
|
||||
export default {
|
||||
//...
|
||||
i18n: {
|
||||
//...
|
||||
error: __('Your error text'),
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Telemetry
|
||||
|
||||
The base implementation of the widget extension framework includes some telemetry events.
|
||||
Each widget reports:
|
||||
|
||||
- `view`: When it is rendered to the screen.
|
||||
- `expand`: When it is expanded.
|
||||
- `full_report_clicked`: When an (optional) input is clicked to view the full report.
|
||||
- Outcome (`expand_success`, `expand_warning`, or `expand_failed`): One of three
|
||||
additional events relating to the status of the widget when it was expanded.
|
||||
|
||||
### Add new widgets
|
||||
|
||||
When adding new widgets, the above events must be marked as `known`, and have metrics
|
||||
created, to be reportable.
|
||||
|
||||
NOTE:
|
||||
Events that are only for EE should include `--ee` at the end of both shell commands below.
|
||||
|
||||
To generate these known events for a single widget:
|
||||
|
||||
1. Widgets should be named `Widget${CamelName}`.
|
||||
- For example: a widget for **Test Reports** should be `WidgetTestReports`.
|
||||
1. Compute the widget name slug by converting the `${CamelName}` to lower-, snake-case.
|
||||
- The previous example would be `test_reports`.
|
||||
1. Add the new widget name slug to `lib/gitlab/usage_data_counters/merge_request_widget_extension_counter.rb`
|
||||
in the `WIDGETS` list.
|
||||
1. Ensure the GDK is running (`gdk start`).
|
||||
1. Generate known events on the command line with the following command.
|
||||
Replace `test_reports` with your appropriate name slug:
|
||||
|
||||
```shell
|
||||
bundle exec rails generate gitlab:usage_metric_definition \
|
||||
counts.i_code_review_merge_request_widget_test_reports_count_view \
|
||||
counts.i_code_review_merge_request_widget_test_reports_count_full_report_clicked \
|
||||
counts.i_code_review_merge_request_widget_test_reports_count_expand \
|
||||
counts.i_code_review_merge_request_widget_test_reports_count_expand_success \
|
||||
counts.i_code_review_merge_request_widget_test_reports_count_expand_warning \
|
||||
counts.i_code_review_merge_request_widget_test_reports_count_expand_failed \
|
||||
--dir=all
|
||||
```
|
||||
|
||||
1. Modify each newly generated file to match the existing files for the merge request widget extension telemetry.
|
||||
- Find existing examples by doing a glob search, like: `metrics/**/*_i_code_review_merge_request_widget_*`
|
||||
- Roughly speaking, each file should have these values:
|
||||
1. `description` = A plain English description of this value. Review existing widget extension telemetry files for examples.
|
||||
1. `product_section` = `dev`
|
||||
1. `product_stage` = `create`
|
||||
1. `product_group` = `code_review`
|
||||
1. `product_category` = `code_review`
|
||||
1. `introduced_by_url` = `'[your MR]'`
|
||||
1. `options.events` = (the event in the command from above that generated this file, like `i_code_review_merge_request_widget_test_reports_count_view`)
|
||||
- This value is how the telemetry events are linked to "metrics" so this is probably one of the more important values.
|
||||
1. `data_source` = `redis`
|
||||
1. `data_category` = `optional`
|
||||
1. Generate known HLL events on the command line with the following command.
|
||||
Replace `test_reports` with your appropriate name slug.
|
||||
|
||||
```shell
|
||||
bundle exec rails generate gitlab:usage_metric_definition:redis_hll code_review \
|
||||
i_code_review_merge_request_widget_test_reports_view \
|
||||
i_code_review_merge_request_widget_test_reports_full_report_clicked \
|
||||
i_code_review_merge_request_widget_test_reports_expand \
|
||||
i_code_review_merge_request_widget_test_reports_expand_success \
|
||||
i_code_review_merge_request_widget_test_reports_expand_warning \
|
||||
i_code_review_merge_request_widget_test_reports_expand_failed \
|
||||
--class_name=RedisHLLMetric
|
||||
```
|
||||
|
||||
1. Repeat step 6, but change the `data_source` to `redis_hll`.
|
||||
1. Add each of the HLL metrics to `lib/gitlab/usage_data_counters/known_events/code_review_events.yml`:
|
||||
1. `name` = (the event)
|
||||
1. `redis_slot` = `code_review`
|
||||
1. `category` = `code_review`
|
||||
1. `aggregation` = `weekly`
|
||||
1. Add each event to the appropriate aggregates in `config/metrics/aggregates/code_review.yml`
|
||||
|
||||
### Add new events
|
||||
|
||||
If you are adding a new event to our known events, include the new event in the
|
||||
`KNOWN_EVENTS` list in `lib/gitlab/usage_data_counters/merge_request_widget_extension_counter.rb`.
|
||||
|
||||
## Icons
|
||||
|
||||
Level 1 and all subsequent levels can have their own status icons. To keep with
|
||||
the design framework, import the `EXTENSION_ICONS` constant
|
||||
from the `constants.js` file:
|
||||
|
||||
```javascript
|
||||
import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants.js';
|
||||
```
|
||||
|
||||
This constant has the below icons available for use. Per the design framework,
|
||||
only some of these icons should be used on level 1:
|
||||
|
||||
- `failed`
|
||||
- `warning`
|
||||
- `success`
|
||||
- `neutral`
|
||||
- `error`
|
||||
- `notice`
|
||||
- `severityCritical`
|
||||
- `severityHigh`
|
||||
- `severityMedium`
|
||||
- `severityLow`
|
||||
- `severityInfo`
|
||||
- `severityUnknown`
|
||||
|
||||
## Text styling
|
||||
|
||||
Any area that has text can be styled with the placeholders below. This
|
||||
technique follows the same technique as `sprintf`. However, instead of specifying
|
||||
these through `sprintf`, the extension does this automatically.
|
||||
|
||||
Every placeholder contains starting and ending tags. For example, `success` uses
|
||||
`Hello %{success_start}world%{success_end}`. The extension then
|
||||
adds the start and end tags with the correct styling classes.
|
||||
|
||||
| Placeholder | Style |
|
||||
|-------------|-----------------------------------------|
|
||||
| success | `gl-font-weight-bold gl-text-green-500` |
|
||||
| danger | `gl-font-weight-bold gl-text-red-500` |
|
||||
| critical | `gl-font-weight-bold gl-text-red-800` |
|
||||
| same | `gl-font-weight-bold gl-text-gray-700` |
|
||||
| strong | `gl-font-weight-bold` |
|
||||
| small | `gl-font-sm` |
|
||||
|
||||
## Action buttons
|
||||
|
||||
You can add action buttons to all level 1 and 2 in each extension. These buttons
|
||||
are meant as a way to provide links or actions for each row:
|
||||
|
||||
- Action buttons for level 1 can be set through the `tertiaryButtons` computed property.
|
||||
This property should return an array of objects for each action button.
|
||||
- Action buttons for level 2 can be set by adding the `actions` key to the level 2 rows object.
|
||||
The value for this key must also be an array of objects for each action button.
|
||||
|
||||
Links must follow this structure:
|
||||
|
||||
```javascript
|
||||
{
|
||||
text: 'Click me',
|
||||
href: this.someLinkHref,
|
||||
target: '_blank', // Optional
|
||||
}
|
||||
```
|
||||
|
||||
For internal action buttons, follow this structure:
|
||||
|
||||
```javascript
|
||||
{
|
||||
text: 'Click me',
|
||||
onClick() {}
|
||||
}
|
||||
```
|
||||
<!-- This redirect file can be deleted after <2022-11-15>. -->
|
||||
<!-- Redirects that point to other docs in the same project expire in three months. -->
|
||||
<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
|
||||
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->
|
||||
|
|
|
@ -1,35 +1,11 @@
|
|||
---
|
||||
stage: none
|
||||
group: Development
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
redirect_to: '../fe_guide/index.md'
|
||||
remove_date: '2022-11-15'
|
||||
---
|
||||
|
||||
# Tips
|
||||
This document was moved to [another location](../fe_guide/index.md).
|
||||
|
||||
## Clearing production compiled assets
|
||||
|
||||
To clear production compiled assets created with `yarn webpack-prod` you can run:
|
||||
|
||||
```shell
|
||||
yarn clean
|
||||
```
|
||||
|
||||
## Creating feature flags in development
|
||||
|
||||
The process for creating a feature flag is the same as [enabling a feature flag in development](../feature_flags/index.md#enabling-a-feature-flag-locally-in-development).
|
||||
|
||||
Your feature flag can now be:
|
||||
|
||||
- [Made available to the frontend](../feature_flags/index.md#frontend) via the `gon`
|
||||
- Queried in [tests](../feature_flags/index.md#feature-flags-in-tests)
|
||||
- Queried in HAML templates and Ruby files via the `Feature.enabled?(:my_shiny_new_feature_flag)` method
|
||||
|
||||
### More on feature flags
|
||||
|
||||
- [Deleting a feature flag](../../api/features.md#delete-a-feature)
|
||||
- [Manage feature flags](https://about.gitlab.com/handbook/product-development-flow/feature-flag-lifecycle/)
|
||||
- [Feature flags API](../../api/features.md)
|
||||
|
||||
## Running tests locally
|
||||
|
||||
This can be done as outlined by the [frontend testing guide](../testing_guide/frontend_testing.md#running-frontend-tests).
|
||||
<!-- This redirect file can be deleted after <2022-11-15>. -->
|
||||
<!-- Redirects that point to other docs in the same project expire in three months. -->
|
||||
<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
|
||||
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->
|
||||
|
|
|
@ -24,8 +24,7 @@ consistent performance of GitLab. Refer to the [Index](#performance-documentatio
|
|||
- [Troubleshooting import/export performance issues](../development/import_export.md#troubleshooting-performance-issues)
|
||||
- [Pipelines performance in the `gitlab` project](../development/pipelines.md#performance)
|
||||
- Frontend:
|
||||
- [Performance guidelines](../development/fe_guide/performance.md)
|
||||
- [Performance dashboards and monitoring guidelines](../development/new_fe_guide/development/performance.md)
|
||||
- [Performance guidelines and monitoring](../development/fe_guide/performance.md)
|
||||
- [Browser performance testing guidelines](../ci/testing/browser_performance_testing.md)
|
||||
- [`gdk measure` and `gdk measure-workflow`](https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/main/doc/gdk_commands.md#measure-performance)
|
||||
- QA:
|
||||
|
|
|
@ -146,6 +146,7 @@ module.exports = (path, options = {}) => {
|
|||
'monaco-yaml',
|
||||
'fast-mersenne-twister',
|
||||
'prosemirror-markdown',
|
||||
'marked',
|
||||
'fault',
|
||||
'dateformat',
|
||||
'lowlight',
|
||||
|
|
|
@ -44,6 +44,7 @@ module Gitlab
|
|||
store.subscribe ::Pages::InvalidateDomainCacheWorker, to: ::Projects::ProjectArchivedEvent
|
||||
store.subscribe ::Pages::InvalidateDomainCacheWorker, to: ::Projects::ProjectTransferedEvent
|
||||
store.subscribe ::Pages::InvalidateDomainCacheWorker, to: ::Groups::GroupTransferedEvent
|
||||
store.subscribe ::Pages::InvalidateDomainCacheWorker, to: ::Groups::GroupPathChangedEvent
|
||||
|
||||
store.subscribe ::MergeRequests::CreateApprovalEventWorker, to: ::MergeRequests::ApprovedEvent
|
||||
store.subscribe ::MergeRequests::CreateApprovalNoteWorker, to: ::MergeRequests::ApprovedEvent
|
||||
|
|
|
@ -46,10 +46,10 @@ namespace :tw do
|
|||
CodeOwnerRule.new('Fuzz Testing', '@rdickenson'),
|
||||
CodeOwnerRule.new('Geo', '@axil'),
|
||||
CodeOwnerRule.new('Gitaly', '@eread'),
|
||||
CodeOwnerRule.new('Global Search', '@sselhorn'),
|
||||
CodeOwnerRule.new('Global Search', '@ashrafkhamis'),
|
||||
CodeOwnerRule.new('Import', '@eread'),
|
||||
CodeOwnerRule.new('Infrastructure', '@sselhorn'),
|
||||
CodeOwnerRule.new('Integrations', '@kpaizee'),
|
||||
CodeOwnerRule.new('Integrations', '@ashrafkhamis'),
|
||||
CodeOwnerRule.new('Knowledge', '@aqualls'),
|
||||
CodeOwnerRule.new('Application Performance', '@sselhorn'),
|
||||
CodeOwnerRule.new('Monitor', '@msedlakjakubowski'),
|
||||
|
|
|
@ -30768,6 +30768,9 @@ msgstr ""
|
|||
msgid "ProjectSettings|Encourage"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|Environments"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|Every merge creates a merge commit."
|
||||
msgstr ""
|
||||
|
||||
|
@ -30780,6 +30783,9 @@ msgstr ""
|
|||
msgid "ProjectSettings|Every project can have its own space to store its packages. Note: The Package Registry is always visible when a project is public."
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|Every project can make deployments to environments either via CI/CD or API calls. Non-project members have read-only access."
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSettings|Everyone"
|
||||
msgstr ""
|
||||
|
||||
|
@ -44412,6 +44418,9 @@ msgstr ""
|
|||
msgid "WorkItem|Something went wrong when deleting the work item. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Something went wrong when fetching the items list. Please refresh this page."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Something went wrong when fetching the work item. Please try again."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -133,7 +133,7 @@
|
|||
"katex": "^0.13.2",
|
||||
"lodash": "^4.17.20",
|
||||
"lowlight": "^2.6.1",
|
||||
"marked": "^0.3.12",
|
||||
"marked": "^4.0.18",
|
||||
"mathjax": "3",
|
||||
"mdurl": "^1.0.1",
|
||||
"mermaid": "^9.1.3",
|
||||
|
|
|
@ -22,7 +22,9 @@ RSpec.describe 'Create a CRM contact', :js do
|
|||
fill_in 'description', with: 'VIP'
|
||||
click_button 'Save changes'
|
||||
|
||||
expect(page).to have_content 'gitlab@example.com'
|
||||
wait_for_requests
|
||||
|
||||
expect(group.contacts.first.email).to eq('gitlab@example.com')
|
||||
expect(page).to have_current_path("#{group_crm_contacts_path(group)}/", ignore_query: true)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -141,6 +141,67 @@ RSpec.describe Crm::ContactsFinder do
|
|||
expect(finder.execute).to match_array([search_test_b])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sorting' do
|
||||
let_it_be(:search_test_c) do
|
||||
create(
|
||||
:contact,
|
||||
group: search_test_group,
|
||||
email: "a@test.com",
|
||||
organization: create(:organization, name: "Company Z")
|
||||
)
|
||||
end
|
||||
|
||||
let_it_be(:search_test_d) do
|
||||
create(
|
||||
:contact,
|
||||
group: search_test_group,
|
||||
email: "b@test.com",
|
||||
organization: create(:organization, name: "Company A")
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns the contacts sorted by email in ascending order' do
|
||||
finder = described_class.new(user, group: search_test_group, sort: { field: 'email', direction: :asc })
|
||||
|
||||
expect(finder.execute).to eq([search_test_c, search_test_d, search_test_a, search_test_b])
|
||||
end
|
||||
|
||||
it 'returns the contacts sorted by description in ascending order' do
|
||||
finder = described_class.new(user, group: search_test_group, sort: { field: 'description', direction: :desc })
|
||||
|
||||
results = finder.execute
|
||||
|
||||
expect(results[0]).to eq(search_test_b)
|
||||
expect(results[1]).to eq(search_test_a)
|
||||
end
|
||||
|
||||
it 'returns the contacts sorted by organization in ascending order' do
|
||||
finder = described_class.new(user, group: search_test_group, sort: { field: 'organization', direction: :asc })
|
||||
|
||||
results = finder.execute
|
||||
|
||||
expect(results[0]).to eq(search_test_d)
|
||||
expect(results[1]).to eq(search_test_c)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.counts_by_state' do
|
||||
let_it_be(:group) { create(:group, :crm_enabled) }
|
||||
let_it_be(:active_contacts) { create_list(:contact, 3, group: group, state: :active) }
|
||||
let_it_be(:inactive_contacts) { create_list(:contact, 2, group: group, state: :inactive) }
|
||||
|
||||
before do
|
||||
group.add_developer(user)
|
||||
end
|
||||
|
||||
it 'returns correct counts' do
|
||||
counts = described_class.counts_by_state(user, group: group)
|
||||
|
||||
expect(counts["active"]).to eq(3)
|
||||
expect(counts["inactive"]).to eq(2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -285,7 +285,7 @@ describe('issue_note', () => {
|
|||
await waitForPromises();
|
||||
expect(alertSpy).not.toHaveBeenCalled();
|
||||
expect(wrapper.vm.note.note_html).toBe(
|
||||
'<p><img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"></p>\n',
|
||||
'<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7">',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -127,6 +127,7 @@ describe('Settings Panel', () => {
|
|||
const findOperationsVisibilityInput = () =>
|
||||
findOperationsSettings().findComponent(ProjectFeatureSetting);
|
||||
const findConfirmDangerButton = () => wrapper.findComponent(ConfirmDanger);
|
||||
const findEnvironmentsSettings = () => wrapper.findComponent({ ref: 'environments-settings' });
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
|
@ -786,4 +787,23 @@ describe('Settings Panel', () => {
|
|||
expect(findOperationsSettings().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Environments', () => {
|
||||
describe('with feature flag', () => {
|
||||
it('should show the environments toggle', () => {
|
||||
wrapper = mountComponent({
|
||||
glFeatures: { splitOperationsVisibilityPermissions: true },
|
||||
});
|
||||
|
||||
expect(findEnvironmentsSettings().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
describe('without feature flag', () => {
|
||||
it('should not show the environments toggle', () => {
|
||||
wrapper = mountComponent({});
|
||||
|
||||
expect(findEnvironmentsSettings().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Vue, { nextTick } from 'vue';
|
||||
import { GlBadge, GlButton } from '@gitlab/ui';
|
||||
import { GlBadge, GlButton, GlAlert } from '@gitlab/ui';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
|
@ -39,12 +39,12 @@ describe('WorkItemLinks', () => {
|
|||
|
||||
const createComponent = async ({
|
||||
data = {},
|
||||
response = workItemHierarchyResponse,
|
||||
fetchHandler = jest.fn().mockResolvedValue(workItemHierarchyResponse),
|
||||
mutationHandler = mutationChangeParentHandler,
|
||||
} = {}) => {
|
||||
mockApollo = createMockApollo(
|
||||
[
|
||||
[getWorkItemLinksQuery, jest.fn().mockResolvedValue(response)],
|
||||
[getWorkItemLinksQuery, fetchHandler],
|
||||
[changeWorkItemParentMutation, mutationHandler],
|
||||
[workItemQuery, childWorkItemQueryHandler],
|
||||
],
|
||||
|
@ -71,6 +71,7 @@ describe('WorkItemLinks', () => {
|
|||
await waitForPromises();
|
||||
};
|
||||
|
||||
const findAlert = () => wrapper.findComponent(GlAlert);
|
||||
const findToggleButton = () => wrapper.findByTestId('toggle-links');
|
||||
const findLinksBody = () => wrapper.findByTestId('links-body');
|
||||
const findEmptyState = () => wrapper.findByTestId('links-empty');
|
||||
|
@ -117,7 +118,9 @@ describe('WorkItemLinks', () => {
|
|||
|
||||
describe('when no child links', () => {
|
||||
beforeEach(async () => {
|
||||
await createComponent({ response: workItemHierarchyEmptyResponse });
|
||||
await createComponent({
|
||||
fetchHandler: jest.fn().mockResolvedValue(workItemHierarchyEmptyResponse),
|
||||
});
|
||||
});
|
||||
|
||||
it('displays empty state if there are no children', () => {
|
||||
|
@ -133,6 +136,18 @@ describe('WorkItemLinks', () => {
|
|||
expect(findFirstLinksMenu().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('shows alert when list loading fails', async () => {
|
||||
const errorMessage = 'Some error';
|
||||
await createComponent({
|
||||
fetchHandler: jest.fn().mockRejectedValue(new Error(errorMessage)),
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findAlert().exists()).toBe(true);
|
||||
expect(findAlert().text()).toBe(errorMessage);
|
||||
});
|
||||
|
||||
it('renders confidentiality icon when child item is confidential', () => {
|
||||
const children = wrapper.findAll('[data-testid="links-child"]');
|
||||
const confidentialIcon = children.at(0).find('[data-testid="confidential-icon"]');
|
||||
|
@ -149,7 +164,9 @@ describe('WorkItemLinks', () => {
|
|||
|
||||
describe('when no permission to update', () => {
|
||||
beforeEach(async () => {
|
||||
await createComponent({ response: workItemHierarchyNoUpdatePermissionResponse });
|
||||
await createComponent({
|
||||
fetchHandler: jest.fn().mockResolvedValue(workItemHierarchyNoUpdatePermissionResponse),
|
||||
});
|
||||
});
|
||||
|
||||
it('does not display button to toggle Add form', () => {
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Resolvers::Crm::ContactStateCountsResolver do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:group) { create(:group, :crm_enabled) }
|
||||
|
||||
before_all do
|
||||
create(:contact, group: group, email: "x@test.com")
|
||||
create(:contact, group: group, email: "y@test.com", state: 'inactive')
|
||||
create_list(:contact, 3, group: group)
|
||||
create_list(:contact, 2, group: group, state: 'inactive')
|
||||
end
|
||||
|
||||
describe '#resolve' do
|
||||
context 'with unauthorized user' do
|
||||
it 'does not raise an error and returns no counts' do
|
||||
expect { resolve_counts(group) }.not_to raise_error
|
||||
expect(resolve_counts(group).all).to be(0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with authorized user' do
|
||||
before do
|
||||
group.add_reporter(user)
|
||||
end
|
||||
|
||||
context 'without parent' do
|
||||
it 'returns no counts' do
|
||||
expect(resolve_counts(nil).all).to be(0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a group' do
|
||||
context 'when no filter is provided' do
|
||||
it 'returns the count of all contacts' do
|
||||
counts = resolve_counts(group)
|
||||
expect(counts.all).to eq(7)
|
||||
expect(counts.active).to eq(4)
|
||||
expect(counts.inactive).to eq(3)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when search term is provided' do
|
||||
it 'returns the correct counts' do
|
||||
counts = resolve_counts(group, { search: "@test.com" })
|
||||
|
||||
expect(counts.all).to be(2)
|
||||
expect(counts.active).to be(1)
|
||||
expect(counts.inactive).to be(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def resolve_counts(parent, args = {}, context = { current_user: user })
|
||||
resolve(described_class, obj: parent, args: args, ctx: context)
|
||||
end
|
||||
end
|
|
@ -16,6 +16,7 @@ RSpec.describe Resolvers::Crm::ContactsResolver do
|
|||
last_name: "DEF",
|
||||
email: "ghi@test.com",
|
||||
description: "LMNO",
|
||||
organization: create(:organization, group: group),
|
||||
state: "inactive"
|
||||
)
|
||||
end
|
||||
|
@ -61,11 +62,29 @@ RSpec.describe Resolvers::Crm::ContactsResolver do
|
|||
end
|
||||
|
||||
context 'when no filter is provided' do
|
||||
it 'returns all the contacts in the correct order' do
|
||||
it 'returns all the contacts in the default order' do
|
||||
expect(resolve_contacts(group)).to eq([contact_a, contact_b])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a sort is provided' do
|
||||
it 'returns all the contacts in the correct order' do
|
||||
expect(resolve_contacts(group, { sort: 'EMAIL_DESC' })).to eq([contact_b, contact_a])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a sort is provided needing offset_pagination' do
|
||||
it 'returns all the contacts in the correct order' do
|
||||
expect(resolve_contacts(group, { sort: 'ORGANIZATION_ASC' })).to eq([contact_a, contact_b])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filtering for all states' do
|
||||
it 'returns all the contacts in the correct order' do
|
||||
expect(resolve_contacts(group, { state: 'all' })).to eq([contact_a, contact_b])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when search term is provided' do
|
||||
it 'returns the correct contacts' do
|
||||
expect(resolve_contacts(group, { search: "x@test.com" })).to contain_exactly(contact_b)
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe GitlabSchema.types['ContactSort'] do
|
||||
specify { expect(described_class.graphql_name).to eq('ContactSort') }
|
||||
|
||||
it_behaves_like 'common sort values'
|
||||
|
||||
it 'exposes all the contact sort values' do
|
||||
expect(described_class.values.keys).to include(
|
||||
*%w[
|
||||
FIRST_NAME_ASC
|
||||
FIRST_NAME_DESC
|
||||
LAST_NAME_ASC
|
||||
LAST_NAME_DESC
|
||||
EMAIL_ASC
|
||||
EMAIL_DESC
|
||||
PHONE_ASC
|
||||
PHONE_DESC
|
||||
DESCRIPTION_ASC
|
||||
DESCRIPTION_DESC
|
||||
ORGANIZATION_ASC
|
||||
ORGANIZATION_DESC
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe GitlabSchema.types['ContactStateCounts'] do
|
||||
let(:fields) do
|
||||
%w[
|
||||
all
|
||||
active
|
||||
inactive
|
||||
]
|
||||
end
|
||||
|
||||
it { expect(described_class.graphql_name).to eq('ContactStateCounts') }
|
||||
it { expect(described_class).to have_graphql_fields(fields) }
|
||||
it { expect(described_class).to require_graphql_authorizations(:read_crm_contact) }
|
||||
end
|
|
@ -24,8 +24,8 @@ RSpec.describe GitlabSchema.types['Group'] do
|
|||
dependency_proxy_blobs dependency_proxy_image_count
|
||||
dependency_proxy_blob_count dependency_proxy_total_size
|
||||
dependency_proxy_image_prefix dependency_proxy_image_ttl_policy
|
||||
shared_runners_setting timelogs organizations contacts work_item_types
|
||||
recent_issue_boards ci_variables
|
||||
shared_runners_setting timelogs organizations contacts contact_state_counts
|
||||
work_item_types recent_issue_boards ci_variables
|
||||
]
|
||||
|
||||
expect(described_class).to include_graphql_fields(*expected_fields)
|
||||
|
@ -55,6 +55,13 @@ RSpec.describe GitlabSchema.types['Group'] do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'contact_state_counts field' do
|
||||
subject { described_class.fields['contactStateCounts'] }
|
||||
|
||||
it { is_expected.to have_graphql_type(Types::CustomerRelations::ContactStateCountsType) }
|
||||
it { is_expected.to have_graphql_resolver(Resolvers::Crm::ContactStateCountsResolver) }
|
||||
end
|
||||
|
||||
it_behaves_like 'a GraphQL type with labels' do
|
||||
let(:labels_resolver_arguments) { [:search_term, :includeAncestorGroups, :includeDescendantGroups, :onlyGroupLabels] }
|
||||
end
|
||||
|
|
|
@ -37,12 +37,6 @@ RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do
|
|||
freeze_time { example.run }
|
||||
end
|
||||
|
||||
before do
|
||||
User.class_eval do
|
||||
include EachBatch
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns the final expected delay' do
|
||||
Sidekiq::Testing.fake! do
|
||||
final_delay = model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.minutes, batch_size: 2)
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe RemoveDeactivatedUserHighestRoleStats do
|
||||
let!(:users) { table(:users) }
|
||||
let!(:user_highest_roles) { table(:user_highest_roles) }
|
||||
|
||||
let!(:user1) do
|
||||
users.create!(username: 'user1', email: 'user1@example.com', projects_limit: 10, state: 'active')
|
||||
end
|
||||
|
||||
let!(:user2) do
|
||||
users.create!(username: 'user2', email: 'user2@example.com', projects_limit: 10, state: 'deactivated')
|
||||
end
|
||||
|
||||
let!(:highest_role1) { user_highest_roles.create!(user_id: user1.id) }
|
||||
let!(:highest_role2) { user_highest_roles.create!(user_id: user2.id) }
|
||||
|
||||
describe '#up' do
|
||||
context 'when on gitlab.com' do
|
||||
it 'does not change user highest role records' do
|
||||
allow(Gitlab).to receive(:com?).and_return(true)
|
||||
expect { migrate! }.not_to change(user_highest_roles, :count)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not on gitlab.com' do
|
||||
it 'removes all user highest role records for deactivated users' do
|
||||
allow(Gitlab).to receive(:com?).and_return(false)
|
||||
migrate!
|
||||
expect(user_highest_roles.pluck(:user_id)).to contain_exactly(
|
||||
user1.id
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -226,15 +226,58 @@ RSpec.describe CustomerRelations::Contact, type: :model do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.sort_by_name' do
|
||||
let_it_be(:contact_a) { create(:contact, group: group, first_name: "c", last_name: "d") }
|
||||
let_it_be(:contact_b) { create(:contact, group: group, first_name: "a", last_name: "b") }
|
||||
let_it_be(:contact_c) { create(:contact, group: group, first_name: "e", last_name: "d") }
|
||||
describe '.counts_by_state' do
|
||||
before do
|
||||
create_list(:contact, 3, group: group)
|
||||
create_list(:contact, 2, group: group, state: 'inactive')
|
||||
end
|
||||
|
||||
context 'when sorting the contacts' do
|
||||
it 'sorts them by last name then first name in ascendent order' do
|
||||
it 'returns only active contacts' do
|
||||
counts = group.contacts.counts_by_state
|
||||
|
||||
expect(counts['active']).to be(3)
|
||||
expect(counts['inactive']).to be(2)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'sorting' do
|
||||
let_it_be(:organization_a) { create(:organization, name: 'a') }
|
||||
let_it_be(:organization_b) { create(:organization, name: 'b') }
|
||||
let_it_be(:contact_a) { create(:contact, group: group, first_name: "c", last_name: "d") }
|
||||
let_it_be(:contact_b) do
|
||||
create(:contact,
|
||||
group: group,
|
||||
first_name: "a",
|
||||
last_name: "b",
|
||||
phone: "123",
|
||||
organization: organization_a)
|
||||
end
|
||||
|
||||
let_it_be(:contact_c) do
|
||||
create(:contact,
|
||||
group: group,
|
||||
first_name: "e",
|
||||
last_name: "d",
|
||||
phone: "456",
|
||||
organization: organization_b)
|
||||
end
|
||||
|
||||
describe '.sort_by_name' do
|
||||
it 'sorts them by last name then first name in ascending order' do
|
||||
expect(group.contacts.sort_by_name).to eq([contact_b, contact_a, contact_c])
|
||||
end
|
||||
end
|
||||
|
||||
describe '.sort_by_organization' do
|
||||
it 'sorts them by organization in descending order' do
|
||||
expect(group.contacts.sort_by_organization(:desc)).to eq([contact_c, contact_b, contact_a])
|
||||
end
|
||||
end
|
||||
|
||||
describe '.sort_by_field' do
|
||||
it 'sorts them by phone in ascending order' do
|
||||
expect(group.contacts.sort_by_field('phone', :asc)).to eq([contact_b, contact_c, contact_a])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe CustomerRelations::ContactStateCounts do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:group) { create(:group, :crm_enabled) }
|
||||
|
||||
let(:counter) { described_class.new(user, group, params) }
|
||||
let(:params) { {} }
|
||||
|
||||
before_all do
|
||||
group.add_reporter(user)
|
||||
create(:contact, group: group, first_name: 'filter')
|
||||
create(:contact, group: group, last_name: 'filter')
|
||||
create(:contact, group: group)
|
||||
create(:contact, group: group, state: 'inactive', email: 'filter@example.com')
|
||||
create(:contact, group: group, state: 'inactive')
|
||||
end
|
||||
|
||||
describe '.declarative_policy_class' do
|
||||
subject { described_class.declarative_policy_class }
|
||||
|
||||
it { is_expected.to eq('CustomerRelations::ContactPolicy') }
|
||||
end
|
||||
|
||||
describe '#all' do
|
||||
it 'returns the total number of contacts' do
|
||||
expect(counter.all).to be(5)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#active' do
|
||||
it 'returns the number of active contacts' do
|
||||
expect(counter.active).to be(3)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#inactive' do
|
||||
it 'returns the number of inactive contacts' do
|
||||
expect(counter.inactive).to be(2)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when filtered' do
|
||||
let(:params) { { search: 'filter' } }
|
||||
|
||||
it '#all returns the number of contacts with a filter' do
|
||||
expect(counter.all).to be(3)
|
||||
end
|
||||
|
||||
it '#active returns the number of active contacts with a filter' do
|
||||
expect(counter.active).to be(2)
|
||||
end
|
||||
|
||||
it '#inactive returns the number of inactive contacts with a filter' do
|
||||
expect(counter.inactive).to be(1)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -12,11 +12,11 @@ RSpec.describe 'getting CRM contacts' do
|
|||
create(
|
||||
:contact,
|
||||
group: group,
|
||||
first_name: "ABC",
|
||||
last_name: "DEF",
|
||||
email: "ghi@test.com",
|
||||
description: "LMNO",
|
||||
state: "inactive"
|
||||
first_name: "PQR",
|
||||
last_name: "STU",
|
||||
email: "aaa@test.com",
|
||||
description: "YZ",
|
||||
state: "active"
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -26,9 +26,9 @@ RSpec.describe 'getting CRM contacts' do
|
|||
group: group,
|
||||
first_name: "ABC",
|
||||
last_name: "DEF",
|
||||
email: "vwx@test.com",
|
||||
description: "YZ",
|
||||
state: "active"
|
||||
email: "ghi@test.com",
|
||||
description: "LMNO",
|
||||
state: "inactive"
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -36,9 +36,9 @@ RSpec.describe 'getting CRM contacts' do
|
|||
create(
|
||||
:contact,
|
||||
group: group,
|
||||
first_name: "PQR",
|
||||
last_name: "STU",
|
||||
email: "aaa@test.com",
|
||||
first_name: "JKL",
|
||||
last_name: "MNO",
|
||||
email: "vwx@test.com",
|
||||
description: "YZ",
|
||||
state: "active"
|
||||
)
|
||||
|
@ -51,7 +51,7 @@ RSpec.describe 'getting CRM contacts' do
|
|||
it_behaves_like 'sorted paginated query' do
|
||||
let(:sort_argument) { {} }
|
||||
let(:first_param) { 2 }
|
||||
let(:all_records) { [contact_a, contact_b, contact_c] }
|
||||
let(:all_records) { [contact_b, contact_c, contact_a] }
|
||||
let(:data_path) { [:group, :contacts] }
|
||||
|
||||
def pagination_query(params)
|
||||
|
|
|
@ -104,6 +104,18 @@ RSpec.describe Pages::InvalidateDomainCacheWorker do
|
|||
{ type: :namespace, id: 5 }
|
||||
]
|
||||
|
||||
it_behaves_like 'clears caches with',
|
||||
event_class: Groups::GroupPathChangedEvent,
|
||||
event_data: {
|
||||
group_id: 1,
|
||||
root_namespace_id: 2,
|
||||
old_path: 'old_path',
|
||||
new_path: 'new_path'
|
||||
},
|
||||
caches: [
|
||||
{ type: :namespace, id: 2 }
|
||||
]
|
||||
|
||||
context 'when namespace based cache keys are duplicated' do
|
||||
# de-dups namespace cache keys
|
||||
it_behaves_like 'clears caches with',
|
||||
|
|
|
@ -25,21 +25,14 @@ RSpec.describe Users::DeactivateDormantUsersWorker do
|
|||
context 'when automatic deactivation of dormant users is enabled' do
|
||||
before do
|
||||
stub_application_setting(deactivate_dormant_users: true)
|
||||
stub_const("#{described_class.name}::PAUSE_SECONDS", 0)
|
||||
end
|
||||
|
||||
it 'deactivates dormant users' do
|
||||
freeze_time do
|
||||
stub_const("#{described_class.name}::BATCH_SIZE", 1)
|
||||
|
||||
expect(worker).to receive(:sleep).twice
|
||||
|
||||
worker.perform
|
||||
|
||||
expect(User.dormant.count).to eq(0)
|
||||
expect(User.with_no_activity.count).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
where(:user_type, :expected_state) do
|
||||
:human | 'deactivated'
|
||||
|
@ -78,6 +71,14 @@ RSpec.describe Users::DeactivateDormantUsersWorker do
|
|||
|
||||
expect(inactive_recently_created.reload.state).to eq('active')
|
||||
end
|
||||
|
||||
it 'triggers update of highest user role for deactivated users', :clean_gitlab_redis_shared_state do
|
||||
[dormant, inactive].each do |user|
|
||||
expect(UpdateHighestRoleWorker).to receive(:perform_in).with(anything, user.id)
|
||||
end
|
||||
|
||||
worker.perform
|
||||
end
|
||||
end
|
||||
|
||||
context 'when automatic deactivation of dormant users is disabled' do
|
||||
|
|
|
@ -8041,10 +8041,10 @@ markdownlint@~0.25.1:
|
|||
dependencies:
|
||||
markdown-it "12.3.2"
|
||||
|
||||
marked@^0.3.12:
|
||||
version "0.3.19"
|
||||
resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.19.tgz#5d47f709c4c9fc3c216b6d46127280f40b39d790"
|
||||
integrity sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg==
|
||||
marked@^4.0.18:
|
||||
version "4.0.18"
|
||||
resolved "https://registry.yarnpkg.com/marked/-/marked-4.0.18.tgz#cd0ac54b2e5610cfb90e8fd46ccaa8292c9ed569"
|
||||
integrity sha512-wbLDJ7Zh0sqA0Vdg6aqlbT+yPxqLblpAZh1mK2+AO2twQkPywvvqQNfEPVwSSRjZ7dZcdeVBIAgiO7MMp3Dszw==
|
||||
|
||||
mathjax@3:
|
||||
version "3.1.2"
|
||||
|
|
Loading…
Reference in New Issue