Commit afdd2a1e authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 960be622 660d5d25
......@@ -1910,20 +1910,3 @@ Rails/SaveBang:
- 'spec/views/projects/imports/new.html.haml_spec.rb'
- 'spec/views/projects/merge_requests/show.html.haml_spec.rb'
- 'spec/views/shared/_label_row.html.haml_spec.rb'
- 'spec/workers/concerns/project_export_options_spec.rb'
- 'spec/workers/gitlab/import/stuck_project_import_jobs_worker_spec.rb'
- 'spec/workers/gitlab/jira_import/stuck_jira_import_jobs_worker_spec.rb'
- 'spec/workers/migrate_external_diffs_worker_spec.rb'
- 'spec/workers/namespaceless_project_destroy_worker_spec.rb'
- 'spec/workers/namespaces/root_statistics_worker_spec.rb'
- 'spec/workers/pages_domain_verification_worker_spec.rb'
- 'spec/workers/process_commit_worker_spec.rb'
- 'spec/workers/propagate_integration_worker_spec.rb'
- 'spec/workers/propagate_service_template_worker_spec.rb'
- 'spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb'
- 'spec/workers/repository_check/single_repository_worker_spec.rb'
- 'spec/workers/repository_cleanup_worker_spec.rb'
- 'spec/workers/repository_import_worker_spec.rb'
- 'spec/workers/repository_update_remote_mirror_worker_spec.rb'
- 'spec/workers/stuck_ci_jobs_worker_spec.rb'
- 'spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb'
13.3.0-rc3
a6091637dcb4c3b601a8860b5f164c0ce90ba0ca
......@@ -94,7 +94,7 @@ export default {
{{ issue.title }}
</h2>
<a
class="gl-text-gray-600 gl-text-decoration-none gl-mb-6 gl-display-block"
class="gl-text-gray-400 gl-text-decoration-none gl-mb-6 gl-display-block"
:href="issue.webUrl"
>{{ issue.webPath }}</a
>
......
......@@ -94,7 +94,7 @@ export default {
{{ issue.title }}
</h2>
<a
class="gl-text-gray-600 gl-text-decoration-none gl-mb-6 gl-display-block"
class="gl-text-gray-400 gl-text-decoration-none gl-mb-6 gl-display-block"
:href="issue.webUrl"
>{{ issue.webPath }}</a
>
......
......@@ -133,12 +133,12 @@ export default {
</div>
<div class="table-section section-100 gl-white-space-normal mt-md-3">
<div class="gl-display-flex gl-text-gray-600">
<div class="gl-display-flex gl-text-gray-400">
<gl-icon name="issues" class="gl-mr-2" />
<gl-link
data-testid="issue-id-link"
:href="issue.webUrl"
class="gl-text-gray-600 gl-mr-5"
class="gl-text-gray-400 gl-mr-5"
@click="onIssueLinkClick(issue.iid, issue.title)"
>#{{ issue.iid }}</gl-link
>
......
......@@ -111,7 +111,7 @@ export default {
<div class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column">
<span class="gl-font-monospace">{{ item.name }}</span>
<span class="gl-text-gray-600">{{ item.subtitle }}</span>
<span class="gl-text-gray-400">{{ item.subtitle }}</span>
</div>
<gl-badge v-if="item.default" size="sm" variant="info">{{
......
......@@ -109,7 +109,7 @@ export default {
<template>
<gl-new-dropdown v-bind="$attrs" class="ref-selector" @shown="focusSearchBox">
<template slot="button-content">
<span class="gl-flex-grow-1 gl-ml-2 gl-text-gray-600" data-testid="button-content">
<span class="gl-flex-grow-1 gl-ml-2 gl-text-gray-400" data-testid="button-content">
<span v-if="selectedRef" class="gl-font-monospace">{{ selectedRef }}</span>
<span v-else>{{ i18n.noRefSelected }}</span>
</span>
......
......@@ -138,7 +138,7 @@ export default {
:aria-label="$options.externalLinkTooltipText"
:title="$options.externalLinkTooltipText"
data-testid="external-link-indicator"
class="gl-ml-2 gl-flex-shrink-0 gl-flex-grow-0 gl-text-gray-600"
class="gl-ml-2 gl-flex-shrink-0 gl-flex-grow-0 gl-text-gray-400"
/>
</gl-link>
</li>
......
......@@ -38,7 +38,7 @@ export default {
<div class="inline">
<label
v-tooltip
:class="{ 'gl-text-gray-600': isDisabled }"
:class="{ 'gl-text-gray-400': isDisabled }"
data-testid="squashLabel"
:data-title="tooltipTitle"
>
......
......@@ -74,16 +74,16 @@ export default {
</div>
<div class="gl-text-gray-700">
<div v-if="user.bio" class="gl-display-flex gl-mb-2">
<icon name="profile" class="gl-text-gray-600 gl-flex-shrink-0" />
<icon name="profile" class="gl-text-gray-400 gl-flex-shrink-0" />
<span ref="bio" class="gl-ml-2" v-html="user.bioHtml"></span>
</div>
<div v-if="user.workInformation" class="gl-display-flex gl-mb-2">
<icon name="work" class="gl-text-gray-600 gl-flex-shrink-0" />
<icon name="work" class="gl-text-gray-400 gl-flex-shrink-0" />
<span ref="workInformation" class="gl-ml-2">{{ user.workInformation }}</span>
</div>
</div>
<div v-if="user.location" class="js-location gl-text-gray-700 gl-display-flex">
<icon name="location" class="gl-text-gray-600 flex-shrink-0" />
<icon name="location" class="gl-text-gray-400 flex-shrink-0" />
<span class="gl-ml-2">{{ user.location }}</span>
</div>
<div v-if="statusHtml" class="js-user-status gl-mt-3">
......
......@@ -274,8 +274,6 @@
svg {
height: 15px;
width: 15px;
position: relative;
top: 2px;
}
svg,
......
......@@ -509,7 +509,7 @@ img.emoji {
}
&.is-dragging {
background-color: $gray-600;
background-color: $gray-400;
}
}
......
......@@ -209,7 +209,7 @@
}
.doc-versions {
color: $gray-600;
color: $gray-400;
&:hover {
color: $gray-900;
......
......@@ -227,7 +227,7 @@ label {
right: 0.8em;
top: 50%;
transform: translateY(-50%);
color: $gray-600;
color: $gray-400;
}
.input-md {
......
......@@ -48,4 +48,12 @@ svg {
@include svg-size(#{$svg-size}px);
}
}
&.s12 {
vertical-align: -1px;
}
&.s16 {
vertical-align: -3px;
}
}
......@@ -326,8 +326,8 @@
line-height: 1;
padding: 0;
min-width: 16px;
color: $gray-600;
fill: $gray-600;
color: $gray-400;
fill: $gray-400;
.fa {
position: relative;
......
......@@ -168,7 +168,7 @@ $gray-200: #bfbfbf !default;
$gray-300: #999 !default;
$gray-400: #868686 !default;
$gray-500: #666 !default;
$gray-600: #919191 !default;
$gray-600: #5e5e5e !default;
$gray-700: #707070 !default;
$gray-800: #4f4f4f !default;
$gray-900: #303030 !default;
......@@ -351,11 +351,11 @@ $gl-font-weight-normal: 400;
$gl-font-weight-bold: 600;
$gl-text-color: $gray-900;
$gl-text-color-secondary: $gray-700;
$gl-text-color-tertiary: $gray-600;
$gl-text-color-tertiary: $gray-400;
$gl-text-color-quaternary: #d6d6d6;
$gl-text-color-inverted: $white;
$gl-text-color-secondary-inverted: rgba($white, 0.85);
$gl-text-color-disabled: $gray-600;
$gl-text-color-disabled: $gray-400;
$gl-grayish-blue: #7f8fa4;
$gl-gray-dark: #313236;
$gl-gray-light: #5c5c5c;
......
......@@ -166,6 +166,6 @@
.cluster-status-indicator {
&.disabled {
background-color: $gray-600;
background-color: $gray-400;
}
}
......@@ -26,7 +26,7 @@
th {
@include gl-bg-transparent;
@include gl-font-weight-bold;
@include gl-text-gray-600;
@include gl-text-gray-400;
&[aria-sort='none']:hover {
background-image: url('data:image/svg+xml, %3csvg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="4 0 8 16"%3e %3cpath style="fill: %23BABABA;" fill-rule="evenodd" d="M11.707085,11.7071 L7.999975,15.4142 L4.292875,11.7071 C3.902375,11.3166 3.902375, 10.6834 4.292875,10.2929 C4.683375,9.90237 5.316575,9.90237 5.707075,10.2929 L6.999975, 11.5858 L6.999975,2 C6.999975,1.44771 7.447695,1 7.999975,1 C8.552255,1 8.999975,1.44771 8.999975,2 L8.999975,11.5858 L10.292865,10.2929 C10.683395 ,9.90237 11.316555,9.90237 11.707085,10.2929 C12.097605,10.6834 12.097605,11.3166 11.707085,11.7071 Z"/%3e %3c/svg%3e');
......
......@@ -727,7 +727,7 @@ $note-form-margin-left: 72px;
display: inline-flex;
align-items: center;
margin-left: 10px;
color: $gray-600;
color: $gray-400;
@include notes-media('max', map-get($grid-breakpoints, sm) - 1) {
float: none;
......
......@@ -809,7 +809,7 @@
&.ci-status-icon-created,
&.ci-status-icon-skipped {
@include mini-pipeline-graph-color($white, $gray-100, $gray-200, $gray-300, $gray-600, $gray-700);
@include mini-pipeline-graph-color($white, $gray-100, $gray-200, $gray-300, $gray-400, $gray-700);
}
}
......
......@@ -159,7 +159,7 @@
font-weight: bold;
.icon {
font-size: $gl-font-size-large;
vertical-align: -1px;
}
.home-panel-topic-list {
......
......@@ -56,7 +56,7 @@
.draggable-remove-link {
cursor: pointer;
color: $gray-600;
color: $gray-400;
background-color: $white;
}
}
......@@ -117,7 +117,7 @@
.prometheus-graph-cursor {
position: absolute;
background: $gray-600;
background: $gray-400;
width: 1px;
}
......@@ -290,7 +290,7 @@
}
> text {
fill: $gray-600;
fill: $gray-400;
font-size: 10px;
}
}
......
......@@ -38,7 +38,7 @@
}
&.ci-preparing {
@include status-color($gray-100, $gray-300, $gray-600);
@include status-color($gray-100, $gray-300, $gray-400);
}
&.ci-pending,
......
......@@ -138,12 +138,6 @@
}
.tree-item {
.file-icon,
.folder-icon {
position: relative;
top: 2px;
}
.link-container {
padding: 0;
......
......@@ -45,7 +45,7 @@ module IconsHelper
ActionController::Base.helpers.image_path('file_icons.svg', host: sprite_base_url)
end
def sprite_icon(icon_name, size: nil, css_class: nil)
def sprite_icon(icon_name, size: DEFAULT_ICON_SIZE, css_class: nil)
if known_sprites&.exclude?(icon_name)
exception = ArgumentError.new("#{icon_name} is not a known icon in @gitlab-org/gitlab-svg")
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(exception)
......@@ -117,7 +117,9 @@ module IconsHelper
'earth'
end
sprite_icon(name, size: DEFAULT_ICON_SIZE, css_class: 'gl-vertical-align-text-bottom')
css_class = options.delete(:class)
sprite_icon(name, size: DEFAULT_ICON_SIZE, css_class: css_class)
end
def file_type_icon_class(type, mode, name)
......
......@@ -18,7 +18,7 @@
= link_to edit_admin_application_settings_integration_path(service.to_param), class: 'gl-text-blue-300!' do
%strong.has-tooltip{ title: s_('AdminSettings|Moved to integrations'), data: { container: 'body' } }
= service.title
%td.gl-cursor-default.gl-text-gray-600
%td.gl-cursor-default.gl-text-gray-400
= service.description
%td
- else
......
......@@ -13,7 +13,7 @@
%h1.home-panel-title.gl-mt-3.gl-mb-2{ data: { qa_selector: 'project_name_content' } }
= @project.name
%span.visibility-icon.text-secondary.gl-ml-2.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) }
= visibility_level_icon(@project.visibility_level, options: {class: 'icon'})
= visibility_level_icon(@project.visibility_level, options: { class: 'icon' })
= render_if_exists 'compliance_management/compliance_framework/compliance_framework_badge', project: @project
.home-panel-metadata.d-flex.flex-wrap.text-secondary
- if can?(current_user, :read_project, @project)
......
......@@ -27,5 +27,5 @@
Squash commits when merge request is accepted.
= link_to icon('question-circle'), help_page_path('user/project/merge_requests/squash_and_merge'), target: '_blank'
- if project.squash_always?
.gl-text-gray-600
.gl-text-gray-400
= _('Required in this project.')
---
title: Refactor spec/workers/* to fix SaveBang Cop
merge_request: 38399
author: Rajendra Kadam
type: fixed
......@@ -42,7 +42,6 @@
- design_management
- design_system
- devops_reports
- digital_experience_management
- disaster_recovery
- dynamic_application_security_testing
- editor_extension
......@@ -72,7 +71,6 @@
- jupyter_notebooks
- kanban_boards
- kubernetes_management
- language_specific
- license_compliance
- live_preview
- load_testing
......
......@@ -548,7 +548,7 @@ or more LDAP group links](#adding-group-links-starter-only).
### Adding group links **(STARTER ONLY)**
For information on adding group links via CNs and filters, refer to [the GitLab groups documentation](../../../user/group/index.md#manage-group-memberships-via-ldap).
For information on adding group links via CNs and filters, refer to [the GitLab groups documentation](../../../user/group/index.md#manage-group-memberships-via-ldap-starter-only).
### Administrator sync **(STARTER ONLY)**
......
---
reading_time: true
stage: Enablement
group: Distribution
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/#designated-technical-writers
---
# Reference architecture: up to 10,000 users
This page describes GitLab reference architecture for up to 10,000 users.
For a full list of reference architectures, see
This page describes GitLab reference architecture for up to 10,000 users. For a
full list of reference architectures, see
[Available reference architectures](index.md#available-reference-architectures).
> - **Supported users (approximate):** 10,000
> - **High Availability:** True
> - **Test RPS rates:** API: 200 RPS, Web: 20 RPS, Git: 20 RPS
| Service | Nodes | Configuration | GCP | AWS | Azure |
|--------------------------------------------------------------|-------|---------------------------------|------------------|-----------------------|----------------|
| External load balancing node | 1 | 2 vCPU, 1.8GB Memory | `n1-highcpu-2` | `c5.large` | `F2s v2` |
| Consul | 3 | 2 vCPU, 1.8GB Memory | `n1-highcpu-2` | `c5.large` | `F2s v2` |
| PostgreSQL | 3 | 4 vCPU, 15GB Memory | `n1-standard-4` | `m5.xlarge` | `D4s v3` |
| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | `n1-highcpu-2` | `c5.large` | `F2s v2` |
| Internal load balancing node | 1 | 2 vCPU, 1.8GB Memory | `n1-highcpu-2` | `c5.large` | `F2s v2` |
| Redis - Cache | 3 | 4 vCPU, 15GB Memory | `n1-standard-4` | `m5.xlarge` | `D4s v3` |
| Redis - Queues / Shared State | 3 | 4 vCPU, 15GB Memory | `n1-standard-4` | `m5.xlarge` | `D4s v3` |
| Redis Sentinel - Cache | 3 | 1 vCPU, 1.7GB Memory | `g1-small` | `t2.small` | `B1MS` |
| Redis Sentinel - Queues / Shared State | 3 | 1 vCPU, 1.7GB Memory | `g1-small` | `t2.small` | `B1MS` |
| Gitaly | 2 minimum | 16 vCPU, 60GB Memory | `n1-standard-16` | `m5.4xlarge` | `D16s v3` |
| Sidekiq | 4 | 4 vCPU, 15GB Memory | `n1-standard-4` | `m5.xlarge` | `D4s v3` |
| GitLab Rails | 3 | 32 vCPU, 28.8GB Memory | `n1-highcpu-32` | `c5.9xlarge` | `F32s v2` |
| Monitoring node | 1 | 4 vCPU, 3.6GB Memory | `n1-highcpu-4` | `c5.xlarge` | `F4s v2` |
| Object Storage | n/a | n/a | n/a | n/a | n/a |
| NFS Server | 1 | 4 vCPU, 3.6GB Memory | `n1-highcpu-4` | `c5.xlarge` | `F4s v2` |
The architectures were built and tested with the [Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms)
CPU platform on GCP. On different hardware you may find that adjustments, either lower
or higher, are required for your CPU or Node counts accordingly. For more information, a
[Sysbench](https://github.com/akopytov/sysbench) benchmark of the CPU can be found
[here](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
For data objects such as LFS, Uploads, Artifacts, etc., an [object storage service](#configure-the-object-storage)
is recommended over NFS where possible, due to better performance and availability.
Since this doesn't require a node to be set up, it's marked as not applicable (n/a)
in the table above.
> - **High Availability:** Yes
> - **Test requests per second (RPS) rates:** API: 200 RPS, Web: 20 RPS, Git: 20 RPS
| Service | Nodes | Configuration | GCP | AWS | Azure |
|--------------------------------------------|-------------|-------------------------|-----------------|-------------|----------|
| External load balancing node | 1 | 2 vCPU, 1.8GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| Consul | 3 | 2 vCPU, 1.8GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| PostgreSQL | 3 | 4 vCPU, 15GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
| PgBouncer | 3 | 2 vCPU, 1.8GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| Internal load balancing node | 1 | 2 vCPU, 1.8GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| Redis - Cache | 3 | 4 vCPU, 15GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
| Redis - Queues / Shared State | 3 | 4 vCPU, 15GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
| Redis Sentinel - Cache | 3 | 1 vCPU, 1.7GB memory | g1-small | t2.small | B1MS |
| Redis Sentinel - Queues / Shared State | 3 | 1 vCPU, 1.7GB memory | g1-small | t2.small | B1MS |
| Gitaly | 2 (minimum) | 16 vCPU, 60GB memory | n1-standard-16 | m5.4xlarge | D16s v3 |
| Sidekiq | 4 | 4 vCPU, 15GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
| GitLab Rails | 3 | 32 vCPU, 28.8GB memory | n1-highcpu-32 | c5.9xlarge | F32s v2 |
| Monitoring node | 1 | 4 vCPU, 3.6GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
| Object Storage | n/a | n/a | n/a | n/a | n/a |
| NFS Server | 1 | 4 vCPU, 3.6GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
The Google Cloud Platform (GCP) architectures were built and tested using the
[Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms)
CPU platform. On different hardware you may find that adjustments, either lower
or higher, are required for your CPU or node counts. For more information, see
our [Sysbench](https://github.com/akopytov/sysbench)-based
[CPU benchmark](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
For data objects (such as LFS, Uploads, or Artifacts), an
[object storage service](#configure-the-object-storage) is recommended instead
of NFS where possible, due to better performance and availability. Since this
doesn't require a node to be set up, *Object Storage* is noted as not
applicable (n/a) in the previous table.
## Setup components
......
---
reading_time: true
stage: Enablement
group: Distribution
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/#designated-technical-writers
---
# Reference architecture: up to 25,000 users
This page describes GitLab reference architecture for up to 25,000 users.
For a full list of reference architectures, see
This page describes GitLab reference architecture for up to 25,000 users. For a
full list of reference architectures, see
[Available reference architectures](index.md#available-reference-architectures).
> - **Supported users (approximate):** 25,000
> - **High Availability:** True
> - **Test RPS rates:** API: 500 RPS, Web: 50 RPS, Git: 50 RPS
| Service | Nodes | Configuration | GCP | AWS | Azure |
|--------------------------------------------------------------|-------|---------------------------------|------------------|-----------------------|----------------|
| External load balancing node | 1 | 4 vCPU, 3.6GB Memory | `n1-highcpu-4` | `c5.xlarge` | `F4s v2` |
| Consul | 3 | 2 vCPU, 1.8GB Memory | `n1-highcpu-2` | `c5.large` | `F2s v2` |
| PostgreSQL | 3 | 8 vCPU, 30GB Memory | `n1-standard-8` | `m5.2xlarge` | `D8s v3` |
| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | `n1-highcpu-2` | `c5.large` | `F2s v2` |
| Internal load balancing node | 1 | 2 vCPU, 1.8GB Memory | `n1-highcpu-2` | `c5.large` | `F2s v2` |
| Redis - Cache | 3 | 4 vCPU, 15GB Memory | `n1-standard-4` | `m5.xlarge` | `D4s v3` |
| Redis - Queues / Shared State | 3 | 4 vCPU, 15GB Memory | `n1-standard-4` | `m5.xlarge` | `D4s v3` |
| Redis Sentinel - Cache | 3 | 1 vCPU, 1.7GB Memory | `g1-small` | `t2.small` | `B1MS` |
| Redis Sentinel - Queues / Shared State | 3 | 1 vCPU, 1.7GB Memory | `g1-small` | `t2.small` | `B1MS` |
| Gitaly | 2 minimum | 32 vCPU, 120GB Memory | `n1-standard-32` | `m5.8xlarge` | `D32s v3` |
| Sidekiq | 4 | 4 vCPU, 15GB Memory | `n1-standard-4` | `m5.xlarge` | `D4s v3` |
| GitLab Rails | 5 | 32 vCPU, 28.8GB Memory | `n1-highcpu-32` | `c5.9xlarge` | `F32s v2` |
| Monitoring node | 1 | 4 vCPU, 3.6GB Memory | `n1-highcpu-4` | `c5.xlarge` | `F4s v2` |
| Object Storage | n/a | n/a | n/a | n/a | n/a |
| NFS Server | 1 | 4 vCPU, 3.6GB Memory | `n1-highcpu-4` | `c5.xlarge` | `F4s v2` |
The architectures were built and tested with the [Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms)
CPU platform on GCP. On different hardware you may find that adjustments, either lower
or higher, are required for your CPU or Node counts accordingly. For more information, a
[Sysbench](https://github.com/akopytov/sysbench) benchmark of the CPU can be found
[here](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
For data objects such as LFS, Uploads, Artifacts, etc., an [object storage service](#configure-the-object-storage)
is recommended over NFS where possible, due to better performance and availability.
Since this doesn't require a node to be set up, it's marked as not applicable (n/a)
in the table above.
> - **High Availability:** Yes
> - **Test requests per second (RPS) rates:** API: 500 RPS, Web: 50 RPS, Git: 50 RPS
| Service | Nodes | Configuration | GCP | AWS | Azure |
|-----------------------------------------|-------------|-------------------------|-----------------|-------------|----------|
| External load balancing node | 1 | 4 vCPU, 3.6GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
| Consul | 3 | 2 vCPU, 1.8GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| PostgreSQL | 3 | 8 vCPU, 30GB memory | n1-standard-8 | m5.2xlarge | D8s v3 |
| PgBouncer | 3 | 2 vCPU, 1.8GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| Internal load balancing node | 1 | 2 vCPU, 1.8GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| Redis - Cache | 3 | 4 vCPU, 15GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
| Redis - Queues / Shared State | 3 | 4 vCPU, 15GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
| Redis Sentinel - Cache | 3 | 1 vCPU, 1.7GB memory | g1-small | t2.small | B1MS |
| Redis Sentinel - Queues / Shared State | 3 | 1 vCPU, 1.7GB memory | g1-small | t2.small | B1MS |
| Gitaly | 2 (minimum) | 32 vCPU, 120GB memory | n1-standard-32 | m5.8xlarge | D32s v3 |
| Sidekiq | 4 | 4 vCPU, 15GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
| GitLab Rails | 5 | 32 vCPU, 28.8GB memory | n1-highcpu-32 | c5.9xlarge | F32s v2 |
| Monitoring node | 1 | 4 vCPU, 3.6GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
| Object Storage | n/a | n/a | n/a | n/a | n/a |
| NFS Server | 1 | 4 vCPU, 3.6GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
The Google Cloud Platform (GCP) architectures were built and tested using the
[Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms)
CPU platform. On different hardware you may find that adjustments, either lower
or higher, are required for your CPU or node counts. For more information, see
our [Sysbench](https://github.com/akopytov/sysbench)-based
[CPU benchmark](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
For data objects (such as LFS, Uploads, or Artifacts), an
[object storage service](#configure-the-object-storage) is recommended instead
of NFS where possible, due to better performance and availability. Since this
doesn't require a node to be set up, *Object Storage* is noted as not
applicable (n/a) in the previous table.
## Setup components
......
---
reading_time: true
stage: Enablement
group: Distribution
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/#designated-technical-writers
---
# Reference architecture: up to 50,000 users
This page describes GitLab reference architecture for up to 50,000 users.
For a full list of reference architectures, see
This page describes GitLab reference architecture for up to 50,000 users. For a
full list of reference architectures, see
[Available reference architectures](index.md#available-reference-architectures).
> - **Supported users (approximate):** 50,000
> - **High Availability:** True
> - **Test RPS rates:** API: 1000 RPS, Web: 100 RPS, Git: 100 RPS
| Service | Nodes | Configuration | GCP | AWS | Azure |
|--------------------------------------------------------------|-------|---------------------------------|------------------|-----------------------|----------------|
| External load balancing node | 1 | 8 vCPU, 7.2GB Memory | `n1-highcpu-8` | `c5.2xlarge` | `F8s v2` |
| Consul | 3 | 2 vCPU, 1.8GB Memory | `n1-highcpu-2` | `c5.large` | `F2s v2` |
| PostgreSQL | 3 | 16 vCPU, 60GB Memory | `n1-standard-16` | `m5.4xlarge` | `D16s v3` |
| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | `n1-highcpu-2` | `c5.large` | `F2s v2` |
| Internal load balancing node | 1 | 8 vCPU, 7.2GB Memory | `n1-highcpu-8` | `c5.2xlarge` | `F8s v2` |
| Redis - Cache | 3 | 4 vCPU, 15GB Memory | `n1-standard-4` | `m5.xlarge` | `D4s v3` |
| Redis - Queues / Shared State | 3 | 4 vCPU, 15GB Memory | `n1-standard-4` | `m5.xlarge` | `D4s v3` |
| Redis Sentinel - Cache | 3 | 1 vCPU, 1.7GB Memory | `g1-small` | `t2.small` | `B1MS` |
| Redis Sentinel - Queues / Shared State | 3 | 1 vCPU, 1.7GB Memory | `g1-small` | `t2.small` | `B1MS` |
| Gitaly | 2 minimum | 64 vCPU, 240GB Memory | `n1-standard-64` | `m5.16xlarge` | `D64s v3` |
| Sidekiq | 4 | 4 vCPU, 15GB Memory | `n1-standard-4` | `m5.xlarge` | `D4s v3` |
| GitLab Rails | 12 | 32 vCPU, 28.8GB Memory | `n1-highcpu-32` | `c5.9xlarge` | `F32s v2` |
| Monitoring node | 1 | 4 vCPU, 3.6GB Memory | `n1-highcpu-4` | `c5.xlarge` | `F4s v2` |
| Object Storage | n/a | n/a | n/a | n/a | n/a |
| NFS Server | 1 | 4 vCPU, 3.6GB Memory | `n1-highcpu-4` | `c5.xlarge` | `F4s v2` |
The architectures were built and tested with the [Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms)
CPU platform on GCP. On different hardware you may find that adjustments, either lower
or higher, are required for your CPU or Node counts accordingly. For more information, a
[Sysbench](https://github.com/akopytov/sysbench) benchmark of the CPU can be found
[here](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
For data objects such as LFS, Uploads, Artifacts, etc., an [object storage service](#configure-the-object-storage)
is recommended over NFS where possible, due to better performance and availability.
Since this doesn't require a node to be set up, it's marked as not applicable (n/a)
in the table above.
> - **High Availability:** Yes
> - **Test requests per second (RPS) rates:** API: 1000 RPS, Web: 100 RPS, Git: 100 RPS
| Service | Nodes | Configuration | GCP | AWS | Azure |
|-----------------------------------------|-------------|-------------------------|-----------------|--------------|----------|
| External load balancing node | 1 | 8 vCPU, 7.2GB memory | n1-highcpu-8 | c5.2xlarge | F8s v2 |
| Consul | 3 | 2 vCPU, 1.8GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| PostgreSQL | 3 | 16 vCPU, 60GB memory | n1-standard-16 | m5.4xlarge | D16s v3 |
| PgBouncer | 3 | 2 vCPU, 1.8GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| Internal load balancing node | 1 | 8 vCPU, 7.2GB memory | n1-highcpu-8 | c5.2xlarge | F8s v2 |
| Redis - Cache | 3 | 4 vCPU, 15GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
| Redis - Queues / Shared State | 3 | 4 vCPU, 15GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
| Redis Sentinel - Cache | 3 | 1 vCPU, 1.7GB memory | g1-small | t2.small | B1MS |
| Redis Sentinel - Queues / Shared State | 3 | 1 vCPU, 1.7GB memory | g1-small | t2.small | B1MS |
| Gitaly | 2 (minimum) | 64 vCPU, 240GB memory | n1-standard-64 | m5.16xlarge | D64s v3 |
| Sidekiq | 4 | 4 vCPU, 15GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
| GitLab Rails | 12 | 32 vCPU, 28.8GB memory | n1-highcpu-32 | c5.9xlarge | F32s v2 |
| Monitoring node | 1 | 4 vCPU, 3.6GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
| Object Storage | n/a | n/a | n/a | n/a | n/a |
| NFS Server | 1 | 4 vCPU, 3.6GB memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
The Google Cloud Platform (GCP) architectures were built and tested using the
[Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms)
CPU platform. On different hardware you may find that adjustments, either lower
or higher, are required for your CPU or node counts. For more information, see
our [Sysbench](https://github.com/akopytov/sysbench)-based
[CPU benchmark](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
For data objects (such as LFS, Uploads, or Artifacts), an
[object storage service](#configure-the-object-storage) is recommended instead
of NFS where possible, due to better performance and availability. Since this
doesn't require a node to be set up, *Object Storage* is noted as not
applicable (n/a) in the previous table.
## Setup components
......
......@@ -36,6 +36,105 @@ since point releases bundle many changes together. Minimizing the time
between when versions are out of sync across the fleet may help mitigate
errors caused by upgrades.
## Requirements for zero downtime upgrades
One way to guarantee zero downtime upgrades for on-premise instances is following the
[expand and contract pattern](https://martinfowler.com/bliki/ParallelChange.html).
This means that every breaking change is broken down in three phases: expand, migrate, and contract.
1. **expand**: a breaking change is introduced keeping the software backward-compatible.
1. **migrate**: all consumers are updated to make use of the new implementation.
1. **contract**: backward compatibility is removed.
Those three phases **must be part of different milestones**, to allow zero downtime upgrades.
Depending on the support level for the feature, the contract phase could be delayed until the next major release.
## Expand and contract examples
Route changes, changing Sidekiq worker parameters, and database migrations are all perfect examples of a breaking change.
Let's see how we can handle them safely.
### Route changes
When changing routing we should pay attention to make sure a route generated from the new version can be served by the old one and vice versa.
As you can see in [an example later on this page](#some-links-to-issues-and-mrs-were-broken), not doing it can lead to an outage.
This type of change may look like an immediate switch between the two implementations. However,
especially with the canary stage, there is an extended period of time where both version of the code
coexists in production.
1. **expand**: a new route is added, pointing to the same controller as the old one. But nothing in the application will generate links for the new routes.
1. **migrate**: now that every machine in the fleet can understand the new route, we can generate links with the new routing.
1. **contract**: the old route can be safely removed. (If the old route was likely to be widely shared, like the link to a repository file, we might want to add redirects and keep the old route for a longer period.)
### Changing Sidekiq worker's parameters
This topic is explained in detail in [Sidekiq Compatibility across Updates](sidekiq_style_guide.md#sidekiq-compatibility-across-updates).
When we need to add a new parameter to a Sidekiq worker class, we can split this into the following steps:
1. **expand**: the worker class adds a new parameter with a default value.
1. **migrate**: we add the new parameter to all the invocations of the worker.
1. **contract**: we remove the default value.
At a first look, it may seem safe to bundle expand and migrate into a single milestone, but this will cause an outage if Puma restarts before Sidekiq.
Puma enqueues jobs with an extra parameter that the old Sidekiq cannot handle.
### Database migrations
The following graph is a simplified visual representation of a deployment, this will guide us in understanding how expand and contract is implemented in our migrations strategy.
There's a special consideration here. Using our post-deployment migrations framework allows us to bundle all three phases into one milestone.
```mermaid
gantt
title Deployment
dateFormat HH:mm
section Deploy box
Run migrations :done, migr, after schemaA, 2m
Run post-deployment migrations :postmigr, after mcvn , 2m
section Database
Schema A :done, schemaA, 00:00 , 1h
Schema B :crit, schemaB, after migr, 58m
Schema C. : schmeaC, after postmigr, 1h
section Machine A
Version N :done, mavn, 00:00 , 75m
Version N+1 : after mavn, 105m
section Machine B
Version N :done, mbvn, 00:00 , 105m
Version N+1 : mbdone, after mbvn, 75m
section Machine C
Version N :done, mcvn, 00:00 , 2h
Version N+1 : mbcdone, after mcvn, 1h
```
If we look at this schema from a database point of view, we can see two deployments feed into a single GitLab deployment:
1. from `Schema A` to `Schema B`
1. from `Schema B` to `Schema C`
And these deployments align perfectly with application changes.
1. At the beginning we have `Version N` on `Schema A`.
1. Then we have a _long_ transition periond with both `Version N` and `Version N+1` on `Schema B`.
1. When we only have `Version N+1` on `Schema B` the schema changes again.
1. Finally we have `Version N+1` on `Schema C`.
With all those details in mind, let's imagine we need to replace a query, and this query has an index to support it.
1. **expand**: this is the from `Schema A` to `Schema B` deployment. We add the new index, but the application will ignore it for now
1. **migrate**: this is the `Version N` to `Version N+1` application deployment. The new code is deployed, at this point in time only the new query will run.
1. **contract**: from `Schema B` to `Schema C` (post-deployment migration). Nothing uses the old index anymore, we can safely remove it.
This is only an example. More complex migrations, especially when background migrations are needed will
still require more than one milestone. For details please refer to our [migration style guide](migration_style_guide.md).
## Examples of previous incidents
### Some links to issues and MRs were broken
......
......@@ -334,7 +334,7 @@ To share a given group, for example, 'Frontend' with another group, for example,
All the members of the 'Engineering' group will have been added to 'Frontend'.
## Manage group memberships via LDAP
## Manage group memberships via LDAP **(STARTER ONLY)**
Group syncing allows LDAP groups to be mapped to GitLab groups. This provides more control over per-group user management. To configure group syncing edit the `group_base` **DN** (`'OU=Global Groups,OU=GitLab INT,DC=GitLab,DC=org'`). This **OU** contains all groups that will be associated with GitLab groups.
......
......@@ -268,7 +268,7 @@ Group SAML on a self-managed instance is limited when compared to the recommende
[instance-wide SAML](../../../integration/saml.md). The recommended solution allows you to take advantage of:
- [LDAP compatibility](../../../administration/auth/ldap/index.md).
- [LDAP Group Sync](../index.md#manage-group-memberships-via-ldap).
- [LDAP Group Sync](../index.md#manage-group-memberships-via-ldap-starter-only).
- [Required groups](../../../integration/saml.md#required-groups-starter-only).
- [Admin groups](../../../integration/saml.md#admin-groups-starter-only).
- [Auditor groups](../../../integration/saml.md#auditor-groups-starter-only).
......
......@@ -474,7 +474,7 @@ for details about the pipelines security model.
## LDAP users permissions
Since GitLab 8.15, LDAP user permissions can now be manually overridden by an admin user.
Read through the documentation on [LDAP users permissions](group/index.md#manage-group-memberships-via-ldap) to learn more.
Read through the documentation on [LDAP users permissions](group/index.md#manage-group-memberships-via-ldap-starter-only) to learn more.
## Project aliases
......
......@@ -22,20 +22,20 @@ visit the [administrator documentation](../../integration/elasticsearch.md).
The Advanced Global Search in GitLab is a powerful search service that saves
you time. Instead of creating duplicate code and wasting time, you can
now search for code within other teams that can help your own project.
now search for code within other projects that can help your own project.
GitLab leverages the search capabilities of [Elasticsearch](https://www.elastic.co/elasticsearch/) and enables it when
searching in:
- Projects
- Repositories
- Commits
- Issues
- Merge requests
- Milestones
- Notes (comments)
- Snippets
- Comments
- Code
- Commits
- Wiki
- Users
## Use cases
......
<script>
import { GlTabs, GlTab, GlTooltipDirective } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { sprintf, __, s__ } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import ApprovalStatus from './approval_status.vue';
import Approvers from './approvers.vue';
import Cookies from 'js-cookie';
import { GlTabs, GlTab } from '@gitlab/ui';
import { __ } from '~/locale';
import MergeRequestsGrid from './merge_requests/grid.vue';
import EmptyState from './empty_state.vue';
import MergeRequest from './merge_request.vue';
import Pagination from './pagination.vue';
import PipelineStatus from './pipeline_status.vue';
import GridColumnHeading from './grid_column_heading.vue';
import { COMPLIANCE_TAB_COOKIE_KEY } from '../constants';
export default {
name: 'ComplianceDashboard',
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
ApprovalStatus,
Approvers,
MergeRequestsGrid,
EmptyState,
GridColumnHeading,
MergeRequest,
Pagination,
PipelineStatus,
GlTab,
GlTabs,
},
mixins: [timeagoMixin],
props: {
emptyStateSvgPath: {
type: String,
......@@ -51,28 +35,13 @@ export default {
},
},
methods: {
key(id, value) {
return `${id}-${value}`;
},
timeAgoString(mergedAt) {
return sprintf(s__('merged %{timeAgo}'), {
timeAgo: this.timeFormatted(mergedAt),
});
},
timeTooltip(mergedAt) {
return this.tooltipTitle(mergedAt);
},
hasStatus(status) {
return !isEmpty(status);
showTabs() {
return Cookies.get(COMPLIANCE_TAB_COOKIE_KEY) === 'true';
},
},
strings: {
heading: __('Compliance Dashboard'),
subheading: __('Here you will find recent merge request activity'),
mergeRequestLabel: __('Merge Request'),
approvalStatusLabel: __('Approval Status'),
pipelineStatusLabel: __('Pipeline'),
updatesLabel: __('Updates'),
mergeRequestsTabLabel: __('Merge Requests'),
},
};
......@@ -84,61 +53,16 @@ export default {
<h4>{{ $options.strings.heading }}</h4>
<p>{{ $options.strings.subheading }}</p>
</header>
<gl-tabs>
<gl-tabs v-if="showTabs()">
<gl-tab>
<template #title>
<span>{{ $options.strings.mergeRequestsTabLabel }}</span>
</template>
<div class="dashboard-grid">
<grid-column-heading :heading="$options.strings.mergeRequestLabel" />
<grid-column-heading
:heading="$options.strings.approvalStatusLabel"
class="gl-text-center"
/>
<grid-column-heading
:heading="$options.strings.pipelineStatusLabel"
class="gl-text-center"
/>
<grid-column-heading :heading="$options.strings.updatesLabel" class="gl-text-right" />
<template v-for="mergeRequest in mergeRequests">
<merge-request :key="key(mergeRequest.id, 'MR')" :merge-request="mergeRequest" />
<div
:key="key(mergeRequest.id, 'approvalStatus')"
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-p-5"
>
<approval-status
v-if="hasStatus(mergeRequest.approval_status)"
:status="mergeRequest.approval_status"
/>
</div>
<div
:key="key(mergeRequest.id, 'pipeline')"
class="dashboard-pipeline gl-display-flex gl-align-items-center gl-justify-content-center gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-p-5"
>
<pipeline-status
v-if="hasStatus(mergeRequest.pipeline_status)"
:status="mergeRequest.pipeline_status"
/>
</div>
<div
:key="key(mergeRequest.id, 'updates')"
class="gl-text-right gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-p-5 gl-relative"
>
<approvers :approvers="mergeRequest.approved_by_users" />
<span class="gl-text-gray-700">
<time v-gl-tooltip.bottom="timeTooltip(mergeRequest.merged_at)">{{
timeAgoString(mergeRequest.merged_at)
}}</time>
</span>
</div>
</template>
</div>
<pagination :is-last-page="isLastPage" />
<merge-requests-grid :merge-requests="mergeRequests" :is-last-page="isLastPage" />
</gl-tab>
</gl-tabs>
<merge-requests-grid v-else :merge-requests="mergeRequests" :is-last-page="isLastPage" />
</div>
<empty-state v-else :image-path="emptyStateSvgPath" />
</template>
<script>
import { sprintf, __ } from '~/locale';
import { GlAvatarLink, GlAvatar, GlAvatarsInline, GlTooltipDirective } from '@gitlab/ui';
import { PRESENTABLE_APPROVERS_LIMIT } from '../constants';
import { PRESENTABLE_APPROVERS_LIMIT } from '../../constants';
export default {
directives: {
......
<script>
import { GlTooltipDirective } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { sprintf, __ } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import ApprovalStatus from './approval_status.vue';
import Approvers from './approvers.vue';
import MergeRequest from './merge_request.vue';
import PipelineStatus from './pipeline_status.vue';
import GridColumnHeading from '../shared/grid_column_heading.vue';
import Pagination from '../shared/pagination.vue';
export default {
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
ApprovalStatus,
Approvers,
GridColumnHeading,
MergeRequest,
PipelineStatus,
Pagination,
},
mixins: [timeagoMixin],
props: {
mergeRequests: {
type: Array,
required: true,
},
isLastPage: {
type: Boolean,
required: false,
default: false,
},
},
methods: {
key(id, value) {
return `${id}-${value}`;
},
timeAgoString(mergedAt) {
return sprintf(__('merged %{timeAgo}'), {
timeAgo: this.timeFormatted(mergedAt),
});
},
timeTooltip(mergedAt) {
return this.tooltipTitle(mergedAt);
},
hasStatus(status) {
return !isEmpty(status);
},
},
strings: {
mergeRequestLabel: __('Merge Request'),
approvalStatusLabel: __('Approval Status'),
pipelineStatusLabel: __('Pipeline'),
updatesLabel: __('Updates'),
},
keyTypes: {
mergeRequest: 'MR',
approvalStatus: 'approvalStatus',
pipeline: 'pipeline',
updates: 'updates',
},
};
</script>
<template>
<div>
<div class="dashboard-grid">
<grid-column-heading :heading="$options.strings.mergeRequestLabel" />
<grid-column-heading :heading="$options.strings.approvalStatusLabel" class="gl-text-center" />
<grid-column-heading :heading="$options.strings.pipelineStatusLabel" class="gl-text-center" />
<grid-column-heading :heading="$options.strings.updatesLabel" class="gl-text-right" />
<template v-for="mergeRequest in mergeRequests">
<merge-request
:key="key(mergeRequest.id, $options.keyTypes.mergeRequest)"
:merge-request="mergeRequest"
/>
<div
:key="key(mergeRequest.id, $options.keyTypes.approvalStatus)"
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-p-5"
>
<approval-status
v-if="hasStatus(mergeRequest.approval_status)"
:status="mergeRequest.approval_status"
/>
</div>
<div
:key="key(mergeRequest.id, $options.keyTypes.pipeline)"
class="dashboard-pipeline gl-display-flex gl-align-items-center gl-justify-content-center gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-p-5"
>
<pipeline-status
v-if="hasStatus(mergeRequest.pipeline_status)"
:status="mergeRequest.pipeline_status"
/>
</div>
<div
:key="key(mergeRequest.id, $options.keyTypes.updates)"
class="gl-text-right gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-p-5 gl-relative"
>
<approvers :approvers="mergeRequest.approved_by_users" />
<span class="gl-text-gray-700">
<time v-gl-tooltip.bottom="timeTooltip(mergeRequest.merged_at)">{{
timeAgoString(mergeRequest.merged_at)
}}</time>
</span>
</div>
</template>
</div>
<pagination class="gl-mt-5" :is-last-page="isLastPage" />
</div>
</template>
export const PRESENTABLE_APPROVERS_LIMIT = 2;
export default {};
export const COMPLIANCE_TAB_COOKIE_KEY = 'compliance_dashboard_tabs';
<script>
import * as Sentry from '@sentry/browser';
import { GlButton, GlTab, GlTabs } from '@gitlab/ui';
import ProfilesListing from './dast_profiles_listing.vue';
import ProfilesList from './dast_profiles_list.vue';
import dastSiteProfilesQuery from '../graphql/dast_site_profiles.query.graphql';
export default {
components: {
GlButton,
GlTab,
GlTabs,
ProfilesListing,
ProfilesList,
},
props: {
newDastSiteProfilePath: {
type: String,
required: true,
},
projectFullPath: {
type: String,
required: true,
},
},
data() {
return {
siteProfiles: [],
siteProfilesPageInfo: {},
hasSiteProfilesLoadingError: false,
};
},
apollo: {
siteProfiles: {
query: dastSiteProfilesQuery,
variables() {
return {
fullPath: this.projectFullPath,
first: this.$options.profilesPerPage,
};
},
result({ data, error }) {
if (!error) {
this.siteProfilesPageInfo = data.project.siteProfiles.pageInfo;
}
},
update(data) {
const siteProfileEdges = data?.project?.siteProfiles?.edges ?? [];
return siteProfileEdges.map(({ node }) => node);
},
error(e) {
this.handleLoadingError(e);
},
},
},
computed: {
hasMoreSiteProfiles() {
return this.siteProfilesPageInfo.hasNextPage;
},
isLoadingSiteProfiles() {
return this.$apollo.queries.siteProfiles.loading;
},
},
methods: {
handleLoadingError(e) {
Sentry.captureException(e);
this.hasSiteProfilesLoadingError = true;
},
fetchMoreProfiles() {
const { $apollo, siteProfilesPageInfo } = this;
this.hasSiteProfilesLoadingError = false;
$apollo.queries.siteProfiles
.fetchMore({
variables: { after: siteProfilesPageInfo.endCursor },
updateQuery: (previousResult, { fetchMoreResult }) => {
const newResult = { ...fetchMoreResult };
const previousEdges = previousResult.project.siteProfiles.edges;
const newEdges = newResult.project.siteProfiles.edges;
newResult.project.siteProfiles.edges = [...previousEdges, ...newEdges];
return newResult;
},
})
.catch(e => {
this.handleLoadingError(e);
});
},
},
profilesPerPage: 10,
};
</script>
......@@ -42,13 +116,21 @@ export default {
}}
</p>
</header>
<gl-tabs>
<gl-tab>
<template #title>
<span>{{ s__('DastProfiles|Site Profiles') }}</span>
</template>
<profiles-listing />
<profiles-list
:has-error="hasSiteProfilesLoadingError"
:has-more-profiles-to-load="hasMoreSiteProfiles"
:is-loading="isLoadingSiteProfiles"
:profiles-per-page="$options.profilesPerPage"
:profiles="siteProfiles"
@loadMoreProfiles="fetchMoreProfiles"
/>
</gl-tab>
</gl-tabs>
</section>
......
<script>
import {
GlAlert,
GlButton,
GlIcon,
GlSkeletonLoader,
GlTable,
GlTooltipDirective,
} from '@gitlab/ui';
export default {
components: {
GlAlert,
GlButton,
GlIcon,
GlSkeletonLoader,
GlTable,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
profiles: {
type: Array,
required: true,
},
hasError: {
type: Boolean,
required: false,
default: false,
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
profilesPerPage: {
type: Number,
required: true,
},
hasMoreProfilesToLoad: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
isErrorDismissed: false,
};
},
computed: {
hasProfiles() {
return this.profiles.length > 0;
},
isLoadingInitialProfiles() {
return this.isLoading && !this.hasProfiles;
},
shouldShowTable() {
return this.isLoadingInitialProfiles || this.hasProfiles || this.hasError;
},
},
tableFields: [
{
key: 'profileName',
class: 'gl-word-break-all',
},
{
key: 'targetUrl',
class: 'gl-word-break-all',
},
{
key: 'validationStatus',
// NOTE: hidden for now, since the site validation is still WIP and will be finished in an upcoming iteration
// roadmap: https://gitlab.com/groups/gitlab-org/-/epics/2912#ui-configuration
class: 'gl-display-none',
},
{
key: 'actions',
},
],
};
</script>
<template>
<section>
<div v-if="shouldShowTable">
<gl-table
:aria-label="s__('DastProfiles|Site Profiles')"
:busy="isLoadingInitialProfiles"
:fields="$options.tableFields"
:items="profiles"
stacked="md"
thead-class="gl-display-none"
>
<template #cell(profileName)="{ value }">
<strong>{{ value }}</strong>
</template>
<template #cell(validationStatus)="{ value }">
<span>
<gl-icon
:size="16"
class="gl-vertical-align-text-bottom gl-text-gray-600"
name="information-o"
/>
{{ value }}
</span>
</template>
<template #cell(actions)>
<!--
NOTE: The tooltip and `disable` on the button is temporary until the edit feature has been implemented
further details: https://gitlab.com/gitlab-org/gitlab/-/issues/222479#proposal
-->
<span
v-gl-tooltip.hover
:title="
s__(
'DastProfiles|Edit feature will come soon. Please create a new profile if changes needed',
)
"
>
<gl-button disabled>{{ __('Edit') }}</gl-button>
</span>
</template>
<template #table-busy>
<div v-for="i in profilesPerPage" :key="i" data-testid="loadingIndicator">
<gl-skeleton-loader :width="1248" :height="52">
<rect x="0" y="16" width="300" height="20" rx="4" />
<rect x="380" y="16" width="300" height="20" rx="4" />
<rect x="770" y="16" width="300" height="20" rx="4" />
<rect x="1140" y="11" width="50" height="30" rx="4" />
</gl-skeleton-loader>
</div>
</template>
<template v-if="hasError && !isErrorDismissed" #bottom-row>
<td :colspan="$options.tableFields.length">
<gl-alert class="gl-my-4" variant="danger" :dismissible="false">
{{
s__(
'DastProfiles|Error fetching the profiles list. Please check your network connection and try again.',
)
}}
</gl-alert>
</td>
</template>
</gl-table>
<p v-if="hasMoreProfilesToLoad" class="gl-display-flex gl-justify-content-center">
<gl-button
data-testid="loadMore"
:loading="isLoading && !hasError"
@click="$emit('loadMoreProfiles')"
>{{ __('Load more') }}</gl-button
>
</p>
</div>
<p v-else class="gl-my-4">
{{ s__('DastProfiles|No profiles created yet') }}
</p>
</section>
</template>
<template>
<section class="gl-py-3">
<p>{{ s__('DastProfiles|No profiles created yet') }}</p>
</section>
</template>
......@@ -10,11 +10,12 @@ export default () => {
}
const {
dataset: { newDastSiteProfilePath },
dataset: { newDastSiteProfilePath, projectFullPath },
} = el;
const props = {
newDastSiteProfilePath,
projectFullPath,
};
return new Vue({
......
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
query DASTSiteProfiles($fullPath: ID!, $after: String, $before: String, $first: Int, $last: Int) {
project(fullPath: $fullPath) {
siteProfiles: DASTSiteProfiles(after: $after, before: $before, first: $first, last: $last) {
pageInfo {
...PageInfo
}
edges {
cursor
node {
id
profileName
targetUrl
validationStatus
}
}
}
}
}
......@@ -126,7 +126,7 @@ export default {
<header class="gl-mb-6">
<h2>{{ s__('OnDemandScans|New on-demand DAST scan') }}</h2>
<p>
<gl-icon name="information-o" class="gl-vertical-align-text-bottom gl-text-gray-600" />
<gl-icon name="information-o" class="gl-vertical-align-text-bottom gl-text-gray-400" />
<gl-sprintf
:message="
s__(
......@@ -149,7 +149,7 @@ export default {
<gl-icon
v-gl-tooltip.hover
name="information-o"
class="gl-vertical-align-text-bottom gl-text-gray-600"
class="gl-vertical-align-text-bottom gl-text-gray-400"
:title="s__('OnDemandScans|Only a passive scan can be performed on demand.')"
/>
</template>
......@@ -162,7 +162,7 @@ export default {
<gl-icon
v-gl-tooltip.hover
name="information-o"
class="gl-vertical-align-text-bottom gl-text-gray-600"
class="gl-vertical-align-text-bottom gl-text-gray-400"
:title="s__('OnDemandScans|Attached branch is where the scan job runs.')"
/>
</template>
......@@ -175,7 +175,7 @@ export default {
<gl-icon
v-gl-tooltip.hover
name="information-o"
class="gl-vertical-align-text-bottom gl-text-gray-600"
class="gl-vertical-align-text-bottom gl-text-gray-400"
:title="s__('OnDemandScans|DAST will scan the target URL and any discovered sub URLs.')"
/>
</template>
......
import initMergeRequestAnalyticsApp from 'ee/analytics/merge_request_analytics';
document.addEventListener('DOMContentLoaded', initMergeRequestAnalyticsApp);
......@@ -57,7 +57,7 @@ export default {
:action-secondary="downloadButton"
>
<!-- heading -->
<div class="row gl-text-gray-600">
<div class="row gl-text-gray-400">
<div class="col-1">{{ __('Method') }}</div>
<div class="col-11">{{ __('URL') }}</div>
</div>
......
......@@ -4,7 +4,6 @@ import createFlash from '~/flash';
import EventItem from 'ee/vue_shared/security_reports/components/event_item.vue';
import { __, s__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import HistoryCommentEditor from './history_comment_editor.vue';
export default {
......@@ -43,9 +42,6 @@ export default {
},
computed: {
noteIdUrl() {
return joinPaths(this.notesUrl, this.comment.id);
},
commentNote() {
return this.comment?.note;
},
......@@ -72,7 +68,7 @@ export default {
getSaveConfig(note) {
const isUpdatingComment = Boolean(this.comment);
const method = isUpdatingComment ? 'put' : 'post';
const url = isUpdatingComment ? this.noteIdUrl : this.notesUrl;
const url = isUpdatingComment ? this.comment.path : this.notesUrl;
const data = { note: { note } };
const emitName = isUpdatingComment ? 'onCommentUpdated' : 'onCommentAdded';
......@@ -105,7 +101,7 @@ export default {
},
deleteComment() {
this.isDeletingComment = true;
const deleteUrl = this.noteIdUrl;
const deleteUrl = this.comment.path;
axios
.delete(deleteUrl)
......
......@@ -195,7 +195,7 @@ html.group-epics-roadmap-html {
.item-label,
.item-sublabel .sublabel-value {
color: $gray-600;
color: $gray-400;
font-weight: 400;
&.label-dark {
......
......@@ -5,4 +5,4 @@
= f.check_box :delayed_project_removal, checked: group.delayed_project_removal?, class: 'form-check-input'
= f.label :delayed_project_removal, class: 'form-check-label' do
%span.gl-display-block= s_('GroupSettings|Enable delayed project removal')
%span.gl-text-gray-600= s_('GroupSettings|Projects will be permanently deleted after a %{waiting_period}-day waiting period. This period can be %{customization_link} in instance settings').html_safe % { waiting_period: ::Gitlab::CurrentSettings.deletion_adjourned_period, customization_link: link_to('customized by an admin', general_admin_application_settings_path) }
%span.gl-text-gray-400= s_('GroupSettings|Projects will be permanently deleted after a %{waiting_period}-day waiting period. This period can be %{customization_link} in instance settings').html_safe % { waiting_period: ::Gitlab::CurrentSettings.deletion_adjourned_period, customization_link: link_to('customized by an admin', general_admin_application_settings_path) }
- page_title _("Merge Request Analytics")
#js-merge-request-analytics-app
......@@ -2,4 +2,5 @@
- breadcrumb_title s_('DastProfiles|Manage profiles')
- page_title s_('DastProfiles|Manage profiles')
.js-dast-profiles{ data: { new_dast_site_profile_path: new_namespace_project_dast_site_profile_path(namespace_id: @project.namespace, project_id: @project.path) } }
.js-dast-profiles{ data: { new_dast_site_profile_path: new_namespace_project_dast_site_profile_path(namespace_id: @project.namespace, project_id: @project.path),
project_full_path: @project.path_with_namespace } }
......@@ -164,7 +164,7 @@
:idempotent:
:tags: []
- :name: cronjob:historical_data
:feature_category: :license_compliance
:feature_category: :billing
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
......
......@@ -7,7 +7,7 @@ class HistoricalDataWorker # rubocop:disable Scalability/IdempotentWorker
include CronjobQueue
# rubocop:enable Scalability/CronWorkerContext
feature_category :license_compliance
feature_category :billing
def perform
return if License.current.nil? || License.current&.trial?
......
---
title: Updating $gray-600 hex value and replacing instances with $gray-400
merge_request: 38448
author:
type: other
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ComplianceDashboard component when there are merge requests matches the snapshot 1`] = `
exports[`ComplianceDashboard component when there are merge requests and the show tabs cookie is true matches the snapshot 1`] = `
<div
class="compliance-dashboard"
>
......@@ -26,97 +26,9 @@ exports[`ComplianceDashboard component when there are merge requests matches the
>
<template>
<div
class="dashboard-grid"
>
<grid-column-heading-stub
heading="Merge Request"
/>
<grid-column-heading-stub
class="gl-text-center"
heading="Approval Status"
/>
<grid-column-heading-stub
class="gl-text-center"
heading="Pipeline"
/>
<grid-column-heading-stub
class="gl-text-right"
heading="Updates"
/>
<div
data-testid="merge-request"
>
Merge request 0
</div>
<div
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-p-5"
>
<!---->
</div>
<div
class="dashboard-pipeline gl-display-flex gl-align-items-center gl-justify-content-center gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-p-5"
>
<!---->
</div>
<div
class="gl-text-right gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-p-5 gl-relative"
>
<approvers-stub
approvers=""
/>
<span
class="gl-text-gray-700"
>
<time>
merged 2 days ago
</time>
</span>
</div>
<div
data-testid="merge-request"
>
Merge request 1
</div>
<div
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-p-5"
>
<!---->
</div>
<div
class="dashboard-pipeline gl-display-flex gl-align-items-center gl-justify-content-center gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-p-5"
>
<!---->
</div>
<div
class="gl-text-right gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-p-5 gl-relative"
>
<approvers-stub
approvers=""
/>
<span
class="gl-text-gray-700"
>
<time>
merged 2 days ago
</time>
</span>
</div>
</div>
<pagination-stub />
<merge-requests-grid-stub
mergerequests="[object Object],[object Object]"
/>
</template>
<template>
<span>
......@@ -128,6 +40,28 @@ exports[`ComplianceDashboard component when there are merge requests matches the
</div>
`;
exports[`ComplianceDashboard component when there are merge requests matches the snapshot 1`] = `
<div
class="compliance-dashboard"
>
<header
class="gl-my-5"
>
<h4>
Compliance Dashboard
</h4>
<p>
Here you will find recent merge request activity
</p>
</header>
<merge-requests-grid-stub
mergerequests="[object Object],[object Object]"
/>
</div>
`;
exports[`ComplianceDashboard component when there are no merge requests matches the snapshot 1`] = `
<empty-state-stub
imagepath="empty.svg"
......
import Cookies from 'js-cookie';
import { shallowMount } from '@vue/test-utils';
import { GlTabs, GlTab } from '@gitlab/ui';
import ComplianceDashboard from 'ee/compliance_dashboard/components/dashboard.vue';
import ApprovalStatus from 'ee/compliance_dashboard/components/approval_status.vue';
import PipelineStatus from 'ee/compliance_dashboard/components/pipeline_status.vue';
import Approvers from 'ee/compliance_dashboard/components/approvers.vue';
import MergeRequestGrid from 'ee/compliance_dashboard/components/merge_requests/grid.vue';
import { COMPLIANCE_TAB_COOKIE_KEY } from 'ee/compliance_dashboard/constants';
import { createMergeRequests } from '../mock_data';
describe('ComplianceDashboard component', () => {
let wrapper;
const findMergeRequests = () => wrapper.findAll('[data-testid="merge-request"]');
const findTime = () => wrapper.find('time');
const findApprovalStatus = () => wrapper.find(ApprovalStatus);
const findPipelineStatus = () => wrapper.find(PipelineStatus);
const findApprovers = () => wrapper.find(Approvers);
const isLastPage = false;
const mergeRequests = createMergeRequests({ count: 2 });
const findMergeRequestsGrid = () => wrapper.find(MergeRequestGrid);
const findDashboardTabs = () => wrapper.find(GlTabs);
const createComponent = (props = {}, options = {}) => {
const createComponent = (props = {}) => {
return shallowMount(ComplianceDashboard, {
propsData: {
mergeRequests: createMergeRequests({ count: 2, options }),
isLastPage: false,
mergeRequests,
isLastPage,
emptyStateSvgPath: 'empty.svg',
...props,
},
stubs: {
GlTab,
MergeRequest: {
props: { mergeRequest: Object },
template: `<div data-testid="merge-request">{{ mergeRequest.title }}</div>`,
},
},
});
};
......@@ -41,50 +36,41 @@ describe('ComplianceDashboard component', () => {
describe('when there are merge requests', () => {
beforeEach(() => {
Cookies.set(COMPLIANCE_TAB_COOKIE_KEY, false);
wrapper = createComponent();
});
afterEach(() => {
Cookies.remove(COMPLIANCE_TAB_COOKIE_KEY);
});
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('renders a list of merge requests', () => {
expect(findMergeRequests().length).toEqual(2);
it('renders the merge requests', () => {
expect(findMergeRequestsGrid().exists()).toBe(true);
});
it('renders the dashboard tabs', () => {
expect(findDashboardTabs().exists()).toEqual(true);
it('sets the MergeRequestGrid properties', () => {
expect(findMergeRequestsGrid().props('mergeRequests')).toBe(mergeRequests);
expect(findMergeRequestsGrid().props('isLastPage')).toBe(isLastPage);
});
describe('approval status', () => {
it('does not render if there is no approval status', () => {
expect(findApprovalStatus().exists()).toBe(false);
describe('and the show tabs cookie is true', () => {
beforeEach(() => {
Cookies.set(COMPLIANCE_TAB_COOKIE_KEY, true);
wrapper = createComponent();
});
it('renders if there is an approval status', () => {
wrapper = createComponent({}, { approvalStatus: 'success' });
expect(findApprovalStatus().exists()).toBe(true);
});
});
describe('pipeline status', () => {
it('does not render if there is no pipeline', () => {
expect(findPipelineStatus().exists()).toBe(false);
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('renders if there is a pipeline', () => {
wrapper = createComponent({}, { addPipeline: true });
expect(findPipelineStatus().exists()).toBe(true);
it('renders the dashboard tabs', () => {
expect(findDashboardTabs().exists()).toBe(true);
});
});
it('renders the approvers list', () => {
expect(findApprovers().exists()).toBe(true);
});
it('renders the "merged at" time', () => {
expect(findTime().text()).toEqual('merged 2 days ago');
});
});
describe('when there are no merge requests', () => {
......@@ -97,11 +83,7 @@ describe('ComplianceDashboard component', () => {
});
it('does not render merge requests', () => {
expect(findMergeRequests().exists()).toEqual(false);
});
it('does not render the dashboard tabs', () => {
expect(findDashboardTabs().exists()).toEqual(false);
expect(findMergeRequestsGrid().exists()).toBe(false);
});
});
});
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MergeRequestsGrid component when intialized matches the snapshot 1`] = `
<div>
<div
class="dashboard-grid"
>
<grid-column-heading-stub
heading="Merge Request"
/>
<grid-column-heading-stub
class="gl-text-center"
heading="Approval Status"
/>
<grid-column-heading-stub
class="gl-text-center"
heading="Pipeline"
/>
<grid-column-heading-stub
class="gl-text-right"
heading="Updates"
/>
<div
data-testid="merge-request"
>
Merge request 0
</div>
<div
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-p-5"
>
<!---->
</div>
<div
class="dashboard-pipeline gl-display-flex gl-align-items-center gl-justify-content-center gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-p-5"
>
<!---->
</div>
<div
class="gl-text-right gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-p-5 gl-relative"
>
<approvers-stub
approvers=""
/>
<span
class="gl-text-gray-700"
>
<time>
merged 2 days ago
</time>
</span>
</div>
<div
data-testid="merge-request"
>
Merge request 1
</div>
<div
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-p-5"
>
<!---->
</div>
<div
class="dashboard-pipeline gl-display-flex gl-align-items-center gl-justify-content-center gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-p-5"
>
<!---->
</div>
<div
class="gl-text-right gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-p-5 gl-relative"
>
<approvers-stub
approvers=""
/>
<span
class="gl-text-gray-700"
>
<time>
merged 2 days ago
</time>
</span>
</div>
</div>
<pagination-stub
class="gl-mt-5"
/>
</div>
`;
import { shallowMount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
import ApprovalStatus from 'ee/compliance_dashboard/components/approval_status.vue';
import ApprovalStatus from 'ee/compliance_dashboard/components/merge_requests/approval_status.vue';
describe('ApprovalStatus component', () => {
let wrapper;
......
import { shallowMount } from '@vue/test-utils';
import { GlAvatarLink } from '@gitlab/ui';
import Approvers from 'ee/compliance_dashboard/components/approvers.vue';
import Approvers from 'ee/compliance_dashboard/components/merge_requests/approvers.vue';
import { PRESENTABLE_APPROVERS_LIMIT } from 'ee/compliance_dashboard/constants';
import { createApprovers } from '../mock_data';
import { createApprovers } from '../../mock_data';
describe('MergeRequest component', () => {
let wrapper;
......
import { shallowMount } from '@vue/test-utils';
import MergeRequestsGrid from 'ee/compliance_dashboard/components/merge_requests/grid.vue';
import ApprovalStatus from 'ee/compliance_dashboard/components/merge_requests/approval_status.vue';
import PipelineStatus from 'ee/compliance_dashboard/components/merge_requests/pipeline_status.vue';
import Approvers from 'ee/compliance_dashboard/components/merge_requests/approvers.vue';
import { createMergeRequests } from '../../mock_data';
describe('MergeRequestsGrid component', () => {
let wrapper;
const findMergeRequests = () => wrapper.findAll('[data-testid="merge-request"]');
const findTime = () => wrapper.find('time');
const findApprovalStatus = () => wrapper.find(ApprovalStatus);
const findPipelineStatus = () => wrapper.find(PipelineStatus);
const findApprovers = () => wrapper.find(Approvers);
const createComponent = (options = {}) => {
return shallowMount(MergeRequestsGrid, {
propsData: {
mergeRequests: createMergeRequests({ count: 2, options }),
isLastPage: false,
},
stubs: {
MergeRequest: {
props: { mergeRequest: Object },
template: `<div data-testid="merge-request">{{ mergeRequest.title }}</div>`,
},
},
});
};
afterEach(() => {
wrapper.destroy();
});
describe('when intialized', () => {
beforeEach(() => {
wrapper = createComponent();
});
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('renders a list of merge requests', () => {
expect(findMergeRequests().length).toBe(2);
});
describe('approval status', () => {
it('does not render if there is no approval status', () => {
expect(findApprovalStatus().exists()).toBe(false);
});
it('renders if there is an approval status', () => {
wrapper = createComponent({ approvalStatus: 'success' });
expect(findApprovalStatus().exists()).toBe(true);
});
});
describe('pipeline status', () => {
it('does not render if there is no pipeline', () => {
expect(findPipelineStatus().exists()).toBe(false);
});
it('renders if there is a pipeline', () => {
wrapper = createComponent({ addPipeline: true });
expect(findPipelineStatus().exists()).toBe(true);
});
});
it('renders the approvers list', () => {
expect(findApprovers().exists()).toBe(true);
});
it('renders the "merged at" time', () => {
expect(findTime().text()).toBe('merged 2 days ago');
});
});
});
import { shallowMount } from '@vue/test-utils';
import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
import MergeRequest from 'ee/compliance_dashboard/components/merge_request.vue';
import { createMergeRequest } from '../mock_data';
import MergeRequest from 'ee/compliance_dashboard/components/merge_requests/merge_request.vue';
import { createMergeRequest } from '../../mock_data';
describe('MergeRequest component', () => {
let wrapper;
......
import { shallowMount } from '@vue/test-utils';
import PipelineStatus from 'ee/compliance_dashboard/components/pipeline_status.vue';
import { createPipelineStatus } from '../mock_data';
import PipelineStatus from 'ee/compliance_dashboard/components/merge_requests/pipeline_status.vue';
import { createPipelineStatus } from '../../mock_data';
describe('PipelineStatus component', () => {
let wrapper;
......
import { shallowMount } from '@vue/test-utils';
import GridColumnHeading from 'ee/compliance_dashboard/components/grid_column_heading.vue';
import GridColumnHeading from 'ee/compliance_dashboard/components/shared/grid_column_heading.vue';
describe('GridColumnHeading component', () => {
let wrapper;
......
import { shallowMount } from '@vue/test-utils';
import { GlPagination } from '@gitlab/ui';
import Pagination from 'ee/compliance_dashboard/components/pagination.vue';
import Pagination from 'ee/compliance_dashboard/components/shared/pagination.vue';
describe('MergeRequest component', () => {
describe('Pagination component', () => {
let wrapper;
const findGlPagination = () => wrapper.find(GlPagination);
......
import { merge } from 'lodash';
import { mount } from '@vue/test-utils';
import { within } from '@testing-library/dom';
import DastProfilesList from 'ee/dast_profiles/components/dast_profiles_list.vue';
describe('EE - DastProfilesList', () => {
let wrapper;
const createComponent = (options = {}) => {
const defaultProps = {
profiles: [],
hasMorePages: false,
profilesPerPage: 10,
};
wrapper = mount(
DastProfilesList,
merge(
{},
{
propsData: defaultProps,
},
options,
),
);
};
const withinComponent = () => within(wrapper.element);
const getTable = () => withinComponent().getByRole('table', { name: /site profiles/i });
const getAllRowGroups = () => within(getTable()).getAllByRole('rowgroup');
const getTableBody = () => {
// first item is the table head
const [, tableBody] = getAllRowGroups();
return tableBody;
};
const getAllTableRows = () => within(getTableBody()).getAllByRole('row');
const getLoadMoreButton = () => wrapper.find('[data-testid="loadMore"]');
const getAllLoadingIndicators = () => withinComponent().queryAllByTestId('loadingIndicator');
const getErrorMessage = () => withinComponent().queryByText(/error fetching the profiles list/i);
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('when loading', () => {
const profilesPerPage = 10;
describe('initial load', () => {
beforeEach(() => {
createComponent({ propsData: { isLoading: true, profilesPerPage } });
});
it('shows a loading indicator for each profile item', () => {
expect(getAllLoadingIndicators()).toHaveLength(profilesPerPage);
});
});
describe('with profiles and more to load', () => {
beforeEach(() => {
createComponent({
propsData: {
isLoading: true,
profilesPerPage,
profiles: [{}],
hasMoreProfilesToLoad: true,
},
});
});
it('does not show a loading indicator for each profile item', () => {
expect(getAllLoadingIndicators()).toHaveLength(0);
});
it('sets the the "load more" button into a loading state', () => {
expect(getLoadMoreButton().props('loading')).toBe(true);
});
});
});
describe('with no existing profiles', () => {
it('shows a message to indicate that no profiles exist', () => {
createComponent();
const emptyStateMessage = withinComponent().getByText(/no profiles created yet/i);
expect(emptyStateMessage).not.toBe(null);
});
});
describe('with existing profiles', () => {
const profiles = [
{
id: 1,
profileName: 'Profile 1',
targetUrl: 'http://example-1.com',
validationStatus: 'Pending',
},
{
id: 2,
profileName: 'Profile 2',
targetUrl: 'http://example-2.com',
validationStatus: 'Pending',
},
];
const getTableRowForProfile = profile => getAllTableRows()[profiles.indexOf(profile)];
it('does not show loading indicators', () => {
createComponent({});
expect(getAllLoadingIndicators()).toHaveLength(0);
});
describe('profiles list', () => {
beforeEach(() => {
createComponent({ propsData: { profiles } });
});
it('renders a list of profiles', () => {
expect(getTable()).not.toBe(null);
expect(getAllTableRows()).toHaveLength(profiles.length);
});
it.each(profiles)('renders list item %# correctly', profile => {
const [
profileCell,
targetUrlCell,
validationStatusCell,
actionsCell,
] = getTableRowForProfile(profile).cells;
expect(profileCell.innerText).toContain(profile.profileName);
expect(targetUrlCell.innerText).toContain(profile.targetUrl);
expect(validationStatusCell.innerText).toContain(profile.validationStatus);
expect(within(actionsCell).getByRole('button', { name: /edit/i })).not.toBe(null);
});
});
describe('load more profiles', () => {
it('does not show that there are more projects to be loaded per default', () => {
createComponent({ propsData: { profiles } });
expect(getLoadMoreButton().exists()).toBe(false);
});
describe('with more profiles', () => {
beforeEach(() => {
createComponent({ propsData: { profiles, hasMoreProfilesToLoad: true } });
});
it('shows that there are more projects to be loaded', () => {
expect(getLoadMoreButton().exists()).toBe(true);
});
it('emits "loadMore" when the load-more button is clicked', async () => {
expect(wrapper.emitted('loadMoreProfiles')).toBe(undefined);
await getLoadMoreButton().trigger('click');
expect(wrapper.emitted('loadMoreProfiles')).toEqual(expect.any(Array));
});
});
});
});
describe('errors', () => {
it('does not show an error message by default', () => {
createComponent();
expect(getErrorMessage()).toBe(null);
});
it('shows an error message', () => {
createComponent({ propsData: { hasError: true } });
expect(getErrorMessage()).not.toBe(null);
});
});
});
import { shallowMount } from '@vue/test-utils';
import { within } from '@testing-library/dom';
import DastProfilesListing from 'ee/dast_profiles/components/dast_profiles_listing.vue';
describe('EE - DastProfilesListing', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMount(DastProfilesListing);
};
const withinComponent = () => within(wrapper.element);
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('empty state', () => {
it('shows a message to indicate that no profiles exist', () => {
const emptyStateMessage = withinComponent().getByText(/no profiles created yet/i);
expect(emptyStateMessage).not.toBe(null);
});
});
});
import { mount } from '@vue/test-utils';
import { mount, shallowMount } from '@vue/test-utils';
import { within } from '@testing-library/dom';
import { merge } from 'lodash';
import DastProfiles from 'ee/dast_profiles/components/dast_profiles.vue';
import DastProfilesList from 'ee/dast_profiles/components/dast_profiles_list.vue';
const TEST_NEW_DAST_SITE_PROFILE_PATH = '/-/on_demand_scans/site_profiles/new';
const TEST_PROJECT_FULL_PATH = '/namespace/project';
describe('EE - DastProfiles', () => {
let wrapper;
const createComponent = () => {
const createComponentFactory = (mountFn = shallowMount) => (options = {}) => {
const defaultProps = {
newDastSiteProfilePath: TEST_NEW_DAST_SITE_PROFILE_PATH,
projectFullPath: TEST_PROJECT_FULL_PATH,
};
wrapper = mount(DastProfiles, {
propsData: defaultProps,
});
const defaultMocks = {
$apollo: {
queries: {
siteProfiles: {},
},
},
};
wrapper = mountFn(
DastProfiles,
merge(
{},
{
propsData: defaultProps,
mocks: defaultMocks,
},
options,
),
);
};
const withinComponent = () => within(wrapper.element);
const createComponent = createComponentFactory();
const createFullComponent = createComponentFactory(mount);
beforeEach(() => {
createComponent();
});
const withinComponent = () => within(wrapper.element);
const getSiteProfilesComponent = () => wrapper.find(DastProfilesList);
afterEach(() => {
wrapper.destroy();
});
describe('header', () => {
beforeEach(() => {
createFullComponent();
});
it('shows a heading that describes the purpose of the page', () => {
const heading = withinComponent().getByRole('heading', { name: /manage profiles/i });
......@@ -42,6 +66,10 @@ describe('EE - DastProfiles', () => {
});
describe('tabs', () => {
beforeEach(() => {
createFullComponent();
});
it('shows a tab-list that contains the different profile categories', () => {
const tabList = withinComponent().getByRole('tablist');
......@@ -63,4 +91,51 @@ describe('EE - DastProfiles', () => {
},
);
});
describe('site profiles', () => {
beforeEach(() => {
createComponent();
});
it('passes down the correct default props', () => {
expect(getSiteProfilesComponent().props()).toEqual({
hasError: false,
hasMoreProfilesToLoad: false,
isLoading: false,
profilesPerPage: expect.any(Number),
profiles: [],
});
});
it.each([true, false])('passes down the error state', async hasError => {
wrapper.setData({ hasSiteProfilesLoadingError: hasError });
await wrapper.vm.$nextTick();
expect(getSiteProfilesComponent().props('hasError')).toBe(hasError);
});
it.each([true, false])('passes down the pagination information', async hasNextPage => {
wrapper.setData({ siteProfilesPageInfo: { hasNextPage } });
await wrapper.vm.$nextTick();
expect(getSiteProfilesComponent().props('hasMoreProfilesToLoad')).toBe(hasNextPage);
});
it.each([true, false])('passes down the loading state', loading => {
createComponent({ mocks: { $apollo: { queries: { siteProfiles: { loading } } } } });
expect(getSiteProfilesComponent().props('isLoading')).toBe(loading);
});
it('passes down the profiles data', async () => {
const siteProfiles = [{}];
wrapper.setData({ siteProfiles });
await wrapper.vm.$nextTick();
expect(getSiteProfilesComponent().props('profiles')).toBe(siteProfiles);
});
});
});
......@@ -15,4 +15,8 @@ RSpec.describe "projects/dast_profiles/index", type: :view do
it 'passes new dast site profile path' do
expect(rendered).to include '/on_demand_scans/profiles/dast_site_profiles/new'
end
it 'passes project\'s full path' do
expect(rendered).to include @project.path_with_namespace
end
end
......@@ -7507,6 +7507,12 @@ msgstr ""
msgid "DastProfiles|Do you want to discard this site profile?"
msgstr ""
msgid "DastProfiles|Edit feature will come soon. Please create a new profile if changes needed"
msgstr ""
msgid "DastProfiles|Error fetching the profiles list. Please check your network connection and try again."
msgstr ""
msgid "DastProfiles|Manage Profiles"
msgstr ""
......@@ -14225,6 +14231,9 @@ msgstr ""
msgid "Live preview"
msgstr ""
msgid "Load more"
msgstr ""
msgid "Loading"
msgstr ""
......
......@@ -41,7 +41,7 @@ exports[`Design management design index page renders design index 1`] = `
</h2>
<a
class="gl-text-gray-600 gl-text-decoration-none gl-mb-6 gl-display-block"
class="gl-text-gray-400 gl-text-decoration-none gl-mb-6 gl-display-block"
href="full-issue-url"
>
ull-issue-path
......@@ -188,7 +188,7 @@ exports[`Design management design index page with error GlAlert is rendered in c
</h2>
<a
class="gl-text-gray-600 gl-text-decoration-none gl-mb-6 gl-display-block"
class="gl-text-gray-400 gl-text-decoration-none gl-mb-6 gl-display-block"
href="full-issue-url"
>
ull-issue-path
......
......@@ -41,7 +41,7 @@ exports[`Design management design index page renders design index 1`] = `
</h2>
<a
class="gl-text-gray-600 gl-text-decoration-none gl-mb-6 gl-display-block"
class="gl-text-gray-400 gl-text-decoration-none gl-mb-6 gl-display-block"
href="full-issue-url"
>
ull-issue-path
......@@ -188,7 +188,7 @@ exports[`Design management design index page with error GlAlert is rendered in c
</h2>
<a
class="gl-text-gray-600 gl-text-decoration-none gl-mb-6 gl-display-block"
class="gl-text-gray-400 gl-text-decoration-none gl-mb-6 gl-display-block"
href="full-issue-url"
>
ull-issue-path
......
......@@ -79,7 +79,7 @@ describe('Squash before merge component', () => {
});
it(expectation, () => {
expect(findLabel().classes('gl-text-gray-600')).toBe(isDisabled);
expect(findLabel().classes('gl-text-gray-400')).toBe(isDisabled);
});
});
});
......
......@@ -48,8 +48,13 @@ RSpec.describe IconsHelper do
describe 'sprite_icon' do
icon_name = 'clock'
it 'returns svg icon html' do
it 'returns svg icon html with DEFAULT_ICON_SIZE' do
expect(sprite_icon(icon_name).to_s)
.to eq "<svg class=\"s#{IconsHelper::DEFAULT_ICON_SIZE}\" data-testid=\"#{icon_name}-icon\"><use xlink:href=\"#{icons_path}##{icon_name}\"></use></svg>"
end
it 'returns svg icon html without size class' do
expect(sprite_icon(icon_name, size: nil).to_s)
.to eq "<svg data-testid=\"#{icon_name}-icon\"><use xlink:href=\"#{icons_path}##{icon_name}\"></use></svg>"
end
......
......@@ -10,7 +10,7 @@ RSpec.describe Gitlab::Import::StuckProjectImportJobsWorker do
let(:import_state) { create(:project, :import_scheduled).import_state }
before do
import_state.update(jid: '123')
import_state.update!(jid: '123')
end
end
end
......@@ -20,7 +20,7 @@ RSpec.describe Gitlab::Import::StuckProjectImportJobsWorker do
let(:import_state) { create(:project, :import_started).import_state }
before do
import_state.update(jid: '123')
import_state.update!(jid: '123')
end
end
end
......
......@@ -12,7 +12,7 @@ RSpec.describe ::Gitlab::JiraImport::StuckJiraImportJobsWorker do
let(:import_state) { create(:jira_import_state, :scheduled, project: project) }
before do
import_state.update(jid: '123')
import_state.update!(jid: '123')
end
end
end
......@@ -22,7 +22,7 @@ RSpec.describe ::Gitlab::JiraImport::StuckJiraImportJobsWorker do
let(:import_state) { create(:jira_import_state, :started, project: project) }
before do
import_state.update(jid: '123')
import_state.update!(jid: '123')
end
end
end
......
......@@ -17,7 +17,7 @@ RSpec.describe MigrateExternalDiffsWorker do
end
it 'does nothing if the diff is missing' do
diff.destroy
diff.destroy!
worker.perform(diff.id)
end
......
......@@ -60,7 +60,7 @@ RSpec.describe NamespacelessProjectDestroyWorker do
let!(:parent_project) { create(:project) }
let(:project) do
namespaceless_project = fork_project(parent_project)
namespaceless_project.save
namespaceless_project.save!
namespaceless_project
end
......
......@@ -51,7 +51,7 @@ RSpec.describe Namespaces::RootStatisticsWorker, '#perform' do
context 'with no namespace' do
before do
group.destroy
group.destroy!
end
it 'does not execute the refresher service' do
......@@ -64,7 +64,7 @@ RSpec.describe Namespaces::RootStatisticsWorker, '#perform' do
context 'with a namespace with no aggregation scheduled' do
before do
group.aggregation_schedule.destroy
group.aggregation_schedule.destroy!
end
it 'does not execute the refresher service' do
......
......@@ -16,7 +16,7 @@ RSpec.describe PagesDomainVerificationWorker do
end
it 'does nothing for a non-existent domain' do
domain.destroy
domain.destroy!
expect(VerifyPagesDomainService).not_to receive(:new)
......
......@@ -160,7 +160,7 @@ RSpec.describe ProcessCommitWorker do
context 'when issue has first_mentioned_in_commit_at earlier than given committed_date' do
before do
issue.metrics.update(first_mentioned_in_commit_at: commit.committed_date - 1.day)
issue.metrics.update!(first_mentioned_in_commit_at: commit.committed_date - 1.day)
end
it "doesn't update issue metrics" do
......@@ -170,7 +170,7 @@ RSpec.describe ProcessCommitWorker do
context 'when issue has first_mentioned_in_commit_at later than given committed_date' do
before do
issue.metrics.update(first_mentioned_in_commit_at: commit.committed_date + 1.day)
issue.metrics.update!(first_mentioned_in_commit_at: commit.committed_date + 1.day)
end
it "doesn't update issue metrics" do
......
......@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe PropagateIntegrationWorker do
describe '#perform' do
let(:integration) do
PushoverService.create(
PushoverService.create!(
template: true,
active: true,
device: 'MyDevice',
......
......@@ -7,7 +7,7 @@ RSpec.describe PropagateServiceTemplateWorker do
describe '#perform' do
it 'calls the propagate service with the template' do
template = PushoverService.create(
template = PushoverService.create!(
template: true,
active: true,
properties: {
......
......@@ -46,7 +46,7 @@ RSpec.describe RemoveUnreferencedLfsObjectsWorker do
end
it 'removes unreferenced lfs objects after project removal' do
project1.destroy
project1.destroy!
worker.perform
......
......@@ -86,7 +86,7 @@ RSpec.describe RepositoryCheck::SingleRepositoryWorker do
end
def create_push_event(project)
project.events.create(action: :pushed, author_id: create(:user).id)
project.events.create!(action: :pushed, author_id: create(:user).id)
end
def break_wiki(project)
......
......@@ -25,13 +25,13 @@ RSpec.describe RepositoryCleanupWorker do
end
it 'raises an error if the project cannot be found' do
project.destroy
project.destroy!
expect { worker.perform(project.id, user.id) }.to raise_error(ActiveRecord::RecordNotFound)
end
it 'raises an error if the user cannot be found' do
user.destroy
user.destroy!
expect { worker.perform(project.id, user.id) }.to raise_error(ActiveRecord::RecordNotFound)
end
......
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment