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: ...@@ -1910,20 +1910,3 @@ Rails/SaveBang:
- 'spec/views/projects/imports/new.html.haml_spec.rb' - 'spec/views/projects/imports/new.html.haml_spec.rb'
- 'spec/views/projects/merge_requests/show.html.haml_spec.rb' - 'spec/views/projects/merge_requests/show.html.haml_spec.rb'
- 'spec/views/shared/_label_row.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 { ...@@ -94,7 +94,7 @@ export default {
{{ issue.title }} {{ issue.title }}
</h2> </h2>
<a <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" :href="issue.webUrl"
>{{ issue.webPath }}</a >{{ issue.webPath }}</a
> >
......
...@@ -94,7 +94,7 @@ export default { ...@@ -94,7 +94,7 @@ export default {
{{ issue.title }} {{ issue.title }}
</h2> </h2>
<a <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" :href="issue.webUrl"
>{{ issue.webPath }}</a >{{ issue.webPath }}</a
> >
......
...@@ -133,12 +133,12 @@ export default { ...@@ -133,12 +133,12 @@ export default {
</div> </div>
<div class="table-section section-100 gl-white-space-normal mt-md-3"> <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-icon name="issues" class="gl-mr-2" />
<gl-link <gl-link
data-testid="issue-id-link" data-testid="issue-id-link"
:href="issue.webUrl" :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)" @click="onIssueLinkClick(issue.iid, issue.title)"
>#{{ issue.iid }}</gl-link >#{{ issue.iid }}</gl-link
> >
......
...@@ -111,7 +111,7 @@ export default { ...@@ -111,7 +111,7 @@ export default {
<div class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column"> <div class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column">
<span class="gl-font-monospace">{{ item.name }}</span> <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> </div>
<gl-badge v-if="item.default" size="sm" variant="info">{{ <gl-badge v-if="item.default" size="sm" variant="info">{{
......
...@@ -109,7 +109,7 @@ export default { ...@@ -109,7 +109,7 @@ export default {
<template> <template>
<gl-new-dropdown v-bind="$attrs" class="ref-selector" @shown="focusSearchBox"> <gl-new-dropdown v-bind="$attrs" class="ref-selector" @shown="focusSearchBox">
<template slot="button-content"> <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-if="selectedRef" class="gl-font-monospace">{{ selectedRef }}</span>
<span v-else>{{ i18n.noRefSelected }}</span> <span v-else>{{ i18n.noRefSelected }}</span>
</span> </span>
......
...@@ -138,7 +138,7 @@ export default { ...@@ -138,7 +138,7 @@ export default {
:aria-label="$options.externalLinkTooltipText" :aria-label="$options.externalLinkTooltipText"
:title="$options.externalLinkTooltipText" :title="$options.externalLinkTooltipText"
data-testid="external-link-indicator" 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> </gl-link>
</li> </li>
......
...@@ -38,7 +38,7 @@ export default { ...@@ -38,7 +38,7 @@ export default {
<div class="inline"> <div class="inline">
<label <label
v-tooltip v-tooltip
:class="{ 'gl-text-gray-600': isDisabled }" :class="{ 'gl-text-gray-400': isDisabled }"
data-testid="squashLabel" data-testid="squashLabel"
:data-title="tooltipTitle" :data-title="tooltipTitle"
> >
......
...@@ -74,16 +74,16 @@ export default { ...@@ -74,16 +74,16 @@ export default {
</div> </div>
<div class="gl-text-gray-700"> <div class="gl-text-gray-700">
<div v-if="user.bio" class="gl-display-flex gl-mb-2"> <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> <span ref="bio" class="gl-ml-2" v-html="user.bioHtml"></span>
</div> </div>
<div v-if="user.workInformation" class="gl-display-flex gl-mb-2"> <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> <span ref="workInformation" class="gl-ml-2">{{ user.workInformation }}</span>
</div> </div>
</div> </div>
<div v-if="user.location" class="js-location gl-text-gray-700 gl-display-flex"> <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> <span class="gl-ml-2">{{ user.location }}</span>
</div> </div>
<div v-if="statusHtml" class="js-user-status gl-mt-3"> <div v-if="statusHtml" class="js-user-status gl-mt-3">
......
...@@ -274,8 +274,6 @@ ...@@ -274,8 +274,6 @@
svg { svg {
height: 15px; height: 15px;
width: 15px; width: 15px;
position: relative;
top: 2px;
} }
svg, svg,
......
...@@ -509,7 +509,7 @@ img.emoji { ...@@ -509,7 +509,7 @@ img.emoji {
} }
&.is-dragging { &.is-dragging {
background-color: $gray-600; background-color: $gray-400;
} }
} }
......
...@@ -209,7 +209,7 @@ ...@@ -209,7 +209,7 @@
} }
.doc-versions { .doc-versions {
color: $gray-600; color: $gray-400;
&:hover { &:hover {
color: $gray-900; color: $gray-900;
......
...@@ -227,7 +227,7 @@ label { ...@@ -227,7 +227,7 @@ label {
right: 0.8em; right: 0.8em;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
color: $gray-600; color: $gray-400;
} }
.input-md { .input-md {
......
...@@ -48,4 +48,12 @@ svg { ...@@ -48,4 +48,12 @@ svg {
@include svg-size(#{$svg-size}px); @include svg-size(#{$svg-size}px);
} }
} }
&.s12 {
vertical-align: -1px;
}
&.s16 {
vertical-align: -3px;
}
} }
...@@ -326,8 +326,8 @@ ...@@ -326,8 +326,8 @@
line-height: 1; line-height: 1;
padding: 0; padding: 0;
min-width: 16px; min-width: 16px;
color: $gray-600; color: $gray-400;
fill: $gray-600; fill: $gray-400;
.fa { .fa {
position: relative; position: relative;
......
...@@ -168,7 +168,7 @@ $gray-200: #bfbfbf !default; ...@@ -168,7 +168,7 @@ $gray-200: #bfbfbf !default;
$gray-300: #999 !default; $gray-300: #999 !default;
$gray-400: #868686 !default; $gray-400: #868686 !default;
$gray-500: #666 !default; $gray-500: #666 !default;
$gray-600: #919191 !default; $gray-600: #5e5e5e !default;
$gray-700: #707070 !default; $gray-700: #707070 !default;
$gray-800: #4f4f4f !default; $gray-800: #4f4f4f !default;
$gray-900: #303030 !default; $gray-900: #303030 !default;
...@@ -351,11 +351,11 @@ $gl-font-weight-normal: 400; ...@@ -351,11 +351,11 @@ $gl-font-weight-normal: 400;
$gl-font-weight-bold: 600; $gl-font-weight-bold: 600;
$gl-text-color: $gray-900; $gl-text-color: $gray-900;
$gl-text-color-secondary: $gray-700; $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-quaternary: #d6d6d6;
$gl-text-color-inverted: $white; $gl-text-color-inverted: $white;
$gl-text-color-secondary-inverted: rgba($white, 0.85); $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-grayish-blue: #7f8fa4;
$gl-gray-dark: #313236; $gl-gray-dark: #313236;
$gl-gray-light: #5c5c5c; $gl-gray-light: #5c5c5c;
......
...@@ -166,6 +166,6 @@ ...@@ -166,6 +166,6 @@
.cluster-status-indicator { .cluster-status-indicator {
&.disabled { &.disabled {
background-color: $gray-600; background-color: $gray-400;
} }
} }
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
th { th {
@include gl-bg-transparent; @include gl-bg-transparent;
@include gl-font-weight-bold; @include gl-font-weight-bold;
@include gl-text-gray-600; @include gl-text-gray-400;
&[aria-sort='none']:hover { &[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'); 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; ...@@ -727,7 +727,7 @@ $note-form-margin-left: 72px;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
margin-left: 10px; margin-left: 10px;
color: $gray-600; color: $gray-400;
@include notes-media('max', map-get($grid-breakpoints, sm) - 1) { @include notes-media('max', map-get($grid-breakpoints, sm) - 1) {
float: none; float: none;
......
...@@ -809,7 +809,7 @@ ...@@ -809,7 +809,7 @@
&.ci-status-icon-created, &.ci-status-icon-created,
&.ci-status-icon-skipped { &.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 @@ ...@@ -159,7 +159,7 @@
font-weight: bold; font-weight: bold;
.icon { .icon {
font-size: $gl-font-size-large; vertical-align: -1px;
} }
.home-panel-topic-list { .home-panel-topic-list {
......
...@@ -56,7 +56,7 @@ ...@@ -56,7 +56,7 @@
.draggable-remove-link { .draggable-remove-link {
cursor: pointer; cursor: pointer;
color: $gray-600; color: $gray-400;
background-color: $white; background-color: $white;
} }
} }
...@@ -117,7 +117,7 @@ ...@@ -117,7 +117,7 @@
.prometheus-graph-cursor { .prometheus-graph-cursor {
position: absolute; position: absolute;
background: $gray-600; background: $gray-400;
width: 1px; width: 1px;
} }
...@@ -290,7 +290,7 @@ ...@@ -290,7 +290,7 @@
} }
> text { > text {
fill: $gray-600; fill: $gray-400;
font-size: 10px; font-size: 10px;
} }
} }
......
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
} }
&.ci-preparing { &.ci-preparing {
@include status-color($gray-100, $gray-300, $gray-600); @include status-color($gray-100, $gray-300, $gray-400);
} }
&.ci-pending, &.ci-pending,
......
...@@ -138,12 +138,6 @@ ...@@ -138,12 +138,6 @@
} }
.tree-item { .tree-item {
.file-icon,
.folder-icon {
position: relative;
top: 2px;
}
.link-container { .link-container {
padding: 0; padding: 0;
......
...@@ -45,7 +45,7 @@ module IconsHelper ...@@ -45,7 +45,7 @@ module IconsHelper
ActionController::Base.helpers.image_path('file_icons.svg', host: sprite_base_url) ActionController::Base.helpers.image_path('file_icons.svg', host: sprite_base_url)
end 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) if known_sprites&.exclude?(icon_name)
exception = ArgumentError.new("#{icon_name} is not a known icon in @gitlab-org/gitlab-svg") exception = ArgumentError.new("#{icon_name} is not a known icon in @gitlab-org/gitlab-svg")
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(exception) Gitlab::ErrorTracking.track_and_raise_for_dev_exception(exception)
...@@ -117,7 +117,9 @@ module IconsHelper ...@@ -117,7 +117,9 @@ module IconsHelper
'earth' 'earth'
end 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 end
def file_type_icon_class(type, mode, name) def file_type_icon_class(type, mode, name)
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
= link_to edit_admin_application_settings_integration_path(service.to_param), class: 'gl-text-blue-300!' do = 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' } } %strong.has-tooltip{ title: s_('AdminSettings|Moved to integrations'), data: { container: 'body' } }
= service.title = service.title
%td.gl-cursor-default.gl-text-gray-600 %td.gl-cursor-default.gl-text-gray-400
= service.description = service.description
%td %td
- else - else
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
%h1.home-panel-title.gl-mt-3.gl-mb-2{ data: { qa_selector: 'project_name_content' } } %h1.home-panel-title.gl-mt-3.gl-mb-2{ data: { qa_selector: 'project_name_content' } }
= @project.name = @project.name
%span.visibility-icon.text-secondary.gl-ml-2.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) } %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 = render_if_exists 'compliance_management/compliance_framework/compliance_framework_badge', project: @project
.home-panel-metadata.d-flex.flex-wrap.text-secondary .home-panel-metadata.d-flex.flex-wrap.text-secondary
- if can?(current_user, :read_project, @project) - if can?(current_user, :read_project, @project)
......
...@@ -27,5 +27,5 @@ ...@@ -27,5 +27,5 @@
Squash commits when merge request is accepted. Squash commits when merge request is accepted.
= link_to icon('question-circle'), help_page_path('user/project/merge_requests/squash_and_merge'), target: '_blank' = link_to icon('question-circle'), help_page_path('user/project/merge_requests/squash_and_merge'), target: '_blank'
- if project.squash_always? - if project.squash_always?
.gl-text-gray-600 .gl-text-gray-400
= _('Required in this project.') = _('Required in this project.')
---
title: Refactor spec/workers/* to fix SaveBang Cop
merge_request: 38399
author: Rajendra Kadam
type: fixed
...@@ -42,7 +42,6 @@ ...@@ -42,7 +42,6 @@
- design_management - design_management
- design_system - design_system
- devops_reports - devops_reports
- digital_experience_management
- disaster_recovery - disaster_recovery
- dynamic_application_security_testing - dynamic_application_security_testing
- editor_extension - editor_extension
...@@ -72,7 +71,6 @@ ...@@ -72,7 +71,6 @@
- jupyter_notebooks - jupyter_notebooks
- kanban_boards - kanban_boards
- kubernetes_management - kubernetes_management
- language_specific
- license_compliance - license_compliance
- live_preview - live_preview
- load_testing - load_testing
......
...@@ -548,7 +548,7 @@ or more LDAP group links](#adding-group-links-starter-only). ...@@ -548,7 +548,7 @@ or more LDAP group links](#adding-group-links-starter-only).
### 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)** ### Administrator sync **(STARTER ONLY)**
......
--- ---
reading_time: true 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 # Reference architecture: up to 10,000 users
This page describes GitLab reference architecture for up to 10,000 users. This page describes GitLab reference architecture for up to 10,000 users. For a
For a full list of reference architectures, see full list of reference architectures, see
[Available reference architectures](index.md#available-reference-architectures). [Available reference architectures](index.md#available-reference-architectures).
> - **Supported users (approximate):** 10,000 > - **Supported users (approximate):** 10,000
> - **High Availability:** True > - **High Availability:** Yes
> - **Test RPS rates:** API: 200 RPS, Web: 20 RPS, Git: 20 RPS > - **Test requests per second (RPS) rates:** API: 200 RPS, Web: 20 RPS, Git: 20 RPS
| Service | Nodes | Configuration | GCP | AWS | Azure | | Service | Nodes | Configuration | GCP | AWS | Azure |
|--------------------------------------------------------------|-------|---------------------------------|------------------|-----------------------|----------------| |--------------------------------------------|-------------|-------------------------|-----------------|-------------|----------|
| External load balancing node | 1 | 2 vCPU, 1.8GB Memory | `n1-highcpu-2` | `c5.large` | `F2s v2` | | 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` | | 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` | | 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` | | 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` | | 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 - 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 - 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 - 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` | | 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` | | 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` | | 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` | | 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` | | 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 | | 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` | | 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) The Google Cloud Platform (GCP) architectures were built and tested using the
CPU platform on GCP. On different hardware you may find that adjustments, either lower [Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms)
or higher, are required for your CPU or Node counts accordingly. For more information, a CPU platform. On different hardware you may find that adjustments, either lower
[Sysbench](https://github.com/akopytov/sysbench) benchmark of the CPU can be found or higher, are required for your CPU or node counts. For more information, see
[here](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks). 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, Artifacts, etc., an [object storage service](#configure-the-object-storage)
is recommended over NFS where possible, due to better performance and availability. For data objects (such as LFS, Uploads, or Artifacts), an
Since this doesn't require a node to be set up, it's marked as not applicable (n/a) [object storage service](#configure-the-object-storage) is recommended instead
in the table above. 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 ## Setup components
......
--- ---
reading_time: true 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 # Reference architecture: up to 25,000 users
This page describes GitLab reference architecture for up to 25,000 users. This page describes GitLab reference architecture for up to 25,000 users. For a
For a full list of reference architectures, see full list of reference architectures, see
[Available reference architectures](index.md#available-reference-architectures). [Available reference architectures](index.md#available-reference-architectures).
> - **Supported users (approximate):** 25,000 > - **Supported users (approximate):** 25,000
> - **High Availability:** True > - **High Availability:** Yes
> - **Test RPS rates:** API: 500 RPS, Web: 50 RPS, Git: 50 RPS > - **Test requests per second (RPS) rates:** API: 500 RPS, Web: 50 RPS, Git: 50 RPS
| Service | Nodes | Configuration | GCP | AWS | Azure | | Service | Nodes | Configuration | GCP | AWS | Azure |
|--------------------------------------------------------------|-------|---------------------------------|------------------|-----------------------|----------------| |-----------------------------------------|-------------|-------------------------|-----------------|-------------|----------|
| External load balancing node | 1 | 4 vCPU, 3.6GB Memory | `n1-highcpu-4` | `c5.xlarge` | `F4s v2` | | 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` | | 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` | | 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` | | 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` | | 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 - 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 - 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 - 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` | | 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` | | 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` | | 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` | | 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` | | 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 | | 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` | | 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) The Google Cloud Platform (GCP) architectures were built and tested using the
CPU platform on GCP. On different hardware you may find that adjustments, either lower [Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms)
or higher, are required for your CPU or Node counts accordingly. For more information, a CPU platform. On different hardware you may find that adjustments, either lower
[Sysbench](https://github.com/akopytov/sysbench) benchmark of the CPU can be found or higher, are required for your CPU or node counts. For more information, see
[here](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks). 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, Artifacts, etc., an [object storage service](#configure-the-object-storage)
is recommended over NFS where possible, due to better performance and availability. For data objects (such as LFS, Uploads, or Artifacts), an
Since this doesn't require a node to be set up, it's marked as not applicable (n/a) [object storage service](#configure-the-object-storage) is recommended instead
in the table above. 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 ## Setup components
......
--- ---
reading_time: true 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 # Reference architecture: up to 50,000 users
This page describes GitLab reference architecture for up to 50,000 users. This page describes GitLab reference architecture for up to 50,000 users. For a
For a full list of reference architectures, see full list of reference architectures, see
[Available reference architectures](index.md#available-reference-architectures). [Available reference architectures](index.md#available-reference-architectures).
> - **Supported users (approximate):** 50,000 > - **Supported users (approximate):** 50,000
> - **High Availability:** True > - **High Availability:** Yes
> - **Test RPS rates:** API: 1000 RPS, Web: 100 RPS, Git: 100 RPS > - **Test requests per second (RPS) rates:** API: 1000 RPS, Web: 100 RPS, Git: 100 RPS
| Service | Nodes | Configuration | GCP | AWS | Azure | | Service | Nodes | Configuration | GCP | AWS | Azure |
|--------------------------------------------------------------|-------|---------------------------------|------------------|-----------------------|----------------| |-----------------------------------------|-------------|-------------------------|-----------------|--------------|----------|
| External load balancing node | 1 | 8 vCPU, 7.2GB Memory | `n1-highcpu-8` | `c5.2xlarge` | `F8s v2` | | 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` | | 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` | | 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` | | 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` | | 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 - 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 - 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 - 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` | | 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` | | 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` | | 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` | | 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` | | 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 | | 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` | | 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) The Google Cloud Platform (GCP) architectures were built and tested using the
CPU platform on GCP. On different hardware you may find that adjustments, either lower [Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms)
or higher, are required for your CPU or Node counts accordingly. For more information, a CPU platform. On different hardware you may find that adjustments, either lower
[Sysbench](https://github.com/akopytov/sysbench) benchmark of the CPU can be found or higher, are required for your CPU or node counts. For more information, see
[here](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks). 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, Artifacts, etc., an [object storage service](#configure-the-object-storage)
is recommended over NFS where possible, due to better performance and availability. For data objects (such as LFS, Uploads, or Artifacts), an
Since this doesn't require a node to be set up, it's marked as not applicable (n/a) [object storage service](#configure-the-object-storage) is recommended instead
in the table above. 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 ## Setup components
......
...@@ -36,6 +36,105 @@ since point releases bundle many changes together. Minimizing the time ...@@ -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 between when versions are out of sync across the fleet may help mitigate
errors caused by upgrades. 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 ## Examples of previous incidents
### Some links to issues and MRs were broken ### 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, ...@@ -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'. 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. 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 ...@@ -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: [instance-wide SAML](../../../integration/saml.md). The recommended solution allows you to take advantage of:
- [LDAP compatibility](../../../administration/auth/ldap/index.md). - [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). - [Required groups](../../../integration/saml.md#required-groups-starter-only).
- [Admin groups](../../../integration/saml.md#admin-groups-starter-only). - [Admin groups](../../../integration/saml.md#admin-groups-starter-only).
- [Auditor groups](../../../integration/saml.md#auditor-groups-starter-only). - [Auditor groups](../../../integration/saml.md#auditor-groups-starter-only).
......
...@@ -474,7 +474,7 @@ for details about the pipelines security model. ...@@ -474,7 +474,7 @@ for details about the pipelines security model.
## LDAP users permissions ## LDAP users permissions
Since GitLab 8.15, LDAP user permissions can now be manually overridden by an admin user. 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 ## Project aliases
......
...@@ -22,20 +22,20 @@ visit the [administrator documentation](../../integration/elasticsearch.md). ...@@ -22,20 +22,20 @@ visit the [administrator documentation](../../integration/elasticsearch.md).
The Advanced Global Search in GitLab is a powerful search service that saves 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 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 GitLab leverages the search capabilities of [Elasticsearch](https://www.elastic.co/elasticsearch/) and enables it when
searching in: searching in:
- Projects - Projects
- Repositories
- Commits
- Issues - Issues
- Merge requests - Merge requests
- Milestones - Milestones
- Notes (comments) - Comments
- Snippets - Code
- Commits
- Wiki - Wiki
- Users
## Use cases ## Use cases
......
<script> <script>
import { GlTabs, GlTab, GlTooltipDirective } from '@gitlab/ui'; import Cookies from 'js-cookie';
import { isEmpty } from 'lodash'; import { GlTabs, GlTab } from '@gitlab/ui';
import { __ } from '~/locale';
import { sprintf, __, s__ } from '~/locale'; import MergeRequestsGrid from './merge_requests/grid.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import ApprovalStatus from './approval_status.vue';
import Approvers from './approvers.vue';
import EmptyState from './empty_state.vue'; import EmptyState from './empty_state.vue';
import MergeRequest from './merge_request.vue'; import { COMPLIANCE_TAB_COOKIE_KEY } from '../constants';
import Pagination from './pagination.vue';
import PipelineStatus from './pipeline_status.vue';
import GridColumnHeading from './grid_column_heading.vue';
export default { export default {
name: 'ComplianceDashboard', name: 'ComplianceDashboard',
directives: {
GlTooltip: GlTooltipDirective,
},
components: { components: {
ApprovalStatus, MergeRequestsGrid,
Approvers,
EmptyState, EmptyState,
GridColumnHeading,
MergeRequest,
Pagination,
PipelineStatus,
GlTab, GlTab,
GlTabs, GlTabs,
}, },
mixins: [timeagoMixin],
props: { props: {
emptyStateSvgPath: { emptyStateSvgPath: {
type: String, type: String,
...@@ -51,28 +35,13 @@ export default { ...@@ -51,28 +35,13 @@ export default {
}, },
}, },
methods: { methods: {
key(id, value) { showTabs() {
return `${id}-${value}`; return Cookies.get(COMPLIANCE_TAB_COOKIE_KEY) === 'true';
},
timeAgoString(mergedAt) {
return sprintf(s__('merged %{timeAgo}'), {
timeAgo: this.timeFormatted(mergedAt),
});
},
timeTooltip(mergedAt) {
return this.tooltipTitle(mergedAt);
},
hasStatus(status) {
return !isEmpty(status);
}, },
}, },
strings: { strings: {
heading: __('Compliance Dashboard'), heading: __('Compliance Dashboard'),
subheading: __('Here you will find recent merge request activity'), subheading: __('Here you will find recent merge request activity'),
mergeRequestLabel: __('Merge Request'),
approvalStatusLabel: __('Approval Status'),
pipelineStatusLabel: __('Pipeline'),
updatesLabel: __('Updates'),
mergeRequestsTabLabel: __('Merge Requests'), mergeRequestsTabLabel: __('Merge Requests'),
}, },
}; };
...@@ -84,61 +53,16 @@ export default { ...@@ -84,61 +53,16 @@ export default {
<h4>{{ $options.strings.heading }}</h4> <h4>{{ $options.strings.heading }}</h4>
<p>{{ $options.strings.subheading }}</p> <p>{{ $options.strings.subheading }}</p>
</header> </header>
<gl-tabs>
<gl-tabs v-if="showTabs()">
<gl-tab> <gl-tab>
<template #title> <template #title>
<span>{{ $options.strings.mergeRequestsTabLabel }}</span> <span>{{ $options.strings.mergeRequestsTabLabel }}</span>
</template> </template>
<div class="dashboard-grid"> <merge-requests-grid :merge-requests="mergeRequests" :is-last-page="isLastPage" />
<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" />
</gl-tab> </gl-tab>
</gl-tabs> </gl-tabs>
<merge-requests-grid v-else :merge-requests="mergeRequests" :is-last-page="isLastPage" />
</div> </div>
<empty-state v-else :image-path="emptyStateSvgPath" /> <empty-state v-else :image-path="emptyStateSvgPath" />
</template> </template>
<script> <script>
import { sprintf, __ } from '~/locale'; import { sprintf, __ } from '~/locale';
import { GlAvatarLink, GlAvatar, GlAvatarsInline, GlTooltipDirective } from '@gitlab/ui'; import { GlAvatarLink, GlAvatar, GlAvatarsInline, GlTooltipDirective } from '@gitlab/ui';
import { PRESENTABLE_APPROVERS_LIMIT } from '../constants'; import { PRESENTABLE_APPROVERS_LIMIT } from '../../constants';
export default { export default {
directives: { 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 const PRESENTABLE_APPROVERS_LIMIT = 2;
export default {}; export const COMPLIANCE_TAB_COOKIE_KEY = 'compliance_dashboard_tabs';
<script> <script>
import * as Sentry from '@sentry/browser';
import { GlButton, GlTab, GlTabs } from '@gitlab/ui'; 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 { export default {
components: { components: {
GlButton, GlButton,
GlTab, GlTab,
GlTabs, GlTabs,
ProfilesListing, ProfilesList,
}, },
props: { props: {
newDastSiteProfilePath: { newDastSiteProfilePath: {
type: String, type: String,
required: true, 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> </script>
...@@ -42,13 +116,21 @@ export default { ...@@ -42,13 +116,21 @@ export default {
}} }}
</p> </p>
</header> </header>
<gl-tabs> <gl-tabs>
<gl-tab> <gl-tab>
<template #title> <template #title>
<span>{{ s__('DastProfiles|Site Profiles') }}</span> <span>{{ s__('DastProfiles|Site Profiles') }}</span>
</template> </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-tab>
</gl-tabs> </gl-tabs>
</section> </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 () => { ...@@ -10,11 +10,12 @@ export default () => {
} }
const { const {
dataset: { newDastSiteProfilePath }, dataset: { newDastSiteProfilePath, projectFullPath },
} = el; } = el;
const props = { const props = {
newDastSiteProfilePath, newDastSiteProfilePath,
projectFullPath,
}; };
return new Vue({ 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 { ...@@ -126,7 +126,7 @@ export default {
<header class="gl-mb-6"> <header class="gl-mb-6">
<h2>{{ s__('OnDemandScans|New on-demand DAST scan') }}</h2> <h2>{{ s__('OnDemandScans|New on-demand DAST scan') }}</h2>
<p> <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 <gl-sprintf
:message=" :message="
s__( s__(
...@@ -149,7 +149,7 @@ export default { ...@@ -149,7 +149,7 @@ export default {
<gl-icon <gl-icon
v-gl-tooltip.hover v-gl-tooltip.hover
name="information-o" 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.')" :title="s__('OnDemandScans|Only a passive scan can be performed on demand.')"
/> />
</template> </template>
...@@ -162,7 +162,7 @@ export default { ...@@ -162,7 +162,7 @@ export default {
<gl-icon <gl-icon
v-gl-tooltip.hover v-gl-tooltip.hover
name="information-o" 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.')" :title="s__('OnDemandScans|Attached branch is where the scan job runs.')"
/> />
</template> </template>
...@@ -175,7 +175,7 @@ export default { ...@@ -175,7 +175,7 @@ export default {
<gl-icon <gl-icon
v-gl-tooltip.hover v-gl-tooltip.hover
name="information-o" 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.')" :title="s__('OnDemandScans|DAST will scan the target URL and any discovered sub URLs.')"
/> />
</template> </template>
......
import initMergeRequestAnalyticsApp from 'ee/analytics/merge_request_analytics';
document.addEventListener('DOMContentLoaded', initMergeRequestAnalyticsApp);
...@@ -57,7 +57,7 @@ export default { ...@@ -57,7 +57,7 @@ export default {
:action-secondary="downloadButton" :action-secondary="downloadButton"
> >
<!-- heading --> <!-- heading -->
<div class="row gl-text-gray-600"> <div class="row gl-text-gray-400">
<div class="col-1">{{ __('Method') }}</div> <div class="col-1">{{ __('Method') }}</div>
<div class="col-11">{{ __('URL') }}</div> <div class="col-11">{{ __('URL') }}</div>
</div> </div>
......
...@@ -4,7 +4,6 @@ import createFlash from '~/flash'; ...@@ -4,7 +4,6 @@ import createFlash from '~/flash';
import EventItem from 'ee/vue_shared/security_reports/components/event_item.vue'; import EventItem from 'ee/vue_shared/security_reports/components/event_item.vue';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import HistoryCommentEditor from './history_comment_editor.vue'; import HistoryCommentEditor from './history_comment_editor.vue';
export default { export default {
...@@ -43,9 +42,6 @@ export default { ...@@ -43,9 +42,6 @@ export default {
}, },
computed: { computed: {
noteIdUrl() {
return joinPaths(this.notesUrl, this.comment.id);
},
commentNote() { commentNote() {
return this.comment?.note; return this.comment?.note;
}, },
...@@ -72,7 +68,7 @@ export default { ...@@ -72,7 +68,7 @@ export default {
getSaveConfig(note) { getSaveConfig(note) {
const isUpdatingComment = Boolean(this.comment); const isUpdatingComment = Boolean(this.comment);
const method = isUpdatingComment ? 'put' : 'post'; 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 data = { note: { note } };
const emitName = isUpdatingComment ? 'onCommentUpdated' : 'onCommentAdded'; const emitName = isUpdatingComment ? 'onCommentUpdated' : 'onCommentAdded';
...@@ -105,7 +101,7 @@ export default { ...@@ -105,7 +101,7 @@ export default {
}, },
deleteComment() { deleteComment() {
this.isDeletingComment = true; this.isDeletingComment = true;
const deleteUrl = this.noteIdUrl; const deleteUrl = this.comment.path;
axios axios
.delete(deleteUrl) .delete(deleteUrl)
......
...@@ -195,7 +195,7 @@ html.group-epics-roadmap-html { ...@@ -195,7 +195,7 @@ html.group-epics-roadmap-html {
.item-label, .item-label,
.item-sublabel .sublabel-value { .item-sublabel .sublabel-value {
color: $gray-600; color: $gray-400;
font-weight: 400; font-weight: 400;
&.label-dark { &.label-dark {
......
...@@ -5,4 +5,4 @@ ...@@ -5,4 +5,4 @@
= f.check_box :delayed_project_removal, checked: group.delayed_project_removal?, class: 'form-check-input' = 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 = f.label :delayed_project_removal, class: 'form-check-label' do
%span.gl-display-block= s_('GroupSettings|Enable delayed project removal') %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 @@ ...@@ -2,4 +2,5 @@
- breadcrumb_title s_('DastProfiles|Manage profiles') - breadcrumb_title s_('DastProfiles|Manage profiles')
- page_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 @@ ...@@ -164,7 +164,7 @@
:idempotent: :idempotent:
:tags: [] :tags: []
- :name: cronjob:historical_data - :name: cronjob:historical_data
:feature_category: :license_compliance :feature_category: :billing
:has_external_dependencies: :has_external_dependencies:
:urgency: :low :urgency: :low
:resource_boundary: :unknown :resource_boundary: :unknown
......
...@@ -7,7 +7,7 @@ class HistoricalDataWorker # rubocop:disable Scalability/IdempotentWorker ...@@ -7,7 +7,7 @@ class HistoricalDataWorker # rubocop:disable Scalability/IdempotentWorker
include CronjobQueue include CronjobQueue
# rubocop:enable Scalability/CronWorkerContext # rubocop:enable Scalability/CronWorkerContext
feature_category :license_compliance feature_category :billing
def perform def perform
return if License.current.nil? || License.current&.trial? 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 // 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 <div
class="compliance-dashboard" class="compliance-dashboard"
> >
...@@ -26,97 +26,9 @@ exports[`ComplianceDashboard component when there are merge requests matches the ...@@ -26,97 +26,9 @@ exports[`ComplianceDashboard component when there are merge requests matches the
> >
<template> <template>
<div <merge-requests-grid-stub
class="dashboard-grid" mergerequests="[object Object],[object Object]"
>
<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 />
</template> </template>
<template> <template>
<span> <span>
...@@ -128,6 +40,28 @@ exports[`ComplianceDashboard component when there are merge requests matches the ...@@ -128,6 +40,28 @@ exports[`ComplianceDashboard component when there are merge requests matches the
</div> </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`] = ` exports[`ComplianceDashboard component when there are no merge requests matches the snapshot 1`] = `
<empty-state-stub <empty-state-stub
imagepath="empty.svg" imagepath="empty.svg"
......
import Cookies from 'js-cookie';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlTabs, GlTab } from '@gitlab/ui'; import { GlTabs, GlTab } from '@gitlab/ui';
import ComplianceDashboard from 'ee/compliance_dashboard/components/dashboard.vue'; import ComplianceDashboard from 'ee/compliance_dashboard/components/dashboard.vue';
import ApprovalStatus from 'ee/compliance_dashboard/components/approval_status.vue'; import MergeRequestGrid from 'ee/compliance_dashboard/components/merge_requests/grid.vue';
import PipelineStatus from 'ee/compliance_dashboard/components/pipeline_status.vue'; import { COMPLIANCE_TAB_COOKIE_KEY } from 'ee/compliance_dashboard/constants';
import Approvers from 'ee/compliance_dashboard/components/approvers.vue';
import { createMergeRequests } from '../mock_data'; import { createMergeRequests } from '../mock_data';
describe('ComplianceDashboard component', () => { describe('ComplianceDashboard component', () => {
let wrapper; let wrapper;
const findMergeRequests = () => wrapper.findAll('[data-testid="merge-request"]'); const isLastPage = false;
const findTime = () => wrapper.find('time'); const mergeRequests = createMergeRequests({ count: 2 });
const findApprovalStatus = () => wrapper.find(ApprovalStatus);
const findPipelineStatus = () => wrapper.find(PipelineStatus); const findMergeRequestsGrid = () => wrapper.find(MergeRequestGrid);
const findApprovers = () => wrapper.find(Approvers);
const findDashboardTabs = () => wrapper.find(GlTabs); const findDashboardTabs = () => wrapper.find(GlTabs);
const createComponent = (props = {}, options = {}) => { const createComponent = (props = {}) => {
return shallowMount(ComplianceDashboard, { return shallowMount(ComplianceDashboard, {
propsData: { propsData: {
mergeRequests: createMergeRequests({ count: 2, options }), mergeRequests,
isLastPage: false, isLastPage,
emptyStateSvgPath: 'empty.svg', emptyStateSvgPath: 'empty.svg',
...props, ...props,
}, },
stubs: { stubs: {
GlTab, GlTab,
MergeRequest: {
props: { mergeRequest: Object },
template: `<div data-testid="merge-request">{{ mergeRequest.title }}</div>`,
},
}, },
}); });
}; };
...@@ -41,49 +36,40 @@ describe('ComplianceDashboard component', () => { ...@@ -41,49 +36,40 @@ describe('ComplianceDashboard component', () => {
describe('when there are merge requests', () => { describe('when there are merge requests', () => {
beforeEach(() => { beforeEach(() => {
Cookies.set(COMPLIANCE_TAB_COOKIE_KEY, false);
wrapper = createComponent(); wrapper = createComponent();
}); });
it('matches the snapshot', () => { afterEach(() => {
expect(wrapper.element).toMatchSnapshot(); Cookies.remove(COMPLIANCE_TAB_COOKIE_KEY);
});
it('renders a list of merge requests', () => {
expect(findMergeRequests().length).toEqual(2);
}); });
it('renders the dashboard tabs', () => { it('matches the snapshot', () => {
expect(findDashboardTabs().exists()).toEqual(true); expect(wrapper.element).toMatchSnapshot();
}); });
describe('approval status', () => { it('renders the merge requests', () => {
it('does not render if there is no approval status', () => { expect(findMergeRequestsGrid().exists()).toBe(true);
expect(findApprovalStatus().exists()).toBe(false);
}); });
it('renders if there is an approval status', () => { it('sets the MergeRequestGrid properties', () => {
wrapper = createComponent({}, { approvalStatus: 'success' }); expect(findMergeRequestsGrid().props('mergeRequests')).toBe(mergeRequests);
expect(findApprovalStatus().exists()).toBe(true); expect(findMergeRequestsGrid().props('isLastPage')).toBe(isLastPage);
});
}); });
describe('pipeline status', () => { describe('and the show tabs cookie is true', () => {
it('does not render if there is no pipeline', () => { beforeEach(() => {
expect(findPipelineStatus().exists()).toBe(false); Cookies.set(COMPLIANCE_TAB_COOKIE_KEY, true);
wrapper = createComponent();
}); });
it('renders if there is a pipeline', () => { it('matches the snapshot', () => {
wrapper = createComponent({}, { addPipeline: true }); expect(wrapper.element).toMatchSnapshot();
expect(findPipelineStatus().exists()).toBe(true);
});
}); });
it('renders the approvers list', () => { it('renders the dashboard tabs', () => {
expect(findApprovers().exists()).toBe(true); expect(findDashboardTabs().exists()).toBe(true);
}); });
it('renders the "merged at" time', () => {
expect(findTime().text()).toEqual('merged 2 days ago');
}); });
}); });
...@@ -97,11 +83,7 @@ describe('ComplianceDashboard component', () => { ...@@ -97,11 +83,7 @@ describe('ComplianceDashboard component', () => {
}); });
it('does not render merge requests', () => { it('does not render merge requests', () => {
expect(findMergeRequests().exists()).toEqual(false); expect(findMergeRequestsGrid().exists()).toBe(false);
});
it('does not render the dashboard tabs', () => {
expect(findDashboardTabs().exists()).toEqual(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 { shallowMount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui'; 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', () => { describe('ApprovalStatus component', () => {
let wrapper; let wrapper;
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlAvatarLink } from '@gitlab/ui'; 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 { PRESENTABLE_APPROVERS_LIMIT } from 'ee/compliance_dashboard/constants';
import { createApprovers } from '../mock_data'; import { createApprovers } from '../../mock_data';
describe('MergeRequest component', () => { describe('MergeRequest component', () => {
let wrapper; 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 { shallowMount } from '@vue/test-utils';
import { GlAvatarLink, GlAvatar } from '@gitlab/ui'; import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
import MergeRequest from 'ee/compliance_dashboard/components/merge_request.vue'; import MergeRequest from 'ee/compliance_dashboard/components/merge_requests/merge_request.vue';
import { createMergeRequest } from '../mock_data'; import { createMergeRequest } from '../../mock_data';
describe('MergeRequest component', () => { describe('MergeRequest component', () => {
let wrapper; let wrapper;
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import PipelineStatus from 'ee/compliance_dashboard/components/pipeline_status.vue'; import PipelineStatus from 'ee/compliance_dashboard/components/merge_requests/pipeline_status.vue';
import { createPipelineStatus } from '../mock_data'; import { createPipelineStatus } from '../../mock_data';
describe('PipelineStatus component', () => { describe('PipelineStatus component', () => {
let wrapper; let wrapper;
......
import { shallowMount } from '@vue/test-utils'; 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', () => { describe('GridColumnHeading component', () => {
let wrapper; let wrapper;
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlPagination } from '@gitlab/ui'; 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; let wrapper;
const findGlPagination = () => wrapper.find(GlPagination); 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 { within } from '@testing-library/dom';
import { merge } from 'lodash';
import DastProfiles from 'ee/dast_profiles/components/dast_profiles.vue'; 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_NEW_DAST_SITE_PROFILE_PATH = '/-/on_demand_scans/site_profiles/new';
const TEST_PROJECT_FULL_PATH = '/namespace/project';
describe('EE - DastProfiles', () => { describe('EE - DastProfiles', () => {
let wrapper; let wrapper;
const createComponent = () => { const createComponentFactory = (mountFn = shallowMount) => (options = {}) => {
const defaultProps = { const defaultProps = {
newDastSiteProfilePath: TEST_NEW_DAST_SITE_PROFILE_PATH, newDastSiteProfilePath: TEST_NEW_DAST_SITE_PROFILE_PATH,
projectFullPath: TEST_PROJECT_FULL_PATH,
}; };
wrapper = mount(DastProfiles, { const defaultMocks = {
$apollo: {
queries: {
siteProfiles: {},
},
},
};
wrapper = mountFn(
DastProfiles,
merge(
{},
{
propsData: defaultProps, propsData: defaultProps,
}); mocks: defaultMocks,
},
options,
),
);
}; };
const withinComponent = () => within(wrapper.element); const createComponent = createComponentFactory();
const createFullComponent = createComponentFactory(mount);
beforeEach(() => { const withinComponent = () => within(wrapper.element);
createComponent(); const getSiteProfilesComponent = () => wrapper.find(DastProfilesList);
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
describe('header', () => { describe('header', () => {
beforeEach(() => {
createFullComponent();
});
it('shows a heading that describes the purpose of the page', () => { it('shows a heading that describes the purpose of the page', () => {
const heading = withinComponent().getByRole('heading', { name: /manage profiles/i }); const heading = withinComponent().getByRole('heading', { name: /manage profiles/i });
...@@ -42,6 +66,10 @@ describe('EE - DastProfiles', () => { ...@@ -42,6 +66,10 @@ describe('EE - DastProfiles', () => {
}); });
describe('tabs', () => { describe('tabs', () => {
beforeEach(() => {
createFullComponent();
});
it('shows a tab-list that contains the different profile categories', () => { it('shows a tab-list that contains the different profile categories', () => {
const tabList = withinComponent().getByRole('tablist'); const tabList = withinComponent().getByRole('tablist');
...@@ -63,4 +91,51 @@ describe('EE - DastProfiles', () => { ...@@ -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 ...@@ -15,4 +15,8 @@ RSpec.describe "projects/dast_profiles/index", type: :view do
it 'passes new dast site profile path' do it 'passes new dast site profile path' do
expect(rendered).to include '/on_demand_scans/profiles/dast_site_profiles/new' expect(rendered).to include '/on_demand_scans/profiles/dast_site_profiles/new'
end end
it 'passes project\'s full path' do
expect(rendered).to include @project.path_with_namespace
end
end end
...@@ -7507,6 +7507,12 @@ msgstr "" ...@@ -7507,6 +7507,12 @@ msgstr ""
msgid "DastProfiles|Do you want to discard this site profile?" msgid "DastProfiles|Do you want to discard this site profile?"
msgstr "" 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" msgid "DastProfiles|Manage Profiles"
msgstr "" msgstr ""
...@@ -14225,6 +14231,9 @@ msgstr "" ...@@ -14225,6 +14231,9 @@ msgstr ""
msgid "Live preview" msgid "Live preview"
msgstr "" msgstr ""
msgid "Load more"
msgstr ""
msgid "Loading" msgid "Loading"
msgstr "" msgstr ""
......
...@@ -41,7 +41,7 @@ exports[`Design management design index page renders design index 1`] = ` ...@@ -41,7 +41,7 @@ exports[`Design management design index page renders design index 1`] = `
</h2> </h2>
<a <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" href="full-issue-url"
> >
ull-issue-path ull-issue-path
...@@ -188,7 +188,7 @@ exports[`Design management design index page with error GlAlert is rendered in c ...@@ -188,7 +188,7 @@ exports[`Design management design index page with error GlAlert is rendered in c
</h2> </h2>
<a <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" href="full-issue-url"
> >
ull-issue-path ull-issue-path
......
...@@ -41,7 +41,7 @@ exports[`Design management design index page renders design index 1`] = ` ...@@ -41,7 +41,7 @@ exports[`Design management design index page renders design index 1`] = `
</h2> </h2>
<a <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" href="full-issue-url"
> >
ull-issue-path ull-issue-path
...@@ -188,7 +188,7 @@ exports[`Design management design index page with error GlAlert is rendered in c ...@@ -188,7 +188,7 @@ exports[`Design management design index page with error GlAlert is rendered in c
</h2> </h2>
<a <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" href="full-issue-url"
> >
ull-issue-path ull-issue-path
......
...@@ -79,7 +79,7 @@ describe('Squash before merge component', () => { ...@@ -79,7 +79,7 @@ describe('Squash before merge component', () => {
}); });
it(expectation, () => { 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 ...@@ -48,8 +48,13 @@ RSpec.describe IconsHelper do
describe 'sprite_icon' do describe 'sprite_icon' do
icon_name = 'clock' 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) 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>" .to eq "<svg data-testid=\"#{icon_name}-icon\"><use xlink:href=\"#{icons_path}##{icon_name}\"></use></svg>"
end end
......
...@@ -10,7 +10,7 @@ RSpec.describe Gitlab::Import::StuckProjectImportJobsWorker do ...@@ -10,7 +10,7 @@ RSpec.describe Gitlab::Import::StuckProjectImportJobsWorker do
let(:import_state) { create(:project, :import_scheduled).import_state } let(:import_state) { create(:project, :import_scheduled).import_state }
before do before do
import_state.update(jid: '123') import_state.update!(jid: '123')
end end
end end
end end
...@@ -20,7 +20,7 @@ RSpec.describe Gitlab::Import::StuckProjectImportJobsWorker do ...@@ -20,7 +20,7 @@ RSpec.describe Gitlab::Import::StuckProjectImportJobsWorker do
let(:import_state) { create(:project, :import_started).import_state } let(:import_state) { create(:project, :import_started).import_state }
before do before do
import_state.update(jid: '123') import_state.update!(jid: '123')
end end
end end
end end
......
...@@ -12,7 +12,7 @@ RSpec.describe ::Gitlab::JiraImport::StuckJiraImportJobsWorker do ...@@ -12,7 +12,7 @@ RSpec.describe ::Gitlab::JiraImport::StuckJiraImportJobsWorker do
let(:import_state) { create(:jira_import_state, :scheduled, project: project) } let(:import_state) { create(:jira_import_state, :scheduled, project: project) }
before do before do
import_state.update(jid: '123') import_state.update!(jid: '123')
end end
end end
end end
...@@ -22,7 +22,7 @@ RSpec.describe ::Gitlab::JiraImport::StuckJiraImportJobsWorker do ...@@ -22,7 +22,7 @@ RSpec.describe ::Gitlab::JiraImport::StuckJiraImportJobsWorker do
let(:import_state) { create(:jira_import_state, :started, project: project) } let(:import_state) { create(:jira_import_state, :started, project: project) }
before do before do
import_state.update(jid: '123') import_state.update!(jid: '123')
end end
end end
end end
......
...@@ -17,7 +17,7 @@ RSpec.describe MigrateExternalDiffsWorker do ...@@ -17,7 +17,7 @@ RSpec.describe MigrateExternalDiffsWorker do
end end
it 'does nothing if the diff is missing' do it 'does nothing if the diff is missing' do
diff.destroy diff.destroy!
worker.perform(diff.id) worker.perform(diff.id)
end end
......
...@@ -60,7 +60,7 @@ RSpec.describe NamespacelessProjectDestroyWorker do ...@@ -60,7 +60,7 @@ RSpec.describe NamespacelessProjectDestroyWorker do
let!(:parent_project) { create(:project) } let!(:parent_project) { create(:project) }
let(:project) do let(:project) do
namespaceless_project = fork_project(parent_project) namespaceless_project = fork_project(parent_project)
namespaceless_project.save namespaceless_project.save!
namespaceless_project namespaceless_project
end end
......
...@@ -51,7 +51,7 @@ RSpec.describe Namespaces::RootStatisticsWorker, '#perform' do ...@@ -51,7 +51,7 @@ RSpec.describe Namespaces::RootStatisticsWorker, '#perform' do
context 'with no namespace' do context 'with no namespace' do
before do before do
group.destroy group.destroy!
end end
it 'does not execute the refresher service' do it 'does not execute the refresher service' do
...@@ -64,7 +64,7 @@ RSpec.describe Namespaces::RootStatisticsWorker, '#perform' do ...@@ -64,7 +64,7 @@ RSpec.describe Namespaces::RootStatisticsWorker, '#perform' do
context 'with a namespace with no aggregation scheduled' do context 'with a namespace with no aggregation scheduled' do
before do before do
group.aggregation_schedule.destroy group.aggregation_schedule.destroy!
end end
it 'does not execute the refresher service' do it 'does not execute the refresher service' do
......
...@@ -16,7 +16,7 @@ RSpec.describe PagesDomainVerificationWorker do ...@@ -16,7 +16,7 @@ RSpec.describe PagesDomainVerificationWorker do
end end
it 'does nothing for a non-existent domain' do it 'does nothing for a non-existent domain' do
domain.destroy domain.destroy!
expect(VerifyPagesDomainService).not_to receive(:new) expect(VerifyPagesDomainService).not_to receive(:new)
......
...@@ -160,7 +160,7 @@ RSpec.describe ProcessCommitWorker do ...@@ -160,7 +160,7 @@ RSpec.describe ProcessCommitWorker do
context 'when issue has first_mentioned_in_commit_at earlier than given committed_date' do context 'when issue has first_mentioned_in_commit_at earlier than given committed_date' do
before 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 end
it "doesn't update issue metrics" do it "doesn't update issue metrics" do
...@@ -170,7 +170,7 @@ RSpec.describe ProcessCommitWorker do ...@@ -170,7 +170,7 @@ RSpec.describe ProcessCommitWorker do
context 'when issue has first_mentioned_in_commit_at later than given committed_date' do context 'when issue has first_mentioned_in_commit_at later than given committed_date' do
before 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 end
it "doesn't update issue metrics" do it "doesn't update issue metrics" do
......
...@@ -5,7 +5,7 @@ require 'spec_helper' ...@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe PropagateIntegrationWorker do RSpec.describe PropagateIntegrationWorker do
describe '#perform' do describe '#perform' do
let(:integration) do let(:integration) do
PushoverService.create( PushoverService.create!(
template: true, template: true,
active: true, active: true,
device: 'MyDevice', device: 'MyDevice',
......
...@@ -7,7 +7,7 @@ RSpec.describe PropagateServiceTemplateWorker do ...@@ -7,7 +7,7 @@ RSpec.describe PropagateServiceTemplateWorker do
describe '#perform' do describe '#perform' do
it 'calls the propagate service with the template' do it 'calls the propagate service with the template' do
template = PushoverService.create( template = PushoverService.create!(
template: true, template: true,
active: true, active: true,
properties: { properties: {
......
...@@ -46,7 +46,7 @@ RSpec.describe RemoveUnreferencedLfsObjectsWorker do ...@@ -46,7 +46,7 @@ RSpec.describe RemoveUnreferencedLfsObjectsWorker do
end end
it 'removes unreferenced lfs objects after project removal' do it 'removes unreferenced lfs objects after project removal' do
project1.destroy project1.destroy!
worker.perform worker.perform
......
...@@ -86,7 +86,7 @@ RSpec.describe RepositoryCheck::SingleRepositoryWorker do ...@@ -86,7 +86,7 @@ RSpec.describe RepositoryCheck::SingleRepositoryWorker do
end end
def create_push_event(project) 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 end
def break_wiki(project) def break_wiki(project)
......
...@@ -25,13 +25,13 @@ RSpec.describe RepositoryCleanupWorker do ...@@ -25,13 +25,13 @@ RSpec.describe RepositoryCleanupWorker do
end end
it 'raises an error if the project cannot be found' do 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) expect { worker.perform(project.id, user.id) }.to raise_error(ActiveRecord::RecordNotFound)
end end
it 'raises an error if the user cannot be found' do 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) expect { worker.perform(project.id, user.id) }.to raise_error(ActiveRecord::RecordNotFound)
end 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