+
&,
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index cae07fc588e..f7fb9a81509 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -23,6 +23,7 @@ class Projects::JobsController < Projects::ApplicationController
before_action :verify_proxy_request!, only: :proxy_websocket_authorize
before_action :reject_if_build_artifacts_size_refreshing!, only: [:erase]
before_action :push_ai_build_failure_cause, only: [:show]
+ before_action :push_filter_by_name, only: [:index]
layout 'project'
feature_category :continuous_integration
@@ -281,6 +282,10 @@ class Projects::JobsController < Projects::ApplicationController
def push_ai_build_failure_cause
push_frontend_feature_flag(:ai_build_failure_cause, @project)
end
+
+ def push_filter_by_name
+ push_frontend_feature_flag(:populate_and_use_build_names_table, @project)
+ end
end
Projects::JobsController.prepend_mod_with('Projects::JobsController')
diff --git a/app/models/project.rb b/app/models/project.rb
index 79d2d0667aa..843b2d4431c 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -63,6 +63,7 @@ class Project < ApplicationRecord
BoardLimitExceeded = Class.new(StandardError)
ExportLimitExceeded = Class.new(StandardError)
+ EPOCH_CACHE_EXPIRATION = 30.days
STATISTICS_ATTRIBUTE = 'repositories_count'
UNKNOWN_IMPORT_URL = 'http://unknown.git'
# Hashed Storage versions handle rolling out new storage to project and dependents models:
@@ -3371,8 +3372,36 @@ class Project < ApplicationRecord
false
end
+ def lfs_file_locks_changed_epoch
+ get_epoch_from(lfs_file_locks_changed_epoch_cache_key)
+ end
+
+ def refresh_lfs_file_locks_changed_epoch
+ refresh_epoch_cache(lfs_file_locks_changed_epoch_cache_key)
+ end
+
private
+ def with_redis(&block)
+ Gitlab::Redis::Cache.with(&block)
+ end
+
+ def lfs_file_locks_changed_epoch_cache_key
+ "project:#{id}:lfs_file_locks_changed_epoch"
+ end
+
+ def get_epoch_from(cache_key)
+ with_redis { |redis| redis.get(cache_key) }&.to_i || refresh_epoch_cache(cache_key)
+ end
+
+ def refresh_epoch_cache(cache_key)
+ # %s = seconds since the Unix Epoch
+ # %L = milliseconds of the second
+ Time.current.strftime('%s%L').to_i.tap do |epoch|
+ with_redis { |redis| redis.set(cache_key, epoch, ex: EPOCH_CACHE_EXPIRATION) }
+ end
+ end
+
# overridden in EE
def project_group_links_with_preload
project_group_links
diff --git a/app/services/lfs/lock_file_service.rb b/app/services/lfs/lock_file_service.rb
index da4ef6e68ed..b4505e2e49d 100644
--- a/app/services/lfs/lock_file_service.rb
+++ b/app/services/lfs/lock_file_service.rb
@@ -25,8 +25,9 @@ module Lfs
# rubocop: enable CodeReuse/ActiveRecord
def create_lock!
- lock = project.lfs_file_locks.create!(user: current_user,
- path: params[:path])
+ lock = project.lfs_file_locks.create!(user: current_user, path: params[:path])
+
+ project.refresh_lfs_file_locks_changed_epoch
success(http_status: 201, lock: lock)
end
diff --git a/app/services/lfs/unlock_file_service.rb b/app/services/lfs/unlock_file_service.rb
index 61f07531e40..a96994af7a9 100644
--- a/app/services/lfs/unlock_file_service.rb
+++ b/app/services/lfs/unlock_file_service.rb
@@ -24,6 +24,8 @@ module Lfs
if lock.can_be_unlocked_by?(current_user, forced)
lock.destroy!
+ project.refresh_lfs_file_locks_changed_epoch
+
success(lock: lock, http_status: :ok)
elsif forced
error(_('You must have maintainer access to force delete a lock'), 403)
diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml
index a0ec7ca20ff..24e04818bf6 100644
--- a/app/views/shared/issuable/form/_metadata.html.haml
+++ b/app/views/shared/issuable/form/_metadata.html.haml
@@ -22,7 +22,7 @@
- if can?(current_user, :"set_#{issuable.to_ability_name}_metadata", issuable)
.row.gl-pt-4
%div{ class: (has_due_date ? "col-lg-6" : "col-12") }
- .form-group.row.merge-request-assignee
+ .form-group.row{ data: { testid: 'merge-request-assignee' } }
= render "shared/issuable/form/metadata_issuable_assignee", issuable: issuable, form: form, has_due_date: has_due_date
- if issuable.allows_reviewers?
diff --git a/doc/development/secure_coding_guidelines.md b/doc/development/secure_coding_guidelines.md
index 4665b6b7e88..18c501eedbf 100644
--- a/doc/development/secure_coding_guidelines.md
+++ b/doc/development/secure_coding_guidelines.md
@@ -486,7 +486,7 @@ References:
##### Vue
-- [isSafeURL](https://gitlab.com/gitlab-org/gitlab/-/blob/v12.7.5-ee/app/assets/javascripts/lib/utils/url_utility.js#L190-207)
+- [isValidURL](https://gitlab.com/gitlab-org/gitlab/-/blob/v17.3.0-ee/app/assets/javascripts/lib/utils/url_utility.js#L427-451)
- [GlSprintf](https://gitlab-org.gitlab.io/gitlab-ui/?path=/docs/utilities-sprintf--sentence-with-link)
#### Content Security Policy
diff --git a/doc/user/application_security/gitlab_advisory_database/index.md b/doc/user/application_security/gitlab_advisory_database/index.md
index 060aee733f4..b5ac80d39eb 100644
--- a/doc/user/application_security/gitlab_advisory_database/index.md
+++ b/doc/user/application_security/gitlab_advisory_database/index.md
@@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# GitLab Advisory Database
-The [GitLab Advisory Database](https://gitlab.com/gitlab-org/security-products/gemnasium-db) serves as a repository for security advisories related to software dependencies.
+The [GitLab Advisory Database](https://gitlab.com/gitlab-org/security-products/gemnasium-db) serves as a repository for security advisories related to software dependencies. It is updated on an hourly basis with the latest security advisories.
The database is an essential component of both [Dependency Scanning](../dependency_scanning/index.md) and [Container Scanning](../container_scanning/index.md).
diff --git a/doc/user/infrastructure/clusters/connect/img/variables_civo.png b/doc/user/infrastructure/clusters/connect/img/variables_civo.png
index a668c3dd53c..acbe4f60df6 100644
Binary files a/doc/user/infrastructure/clusters/connect/img/variables_civo.png and b/doc/user/infrastructure/clusters/connect/img/variables_civo.png differ
diff --git a/doc/user/infrastructure/clusters/connect/new_civo_cluster.md b/doc/user/infrastructure/clusters/connect/new_civo_cluster.md
index 37310040f28..a18a3836265 100644
--- a/doc/user/infrastructure/clusters/connect/new_civo_cluster.md
+++ b/doc/user/infrastructure/clusters/connect/new_civo_cluster.md
@@ -53,9 +53,9 @@ This project provides you with:
To create a GitLab agent for Kubernetes:
1. On the left sidebar, select **Operate > Kubernetes clusters**.
-1. Select **Connect a cluster (agent)**.
-1. From the **Select an agent** dropdown list, select `civo-agent` and select **Register an agent**.
-1. GitLab generates a registration token for the agent. Securely store this secret token, as you will need it later.
+1. Select **Connect a cluster**.
+1. From the **Select an agent** dropdown list, select `civo-agent` and select **Register**.
+1. GitLab generates an agent access token for the agent. Securely store this secret token, as you will need it later.
1. GitLab provides an address for the agent server (KAS), which you will also need later.
## Configure your project
@@ -66,7 +66,7 @@ Use CI/CD environment variables to configure your project.
1. On the left sidebar, select **Settings > CI/CD**.
1. Expand **Variables**.
-1. Set the variable `BASE64_CIVO_TOKEN` to the token from your Civo account.
+1. Set the variable `CIVO_TOKEN` to the token from your Civo account.
1. Set the variable `TF_VAR_agent_token` to the agent token you received in the previous task.
1. Set the variable `TF_VAR_kas_address` to the agent server address in the previous task.
@@ -92,14 +92,17 @@ Refer to the [Civo Terraform provider](https://registry.terraform.io/providers/c
After configuring your project, manually trigger the provisioning of your cluster. In GitLab:
1. On the left sidebar, go to **Build > Pipelines**.
-1. Next to **Play** (**{play}**), select the dropdown list icon (**{chevron-lg-down}**).
-1. Select **Deploy** to manually trigger the deployment job.
+1. On the left sidebar, select **Build > Pipelines**.
+1. Select **Run pipeline**, and then select the newly created pipeline from the list.
+1. Next to the **deploy** job, select **Manual action** (**{status_manual}**).
When the pipeline finishes successfully, you can see your new cluster:
- In Civo dashboard: on your Kubernetes tab.
- In GitLab: from your project's sidebar, select **Operate > Kubernetes clusters**.
+If you didn't set the `TF_VAR_civo_region` variable, the cluster will be created in the 'lon1' region.
+
## Use your cluster
After you provision the cluster, it is connected to GitLab and is ready for deployments. To check the connection:
@@ -111,28 +114,12 @@ For more information about the capabilities of the connection, see [the GitLab a
## Remove the cluster
-A cleanup job is not included in your pipeline by default. To remove all created resources, you
-must modify your GitLab CI/CD template before running the cleanup job.
+A cleanup job is included in your pipeline by default.
-To remove all resources:
+To remove all created resources:
-1. Add the following to your `.gitlab-ci.yml` file:
-
- ```yaml
- stages:
- - init
- - validate
- - build
- - deploy
- - cleanup
-
- destroy:
- extends: .destroy
- needs: []
- ```
-
-1. On the left sidebar, select **Build > Pipelines** and select the most recent pipeline.
-1. For the `destroy` job, select **Play** (**{play}**).
+1. On the left sidebar, select **Build > Pipelines**, and then select the most recent pipeline.
+1. Next to the **destroy-environment** job, select **Manual action** (**{status_manual}**).
## Civo support
diff --git a/doc/user/project/merge_requests/duo_in_merge_requests.md b/doc/user/project/merge_requests/duo_in_merge_requests.md
index cf2c8857028..70dc60b94d9 100644
--- a/doc/user/project/merge_requests/duo_in_merge_requests.md
+++ b/doc/user/project/merge_requests/duo_in_merge_requests.md
@@ -37,41 +37,6 @@ Provide feedback on this feature in [issue 443236](https://gitlab.com/gitlab-org
**Data usage**: The diff of changes between the source branch's head and the target branch is sent to the large language model.
-## Generate a description from a template
-
-DETAILS:
-**Tier:** For a limited time, Ultimate. In the future, [GitLab Duo Enterprise](../../../subscriptions/subscription-add-ons.md).
-**Offering:** GitLab.com
-**Status:** Beta
-
-> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/10591) in GitLab 16.3 as an [experiment](../../../policy/experiment-beta-support.md#experiment).
-> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/429882) to beta in GitLab 16.10.
-
-Many projects include [templates](../description_templates.md#create-a-merge-request-template)
-that you populate when you create a merge request. These templates help populate the description
-of the merge request. They can help the team conform to standards, and help reviewers
-and others understand the purpose and changes proposed in the merge request.
-
-When you create a merge request, GitLab Duo Merge request template population
-can generate a description for your merge request, based on the contents of the template.
-GitLab Duo fills in the template and replaces the contents of the description.
-
-To use GitLab Duo to generate a merge request description:
-
-1. [Create a new merge request](creating_merge_requests.md) and go to the **Description** field.
-1. Select **GitLab Duo** (**{tanuki-ai}**).
-1. Select **Fill in merge request template**.
-
-The updated description is applied. You can edit or revise the description before you finish creating your merge request.
-
-Provide feedback on this experimental feature in [issue 416537](https://gitlab.com/gitlab-org/gitlab/-/issues/416537).
-
-**Data usage**: When you use this feature, the following data is sent to the large language model referenced above:
-
-- Title of the merge request
-- Contents of the description
-- Diff of changes between the source branch's head and the target branch
-
## Summarize a code review
DETAILS:
diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md
index 68ccf35b482..a867010a0a9 100644
--- a/doc/user/project/protected_branches.md
+++ b/doc/user/project/protected_branches.md
@@ -207,6 +207,12 @@ check in directly to a protected branch:
1. From the **Allowed to push and merge** list, select **No one**.
1. Select **Protect**.
+Alternatively, you can [create](repository/branches/index.md#create-a-branch-rule) or [edit](repository/branches/index.md#edit-a-branch-rule) a branch rule. Then:
+
+1. Select **Edit** in the **Allowed to merge** section.
+1. Select **Developers and Maintainers**.
+1. Select **Save changes**.
+
## Allow everyone to push directly to a protected branch
You can allow everyone with write access to push to the protected branch.
@@ -219,6 +225,12 @@ You can allow everyone with write access to push to the protected branch.
1. From the **Allowed to push and merge** list, select **Developers + Maintainers**.
1. Select **Protect**.
+Alternatively, you can [create](repository/branches/index.md#create-a-branch-rule) or [edit](repository/branches/index.md#edit-a-branch-rule) a branch rule. Then:
+
+1. Select **Edit** in the **Allowed to push and merge** section.
+1. Select **Developers and Maintainers**.
+1. Select **Save changes**.
+
## Allow deploy keys to push to a protected branch
> - More restrictions on deploy keys [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/425926) in GitLab 16.5 [with a flag](../../administration/feature_flags.md) named `check_membership_in_protected_ref_access`. Disabled by default.
@@ -273,6 +285,10 @@ To enable force pushes on branches that are already protected:
1. Select **Add protected branch**.
1. In the list of protected branches, next to the branch, turn on the **Allowed to force push** toggle.
+Alternatively, you can [create](repository/branches/index.md#create-a-branch-rule) or [edit](repository/branches/index.md#edit-a-branch-rule) a branch rule. Then:
+
+1. In the list of protected branches, next to the branch, turn on the **Allowed to force push** toggle.
+
Members who can push to this branch can now also force push.
### When a branch matches multiple rules
@@ -328,6 +344,10 @@ To enable Code Owner's approval on branches that are already protected:
1. Select **Add protected branch**.
1. In the list of protected branches, next to the branch, turn on the **Code owner approval** toggle.
+Alternatively, you can [create](repository/branches/index.md#create-a-branch-rule) or [edit](repository/branches/index.md#edit-a-branch-rule) a branch rule.
+Then, in the list of protected branches, next to the branch,
+turn on the **Code owner approval** toggle.
+
When enabled, all merge requests for these branches require approval
by a Code Owner per matched rule before they can be merged.
Additionally, direct pushes to the protected branch are denied if a rule is matched.
diff --git a/doc/user/project/repository/branches/index.md b/doc/user/project/repository/branches/index.md
index ce2c3db1ff7..8c0f9c4a489 100644
--- a/doc/user/project/repository/branches/index.md
+++ b/doc/user/project/repository/branches/index.md
@@ -101,14 +101,14 @@ To create a branch from an issue:
GitLab provides multiple methods to protect individual branches. These methods
ensure your branches receive oversight and quality checks from their creation to their deletion:
-- The [default branch](default.md) in your project receives extra protection.
-- Configure [protected branches](../../protected_branches.md)
- to restrict who can commit to a branch, merge other branches into it, or merge
- the branch itself into another branch.
-- Configure [approval rules](../../merge_requests/approvals/rules.md) to set review
- requirements, including [security-related approvals](../../merge_requests/approvals/rules.md#security-approvals), before a branch can merge.
+- Apply enhanced security and protection to your project's [default branch](default.md).
+- Configure [protected branches](../../protected_branches.md) to:
+ - Limit who can push and merge to a branch.
+ - Manage if users can force push to the branch.
+ - Manage if changes to files listed in the `CODEOWNERS` file can be pushed directly to the branch.
+- Configure [approval rules](../../merge_requests/approvals/rules.md#approvals-for-protected-branches) to manage review requirements and implement [security-related approvals](../../merge_requests/approvals/rules.md#security-approvals).
- Integrate with third-party [status checks](../../merge_requests/status_checks.md)
- to ensure your branch contents meet your standards of quality.
+ to ensure the contents of your branch meets your defined quality standards.
You can manage your branches:
@@ -133,20 +133,13 @@ On this page, you can:
- [Compare branches](#compare-branches).
- Delete merged branches.
-### View branches with configured protections
+### View branch rules
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/88279) in GitLab 15.1 with a flag named `branch_rules`. Disabled by default.
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/363170) in GitLab 15.10.
> - [Enabled on self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/363170) in GitLab 15.11.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123368) in GitLab 16.1. Feature flag `branch_rules` removed.
-Branches in your repository can be [protected](../../protected_branches.md) in multiple ways. You can:
-
-- Limit who can push to the branch.
-- Limit who can merge the branch.
-- Require approval of all changes.
-- Require external tests to pass.
-
The **Branch rules overview** page shows all branches with any configured protections,
and their protection methods:
@@ -168,7 +161,7 @@ To view the **Branch rules overview** list:
1. Identify the branch you want more information about.
1. Select **View details** to see information about its:
- [Branch protections](../../protected_branches.md).
- - [Approval rules](../../merge_requests/approvals/rules.md).
+ - [Approval rules](../../merge_requests/approvals/rules.md#approvals-for-protected-branches).
- [Status checks](../../merge_requests/status_checks.md).
#### Create a branch rule
@@ -223,7 +216,7 @@ To edit a branch rule:
1. Expand **Branch rules**.
1. Next to a rule you want to edit, select **View details**.
1. In the upper-right corner, select **Edit**.
-1. In the dialog, from the **Create branch rule** dropdown list, select a branch name or create a wildcard by typing `*`.
+1. Edit the information as needed.
1. Select **Update**.
#### Delete a branch rule
diff --git a/lib/gitlab/metrics/templates/Area.metrics-dashboard.yml b/lib/gitlab/metrics/templates/Area.metrics-dashboard.yml
deleted file mode 100644
index 1f7dd25aaee..00000000000
--- a/lib/gitlab/metrics/templates/Area.metrics-dashboard.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-# Only one dashboard should be defined per file
-# More info: https://docs.gitlab.com/ee/operations/metrics/dashboards/yaml.html
-dashboard: 'Area Panel Example'
-
-# For more information about the required properties of panel_groups
-# please visit: https://docs.gitlab.com/ee/operations/metrics/dashboards/yaml.html#panel-group-panel_groups-properties
-panel_groups:
- - group: 'Server Statistics'
- panels:
- - title: Average amount of time spent by the CPU
- type: area-chart
- metrics:
- - query_range: 'rate(node_cpu_seconds_total[15m])'
- unit: 'Seconds'
- label: "Time in Seconds"
diff --git a/lib/gitlab/metrics/templates/Default.metrics-dashboard.yml b/lib/gitlab/metrics/templates/Default.metrics-dashboard.yml
deleted file mode 100644
index b331e792461..00000000000
--- a/lib/gitlab/metrics/templates/Default.metrics-dashboard.yml
+++ /dev/null
@@ -1,24 +0,0 @@
-# Only one dashboard should be defined per file
-# More info: https://docs.gitlab.com/ee/operations/metrics/dashboards/yaml.html
-dashboard: 'Single Stat'
-
-# This is where all of the variables that can be manipulated via the UI
-# are initialized
-# Check out: https://docs.gitlab.com/ee/operations/metrics/dashboards/templating_variables.html#templating-variables-for-metrics-dashboards-core
-templating:
- variables:
- job: 'prometheus'
-
-# For more information about the required properties of panel_groups
-# please visit: https://docs.gitlab.com/ee/operations/metrics/dashboards/yaml.html#panel-group-panel_groups-properties
-panel_groups:
- - group: 'Memory'
- panels:
- - title: Prometheus
- type: single-stat
- metrics:
- # Queries that make use of variables need to have double curly brackets {}
- # set to the variables, per the example below
- - query: 'max(go_memstats_alloc_bytes{job="{{job}}"}) / 1024 /1024'
- unit: '%'
- label: "Max"
diff --git a/lib/gitlab/metrics/templates/gauge.metrics-dashboard.yml b/lib/gitlab/metrics/templates/gauge.metrics-dashboard.yml
deleted file mode 100644
index 1c17a3a4d40..00000000000
--- a/lib/gitlab/metrics/templates/gauge.metrics-dashboard.yml
+++ /dev/null
@@ -1,23 +0,0 @@
-# Only one dashboard should be defined per file
-# More info: https://docs.gitlab.com/ee/operations/metrics/dashboards/yaml.html
-dashboard: 'Gauge Panel Example'
-
-# For more information about the required properties of panel_groups
-# please visit: https://docs.gitlab.com/ee/operations/metrics/dashboards/yaml.html#panel-group-panel_groups-properties
-panel_groups:
- - group: 'Server Statistics'
- panels:
- - title: "Memory usage"
- # More information about gauge panel types can be found here:
- # https://docs.gitlab.com/ee/operations/metrics/dashboards/panel_types.html#gauge
- type: "gauge-chart"
- min_value: 0
- max_value: 1024
- split: 10
- thresholds:
- mode: "percentage"
- values: [60, 90]
- format: "megabytes"
- metrics:
- - query: '(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) / 1024 / 1024'
- unit: 'MB'
diff --git a/lib/gitlab/metrics/templates/index.md b/lib/gitlab/metrics/templates/index.md
deleted file mode 100644
index 59fc85899da..00000000000
--- a/lib/gitlab/metrics/templates/index.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Development guide for Metrics Dashboard templates
-
-Please follow [the development guideline](../../../../doc/development/operations/metrics/templates.md)
diff --git a/lib/gitlab/metrics/templates/k8s_area.metrics-dashboard.yml b/lib/gitlab/metrics/templates/k8s_area.metrics-dashboard.yml
deleted file mode 100644
index aea816658d0..00000000000
--- a/lib/gitlab/metrics/templates/k8s_area.metrics-dashboard.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-# Only one dashboard should be defined per file
-# More info: https://docs.gitlab.com/ee/operations/metrics/dashboards/yaml.html
-dashboard: 'Area Panel Example'
-
-# For more information about the required properties of panel_groups
-# please visit: https://docs.gitlab.com/ee/operations/metrics/dashboards/yaml.html#panel-group-panel_groups-properties
-panel_groups:
- - group: 'Server Statistics'
- panels:
- - title: "Core Usage (Pod Average)"
- type: area-chart
- metrics:
- - query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container!="POD",pod=~"^{{ci_environment_slug}}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="{{kube_namespace}}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container!="POD",pod=~"^{{ci_environment_slug}}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="{{kube_namespace}}"}[15m])) by (pod)) OR avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^{{ci_environment_slug}}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="{{kube_namespace}}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^{{ci_environment_slug}}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="{{kube_namespace}}"}[15m])) by (pod_name))'
- unit: 'cores'
- label: "Pod Average (in seconds)"
diff --git a/lib/gitlab/metrics/templates/k8s_gauge.metrics-dashboard.yml b/lib/gitlab/metrics/templates/k8s_gauge.metrics-dashboard.yml
deleted file mode 100644
index 7f97719765b..00000000000
--- a/lib/gitlab/metrics/templates/k8s_gauge.metrics-dashboard.yml
+++ /dev/null
@@ -1,23 +0,0 @@
-# Only one dashboard should be defined per file
-# More info: https://docs.gitlab.com/ee/operations/metrics/dashboards/yaml.html
-dashboard: 'Gauge K8s Panel Example'
-
-# For more information about the required properties of panel_groups
-# please visit: https://docs.gitlab.com/ee/operations/metrics/dashboards/yaml.html#panel-group-panel_groups-properties
-panel_groups:
- - group: 'Server Statistics'
- panels:
- - title: "Memory usage"
- # More information about gauge panel types can be found here:
- # https://docs.gitlab.com/ee/operations/metrics/dashboards/panel_types.html#gauge
- type: "gauge-chart"
- min_value: 0
- max_value: 1024
- split: 10
- thresholds:
- mode: "percentage"
- values: [60, 90]
- format: "megabytes"
- metrics:
- - query: 'avg(sum(container_memory_usage_bytes{container!="POD",pod=~"^{{ci_environment_slug}}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="{{kube_namespace}}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container!="POD",pod=~"^{{ci_environment_slug}}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="{{kube_namespace}}"}) without (job)) /1024/1024 OR avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^{{ci_environment_slug}}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="{{kube_namespace}}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^{{ci_environment_slug}}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="{{kube_namespace}}"}) without (job)) /1024/1024'
- unit: 'MB'
diff --git a/lib/gitlab/metrics/templates/k8s_single-stat.metrics-dashboard.yml b/lib/gitlab/metrics/templates/k8s_single-stat.metrics-dashboard.yml
deleted file mode 100644
index 829e12357ff..00000000000
--- a/lib/gitlab/metrics/templates/k8s_single-stat.metrics-dashboard.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-# Only one dashboard should be defined per file
-# More info: https://docs.gitlab.com/ee/operations/metrics/dashboards/yaml.html
-dashboard: 'Single Stat Panel Example'
-
-# For more information about the required properties of panel_groups
-# please visit: https://docs.gitlab.com/ee/operations/metrics/dashboards/yaml.html#panel-group-panel_groups-properties
-panel_groups:
- - group: 'Server Statistics'
- panels:
- - title: "Memory usage"
- # More information about heatmap panel types can be found here:
- # https://docs.gitlab.com/ee/operations/metrics/dashboards/panel_types.html#single-stat
- type: "single-stat"
- metrics:
- - query: 'avg(sum(container_memory_usage_bytes{container!="POD",pod=~"^{{ci_environment_slug}}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="{{kube_namespace}}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container!="POD",pod=~"^{{ci_environment_slug}}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="{{kube_namespace}}"}) without (job)) /1024/1024 OR avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^{{ci_environment_slug}}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="{{kube_namespace}}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^{{ci_environment_slug}}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="{{kube_namespace}}"}) without (job)) /1024/1024'
- unit: 'MB'
- label: "Used memory"
diff --git a/lib/gitlab/metrics/templates/single-stat.metrics-dashboard.yml b/lib/gitlab/metrics/templates/single-stat.metrics-dashboard.yml
deleted file mode 100644
index 18c27fffc7c..00000000000
--- a/lib/gitlab/metrics/templates/single-stat.metrics-dashboard.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-# Only one dashboard should be defined per file
-# More info: https://docs.gitlab.com/ee/operations/metrics/dashboards/yaml.html
-dashboard: 'Heatmap Panel Example'
-
-# For more information about the required properties of panel_groups
-# please visit: https://docs.gitlab.com/ee/operations/metrics/dashboards/yaml.html#panel-group-panel_groups-properties
-panel_groups:
- - group: 'Server Statistics'
- panels:
- - title: "Memory usage"
- # More information about heatmap panel types can be found here:
- # https://docs.gitlab.com/ee/operations/metrics/dashboards/panel_types.html#single-stat
- type: "single-stat"
- metrics:
- - query: '(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) / 1024 / 1024'
- unit: 'MB'
- label: "Used memory"
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 6aac19a426c..8b8f1a4c437 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -20483,9 +20483,6 @@ msgstr ""
msgid "Environments|Auto stops %{autoStopAt}"
msgstr ""
-msgid "Environments|Cancel"
-msgstr ""
-
msgid "Environments|Clean up"
msgstr ""
@@ -29972,6 +29969,9 @@ msgstr ""
msgid "Jobs|Raw text search is not currently supported for the jobs filtered search feature. Please use the available search tokens."
msgstr ""
+msgid "Jobs|Search or filter jobs..."
+msgstr ""
+
msgid "Jobs|Stage"
msgstr ""
@@ -30323,6 +30323,9 @@ msgstr ""
msgid "Kubernetes deployment not found"
msgstr ""
+msgid "KubernetesDashboard|Actions"
+msgstr ""
+
msgid "KubernetesDashboard|Age"
msgstr ""
@@ -30356,7 +30359,7 @@ msgstr ""
msgid "KubernetesDashboard|Dashboard"
msgstr ""
-msgid "KubernetesDashboard|Delete Pod"
+msgid "KubernetesDashboard|Delete pod"
msgstr ""
msgid "KubernetesDashboard|Deployment"
@@ -57809,6 +57812,9 @@ msgstr ""
msgid "UserMapping|Pending approval"
msgstr ""
+msgid "UserMapping|Placeholder %{name} (@%{username}) kept as placeholder."
+msgstr ""
+
msgid "UserMapping|Placeholder user"
msgstr ""
diff --git a/spec/features/broadcast_messages_spec.rb b/spec/features/broadcast_messages_spec.rb
index c1a8e4d4b15..768f73d55ab 100644
--- a/spec/features/broadcast_messages_spec.rb
+++ b/spec/features/broadcast_messages_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe 'Broadcast Messages', feature_category: :notifications do
expect_to_be_on_explore_projects_page
- find('body.page-initialised .js-dismiss-current-broadcast-notification').click
+ find(".js-dismiss-current-broadcast-notification[data-id='#{broadcast_message.id}']").click
expect_message_dismissed
end
@@ -41,7 +41,7 @@ RSpec.describe 'Broadcast Messages', feature_category: :notifications do
expect_to_be_on_explore_projects_page
- find('body.page-initialised .js-dismiss-current-broadcast-notification').click
+ find(".js-dismiss-current-broadcast-notification[data-id='#{broadcast_message.id}']").click
expect_message_dismissed
@@ -57,7 +57,7 @@ RSpec.describe 'Broadcast Messages', feature_category: :notifications do
expect_to_be_on_explore_projects_page
- find('body.page-initialised .js-dismiss-current-broadcast-notification').click
+ find(".js-dismiss-current-broadcast-notification[data-id='#{broadcast_message.id}']").click
expect_message_dismissed
@@ -79,7 +79,7 @@ RSpec.describe 'Broadcast Messages', feature_category: :notifications do
it 'is not dismissible' do
visit path
- expect(page).not_to have_selector('.js-dismiss-current-broadcast-notification')
+ expect(page).not_to have_selector(".js-dismiss-current-broadcast-notification[data-id=#{broadcast_message.id}]")
end
it 'does not replace placeholders' do
@@ -127,7 +127,7 @@ RSpec.describe 'Broadcast Messages', feature_category: :notifications do
visit path
- expect_broadcast_message(text)
+ expect_broadcast_message(message.id, text)
# seed the other cache
original_strategy_value = Gitlab::Cache::JsonCache::STRATEGY_KEY_COMPONENTS
@@ -135,7 +135,7 @@ RSpec.describe 'Broadcast Messages', feature_category: :notifications do
page.refresh
- expect_broadcast_message(text)
+ expect_broadcast_message(message.id, text)
# delete on original cache
stub_const('Gitlab::Cache::JsonCaches::JsonKeyed::STRATEGY_KEY_COMPONENTS', original_strategy_value)
@@ -153,27 +153,27 @@ RSpec.describe 'Broadcast Messages', feature_category: :notifications do
visit path
- expect_no_broadcast_message
+ expect_no_broadcast_message(message.id)
# other revision of GitLab does gets cache destroyed
stub_const('Gitlab::Cache::JsonCaches::JsonKeyed::STRATEGY_KEY_COMPONENTS', new_strategy_value)
page.refresh
- expect_no_broadcast_message
+ expect_no_broadcast_message(message.id)
end
end
- def expect_broadcast_message(text)
- within_testid('banner-broadcast-message') do
+ def expect_broadcast_message(id, text)
+ within(".js-broadcast-notification-#{id}") do
expect(page).to have_content text
end
end
- def expect_no_broadcast_message
+ def expect_no_broadcast_message(id)
expect_to_be_on_explore_projects_page
- expect(page).not_to have_selector('[data-testid="banner-broadcast-message"]')
+ expect(page).not_to have_selector(".js-broadcast-notification-#{id}")
end
def expect_to_be_on_explore_projects_page
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index 3aa1f5cc06e..6508f3ccf36 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -353,7 +353,7 @@ RSpec.describe 'GFM autocomplete', :js, feature_category: :team_planning do
end
end
- context 'issues' do
+ context 'issues', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/471790' do
let(:object) { issue }
let(:expected_body) { object.to_reference }
diff --git a/spec/features/issues/user_sees_sidebar_updates_in_realtime_spec.rb b/spec/features/issues/user_sees_sidebar_updates_in_realtime_spec.rb
index 28bef4ab4c0..f6b97545863 100644
--- a/spec/features/issues/user_sees_sidebar_updates_in_realtime_spec.rb
+++ b/spec/features/issues/user_sees_sidebar_updates_in_realtime_spec.rb
@@ -50,8 +50,10 @@ RSpec.describe 'Issues > Real-time sidebar', :js, :with_license, feature_categor
wait_for_all_requests
- click_button label.name
- click_button 'Close'
+ page.within(labels_widget) do
+ click_button label.name
+ click_button 'Close'
+ end
wait_for_requests
diff --git a/spec/features/projects/work_items/work_item_children_spec.rb b/spec/features/projects/work_items/work_item_children_spec.rb
index 52de3bcd3f7..94e35ed5a19 100644
--- a/spec/features/projects/work_items/work_item_children_spec.rb
+++ b/spec/features/projects/work_items/work_item_children_spec.rb
@@ -59,7 +59,7 @@ RSpec.describe 'Work item children', :js, feature_category: :team_planning do
end
it 'adds a new child task', :aggregate_failures do
- allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(104)
+ allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(108)
within_testid('work-item-links') do
click_button 'Add'
@@ -79,7 +79,7 @@ RSpec.describe 'Work item children', :js, feature_category: :team_planning do
end
it 'removes a child task and undoing', :aggregate_failures do
- allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(104)
+ allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(108)
within_testid('work-item-links') do
click_button 'Add'
click_button 'New task'
diff --git a/spec/frontend/ci/common/private/jobs_filtered_search/jobs_filtered_search_spec.js b/spec/frontend/ci/common/private/jobs_filtered_search/app_spec.js
similarity index 65%
rename from spec/frontend/ci/common/private/jobs_filtered_search/jobs_filtered_search_spec.js
rename to spec/frontend/ci/common/private/jobs_filtered_search/app_spec.js
index 079738557a4..ecf0509745a 100644
--- a/spec/frontend/ci/common/private/jobs_filtered_search/jobs_filtered_search_spec.js
+++ b/spec/frontend/ci/common/private/jobs_filtered_search/app_spec.js
@@ -28,7 +28,7 @@ describe('Jobs filtered search', () => {
...props,
},
provide: {
- glFeatures: { adminJobsFilterRunnerType: true },
+ glFeatures: { adminJobsFilterRunnerType: true, populateAndUseBuildNamesTable: true },
...provideOptions,
},
});
@@ -40,6 +40,19 @@ describe('Jobs filtered search', () => {
expect(findFilteredSearch().exists()).toBe(true);
});
+ it('displays filtered search placeholder', () => {
+ createComponent();
+
+ expect(findFilteredSearch().props('placeholder')).toBe('Search or filter jobs...');
+ });
+
+ it('displays filtered search text label', () => {
+ createComponent();
+
+ expect(findFilteredSearch().props('searchTextOptionLabel')).toBe('Search for this text');
+ expect(findFilteredSearch().props('termsAsTokens')).toBe(true);
+ });
+
it('displays status token', () => {
createComponent();
@@ -82,7 +95,11 @@ describe('Jobs filtered search', () => {
const tokenRunnerTypesValue = 'INSTANCE_VALUE';
createComponent({
- queryString: { statuses: tokenStatusesValue, runnerTypes: tokenRunnerTypesValue },
+ queryString: {
+ statuses: tokenStatusesValue,
+ runnerTypes: tokenRunnerTypesValue,
+ name: 'rspec',
+ },
});
expect(findFilteredSearch().props('value')).toEqual([
@@ -91,6 +108,12 @@ describe('Jobs filtered search', () => {
type: TOKEN_TYPE_JOBS_RUNNER_TYPE,
value: { data: tokenRunnerTypesValue, operator: '=' },
},
+ {
+ type: 'filtered-search-term',
+ value: {
+ data: 'rspec',
+ },
+ },
]);
});
});
@@ -120,4 +143,37 @@ describe('Jobs filtered search', () => {
});
});
});
+
+ describe('when feature flag `populateAndUseBuildNamesTable` is disabled', () => {
+ const provideOptions = { glFeatures: { populateAndUseBuildNamesTable: false } };
+
+ describe('with query string passed', () => {
+ it('filtered search returns only data shape for search token `status`', () => {
+ const tokenStatusesValue = 'SUCCESS';
+ const tokenRunnerTypesValue = 'INSTANCE_VALUE';
+
+ createComponent(
+ {
+ queryString: {
+ statuses: tokenStatusesValue,
+ runnerTypes: tokenRunnerTypesValue,
+ name: 'rspec',
+ },
+ },
+ provideOptions,
+ );
+
+ expect(findFilteredSearch().props('value')).toEqual([
+ { type: TOKEN_TYPE_STATUS, value: { data: tokenStatusesValue, operator: '=' } },
+ ]);
+ });
+ });
+
+ it('displays legacy filtered search attributes', () => {
+ createComponent({}, provideOptions);
+
+ expect(findFilteredSearch().props('placeholder')).toBe('Filter jobs');
+ expect(findFilteredSearch().props('termsAsTokens')).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/ci/common/private/jobs_filtered_search/utils_spec.js b/spec/frontend/ci/common/private/jobs_filtered_search/utils_spec.js
index 8f6d2368bf4..59f606761b8 100644
--- a/spec/frontend/ci/common/private/jobs_filtered_search/utils_spec.js
+++ b/spec/frontend/ci/common/private/jobs_filtered_search/utils_spec.js
@@ -12,6 +12,7 @@ describe('Filtered search utils', () => {
${{ wrong: 'SUCCESS' }} | ${null}
${{ statuses: 'wrong' }} | ${null}
${{ wrong: 'wrong' }} | ${null}
+ ${{ name: 'rspec' }} | ${{ name: 'rspec' }}
`(
'when provided $queryStringObject, the expected result is $expected',
({ queryStringObject, expected }) => {
diff --git a/spec/frontend/ci/jobs_page/job_page_app_spec.js b/spec/frontend/ci/jobs_page/job_page_app_spec.js
index 800435826e4..f2ba71b9d77 100644
--- a/spec/frontend/ci/jobs_page/job_page_app_spec.js
+++ b/spec/frontend/ci/jobs_page/job_page_app_spec.js
@@ -27,6 +27,8 @@ Vue.use(VueApollo);
jest.mock('~/alert');
+const mockJobName = 'rspec-job';
+
describe('Job table app', () => {
let wrapper;
@@ -60,10 +62,14 @@ describe('Job table app', () => {
handler = successHandler,
countHandler = countSuccessHandler,
mountFn = shallowMount,
+ flagState = false,
} = {}) => {
wrapper = mountFn(JobsTableApp, {
provide: {
fullPath: projectPath,
+ glFeatures: {
+ populateAndUseBuildNamesTable: flagState,
+ },
},
apolloProvider: createMockApolloProvider(handler, countHandler),
});
@@ -246,6 +252,22 @@ describe('Job table app', () => {
},
);
+ it('filters jobs by status', async () => {
+ createComponent();
+
+ await findFilteredSearch().vm.$emit('filterJobsBySearch', [mockFailedSearchToken]);
+
+ expect(successHandler).toHaveBeenCalledWith({
+ first: 30,
+ fullPath: 'gitlab-org/gitlab',
+ statuses: 'FAILED',
+ });
+ expect(countSuccessHandler).toHaveBeenCalledWith({
+ fullPath: 'gitlab-org/gitlab',
+ statuses: 'FAILED',
+ });
+ });
+
it('refetches jobs query when filtering', async () => {
createComponent();
@@ -334,5 +356,87 @@ describe('Job table app', () => {
statuses: null,
});
});
+
+ describe('with feature flag populateAndUseBuildNamesTable enabled', () => {
+ beforeEach(() => {
+ createComponent({ flagState: true });
+ });
+
+ it('filters jobs by name', async () => {
+ await findFilteredSearch().vm.$emit('filterJobsBySearch', [mockJobName]);
+
+ expect(successHandler).toHaveBeenCalledWith({
+ first: 30,
+ fullPath: 'gitlab-org/gitlab',
+ name: mockJobName,
+ });
+ expect(countSuccessHandler).toHaveBeenCalledWith({
+ fullPath: 'gitlab-org/gitlab',
+ name: mockJobName,
+ });
+ });
+
+ it('updates URL query string when filtering jobs by name', async () => {
+ jest.spyOn(urlUtils, 'updateHistory');
+
+ await findFilteredSearch().vm.$emit('filterJobsBySearch', [mockJobName]);
+
+ expect(urlUtils.updateHistory).toHaveBeenCalledWith({
+ url: `${TEST_HOST}/?name=${mockJobName}`,
+ });
+ });
+
+ it('updates URL query string when filtering jobs by name and status', async () => {
+ jest.spyOn(urlUtils, 'updateHistory');
+
+ await findFilteredSearch().vm.$emit('filterJobsBySearch', [
+ mockFailedSearchToken,
+ mockJobName,
+ ]);
+
+ expect(urlUtils.updateHistory).toHaveBeenCalledWith({
+ url: `${TEST_HOST}/?statuses=FAILED&name=${mockJobName}`,
+ });
+ });
+
+ it('resets query param after clearing tokens', () => {
+ jest.spyOn(urlUtils, 'updateHistory');
+
+ findFilteredSearch().vm.$emit('filterJobsBySearch', [mockFailedSearchToken, mockJobName]);
+
+ expect(successHandler).toHaveBeenCalledWith({
+ first: 30,
+ fullPath: 'gitlab-org/gitlab',
+ statuses: 'FAILED',
+ name: mockJobName,
+ });
+ expect(countSuccessHandler).toHaveBeenCalledWith({
+ fullPath: 'gitlab-org/gitlab',
+ statuses: 'FAILED',
+ name: mockJobName,
+ });
+ expect(urlUtils.updateHistory).toHaveBeenCalledWith({
+ url: `${TEST_HOST}/?statuses=FAILED&name=${mockJobName}`,
+ });
+
+ findFilteredSearch().vm.$emit('filterJobsBySearch', []);
+
+ expect(urlUtils.updateHistory).toHaveBeenCalledWith({
+ url: `${TEST_HOST}/`,
+ });
+
+ expect(successHandler).toHaveBeenCalledWith({
+ first: 30,
+ fullPath: 'gitlab-org/gitlab',
+ statuses: null,
+ name: null,
+ });
+ expect(countSuccessHandler).toHaveBeenCalledWith({
+ fullPath: 'gitlab-org/gitlab',
+ statuses: null,
+ name: null,
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_overview_spec.js b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_overview_spec.js
index 44d7fc4f6d6..c7560501a62 100644
--- a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_overview_spec.js
+++ b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_overview_spec.js
@@ -377,6 +377,16 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_ov
expect(findDeletePodModal().props('pod')).toEqual(podToDelete);
});
+
+ it('provides correct pod when emitted from the details drawer', async () => {
+ const podToDelete = mockPodsTableItems[1];
+ findKubernetesTabs().vm.$emit('show-resource-details', mockPodsTableItems[1]);
+ await nextTick();
+ findWorkloadDetails().vm.$emit('delete-pod', podToDelete);
+ await nextTick();
+
+ expect(findDeletePodModal().props('pod')).toEqual(podToDelete);
+ });
});
describe('on child component error', () => {
diff --git a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_pods_spec.js b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_pods_spec.js
index fd5f26f96ea..23ac293351b 100644
--- a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_pods_spec.js
+++ b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_pods_spec.js
@@ -106,8 +106,9 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_po
const actions = [
{
name: 'delete-pod',
- text: 'Delete Pod',
+ text: 'Delete pod',
icon: 'remove',
+ variant: 'danger',
class: '!gl-text-red-500',
},
];
diff --git a/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js b/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js
index a3e83672637..59d82f4132c 100644
--- a/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js
+++ b/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js
@@ -374,9 +374,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
});
describe('deleteKubernetesPod', () => {
- const mockPodsDeleteFn = jest.fn().mockImplementation(() => {
- return Promise.resolve({ errors: [] });
- });
+ const mockPodsDeleteFn = jest.fn().mockResolvedValue({ errors: [] });
const podToDelete = 'my-pod';
it('should request delete pod API from the cluster_client library', async () => {
diff --git a/spec/frontend/kubernetes_dashboard/components/workload_details_spec.js b/spec/frontend/kubernetes_dashboard/components/workload_details_spec.js
index 2447bf0f5d5..cbc9483ffac 100644
--- a/spec/frontend/kubernetes_dashboard/components/workload_details_spec.js
+++ b/spec/frontend/kubernetes_dashboard/components/workload_details_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlBadge, GlTruncate } from '@gitlab/ui';
+import { GlBadge, GlTruncate, GlButton } from '@gitlab/ui';
import WorkloadDetails from '~/kubernetes_dashboard/components/workload_details.vue';
import WorkloadDetailsItem from '~/kubernetes_dashboard/components/workload_details_item.vue';
import { WORKLOAD_STATUS_BADGE_VARIANTS } from '~/kubernetes_dashboard/constants';
@@ -25,6 +25,8 @@ const findAllBadges = () => wrapper.findAllComponents(GlBadge);
const findBadge = (at) => findAllBadges().at(at);
const findAllPodLogsButtons = () => wrapper.findAllComponents(PodLogsButton);
const findPodLogsButton = (at) => findAllPodLogsButtons().at(at);
+const findAllButtons = () => wrapper.findAllComponents(GlButton);
+const findButton = (at) => findAllButtons().at(at);
describe('Workload details component', () => {
describe('when minimal fields are provided', () => {
@@ -74,6 +76,48 @@ describe('Workload details component', () => {
expect(findWorkloadDetailsItem(index).props('collapsible')).toBe(true);
});
+ describe('when actions are provided', () => {
+ const actions = [
+ {
+ name: 'delete-pod',
+ text: 'Delete pod',
+ icon: 'remove',
+ variant: 'danger',
+ },
+ ];
+ const mockTableItemsWithActions = {
+ ...mockPodsTableItems[0],
+ actions,
+ };
+
+ beforeEach(() => {
+ createWrapper(mockTableItemsWithActions);
+ });
+
+ it('renders a non-collapsible list item for containers', () => {
+ expect(findWorkloadDetailsItem(1).props('label')).toBe('Actions');
+ expect(findWorkloadDetailsItem(1).props('collapsible')).toBe(false);
+ });
+
+ it('renders a button for each action', () => {
+ expect(findAllButtons()).toHaveLength(1);
+ });
+
+ it.each(actions)('renders a button with the correct props', (action) => {
+ const currentIndex = actions.indexOf(action);
+
+ expect(findButton(currentIndex).props()).toMatchObject({
+ variant: action.variant,
+ icon: action.icon,
+ });
+
+ expect(findButton(currentIndex).attributes()).toMatchObject({
+ title: action.text,
+ 'aria-label': action.text,
+ });
+ });
+ });
+
describe('when containers are provided', () => {
const mockTableItemsWithContainers = {
...mockPodsTableItems[0],
diff --git a/spec/frontend/kubernetes_dashboard/components/workload_table_spec.js b/spec/frontend/kubernetes_dashboard/components/workload_table_spec.js
index 8866b550a91..4ac6035be12 100644
--- a/spec/frontend/kubernetes_dashboard/components/workload_table_spec.js
+++ b/spec/frontend/kubernetes_dashboard/components/workload_table_spec.js
@@ -157,7 +157,7 @@ describe('Workload table component', () => {
});
it('renders correct props for each dropdown', () => {
- expect(findAllActionsDropdowns().at(0).attributes('title')).toEqual('Actions');
+ expect(findAllActionsDropdowns().at(0).attributes('title')).toBe('Actions');
expect(findAllActionsDropdowns().at(0).props('items')).toMatchObject([
{
text: 'Delete Pod',
diff --git a/spec/frontend/lib/utils/mock_data.js b/spec/frontend/lib/utils/mock_data.js
index 89afed6fbc5..84d94d00a64 100644
--- a/spec/frontend/lib/utils/mock_data.js
+++ b/spec/frontend/lib/utils/mock_data.js
@@ -38,8 +38,8 @@ const encodedJavaScriptUrls = [
'\\u006A\\u0061\\u0076\\u0061\\u0073\\u0063\\u0072\\u0069\\u0070\\u0074\\u003A\\u0061\\u006C\\u0065\\u0072\\u0074\\u0028\\u0027\\u0058\\u0053\\u0053\\u0027\\u0029',
];
-export const safeUrls = [...absoluteUrls, ...rootRelativeUrls];
-export const unsafeUrls = [
+export const validURLs = [...absoluteUrls, ...rootRelativeUrls];
+export const invalidURLs = [
...relativeUrls,
...urlsWithoutHost,
...nonHttpUrls,
diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index f1240ff9ceb..75f8a1f342d 100644
--- a/spec/frontend/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -2,7 +2,7 @@ import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
import * as urlUtils from '~/lib/utils/url_utility';
import { setGlobalAlerts } from '~/lib/utils/global_alerts';
-import { safeUrls, unsafeUrls } from './mock_data';
+import { validURLs, invalidURLs } from './mock_data';
jest.mock('~/lib/utils/global_alerts', () => ({
getGlobalAlerts: jest.fn().mockImplementation(() => [
@@ -781,24 +781,24 @@ describe('URL utility', () => {
);
});
- describe('isSafeUrl', () => {
+ describe('isValidURL', () => {
describe('with URL constructor support', () => {
- it.each(safeUrls)('returns true for %s', (url) => {
- expect(urlUtils.isSafeURL(url)).toBe(true);
+ it.each(validURLs)('returns true for %s', (url) => {
+ expect(urlUtils.isValidURL(url)).toBe(true);
});
- it.each(unsafeUrls)('returns false for %s', (url) => {
- expect(urlUtils.isSafeURL(url)).toBe(false);
+ it.each(invalidURLs)('returns false for %s', (url) => {
+ expect(urlUtils.isValidURL(url)).toBe(false);
});
});
});
describe('sanitizeUrl', () => {
- it.each(safeUrls)('returns the url for %s', (url) => {
+ it.each(validURLs)('returns the url for %s', (url) => {
expect(urlUtils.sanitizeUrl(url)).toBe(url);
});
- it.each(unsafeUrls)('returns `about:blank` for %s', (url) => {
+ it.each(invalidURLs)('returns `about:blank` for %s', (url) => {
expect(urlUtils.sanitizeUrl(url)).toBe('about:blank');
});
});
diff --git a/spec/frontend/members/placeholders/components/app_spec.js b/spec/frontend/members/placeholders/components/app_spec.js
index 88559e9db27..a9264a69cee 100644
--- a/spec/frontend/members/placeholders/components/app_spec.js
+++ b/spec/frontend/members/placeholders/components/app_spec.js
@@ -1,7 +1,9 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
+// eslint-disable-next-line no-restricted-imports
+import Vuex from 'vuex';
import VueApollo from 'vue-apollo';
import { shallowMount } from '@vue/test-utils';
-import { GlTabs } from '@gitlab/ui';
+import { GlTab, GlTabs } from '@gitlab/ui';
import { createAlert } from '~/alert';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -9,13 +11,16 @@ import waitForPromises from 'helpers/wait_for_promises';
import PlaceholdersTabApp from '~/members/placeholders/components/app.vue';
import PlaceholdersTable from '~/members/placeholders/components/placeholders_table.vue';
import importSourceUsersQuery from '~/members/placeholders/graphql/queries/import_source_users.query.graphql';
-import { mockSourceUsersQueryResponse } from '../mock_data';
+import { MEMBERS_TAB_TYPES } from '~/members/constants';
+import { mockSourceUsersQueryResponse, mockSourceUsers, pagination } from '../mock_data';
+Vue.use(Vuex);
Vue.use(VueApollo);
jest.mock('~/alert');
describe('PlaceholdersTabApp', () => {
let wrapper;
+ let store;
let mockApollo;
const mockGroup = {
@@ -23,19 +28,37 @@ describe('PlaceholdersTabApp', () => {
name: 'Imported group',
};
const sourceUsersQueryHandler = jest.fn().mockResolvedValue(mockSourceUsersQueryResponse());
+ const $toast = {
+ show: jest.fn(),
+ };
const createComponent = ({ queryHandler = sourceUsersQueryHandler } = {}) => {
+ store = new Vuex.Store({
+ modules: {
+ [MEMBERS_TAB_TYPES.placeholder]: {
+ namespaced: true,
+ state: {
+ pagination,
+ },
+ },
+ },
+ });
+
mockApollo = createMockApollo([[importSourceUsersQuery, queryHandler]]);
wrapper = shallowMount(PlaceholdersTabApp, {
apolloProvider: mockApollo,
+ store,
provide: {
group: mockGroup,
},
+ mocks: { $toast },
+ stubs: { GlTab },
});
};
const findTabs = () => wrapper.findComponent(GlTabs);
+ const findTabAt = (index) => wrapper.findAllComponents(GlTab).at(index);
const findPlaceholdersTable = () => wrapper.findComponent(PlaceholdersTable);
it('renders tabs', () => {
@@ -44,6 +67,41 @@ describe('PlaceholdersTabApp', () => {
expect(findTabs().exists()).toBe(true);
});
+ it('renders tab titles with counts', async () => {
+ createComponent();
+ await nextTick();
+
+ expect(findTabAt(0).text()).toBe(
+ `Awaiting reassignment ${pagination.awaitingReassignmentItems}`,
+ );
+ expect(findTabAt(1).text()).toBe(`Reassigned ${pagination.reassignedItems}`);
+ });
+
+ describe('on table "confirm" event', () => {
+ const mockSourceUser = mockSourceUsers[1];
+
+ beforeEach(async () => {
+ createComponent();
+ await nextTick();
+
+ findPlaceholdersTable().vm.$emit('confirm', mockSourceUser);
+ await nextTick();
+ });
+
+ it('updates tab counts', () => {
+ expect(findTabAt(0).text()).toBe(
+ `Awaiting reassignment ${pagination.awaitingReassignmentItems - 1}`,
+ );
+ expect(findTabAt(1).text()).toBe(`Reassigned ${pagination.reassignedItems + 1}`);
+ });
+
+ it('shows toast', () => {
+ expect($toast.show).toHaveBeenCalledWith(
+ 'Placeholder Placeholder 2 (@placeholder_2) kept as placeholder.',
+ );
+ });
+ });
+
describe('when sourceUsers query is loading', () => {
it('renders placeholders table as loading', () => {
createComponent();
@@ -86,12 +144,12 @@ describe('PlaceholdersTabApp', () => {
});
it('renders placeholders table', () => {
- const mockSourceUsers = mockSourceUsersQueryResponse().data.namespace.importSourceUsers;
+ const sourceUsers = mockSourceUsersQueryResponse().data.namespace.importSourceUsers;
expect(findPlaceholdersTable().props()).toMatchObject({
isLoading: false,
- items: mockSourceUsers.nodes,
- pageInfo: mockSourceUsers.pageInfo,
+ items: sourceUsers.nodes,
+ pageInfo: sourceUsers.pageInfo,
});
});
});
diff --git a/spec/frontend/members/placeholders/components/placeholder_actions_spec.js b/spec/frontend/members/placeholders/components/placeholder_actions_spec.js
index e28ba02b815..10b94565187 100644
--- a/spec/frontend/members/placeholders/components/placeholder_actions_spec.js
+++ b/spec/frontend/members/placeholders/components/placeholder_actions_spec.js
@@ -8,6 +8,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import PlaceholderActions from '~/members/placeholders/components/placeholder_actions.vue';
import searchUsersQuery from '~/graphql_shared/queries/users_search_all_paginated.query.graphql';
+import importSourceUsersQuery from '~/members/placeholders/graphql/queries/import_source_users.query.graphql';
import importSourceUserReassignMutation from '~/members/placeholders/graphql/mutations/reassign.mutation.graphql';
import importSourceUserKeepAsPlaceholderMutation from '~/members/placeholders/graphql/mutations/keep_as_placeholder.mutation.graphql';
import importSourceUserResendNotificationMutation from '~/members/placeholders/graphql/mutations/resend_notification.mutation.graphql';
@@ -15,6 +16,7 @@ import importSourceUserCancelReassignmentMutation from '~/members/placeholders/g
import {
mockSourceUsers,
+ mockSourceUsersQueryResponse,
mockReassignMutationResponse,
mockKeepAsPlaceholderMutationResponse,
mockResendNotificationMutationResponse,
@@ -36,6 +38,7 @@ describe('PlaceholderActions', () => {
sourceUser: mockSourceUsers[0],
};
const usersQueryHandler = jest.fn().mockResolvedValue(mockUsersQueryResponse);
+ const sourceUsersQueryHandler = jest.fn().mockResolvedValue(mockSourceUsersQueryResponse());
const reassignMutationHandler = jest.fn().mockResolvedValue(mockReassignMutationResponse);
const keepAsPlaceholderMutationHandler = jest
.fn()
@@ -53,11 +56,22 @@ describe('PlaceholderActions', () => {
const createComponent = ({ seachUsersQueryHandler = usersQueryHandler, props = {} } = {}) => {
mockApollo = createMockApollo([
[searchUsersQuery, seachUsersQueryHandler],
+ [importSourceUsersQuery, sourceUsersQueryHandler],
[importSourceUserReassignMutation, reassignMutationHandler],
[importSourceUserKeepAsPlaceholderMutation, keepAsPlaceholderMutationHandler],
[importSourceUserResendNotificationMutation, resendNotificationMutationHandler],
[importSourceUserCancelReassignmentMutation, cancelReassignmentMutationHandler],
]);
+ // refetchQueries will only refetch active queries, so simply registering a query handler is not enough.
+ // We need to call `subscribe()` to make the query observable and avoid "Unknown query" errors.
+ // This simulates what the actual code in VueApollo is doing when adding a smart query.
+ // Docs: https://www.apollographql.com/docs/react/api/core/ApolloClient/#watchquery
+ mockApollo.clients.defaultClient
+ .watchQuery({
+ query: importSourceUsersQuery,
+ variables: { fullPath: 'test' },
+ })
+ .subscribe();
wrapper = shallowMountExtended(PlaceholderActions, {
apolloProvider: mockApollo,
@@ -158,6 +172,15 @@ describe('PlaceholderActions', () => {
id: mockSourceUsers[0].id,
});
});
+
+ it('refetches sourceUsersQuery', () => {
+ expect(sourceUsersQueryHandler).toHaveBeenCalledTimes(2);
+ });
+
+ it('emits "confirm" event', async () => {
+ await waitForPromises();
+ expect(wrapper.emitted('confirm')[0]).toEqual([]);
+ });
});
});
@@ -194,6 +217,15 @@ describe('PlaceholderActions', () => {
userId: mockUser1.id,
});
});
+
+ it('does not refetch sourceUsersQuery', () => {
+ expect(sourceUsersQueryHandler).toHaveBeenCalledTimes(1);
+ });
+
+ it('does not emit "confirm" event', async () => {
+ await waitForPromises();
+ expect(wrapper.emitted('confirm')).toBeUndefined();
+ });
});
});
});
diff --git a/spec/frontend/members/placeholders/components/placeholders_table_spec.js b/spec/frontend/members/placeholders/components/placeholders_table_spec.js
index d10732a38ce..15cd830d68a 100644
--- a/spec/frontend/members/placeholders/components/placeholders_table_spec.js
+++ b/spec/frontend/members/placeholders/components/placeholders_table_spec.js
@@ -206,4 +206,18 @@ describe('PlaceholdersTable', () => {
expect(wrapper.emitted('next')[0]).toEqual([]);
});
});
+
+ describe('actions events', () => {
+ beforeEach(() => {
+ createComponent({ mountFn: mount });
+ });
+
+ it('emits "confirm" event with item', () => {
+ const actions = findTableRows().at(2).findComponent(PlaceholderActions);
+
+ actions.vm.$emit('confirm');
+
+ expect(wrapper.emitted('confirm')[0]).toEqual([mockSourceUsers[2]]);
+ });
+ });
});
diff --git a/spec/frontend/members/placeholders/mock_data.js b/spec/frontend/members/placeholders/mock_data.js
index 1d39026bb2b..6089b4a0d87 100644
--- a/spec/frontend/members/placeholders/mock_data.js
+++ b/spec/frontend/members/placeholders/mock_data.js
@@ -186,3 +186,9 @@ export const mockUsersWithPaginationQueryResponse = {
},
},
};
+
+export const pagination = {
+ awaitingReassignmentItems: 5,
+ reassignedItems: 2,
+ totalItems: 7,
+};
diff --git a/spec/frontend/users_select/test_helper.js b/spec/frontend/users_select/test_helper.js
index 05b0bf314a0..150818cecf2 100644
--- a/spec/frontend/users_select/test_helper.js
+++ b/spec/frontend/users_select/test_helper.js
@@ -10,7 +10,9 @@ import UsersSelect from '~/users_select';
const getUserSearchHTML = memoize((fixture) => {
const parser = new DOMParser();
- const el = parser.parseFromString(fixture, 'text/html').querySelector('.merge-request-assignee');
+ const el = parser
+ .parseFromString(fixture, 'text/html')
+ .querySelector('[data-testid=merge-request-assignee]');
return el.outerHTML;
});
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 6468a210441..6d5be32e8d5 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -9559,4 +9559,49 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
it { expect(project.merge_trains_enabled?).to eq(false) }
end
+
+ describe '#lfs_file_locks_changed_epoch', :clean_gitlab_redis_cache do
+ let(:project) { build(:project, id: 1) }
+ let(:epoch) { Time.current.strftime('%s%L').to_i }
+
+ it 'returns a cached epoch value in milliseconds', :aggregate_failures, :freeze_time do
+ cold_cache_control = RedisCommands::Recorder.new do
+ expect(project.lfs_file_locks_changed_epoch).to eq epoch
+ end
+
+ expect(cold_cache_control.by_command('get').count).to eq 1
+ expect(cold_cache_control.by_command('set').count).to eq 1
+
+ warm_cache_control = RedisCommands::Recorder.new do
+ expect(project.lfs_file_locks_changed_epoch).to eq epoch
+ end
+
+ expect(warm_cache_control.by_command('get').count).to eq 1
+ expect(warm_cache_control.by_command('set').count).to eq 0
+ end
+ end
+
+ describe '#refresh_lfs_file_locks_changed_epoch' do
+ let(:project) { build(:project, id: 1) }
+ let(:original_time) { Time.current }
+ let(:refresh_time) { original_time + 1.second }
+ let(:original_epoch) { original_time.strftime('%s%L').to_i }
+ let(:refreshed_epoch) { original_epoch + 1.second.in_milliseconds }
+
+ it 'refreshes the cache and returns the new epoch value', :aggregate_failures, :freeze_time do
+ expect(project.lfs_file_locks_changed_epoch).to eq(original_epoch)
+
+ travel_to(refresh_time)
+
+ expect(project.lfs_file_locks_changed_epoch).to eq(original_epoch)
+
+ control = RedisCommands::Recorder.new do
+ expect(project.refresh_lfs_file_locks_changed_epoch).to eq(refreshed_epoch)
+ end
+ expect(control.by_command('get').count).to eq 0
+ expect(control.by_command('set').count).to eq 1
+
+ expect(project.lfs_file_locks_changed_epoch).to eq(refreshed_epoch)
+ end
+ end
end
diff --git a/spec/services/lfs/lock_file_service_spec.rb b/spec/services/lfs/lock_file_service_spec.rb
index 47bf0c5f4ce..8502c7d7476 100644
--- a/spec/services/lfs/lock_file_service_spec.rb
+++ b/spec/services/lfs/lock_file_service_spec.rb
@@ -6,19 +6,21 @@ RSpec.describe Lfs::LockFileService, feature_category: :source_code_management d
let(:project) { create(:project) }
let(:current_user) { create(:user) }
- subject { described_class.new(project, current_user, params) }
-
describe '#execute' do
+ subject(:execute) { described_class.new(project, current_user, params).execute }
+
let(:params) { { path: 'README.md' } }
context 'when not authorized' do
it "doesn't succeed" do
- result = subject.execute
+ result = execute
expect(result[:status]).to eq(:error)
expect(result[:http_status]).to eq(403)
expect(result[:message]).to eq('You have no permissions')
end
+
+ it_behaves_like 'does not refresh project.lfs_file_locks_changed_epoch'
end
context 'when authorized' do
@@ -30,36 +32,40 @@ RSpec.describe Lfs::LockFileService, feature_category: :source_code_management d
let!(:lock) { create(:lfs_file_lock, project: project) }
it "doesn't succeed" do
- expect(subject.execute[:status]).to eq(:error)
+ expect(execute[:status]).to eq(:error)
end
it "doesn't create the Lock" do
- expect do
- subject.execute
- end.not_to change { LfsFileLock.count }
+ expect { execute }.not_to change { LfsFileLock.count }
end
+
+ it_behaves_like 'does not refresh project.lfs_file_locks_changed_epoch'
end
context 'without an existent lock' do
it "succeeds" do
- expect(subject.execute[:status]).to eq(:success)
+ expect(execute[:status]).to eq(:success)
end
it "creates the Lock" do
- expect do
- subject.execute
- end.to change { LfsFileLock.count }.by(1)
+ expect { execute }.to change { LfsFileLock.count }.by(1)
end
+
+ it_behaves_like 'refreshes project.lfs_file_locks_changed_epoch value'
end
context 'when an error is raised' do
- it "doesn't succeed" do
+ before do
allow_next_instance_of(described_class) do |instance|
allow(instance).to receive(:create_lock!).and_raise(StandardError)
end
-
- expect(subject.execute[:status]).to eq(:error)
end
+
+ it "doesn't succeed" do
+ expect(execute[:status]).to eq(:error)
+ end
+
+ it_behaves_like 'does not refresh project.lfs_file_locks_changed_epoch'
end
end
end
diff --git a/spec/services/lfs/unlock_file_service_spec.rb b/spec/services/lfs/unlock_file_service_spec.rb
index 51d5391e793..426cc50727a 100644
--- a/spec/services/lfs/unlock_file_service_spec.rb
+++ b/spec/services/lfs/unlock_file_service_spec.rb
@@ -9,17 +9,19 @@ RSpec.describe Lfs::UnlockFileService, feature_category: :source_code_management
let!(:lock) { create(:lfs_file_lock, user: lock_author, project: project) }
let(:params) { {} }
- subject { described_class.new(project, current_user, params) }
-
describe '#execute' do
+ subject(:execute) { described_class.new(project, current_user, params).execute }
+
context 'when not authorized' do
it "doesn't succeed" do
- result = subject.execute
+ result = execute
expect(result[:status]).to eq(:error)
expect(result[:http_status]).to eq(403)
expect(result[:message]).to eq(_('You have no permissions'))
end
+
+ it_behaves_like 'does not refresh project.lfs_file_locks_changed_epoch'
end
context 'when authorized' do
@@ -31,11 +33,13 @@ RSpec.describe Lfs::UnlockFileService, feature_category: :source_code_management
let(:params) { { id: 123 } }
it "doesn't succeed" do
- result = subject.execute
+ result = execute
expect(result[:status]).to eq(:error)
expect(result[:http_status]).to eq(404)
end
+
+ it_behaves_like 'does not refresh project.lfs_file_locks_changed_epoch'
end
context 'when unlocked by the author' do
@@ -43,11 +47,13 @@ RSpec.describe Lfs::UnlockFileService, feature_category: :source_code_management
let(:params) { { id: lock.id } }
it "succeeds" do
- result = subject.execute
+ result = execute
expect(result[:status]).to eq(:success)
expect(result[:lock]).to be_present
end
+
+ it_behaves_like 'refreshes project.lfs_file_locks_changed_epoch value'
end
context 'when unlocked by a different user' do
@@ -55,12 +61,14 @@ RSpec.describe Lfs::UnlockFileService, feature_category: :source_code_management
let(:params) { { id: lock.id } }
it "doesn't succeed" do
- result = subject.execute
+ result = execute
expect(result[:status]).to eq(:error)
expect(result[:message]).to match(/'README.md' is locked by @#{lock_author.username}/)
expect(result[:http_status]).to eq(403)
end
+
+ it_behaves_like 'does not refresh project.lfs_file_locks_changed_epoch'
end
context 'when forced' do
@@ -80,12 +88,14 @@ RSpec.describe Lfs::UnlockFileService, feature_category: :source_code_management
end
it "doesn't succeed" do
- result = subject.execute
+ result = execute
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq(_('You must have maintainer access to force delete a lock'))
expect(result[:http_status]).to eq(403)
end
+
+ it_behaves_like 'does not refresh project.lfs_file_locks_changed_epoch'
end
context 'by a maintainer user' do
@@ -96,11 +106,13 @@ RSpec.describe Lfs::UnlockFileService, feature_category: :source_code_management
end
it "succeeds" do
- result = subject.execute
+ result = execute
expect(result[:status]).to eq(:success)
expect(result[:lock]).to be_present
end
+
+ it_behaves_like 'refreshes project.lfs_file_locks_changed_epoch value'
end
end
end
diff --git a/spec/support/shared_examples/features/work_items_shared_examples.rb b/spec/support/shared_examples/features/work_items_shared_examples.rb
index 00bbc0b25fb..15011d6085e 100644
--- a/spec/support/shared_examples/features/work_items_shared_examples.rb
+++ b/spec/support/shared_examples/features/work_items_shared_examples.rb
@@ -27,9 +27,12 @@ end
RSpec.shared_examples 'work items toggle status button' do
it 'successfully shows and changes the status of the work item' do
- click_button 'Close', match: :first
+ within_testid 'work-item-comment-form-actions' do
+ # Depending of the context, the button's text could be `Close issue`, `Close key result`, `Close objective`, etc.
+ click_button 'Close', match: :first
- expect(page).to have_button 'Reopen'
+ expect(page).to have_button 'Reopen'
+ end
expect(work_item.reload.state).to eq('closed')
end
end
@@ -624,7 +627,9 @@ RSpec.shared_examples 'work items time tracking' do
expect(page).to be_axe_clean.within('[role="dialog"]')
- click_button 'Close'
+ within_testid 'set-time-estimate-modal' do
+ click_button 'Close'
+ end
click_button 'time spent'
expect(page).to be_axe_clean.within('[role="dialog"]')
@@ -632,15 +637,19 @@ RSpec.shared_examples 'work items time tracking' do
it 'adds and removes an estimate', :aggregate_failures do
click_button 'estimate'
- fill_in 'Estimate', with: '5d'
- click_button 'Save'
+ within_testid 'set-time-estimate-modal' do
+ fill_in 'Estimate', with: '5d'
+ click_button 'Save'
+ end
expect(page).to have_text 'Estimate 5d'
expect(page).to have_button '5d'
expect(page).not_to have_button 'estimate'
click_button '5d'
- click_button 'Remove'
+ within_testid 'set-time-estimate-modal' do
+ click_button 'Remove'
+ end
expect(page).not_to have_text 'Estimate 5d'
expect(page).not_to have_button '5d'
@@ -648,31 +657,39 @@ RSpec.shared_examples 'work items time tracking' do
end
it 'adds and deletes time entries and view report', :aggregate_failures do
- click_button 'time entry'
- fill_in 'Time spent', with: '1d'
- fill_in 'Summary', with: 'First summary'
- click_button 'Save'
+ click_button 'Add time entry'
+
+ within_testid 'create-timelog-modal' do
+ fill_in 'Time spent', with: '1d'
+ fill_in 'Summary', with: 'First summary'
+ click_button 'Save'
+ end
click_button 'Add time entry'
- fill_in 'Time spent', with: '2d'
- fill_in 'Summary', with: 'Second summary'
- click_button 'Save'
+
+ within_testid 'create-timelog-modal' do
+ fill_in 'Time spent', with: '2d'
+ fill_in 'Summary', with: 'Second summary'
+ click_button 'Save'
+ end
expect(page).to have_text 'Spent 3d'
expect(page).to have_button '3d'
click_button '3d'
- expect(page).to have_css 'h2', text: 'Time tracking report'
- expect(page).to have_text "1d #{user.name} First summary"
- expect(page).to have_text "2d #{user.name} Second summary"
+ within_testid 'time-tracking-report-modal' do
+ expect(page).to have_css 'h2', text: 'Time tracking report'
+ expect(page).to have_text "1d #{user.name} First summary"
+ expect(page).to have_text "2d #{user.name} Second summary"
- click_button 'Delete time spent', match: :first
+ click_button 'Delete time spent', match: :first
- expect(page).to have_text "1d #{user.name} First summary"
- expect(page).not_to have_text "2d #{user.name} Second summary"
+ expect(page).to have_text "1d #{user.name} First summary"
+ expect(page).not_to have_text "2d #{user.name} Second summary"
- click_button 'Close'
+ click_button 'Close'
+ end
expect(page).to have_text 'Spent 1d'
expect(page).to have_button '1d'
diff --git a/spec/support/shared_examples/models/project_shared_examples.rb b/spec/support/shared_examples/models/project_shared_examples.rb
index 0b880f00a22..9b7682e0f96 100644
--- a/spec/support/shared_examples/models/project_shared_examples.rb
+++ b/spec/support/shared_examples/models/project_shared_examples.rb
@@ -60,3 +60,27 @@ RSpec.shared_examples 'checks parent group feature flag' do
it { is_expected.to be_truthy }
end
end
+
+RSpec.shared_examples 'refreshes project.lfs_file_locks_changed_epoch value' do
+ it 'updates the lfs_file_locks_changed_epoch value', :clean_gitlab_redis_cache do
+ travel_to(1.hour.ago) { project.refresh_lfs_file_locks_changed_epoch }
+
+ original_epoch = project.lfs_file_locks_changed_epoch
+
+ subject
+
+ expect(project.lfs_file_locks_changed_epoch).to be > original_epoch
+ end
+end
+
+RSpec.shared_examples 'does not refresh project.lfs_file_locks_changed_epoch' do
+ it 'does not update the lfs_file_locks_changed_epoch value', :clean_gitlab_redis_cache do
+ travel_to(1.hour.ago) { project.refresh_lfs_file_locks_changed_epoch }
+
+ original_epoch = project.lfs_file_locks_changed_epoch
+
+ subject
+
+ expect(project.lfs_file_locks_changed_epoch).to eq original_epoch
+ end
+end