Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/diffs/components/collapsed_files_warning.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js2
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/constants/details.js2
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js4
-rw-r--r--app/assets/javascripts/pipelines/pipeline_tabs.js32
-rw-r--r--app/components/pajamas/card_component.html.haml9
-rw-r--r--app/components/pajamas/card_component.rb21
-rw-r--r--app/graphql/resolvers/clusters/agent_tokens_resolver.rb2
-rw-r--r--app/graphql/resolvers/clusters/agents_resolver.rb2
-rw-r--r--app/models/clusters/agent.rb3
-rw-r--r--app/models/work_item.rb7
-rw-r--r--app/models/work_items/parent_link.rb10
-rw-r--r--app/views/admin/dashboard/index.html.haml40
-rw-r--r--app/views/profiles/chat_names/_chat_name.html.haml2
-rw-r--r--config/metrics/counts_28d/20210916080405_promoted_issues.yml4
-rw-r--r--db/docs/work_item_parent_links.yml10
-rw-r--r--db/migrate/20220511144946_add_work_item_parent_child_table.rb27
-rw-r--r--db/schema_migrations/202205111449461
-rw-r--r--db/structure.sql33
-rw-r--r--doc/api/environments.md2
-rw-r--r--doc/development/service_ping/implement.md3
-rw-r--r--doc/user/usage_quotas.md2
-rw-r--r--lib/gitlab/database/gitlab_schemas.yml1
-rw-r--r--lib/gitlab/usage_data.rb4
-rw-r--r--locale/gitlab.pot8
-rw-r--r--spec/components/pajamas/card_component_spec.rb80
-rw-r--r--spec/features/projects/container_registry_spec.rb2
-rw-r--r--spec/frontend/pipelines/pipeline_tabs_spec.js91
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb1
-rw-r--r--spec/models/clusters/agent_spec.rb20
-rw-r--r--spec/models/work_item_spec.rb22
-rw-r--r--spec/models/work_items/parent_link_spec.rb10
32 files changed, 385 insertions, 74 deletions
diff --git a/app/assets/javascripts/diffs/components/collapsed_files_warning.vue b/app/assets/javascripts/diffs/components/collapsed_files_warning.vue
index b7eea32e699..ebb6ec1e7c8 100644
--- a/app/assets/javascripts/diffs/components/collapsed_files_warning.vue
+++ b/app/assets/javascripts/diffs/components/collapsed_files_warning.vue
@@ -55,7 +55,7 @@ export default {
{{ __('For a faster browsing experience, some files are collapsed by default.') }}
</p>
<template #actions>
- <gl-button category="secondary" variant="warning" class="gl-alert-action" @click="expand">
+ <gl-button class="gl-alert-action" @click="expand">
{{ __('Expand all files') }}
</gl-button>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js
index 2a58933cd64..98c24350f09 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js
@@ -67,7 +67,7 @@ export const MISSING_MANIFEST_WARNING_TOOLTIP = s__(
export const UPDATED_AT = s__('ContainerRegistry|Last updated %{time}');
-export const NOT_AVAILABLE_TEXT = __('N/A');
+export const NOT_AVAILABLE_TEXT = __('Not applicable.');
export const NOT_AVAILABLE_SIZE = __('0 bytes');
export const CLEANUP_UNSCHEDULED_TEXT = s__('ContainerRegistry|Cleanup will run %{time}');
diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/constants/details.js b/app/assets/javascripts/packages_and_registries/harbor_registry/constants/details.js
index 2519f6b74a2..b62c51bd208 100644
--- a/app/assets/javascripts/packages_and_registries/harbor_registry/constants/details.js
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/constants/details.js
@@ -35,5 +35,5 @@ export const MISSING_MANIFEST_WARNING_TOOLTIP = s__(
'HarborRegistry|Invalid tag: missing manifest digest',
);
-export const NOT_AVAILABLE_TEXT = __('N/A');
+export const NOT_AVAILABLE_TEXT = __('Not applicable.');
export const NOT_AVAILABLE_SIZE = __('0 bytes');
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index fd869014570..3e50cb6ef07 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -40,10 +40,12 @@ export default async function initPipelineDetailsBundle() {
}
if (gon.features?.pipelineTabsVue) {
+ const { createAppOptions } = await import('ee_else_ce/pipelines/pipeline_tabs');
const { createPipelineTabs } = await import('./pipeline_tabs');
try {
- createPipelineTabs(SELECTORS.PIPELINE_TABS, apolloProvider);
+ const appOptions = createAppOptions(SELECTORS.PIPELINE_TABS, apolloProvider);
+ createPipelineTabs(appOptions);
} catch {
createFlash({
message: __('An error occurred while loading a section of this page.'),
diff --git a/app/assets/javascripts/pipelines/pipeline_tabs.js b/app/assets/javascripts/pipelines/pipeline_tabs.js
index 3bf23d9d001..6d4f3762aea 100644
--- a/app/assets/javascripts/pipelines/pipeline_tabs.js
+++ b/app/assets/javascripts/pipelines/pipeline_tabs.js
@@ -8,12 +8,12 @@ import { getPipelineDefaultTab, reportToSentry } from './utils';
Vue.use(VueApollo);
-const createPipelineTabs = (selector, apolloProvider) => {
+export const createAppOptions = (selector, apolloProvider) => {
const el = document.querySelector(selector);
- if (!el) return;
+ if (!el) return null;
- const { dataset } = document.querySelector(selector);
+ const { dataset } = el;
const {
canGenerateCodequalityReports,
codequalityReportDownloadPath,
@@ -29,15 +29,8 @@ const createPipelineTabs = (selector, apolloProvider) => {
const defaultTabValue = getPipelineDefaultTab(window.location.href);
- updateHistory({
- url: removeParams([TAB_QUERY_PARAM]),
- title: document.title,
- replace: true,
- });
-
- // eslint-disable-next-line no-new
- new Vue({
- el: selector,
+ return {
+ el,
components: {
PipelineTabs,
},
@@ -61,7 +54,18 @@ const createPipelineTabs = (selector, apolloProvider) => {
render(createElement) {
return createElement(PipelineTabs);
},
- });
+ };
};
-export { createPipelineTabs };
+export const createPipelineTabs = (options) => {
+ if (!options) return;
+
+ updateHistory({
+ url: removeParams([TAB_QUERY_PARAM]),
+ title: document.title,
+ replace: true,
+ });
+
+ // eslint-disable-next-line no-new
+ new Vue(options);
+};
diff --git a/app/components/pajamas/card_component.html.haml b/app/components/pajamas/card_component.html.haml
new file mode 100644
index 00000000000..007229cc69f
--- /dev/null
+++ b/app/components/pajamas/card_component.html.haml
@@ -0,0 +1,9 @@
+.gl-card{ @card_options }
+ - if header?
+ .gl-card-header{ @header_options }
+ = header
+ .gl-card-body{ @body_options }
+ = body
+ - if footer?
+ .gl-card-footer{ @footer_options }
+ = footer
diff --git a/app/components/pajamas/card_component.rb b/app/components/pajamas/card_component.rb
new file mode 100644
index 00000000000..bcc71db1c34
--- /dev/null
+++ b/app/components/pajamas/card_component.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+# Renders a GlCard root element
+module Pajamas
+ class CardComponent < Pajamas::Component
+ # @param [Hash] card_options
+ # @param [Hash] header_options
+ # @param [Hash] body_options
+ # @param [Hash] footer_options
+ def initialize(card_options: {}, header_options: {}, body_options: {}, footer_options: {})
+ @card_options = card_options
+ @header_options = header_options
+ @body_options = body_options
+ @footer_options = footer_options
+ end
+
+ renders_one :header
+ renders_one :body
+ renders_one :footer
+ end
+end
diff --git a/app/graphql/resolvers/clusters/agent_tokens_resolver.rb b/app/graphql/resolvers/clusters/agent_tokens_resolver.rb
index 722fbab3bb7..9740bc6bb6a 100644
--- a/app/graphql/resolvers/clusters/agent_tokens_resolver.rb
+++ b/app/graphql/resolvers/clusters/agent_tokens_resolver.rb
@@ -16,7 +16,7 @@ module Resolvers
def resolve(**args)
return ::Clusters::AgentToken.none unless can_read_agent_tokens?
- tokens = agent.last_used_agent_tokens
+ tokens = agent.agent_tokens
tokens = tokens.with_status(args[:status]) if args[:status].present?
tokens
diff --git a/app/graphql/resolvers/clusters/agents_resolver.rb b/app/graphql/resolvers/clusters/agents_resolver.rb
index 5ad66ed7cdd..28618bef807 100644
--- a/app/graphql/resolvers/clusters/agents_resolver.rb
+++ b/app/graphql/resolvers/clusters/agents_resolver.rb
@@ -30,7 +30,7 @@ module Resolvers
def preloads
{
activity_events: { activity_events: [:user, agent_token: :agent] },
- tokens: :last_used_agent_tokens
+ tokens: :agent_tokens
}
end
end
diff --git a/app/models/clusters/agent.rb b/app/models/clusters/agent.rb
index 79fc2b58237..c77b57575ff 100644
--- a/app/models/clusters/agent.rb
+++ b/app/models/clusters/agent.rb
@@ -10,8 +10,7 @@ module Clusters
belongs_to :created_by_user, class_name: 'User', optional: true
belongs_to :project, class_name: '::Project' # Otherwise, it will load ::Clusters::Project
- has_many :agent_tokens, class_name: 'Clusters::AgentToken'
- has_many :last_used_agent_tokens, -> { order_last_used_at_desc }, class_name: 'Clusters::AgentToken', inverse_of: :agent
+ has_many :agent_tokens, -> { order_last_used_at_desc }, class_name: 'Clusters::AgentToken', inverse_of: :agent
has_many :group_authorizations, class_name: 'Clusters::Agents::GroupAuthorization'
has_many :authorized_groups, class_name: '::Group', through: :group_authorizations, source: :group
diff --git a/app/models/work_item.rb b/app/models/work_item.rb
index 557694da35a..84646daf59b 100644
--- a/app/models/work_item.rb
+++ b/app/models/work_item.rb
@@ -4,6 +4,13 @@ class WorkItem < Issue
self.table_name = 'issues'
self.inheritance_column = :_type_disabled
+ has_one :parent_link, class_name: '::WorkItems::ParentLink', foreign_key: :work_item_id
+ has_one :work_item_parent, through: :parent_link, class_name: 'WorkItem'
+
+ has_many :child_links, class_name: '::WorkItems::ParentLink', foreign_key: :work_item_parent_id
+ has_many :work_item_children, through: :child_links, class_name: 'WorkItem',
+ foreign_key: :work_item_id, source: :work_item
+
def noteable_target_type_name
'issue'
end
diff --git a/app/models/work_items/parent_link.rb b/app/models/work_items/parent_link.rb
new file mode 100644
index 00000000000..11367846b36
--- /dev/null
+++ b/app/models/work_items/parent_link.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module WorkItems
+ class ParentLink < ApplicationRecord
+ self.table_name = 'work_item_parent_links'
+
+ belongs_to :work_item
+ belongs_to :work_item_parent, class_name: 'WorkItem'
+ end
+end
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 69033d274a2..75613827ed9 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -22,22 +22,24 @@
.admin-dashboard.gl-mt-3
.h3.gl-mb-5.gl-mt-0= _('Instance overview')
.row
+ - component_params = { body_options: { class: 'gl-display-flex gl-justify-content-space-between gl-align-items-center gl-p-6' },
+ footer_options: { class: 'gl-bg-transparent'} }
.col-md-4.gl-mb-6
- .gl-card
- .gl-card-body.d-flex.justify-content-between.align-items-center.gl-p-6
+ = render Pajamas::CardComponent.new(**component_params) do |c|
+ = c.body do
%span
.d-flex.align-items-center
= sprite_icon('project', size: 16, css_class: 'gl-text-gray-700')
%h3.gl-m-0.gl-ml-3= approximate_count_with_delimiters(@counts, Project)
.gl-mt-3.text-uppercase= s_('AdminArea|Projects')
= link_to(s_('AdminArea|New project'), new_project_path, class: "btn gl-button btn-default")
- .gl-card-footer.gl-bg-transparent
+ = c.footer do
.d-flex.align-items-center
= link_to(s_('AdminArea|View latest projects'), admin_projects_path)
= sprite_icon('angle-right', size: 12, css_class: 'gl-text-gray-700 gl-ml-2')
.col-md-4.gl-mb-6
- .gl-card
- .gl-card-body.d-flex.justify-content-between.align-items-center.gl-p-6
+ = render Pajamas::CardComponent.new(**component_params) do |c|
+ = c.body do
%span
.d-flex.align-items-center
= sprite_icon('users', size: 16, css_class: 'gl-text-gray-700')
@@ -54,20 +56,20 @@
= s_('AdminArea|Users')
= link_to(s_('AdminArea|Users statistics'), admin_dashboard_stats_path, class: "text-capitalize gl-ml-2")
= link_to(s_('AdminArea|New user'), new_admin_user_path, class: "btn gl-button btn-default")
- .gl-card-footer.gl-bg-transparent
+ = c.footer do
.d-flex.align-items-center
= link_to(s_('AdminArea|View latest users'), admin_users_path)
= sprite_icon('angle-right', size: 12, css_class: 'gl-text-gray-700 gl-ml-2')
.col-md-4.gl-mb-6
- .gl-card
- .gl-card-body.d-flex.justify-content-between.align-items-center.gl-p-6
+ = render Pajamas::CardComponent.new(**component_params) do |c|
+ = c.body do
%span
.d-flex.align-items-center
= sprite_icon('group', size: 16, css_class: 'gl-text-gray-700')
%h3.gl-m-0.gl-ml-3= approximate_count_with_delimiters(@counts, Group)
.gl-mt-3.text-uppercase= s_('AdminArea|Groups')
= link_to(s_('AdminArea|New group'), new_admin_group_path, class: "btn gl-button btn-default")
- .gl-card-footer.gl-bg-transparent
+ = c.footer do
.d-flex.align-items-center
= link_to(s_('AdminArea|View latest groups'), admin_groups_path)
= sprite_icon('angle-right', size: 12, css_class: 'gl-text-gray-700 gl-ml-2')
@@ -75,8 +77,8 @@
.col-md-4.gl-mb-6
#js-admin-statistics-container
.col-md-4.gl-mb-6
- .gl-card
- .gl-card-body
+ = render Pajamas::CardComponent.new do |c|
+ = c.body do
%h4= s_('AdminArea|Features')
= feature_entry(_('Sign up'),
href: general_admin_application_settings_path(anchor: 'js-signup-settings'),
@@ -114,8 +116,8 @@
href: admin_runners_path,
enabled: Gitlab.config.gitlab_ci.shared_runners_enabled)
.col-md-4.gl-mb-6
- .gl-card
- .gl-card-body
+ = render Pajamas::CardComponent.new do |c|
+ = c.body do
%h4
= s_('AdminArea|Components')
- if show_version_check?
@@ -171,8 +173,8 @@
= link_to _("Gitaly Servers"), admin_gitaly_servers_path
.row
.col-md-4.gl-mb-6
- .gl-card
- .gl-card-body
+ = render Pajamas::CardComponent.new do |c|
+ = c.body do
%h4= s_('AdminArea|Latest projects')
- @projects.each do |project|
.gl-display-flex.gl-py-3
@@ -181,8 +183,8 @@
%span.gl-white-space-nowrap.gl-text-right
#{time_ago_with_tooltip(project.created_at)}
.col-md-4.gl-mb-6
- .gl-card
- .gl-card-body
+ = render Pajamas::CardComponent.new do |c|
+ = c.body do
%h4= s_('AdminArea|Latest users')
- @users.each do |user|
.gl-display-flex.gl-py-3
@@ -192,8 +194,8 @@
%span.gl-white-space-nowrap.gl-text-right
#{time_ago_with_tooltip(user.created_at)}
.col-md-4.gl-mb-6
- .gl-card
- .gl-card-body
+ = render Pajamas::CardComponent.new do |c|
+ = c.body do
%h4= s_('AdminArea|Latest groups')
- @groups.each do |group|
.gl-display-flex.gl-py-3
diff --git a/app/views/profiles/chat_names/_chat_name.html.haml b/app/views/profiles/chat_names/_chat_name.html.haml
index 8f80c9fdc6c..2c46bf1e074 100644
--- a/app/views/profiles/chat_names/_chat_name.html.haml
+++ b/app/views/profiles/chat_names/_chat_name.html.haml
@@ -6,7 +6,7 @@
- if can?(current_user, :read_project, project)
= link_to project.full_name, project_path(project)
- else
- .light= _('N/A')
+ .light= _('Not applicable.')
%td
%strong
- if can?(current_user, :admin_project, project)
diff --git a/config/metrics/counts_28d/20210916080405_promoted_issues.yml b/config/metrics/counts_28d/20210916080405_promoted_issues.yml
index ec73682b1f3..27c9f01edbe 100644
--- a/config/metrics/counts_28d/20210916080405_promoted_issues.yml
+++ b/config/metrics/counts_28d/20210916080405_promoted_issues.yml
@@ -7,8 +7,10 @@ product_stage: growth
product_group: group::product intelligence
product_category: collection
value_type: number
-status: deprecated
+status: removed
milestone: "14.3"
+milestone_removed: "15.1"
+removed_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/88113
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70485
time_frame: 28d
data_source: database
diff --git a/db/docs/work_item_parent_links.yml b/db/docs/work_item_parent_links.yml
new file mode 100644
index 00000000000..f4b5cd20abb
--- /dev/null
+++ b/db/docs/work_item_parent_links.yml
@@ -0,0 +1,10 @@
+---
+table_name: work_item_parent_links
+classes:
+- WorkItem
+- WorkItems::ParentLink
+feature_categories:
+- team_planning
+description: Persists link between work item and its parent.
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/87283
+milestone: '15.1'
diff --git a/db/migrate/20220511144946_add_work_item_parent_child_table.rb b/db/migrate/20220511144946_add_work_item_parent_child_table.rb
new file mode 100644
index 00000000000..160dac78160
--- /dev/null
+++ b/db/migrate/20220511144946_add_work_item_parent_child_table.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+class AddWorkItemParentChildTable < Gitlab::Database::Migration[2.0]
+ def up
+ create_table :work_item_parent_links do |t|
+ t.references :work_item,
+ index: false,
+ unique: true,
+ foreign_key: { to_table: :issues, on_delete: :cascade },
+ null: false
+ t.references :work_item_parent,
+ index: true,
+ foreign_key: { to_table: :issues, on_delete: :cascade },
+ null: false
+ t.integer :relative_position
+ t.timestamps_with_timezone null: false
+
+ t.index [:work_item_id, :work_item_parent_id],
+ unique: true,
+ name: :index_parent_links_on_work_item_id_and_work_item_parent_id
+ end
+ end
+
+ def down
+ drop_table :work_item_parent_links
+ end
+end
diff --git a/db/schema_migrations/20220511144946 b/db/schema_migrations/20220511144946
new file mode 100644
index 00000000000..c443848a5fb
--- /dev/null
+++ b/db/schema_migrations/20220511144946
@@ -0,0 +1 @@
+a11b32eeb9269e70ab0457eea3fbd42520e15fa6c089b349e4f9655190678cff \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 91b8149e38c..f7530e9dbda 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -22308,6 +22308,24 @@ CREATE SEQUENCE wiki_page_slugs_id_seq
ALTER SEQUENCE wiki_page_slugs_id_seq OWNED BY wiki_page_slugs.id;
+CREATE TABLE work_item_parent_links (
+ id bigint NOT NULL,
+ work_item_id bigint NOT NULL,
+ work_item_parent_id bigint NOT NULL,
+ relative_position integer,
+ created_at timestamp with time zone NOT NULL,
+ updated_at timestamp with time zone NOT NULL
+);
+
+CREATE SEQUENCE work_item_parent_links_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE work_item_parent_links_id_seq OWNED BY work_item_parent_links.id;
+
CREATE TABLE work_item_types (
id bigint NOT NULL,
base_type smallint DEFAULT 0 NOT NULL,
@@ -23409,6 +23427,8 @@ ALTER TABLE ONLY wiki_page_meta ALTER COLUMN id SET DEFAULT nextval('wiki_page_m
ALTER TABLE ONLY wiki_page_slugs ALTER COLUMN id SET DEFAULT nextval('wiki_page_slugs_id_seq'::regclass);
+ALTER TABLE ONLY work_item_parent_links ALTER COLUMN id SET DEFAULT nextval('work_item_parent_links_id_seq'::regclass);
+
ALTER TABLE ONLY work_item_types ALTER COLUMN id SET DEFAULT nextval('work_item_types_id_seq'::regclass);
ALTER TABLE ONLY x509_certificates ALTER COLUMN id SET DEFAULT nextval('x509_certificates_id_seq'::regclass);
@@ -25701,6 +25721,9 @@ ALTER TABLE ONLY wiki_page_meta
ALTER TABLE ONLY wiki_page_slugs
ADD CONSTRAINT wiki_page_slugs_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY work_item_parent_links
+ ADD CONSTRAINT work_item_parent_links_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY work_item_types
ADD CONSTRAINT work_item_types_pkey PRIMARY KEY (id);
@@ -28803,6 +28826,8 @@ CREATE INDEX index_pages_domains_on_verified_at_and_enabled_until ON pages_domai
CREATE INDEX index_pages_domains_on_wildcard ON pages_domains USING btree (wildcard);
+CREATE UNIQUE INDEX index_parent_links_on_work_item_id_and_work_item_parent_id ON work_item_parent_links USING btree (work_item_id, work_item_parent_id);
+
CREATE INDEX index_partial_ci_builds_on_user_id_name_parser_features ON ci_builds USING btree (user_id, name) WHERE (((type)::text = 'Ci::Build'::text) AND ((name)::text = ANY (ARRAY[('container_scanning'::character varying)::text, ('dast'::character varying)::text, ('dependency_scanning'::character varying)::text, ('license_management'::character varying)::text, ('license_scanning'::character varying)::text, ('sast'::character varying)::text, ('coverage_fuzzing'::character varying)::text, ('secret_detection'::character varying)::text])));
CREATE INDEX index_pat_on_user_id_and_expires_at ON personal_access_tokens USING btree (user_id, expires_at);
@@ -29791,6 +29816,8 @@ CREATE UNIQUE INDEX index_wiki_page_slugs_on_slug_and_wiki_page_meta_id ON wiki_
CREATE INDEX index_wiki_page_slugs_on_wiki_page_meta_id ON wiki_page_slugs USING btree (wiki_page_meta_id);
+CREATE INDEX index_work_item_parent_links_on_work_item_parent_id ON work_item_parent_links USING btree (work_item_parent_id);
+
CREATE INDEX index_x509_certificates_on_subject_key_identifier ON x509_certificates USING btree (subject_key_identifier);
CREATE INDEX index_x509_certificates_on_x509_issuer_id ON x509_certificates USING btree (x509_issuer_id);
@@ -32355,6 +32382,9 @@ ALTER TABLE ONLY service_desk_settings
ALTER TABLE ONLY saml_group_links
ADD CONSTRAINT fk_rails_22e312c530 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
+ALTER TABLE ONLY work_item_parent_links
+ ADD CONSTRAINT fk_rails_231dba8959 FOREIGN KEY (work_item_parent_id) REFERENCES issues(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY dast_profiles
ADD CONSTRAINT fk_rails_23cae5abe1 FOREIGN KEY (dast_scanner_profile_id) REFERENCES dast_scanner_profiles(id) ON DELETE CASCADE;
@@ -32727,6 +32757,9 @@ ALTER TABLE ONLY approval_project_rules
ALTER TABLE ONLY incident_management_oncall_participants
ADD CONSTRAINT fk_rails_5fe86ea341 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
+ALTER TABLE ONLY work_item_parent_links
+ ADD CONSTRAINT fk_rails_601d5bec3a FOREIGN KEY (work_item_id) REFERENCES issues(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY user_highest_roles
ADD CONSTRAINT fk_rails_60f6c325a6 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
diff --git a/doc/api/environments.md b/doc/api/environments.md
index a806d8a50dd..d67808bfd61 100644
--- a/doc/api/environments.md
+++ b/doc/api/environments.md
@@ -386,7 +386,7 @@ POST /projects/:id/environments/:environment_id/stop
|------------------|----------------|----------|----------------------------------------------------------------------------------------------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user |
| `environment_id` | integer | yes | The ID of the environment |
-| `force` | boolean | no | Force environment to stop even when `on_stop` action fails |
+| `force` | boolean | no | Force environment to stop without executing `on_stop` actions |
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/environments/1/stop"
diff --git a/doc/development/service_ping/implement.md b/doc/development/service_ping/implement.md
index 8021b24461c..ad7ea3b9e07 100644
--- a/doc/development/service_ping/implement.md
+++ b/doc/development/service_ping/implement.md
@@ -652,9 +652,10 @@ We return fallback values in these cases:
| Case | Value |
|-----------------------------|-------|
-| Deprecated Metric | -1000 |
+| Deprecated Metric ([Removed with version 14.3](https://gitlab.com/gitlab-org/gitlab/-/issues/335894)) | -1000 |
| Timeouts, general failures | -1 |
| Standard errors in counters | -2 |
+| Histogram metrics failure | { '-1' => -1 } |
## Test counters manually using your Rails console
diff --git a/doc/user/usage_quotas.md b/doc/user/usage_quotas.md
index fd658c96184..84c98a60917 100644
--- a/doc/user/usage_quotas.md
+++ b/doc/user/usage_quotas.md
@@ -32,7 +32,7 @@ You can view storage usage for your project or [namespace](../user/group/#namesp
The statistics are displayed. Select any title to view details. The information on this page
is updated every 90 minutes.
-If your namespace shows `N/A`, push a commit to any project in the
+If your namespace shows `'Not applicable.'`, push a commit to any project in the
namespace to recalculate the storage.
## Storage usage statistics
diff --git a/lib/gitlab/database/gitlab_schemas.yml b/lib/gitlab/database/gitlab_schemas.yml
index 48729f8dbd9..8e3801c382e 100644
--- a/lib/gitlab/database/gitlab_schemas.yml
+++ b/lib/gitlab/database/gitlab_schemas.yml
@@ -559,6 +559,7 @@ web_hook_logs: :gitlab_main
web_hooks: :gitlab_main
wiki_page_meta: :gitlab_main
wiki_page_slugs: :gitlab_main
+work_item_parent_links: :gitlab_main
work_item_types: :gitlab_main
x509_certificates: :gitlab_main
x509_commit_signatures: :gitlab_main
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 7a17288e5e5..c64a6508d34 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -18,7 +18,6 @@
module Gitlab
class UsageData
- DEPRECATED_VALUE = -1000
MAX_GENERATION_TIME_FOR_SAAS = 40.hours
CE_MEMOIZED_VALUES = %i(
@@ -193,8 +192,7 @@ module Gitlab
packages: count(::Packages::Package.where(monthly_time_range_db_params)),
personal_snippets: count(PersonalSnippet.where(monthly_time_range_db_params)),
project_snippets: count(ProjectSnippet.where(monthly_time_range_db_params)),
- projects_with_alerts_created: distinct_count(::AlertManagement::Alert.where(monthly_time_range_db_params), :project_id),
- promoted_issues: DEPRECATED_VALUE
+ projects_with_alerts_created: distinct_count(::AlertManagement::Alert.where(monthly_time_range_db_params), :project_id)
}.tap do |data|
data[:snippets] = add(data[:personal_snippets], data[:project_snippets])
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 94e8c4ff946..1928fb830f6 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -16508,7 +16508,7 @@ msgid_plural "%d shards selected"
msgstr[0] ""
msgstr[1] ""
-msgid "Geo|%{boldStart}N/A%{boldEnd}: Geo does not verify this component yet. See the %{linkStart}data types we plan to support%{linkEnd}."
+msgid "Geo|%{boldStart}Not applicable%{boldEnd}: Geo does not verify this component yet. See the %{linkStart}data types we plan to support%{linkEnd}."
msgstr ""
msgid "Geo|%{component} synced"
@@ -25027,9 +25027,6 @@ msgstr ""
msgid "My-Reaction"
msgstr ""
-msgid "N/A"
-msgstr ""
-
msgid "Name"
msgstr ""
@@ -25813,6 +25810,9 @@ msgstr ""
msgid "Not all data has been processed yet, the accuracy of the chart for the selected timeframe is limited."
msgstr ""
+msgid "Not applicable."
+msgstr ""
+
msgid "Not available"
msgstr ""
diff --git a/spec/components/pajamas/card_component_spec.rb b/spec/components/pajamas/card_component_spec.rb
new file mode 100644
index 00000000000..65522a9023f
--- /dev/null
+++ b/spec/components/pajamas/card_component_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+require "spec_helper"
+
+RSpec.describe Pajamas::CardComponent, :aggregate_failures, type: :component do
+ let(:header) { 'Card header' }
+ let(:body) { 'Card body' }
+ let(:footer) { 'Card footer' }
+
+ context 'slots' do
+ before do
+ render_inline described_class.new do |c|
+ c.header { header }
+ c.body { body }
+ c.footer { footer }
+ end
+ end
+
+ it 'renders card header' do
+ expect(rendered_component).to have_content(header)
+ end
+
+ it 'renders card body' do
+ expect(rendered_component).to have_content(body)
+ end
+
+ it 'renders footer' do
+ expect(rendered_component).to have_content(footer)
+ end
+ end
+
+ context 'with defaults' do
+ before do
+ render_inline described_class.new
+ end
+
+ it 'does not have a header or footer' do
+ expect(rendered_component).not_to have_selector('.gl-card-header')
+ expect(rendered_component).not_to have_selector('.gl-card-footer')
+ end
+
+ it 'renders the card and body' do
+ expect(rendered_component).to have_selector('.gl-card')
+ expect(rendered_component).to have_selector('.gl-card-body')
+ end
+ end
+
+ context 'with custom options' do
+ before do
+ render_inline described_class.new(
+ card_options: { class: '_card_class_', data: { testid: '_card_testid_' } },
+ header_options: { class: '_header_class_', data: { testid: '_header_testid_' } },
+ body_options: { class: '_body_class_', data: { testid: '_body_testid_' } },
+ footer_options: { class: '_footer_class_', data: { testid: '_footer_testid_' } }) do |c|
+ c.header { header }
+ c.body { body }
+ c.footer { footer }
+ end
+ end
+
+ it 'renders card options' do
+ expect(rendered_component).to have_selector('._card_class_')
+ expect(rendered_component).to have_selector('[data-testid="_card_testid_"]')
+ end
+
+ it 'renders header options' do
+ expect(rendered_component).to have_selector('._header_class_')
+ expect(rendered_component).to have_selector('[data-testid="_header_testid_"]')
+ end
+
+ it 'renders body options' do
+ expect(rendered_component).to have_selector('._body_class_')
+ expect(rendered_component).to have_selector('[data-testid="_body_testid_"]')
+ end
+
+ it 'renders footer options' do
+ expect(rendered_component).to have_selector('._footer_class_')
+ expect(rendered_component).to have_selector('[data-testid="_footer_testid_"]')
+ end
+ end
+end
diff --git a/spec/features/projects/container_registry_spec.rb b/spec/features/projects/container_registry_spec.rb
index 17eb421191f..54685441300 100644
--- a/spec/features/projects/container_registry_spec.rb
+++ b/spec/features/projects/container_registry_spec.rb
@@ -122,7 +122,7 @@ RSpec.describe 'Container Registry', :js do
it 'renders the tags list correctly' do
expect(page).to have_content('latest')
expect(page).to have_content('stable')
- expect(page).to have_content('Digest: N/A')
+ expect(page).to have_content('Digest: Not applicable.')
end
end
diff --git a/spec/frontend/pipelines/pipeline_tabs_spec.js b/spec/frontend/pipelines/pipeline_tabs_spec.js
new file mode 100644
index 00000000000..2c58780aa1c
--- /dev/null
+++ b/spec/frontend/pipelines/pipeline_tabs_spec.js
@@ -0,0 +1,91 @@
+import { createAppOptions, createPipelineTabs } from '~/pipelines/pipeline_tabs';
+import { updateHistory } from '~/lib/utils/url_utility';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ removeParams: () => 'gitlab.com',
+ updateHistory: jest.fn(),
+ joinPaths: () => {},
+ setUrlFragment: () => {},
+}));
+
+jest.mock('~/pipelines/utils', () => ({
+ getPipelineDefaultTab: () => '',
+}));
+
+describe('~/pipelines/pipeline_tabs.js', () => {
+ describe('createAppOptions', () => {
+ const SELECTOR = 'SELECTOR';
+
+ let el;
+
+ const createElement = () => {
+ el = document.createElement('div');
+ el.id = SELECTOR;
+ el.dataset.canGenerateCodequalityReports = 'true';
+ el.dataset.codequalityReportDownloadPath = 'codequalityReportDownloadPath';
+ el.dataset.downloadablePathForReportType = 'downloadablePathForReportType';
+ el.dataset.exposeSecurityDashboard = 'true';
+ el.dataset.exposeLicenseScanningData = 'true';
+ el.dataset.graphqlResourceEtag = 'graphqlResourceEtag';
+ el.dataset.pipelineIid = '123';
+ el.dataset.pipelineProjectPath = 'pipelineProjectPath';
+
+ document.body.appendChild(el);
+ };
+
+ afterEach(() => {
+ el = null;
+ });
+
+ it("extracts the properties from the element's dataset", () => {
+ createElement();
+ const options = createAppOptions(`#${SELECTOR}`, null);
+
+ expect(options).toMatchObject({
+ el,
+ provide: {
+ canGenerateCodequalityReports: true,
+ codequalityReportDownloadPath: 'codequalityReportDownloadPath',
+ downloadablePathForReportType: 'downloadablePathForReportType',
+ exposeSecurityDashboard: true,
+ exposeLicenseScanningData: true,
+ graphqlResourceEtag: 'graphqlResourceEtag',
+ pipelineIid: '123',
+ pipelineProjectPath: 'pipelineProjectPath',
+ },
+ });
+ });
+
+ it('returns `null` if el does not exist', () => {
+ expect(createAppOptions('foo', null)).toBe(null);
+ });
+ });
+
+ describe('createPipelineTabs', () => {
+ const title = 'Pipeline Tabs';
+
+ beforeAll(() => {
+ document.title = title;
+ });
+
+ afterAll(() => {
+ document.title = '';
+ });
+
+ it('calls `updateHistory` with correct params', () => {
+ createPipelineTabs({});
+
+ expect(updateHistory).toHaveBeenCalledWith({
+ title,
+ url: 'gitlab.com',
+ replace: true,
+ });
+ });
+
+ it("returns early if options aren't provided", () => {
+ createPipelineTabs();
+
+ expect(updateHistory).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 7edec6d13f4..cf65f1e7d30 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -723,7 +723,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
expect(counts_monthly[:projects_with_alerts_created]).to eq(1)
expect(counts_monthly[:projects]).to eq(1)
expect(counts_monthly[:packages]).to eq(1)
- expect(counts_monthly[:promoted_issues]).to eq(Gitlab::UsageData::DEPRECATED_VALUE)
end
end
diff --git a/spec/models/clusters/agent_spec.rb b/spec/models/clusters/agent_spec.rb
index f10e0cc8fa7..f97e89912c6 100644
--- a/spec/models/clusters/agent_spec.rb
+++ b/spec/models/clusters/agent_spec.rb
@@ -7,8 +7,7 @@ RSpec.describe Clusters::Agent do
it { is_expected.to belong_to(:created_by_user).class_name('User').optional }
it { is_expected.to belong_to(:project).class_name('::Project') }
- it { is_expected.to have_many(:agent_tokens).class_name('Clusters::AgentToken') }
- it { is_expected.to have_many(:last_used_agent_tokens).class_name('Clusters::AgentToken') }
+ it { is_expected.to have_many(:agent_tokens).class_name('Clusters::AgentToken').order(Clusters::AgentToken.arel_table[:last_used_at].desc.nulls_last) }
it { is_expected.to have_many(:group_authorizations).class_name('Clusters::Agents::GroupAuthorization') }
it { is_expected.to have_many(:authorized_groups).through(:group_authorizations) }
it { is_expected.to have_many(:project_authorizations).class_name('Clusters::Agents::ProjectAuthorization') }
@@ -117,23 +116,6 @@ RSpec.describe Clusters::Agent do
end
end
- describe '#last_used_agent_tokens' do
- let_it_be(:agent) { create(:cluster_agent) }
-
- subject { agent.last_used_agent_tokens }
-
- context 'agent has no tokens' do
- it { is_expected.to be_empty }
- end
-
- context 'agent has active and inactive tokens' do
- let!(:active_token) { create(:cluster_agent_token, agent: agent, last_used_at: 1.minute.ago) }
- let!(:inactive_token) { create(:cluster_agent_token, agent: agent, last_used_at: 2.hours.ago) }
-
- it { is_expected.to contain_exactly(active_token, inactive_token) }
- end
- end
-
describe '#activity_event_deletion_cutoff' do
let_it_be(:agent) { create(:cluster_agent) }
let_it_be(:event1) { create(:agent_activity_event, agent: agent, recorded_at: 1.hour.ago) }
diff --git a/spec/models/work_item_spec.rb b/spec/models/work_item_spec.rb
index e92ae746911..d126e81f1cd 100644
--- a/spec/models/work_item_spec.rb
+++ b/spec/models/work_item_spec.rb
@@ -3,6 +3,28 @@
require 'spec_helper'
RSpec.describe WorkItem do
+ describe 'associations' do
+ it { is_expected.to have_one(:work_item_parent).class_name('WorkItem') }
+
+ it 'has one `parent_link`' do
+ is_expected.to have_one(:parent_link)
+ .class_name('::WorkItems::ParentLink')
+ .with_foreign_key('work_item_id')
+ end
+
+ it 'has many `work_item_children`' do
+ is_expected.to have_many(:work_item_children)
+ .class_name('WorkItem')
+ .with_foreign_key('work_item_id')
+ end
+
+ it 'has many `child_links`' do
+ is_expected.to have_many(:child_links)
+ .class_name('::WorkItems::ParentLink')
+ .with_foreign_key('work_item_parent_id')
+ end
+ end
+
describe '#noteable_target_type_name' do
it 'returns `issue` as the target name' do
work_item = build(:work_item)
diff --git a/spec/models/work_items/parent_link_spec.rb b/spec/models/work_items/parent_link_spec.rb
new file mode 100644
index 00000000000..a66f6e54734
--- /dev/null
+++ b/spec/models/work_items/parent_link_spec.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::ParentLink do
+ describe 'associations' do
+ it { is_expected.to belong_to(:work_item) }
+ it { is_expected.to belong_to(:work_item_parent).class_name('WorkItem') }
+ end
+end