Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-08-15 18:12:51 +00:00
parent 51d5328e82
commit efbd20fd1e
60 changed files with 1527 additions and 726 deletions

View File

@ -53,7 +53,6 @@ rules:
- '^$'
- '^variables$'
- 'attrs?$'
'@gitlab/vue-prefer-dollar-scopedslots': error
no-param-reassign:
- error
- props: true

View File

@ -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

View File

@ -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';

View File

@ -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';

View File

@ -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">

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.' \

View File

@ -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

View File

@ -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

View File

@ -30,6 +30,7 @@ class User < ApplicationRecord
include Gitlab::Auth::Otp::Fortinet
include RestrictedSignup
include StripAttribute
include EachBatch
DEFAULT_NOTIFICATION_LEVEL = :participating

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
4de7fddbc2f44cf1450af25bd55a5f2586c3daf79b1443ec26ba9d47002707d7

View File

@ -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`

View File

@ -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
```

View File

@ -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. |

View File

@ -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() {}
}
```

View File

@ -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
@ -77,8 +92,8 @@ performance.getEntriesByType('mark');
performance.getEntriesByType('measure');
```
Using `getEntriesByName()` or `getEntriesByType()` returns an Array of
[the PerformanceMeasure objects](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceMeasure)
Using `getEntriesByName()` or `getEntriesByType()` returns an Array of
[the PerformanceMeasure objects](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceMeasure)
which contain information about the measurement's start time and duration.
### User Timing API utility

View File

@ -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).

View File

@ -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.

View File

@ -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. -->

View File

@ -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 -->

View File

@ -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 -->

View File

@ -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 -->

View File

@ -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 -->

View File

@ -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 -->

View File

@ -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 -->

View File

@ -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 -->

View File

@ -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 -->

View File

@ -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 -->

View File

@ -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:

View File

@ -146,6 +146,7 @@ module.exports = (path, options = {}) => {
'monaco-yaml',
'fast-mersenne-twister',
'prosemirror-markdown',
'marked',
'fault',
'dateformat',
'lowlight',

View File

@ -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

View File

@ -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'),

View File

@ -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 ""

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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">',
);
});
});

View File

@ -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);
});
});
});
});

View File

@ -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', () => {

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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',

View File

@ -25,20 +25,13 @@ 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)
worker.perform
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
expect(User.dormant.count).to eq(0)
expect(User.with_no_activity.count).to eq(0)
end
where(:user_type, :expected_state) do
@ -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

View File

@ -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"