summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/ci/frontend.gitlab-ci.yml15
-rw-r--r--app/assets/javascripts/monitoring/components/charts/time_series.vue7
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_panel.vue107
-rw-r--r--app/assets/javascripts/monitoring/stores/utils.js2
-rw-r--r--app/assets/javascripts/reports/components/issue_status_icon.vue3
-rw-r--r--app/helpers/explore_helper.rb2
-rw-r--r--app/models/group.rb1
-rw-r--r--app/models/project.rb1
-rw-r--r--app/models/sprint.rb6
-rw-r--r--app/services/groups/import_export/export_service.rb10
-rw-r--r--changelogs/unreleased/205570-sprint_initial_migrations.yml5
-rw-r--r--changelogs/unreleased/mg-dedupe-monaco-chunks.yml5
-rw-r--r--changelogs/unreleased/sh-fix-error-500-explore.yml5
-rw-r--r--config/webpack.config.js8
-rw-r--r--db/migrate/20200213224220_add_sprints.rb35
-rw-r--r--db/migrate/20200420172113_add_text_limit_to_sprints_title.rb19
-rw-r--r--db/migrate/20200420172752_add_sprints_foreign_key_to_projects.rb19
-rw-r--r--db/migrate/20200420172927_add_sprints_foreign_key_to_groups.rb19
-rw-r--r--db/structure.sql58
-rw-r--r--doc/README.md1
-rw-r--r--doc/api/members.md72
-rw-r--r--doc/ci/img/metrics_reports.pngbin19450 -> 0 bytes
-rw-r--r--doc/ci/img/metrics_reports_v13_0.pngbin0 -> 17996 bytes
-rw-r--r--doc/ci/introduction/index.md5
-rw-r--r--doc/ci/metrics_reports.md2
-rw-r--r--doc/user/compliance/license_compliance/img/license_compliance_pipeline_tab_v12_3.pngbin16435 -> 0 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/license_compliance_pipeline_tab_v13_0.pngbin0 -> 95140 bytes
-rw-r--r--doc/user/compliance/license_compliance/index.md2
-rw-r--r--doc/user/project/file_lock.md6
-rw-r--r--doc/user/project/issues/index.md5
-rw-r--r--doc/user/project/labels.md19
-rw-r--r--lib/api/members.rb2
-rw-r--r--lib/gitlab/import_export.rb4
-rw-r--r--lib/gitlab/import_export/group/import_export.yml84
-rw-r--r--lib/gitlab/import_export/group/tree_saver.rb72
-rw-r--r--locale/gitlab.pot3
-rw-r--r--spec/frontend/monitoring/components/charts/time_series_spec.js40
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_spec.js56
-rw-r--r--spec/frontend/monitoring/store/utils_spec.js6
-rw-r--r--spec/frontend/reports/components/__snapshots__/issue_status_icon_spec.js.snap37
-rw-r--r--spec/frontend/reports/components/issue_status_icon_spec.js29
-rw-r--r--spec/helpers/explore_helper_spec.rb21
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/gitlab/import_export/group/tree_saver_spec.rb140
-rw-r--r--spec/lib/gitlab/path_regex_spec.rb15
-rw-r--r--spec/services/groups/import_export/export_service_spec.rb14
-rw-r--r--spec/support/shared_examples/models/concerns/blob_replicator_strategy_shared_examples.rb2
47 files changed, 880 insertions, 85 deletions
diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml
index 2b22162b0c2..e3de301e0a5 100644
--- a/.gitlab/ci/frontend.gitlab-ci.yml
+++ b/.gitlab/ci/frontend.gitlab-ci.yml
@@ -286,15 +286,24 @@ qa-frontend-node:latest:
webpack-dev-server:
extends:
- .default-retry
- - .default-cache
- .frontend:rules:default-frontend-jobs
stage: test
- needs: ["setup-test-env pg11", "compile-assets pull-cache"]
+ needs: []
variables:
WEBPACK_MEMORY_TEST: "true"
WEBPACK_VENDOR_DLL: "true"
+ cache:
+ key:
+ files:
+ - yarn.lock
+ - config/webpack.vendor.config.js
+ prefix: "v2"
+ paths:
+ - node_modules/
script:
- - yarn webpack-vendor
+ - source scripts/utils.sh
+ - retry yarn install --frozen-lockfile
+ - retry yarn webpack-vendor
- node --expose-gc node_modules/.bin/webpack-dev-server --config config/webpack.config.js
artifacts:
name: webpack-dev-server
diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue
index 547f33faaa2..8f37a12af75 100644
--- a/app/assets/javascripts/monitoring/components/charts/time_series.vue
+++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue
@@ -64,10 +64,10 @@ export default {
required: false,
default: '',
},
- singleEmbed: {
- type: Boolean,
+ height: {
+ type: Number,
required: false,
- default: false,
+ default: chartHeight,
},
thresholds: {
type: Array,
@@ -100,7 +100,6 @@ export default {
sha: '',
},
width: 0,
- height: chartHeight,
svgs: {},
primaryColor: null,
throttledDatazoom: null,
diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel.vue b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
index 7b09c78aae2..2b1791ad3e8 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_panel.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
@@ -31,16 +31,12 @@ import { timeRangeToUrl, downloadCSVOptions, generateLinkToChartOptions } from '
const events = {
timeRangeZoom: 'timerangezoom',
+ expand: 'expand',
};
export default {
components: {
MonitorEmptyChart,
- MonitorSingleStatChart,
- MonitorHeatmapChart,
- MonitorColumnChart,
- MonitorBarChart,
- MonitorStackedColumnChart,
AlertWidget,
GlIcon,
GlLoadingIcon,
@@ -65,11 +61,6 @@ export default {
type: Object,
required: true,
},
- index: {
- type: String,
- required: false,
- default: '',
- },
groupId: {
type: String,
required: false,
@@ -96,6 +87,7 @@ export default {
showTitleTooltip: false,
zoomedTimeRange: null,
allAlerts: {},
+ expandBtnAvailable: Boolean(this.$listeners[events.expand]),
};
},
computed: {
@@ -156,20 +148,55 @@ export default {
const data = new Blob([this.csvText], { type: 'text/plain' });
return window.URL.createObjectURL(data);
},
- timeChartComponent() {
+
+ /**
+ * A chart is "basic" if it doesn't support
+ * the same features as the TimeSeries based components
+ * such as "annotations".
+ *
+ * @returns Vue Component wrapping a basic visualization
+ */
+ basicChartComponent() {
+ if (this.isPanelType(panelTypes.SINGLE_STAT)) {
+ return MonitorSingleStatChart;
+ }
+ if (this.isPanelType(panelTypes.HEATMAP)) {
+ return MonitorHeatmapChart;
+ }
+ if (this.isPanelType(panelTypes.BAR)) {
+ return MonitorBarChart;
+ }
+ if (this.isPanelType(panelTypes.COLUMN)) {
+ return MonitorColumnChart;
+ }
+ if (this.isPanelType(panelTypes.STACKED_COLUMN)) {
+ return MonitorStackedColumnChart;
+ }
+ if (this.isPanelType(panelTypes.ANOMALY_CHART)) {
+ return MonitorAnomalyChart;
+ }
+ return null;
+ },
+
+ /**
+ * In monitoring, Time Series charts typically support
+ * a larger feature set like "annotations", "deployment
+ * data", alert "thresholds" and "datazoom".
+ *
+ * This is intentional as Time Series are more frequently
+ * used.
+ *
+ * @returns Vue Component wrapping a time series visualization,
+ * Area Charts are rendered by default.
+ */
+ timeSeriesChartComponent() {
if (this.isPanelType(panelTypes.ANOMALY_CHART)) {
return MonitorAnomalyChart;
}
return MonitorTimeSeriesChart;
},
isContextualMenuShown() {
- return (
- this.graphDataHasResult &&
- !this.isPanelType(panelTypes.SINGLE_STAT) &&
- !this.isPanelType(panelTypes.HEATMAP) &&
- !this.isPanelType(panelTypes.COLUMN) &&
- !this.isPanelType(panelTypes.STACKED_COLUMN)
- );
+ return Boolean(this.graphDataHasResult && !this.basicChartComponent);
},
editCustomMetricLink() {
return this.graphData?.metrics[0].edit_path;
@@ -224,6 +251,9 @@ export default {
this.zoomedTimeRange = { start, end };
this.$emit(events.timeRangeZoom, { start, end });
},
+ onExpand() {
+ this.$emit(events.expand);
+ },
setAlerts(alertPath, alertAttributes) {
if (alertAttributes) {
this.$set(this.allAlerts, alertPath, alertAttributes);
@@ -238,6 +268,7 @@ export default {
<template>
<div v-gl-resize-observer="onResize" class="prometheus-graph">
<div class="d-flex align-items-center mr-3">
+ <slot name="topLeft"></slot>
<h5
ref="graphTitle"
class="prometheus-graph-title gl-font-size-large font-weight-bold text-truncate append-right-8"
@@ -250,7 +281,7 @@ export default {
<alert-widget
v-if="isContextualMenuShown && alertWidgetAvailable"
class="mx-1"
- :modal-id="`alert-modal-${index}`"
+ :modal-id="`alert-modal-${graphData.id}`"
:alerts-endpoint="alertsEndpoint"
:relevant-queries="graphData.metrics"
:alerts-to-manage="getGraphAlerts(graphData.metrics)"
@@ -277,6 +308,9 @@ export default {
<template slot="button-content">
<gl-icon name="ellipsis_v" class="text-secondary" />
</template>
+ <gl-dropdown-item v-if="expandBtnAvailable" ref="expandBtn" @click="onExpand">
+ {{ s__('Metrics|Expand panel') }}
+ </gl-dropdown-item>
<gl-dropdown-item
v-if="editCustomMetricLink"
ref="editMetricLink"
@@ -312,7 +346,7 @@ export default {
</gl-dropdown-item>
<gl-dropdown-item
v-if="alertWidgetAvailable"
- v-gl-modal="`alert-modal-${index}`"
+ v-gl-modal="`alert-modal-${graphData.id}`"
data-qa-selector="alert_widget_menu_item"
>
{{ __('Alerts') }}
@@ -322,38 +356,27 @@ export default {
</div>
</div>
- <monitor-single-stat-chart
- v-if="isPanelType($options.panelTypes.SINGLE_STAT) && graphDataHasResult"
- :graph-data="graphData"
- />
- <monitor-heatmap-chart
- v-else-if="isPanelType($options.panelTypes.HEATMAP) && graphDataHasResult"
- :graph-data="graphData"
- />
- <monitor-bar-chart
- v-else-if="isPanelType($options.panelTypes.BAR) && graphDataHasResult"
- :graph-data="graphData"
- />
- <monitor-column-chart
- v-else-if="isPanelType($options.panelTypes.COLUMN) && graphDataHasResult"
- :graph-data="graphData"
- />
- <monitor-stacked-column-chart
- v-else-if="isPanelType($options.panelTypes.STACKED_COLUMN) && graphDataHasResult"
+ <monitor-empty-chart v-if="!graphDataHasResult" />
+ <component
+ :is="basicChartComponent"
+ v-else-if="basicChartComponent"
:graph-data="graphData"
+ v-bind="$attrs"
+ v-on="$listeners"
/>
<component
- :is="timeChartComponent"
- v-else-if="graphDataHasResult"
- ref="timeChart"
+ :is="timeSeriesChartComponent"
+ v-else
+ ref="timeSeriesChart"
:graph-data="graphData"
:deployment-data="deploymentData"
:annotations="annotations"
:project-path="projectPath"
:thresholds="getGraphAlertValues(graphData.metrics)"
:group-id="groupId"
+ v-bind="$attrs"
+ v-on="$listeners"
@datazoom="onDatazoom"
/>
- <monitor-empty-chart v-else v-bind="$attrs" v-on="$listeners" />
</div>
</template>
diff --git a/app/assets/javascripts/monitoring/stores/utils.js b/app/assets/javascripts/monitoring/stores/utils.js
index 9f06d18c46f..a47e5f598f5 100644
--- a/app/assets/javascripts/monitoring/stores/utils.js
+++ b/app/assets/javascripts/monitoring/stores/utils.js
@@ -144,6 +144,7 @@ const mapYAxisToViewModel = ({
* @returns {Object}
*/
const mapPanelToViewModel = ({
+ id = null,
title = '',
type,
x_axis = {},
@@ -162,6 +163,7 @@ const mapPanelToViewModel = ({
const yAxis = mapYAxisToViewModel({ name: y_label, ...y_axis }); // eslint-disable-line babel/camelcase
return {
+ id,
title,
type,
xLabel: xAxis.name,
diff --git a/app/assets/javascripts/reports/components/issue_status_icon.vue b/app/assets/javascripts/reports/components/issue_status_icon.vue
index 62a9338b864..d79e3ddd798 100644
--- a/app/assets/javascripts/reports/components/issue_status_icon.vue
+++ b/app/assets/javascripts/reports/components/issue_status_icon.vue
@@ -8,7 +8,6 @@ export default {
Icon,
},
props: {
- // failed || success
status: {
type: String,
required: true,
@@ -27,7 +26,7 @@ export default {
return 'status_success_borderless';
}
- return 'status_created_borderless';
+ return 'dash';
},
isStatusFailed() {
return this.status === STATUS_FAILED;
diff --git a/app/helpers/explore_helper.rb b/app/helpers/explore_helper.rb
index b341cc795a0..b66c7a69b71 100644
--- a/app/helpers/explore_helper.rb
+++ b/app/helpers/explore_helper.rb
@@ -52,7 +52,7 @@ module ExploreHelper
end
def public_visibility_restricted?
- Gitlab::CurrentSettings.restricted_visibility_levels.include? Gitlab::VisibilityLevel::PUBLIC
+ Gitlab::CurrentSettings.restricted_visibility_levels&.include? Gitlab::VisibilityLevel::PUBLIC
end
private
diff --git a/app/models/group.rb b/app/models/group.rb
index a1e3850d362..be101ff40df 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -31,6 +31,7 @@ class Group < Namespace
has_many :members_and_requesters, as: :source, class_name: 'GroupMember'
has_many :milestones
+ has_many :sprints
has_many :shared_group_links, foreign_key: :shared_with_group_id, class_name: 'GroupGroupLink'
has_many :shared_with_group_links, foreign_key: :shared_group_id, class_name: 'GroupGroupLink'
has_many :shared_groups, through: :shared_group_links, source: :shared_group
diff --git a/app/models/project.rb b/app/models/project.rb
index d9c09fe8ace..d2e8c8e6017 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -208,6 +208,7 @@ class Project < ApplicationRecord
has_many :services
has_many :events
has_many :milestones
+ has_many :sprints
has_many :notes
has_many :snippets, class_name: 'ProjectSnippet'
has_many :hooks, class_name: 'ProjectHook'
diff --git a/app/models/sprint.rb b/app/models/sprint.rb
new file mode 100644
index 00000000000..b2c6d9df010
--- /dev/null
+++ b/app/models/sprint.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class Sprint < ApplicationRecord
+ belongs_to :project
+ belongs_to :group
+end
diff --git a/app/services/groups/import_export/export_service.rb b/app/services/groups/import_export/export_service.rb
index f8715b57d6e..c0de1c7c961 100644
--- a/app/services/groups/import_export/export_service.rb
+++ b/app/services/groups/import_export/export_service.rb
@@ -56,7 +56,7 @@ module Groups
end
def tree_exporter
- Gitlab::ImportExport::Group::LegacyTreeSaver.new(
+ tree_exporter_class.new(
group: @group,
current_user: @current_user,
shared: @shared,
@@ -64,6 +64,14 @@ module Groups
)
end
+ def tree_exporter_class
+ if ::Feature.enabled?(:group_import_export_ndjson, @group&.parent)
+ Gitlab::ImportExport::Group::TreeSaver
+ else
+ Gitlab::ImportExport::Group::LegacyTreeSaver
+ end
+ end
+
def file_saver
Gitlab::ImportExport::Saver.new(exportable: @group, shared: @shared)
end
diff --git a/changelogs/unreleased/205570-sprint_initial_migrations.yml b/changelogs/unreleased/205570-sprint_initial_migrations.yml
new file mode 100644
index 00000000000..c368cde2673
--- /dev/null
+++ b/changelogs/unreleased/205570-sprint_initial_migrations.yml
@@ -0,0 +1,5 @@
+---
+title: Create Sprints table and barebones model
+merge_request: 30125
+author:
+type: added
diff --git a/changelogs/unreleased/mg-dedupe-monaco-chunks.yml b/changelogs/unreleased/mg-dedupe-monaco-chunks.yml
new file mode 100644
index 00000000000..fb249df3f8c
--- /dev/null
+++ b/changelogs/unreleased/mg-dedupe-monaco-chunks.yml
@@ -0,0 +1,5 @@
+---
+title: Improve cacheability of monaco-editor code
+merge_request: 30032
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-fix-error-500-explore.yml b/changelogs/unreleased/sh-fix-error-500-explore.yml
new file mode 100644
index 00000000000..dbf3c804b31
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-error-500-explore.yml
@@ -0,0 +1,5 @@
+---
+title: Fix 500 error on accessing restricted levels
+merge_request: 30313
+author:
+type: fixed
diff --git a/config/webpack.config.js b/config/webpack.config.js
index e220482d769..e2e1139d49e 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -260,6 +260,14 @@ module.exports = {
chunks: 'initial',
minChunks: autoEntriesCount * 0.9,
}),
+ monaco: {
+ priority: 15,
+ name: 'monaco',
+ chunks: 'initial',
+ test: /[\\/]node_modules[\\/]monaco-editor[\\/]/,
+ minChunks: 2,
+ reuseExistingChunk: true,
+ },
vendors: {
priority: 10,
chunks: 'async',
diff --git a/db/migrate/20200213224220_add_sprints.rb b/db/migrate/20200213224220_add_sprints.rb
new file mode 100644
index 00000000000..8d82d1e261a
--- /dev/null
+++ b/db/migrate/20200213224220_add_sprints.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+class AddSprints < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def change
+ create_table :sprints, id: :bigserial do |t|
+ t.timestamps_with_timezone null: false
+ t.date :start_date
+ t.date :due_date
+
+ t.references :project, foreign_key: false, index: false
+ t.references :group, foreign_key: false, index: true
+
+ t.integer :iid, null: false
+ t.integer :cached_markdown_version
+ t.integer :state, limit: 2
+ # rubocop:disable Migration/AddLimitToTextColumns
+ t.text :title, null: false
+ t.text :title_html
+ t.text :description
+ t.text :description_html
+ # rubocop:enable Migration/AddLimitToTextColumns
+
+ t.index :description, name: "index_sprints_on_description_trigram", opclass: :gin_trgm_ops, using: :gin
+ t.index :due_date
+ t.index %w(project_id iid), unique: true
+ t.index :title
+ t.index :title, name: "index_sprints_on_title_trigram", opclass: :gin_trgm_ops, using: :gin
+
+ t.index %w(project_id title), unique: true, where: 'project_id IS NOT NULL'
+ t.index %w(group_id title), unique: true, where: 'group_id IS NOT NULL'
+ end
+ end
+end
diff --git a/db/migrate/20200420172113_add_text_limit_to_sprints_title.rb b/db/migrate/20200420172113_add_text_limit_to_sprints_title.rb
new file mode 100644
index 00000000000..17707cc9dd1
--- /dev/null
+++ b/db/migrate/20200420172113_add_text_limit_to_sprints_title.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddTextLimitToSprintsTitle < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ CONSTRAINT_NAME = 'sprints_title'
+
+ def up
+ add_text_limit :sprints, :title, 255, constraint_name: CONSTRAINT_NAME
+ end
+
+ def down
+ remove_check_constraint :sprints, CONSTRAINT_NAME
+ end
+end
diff --git a/db/migrate/20200420172752_add_sprints_foreign_key_to_projects.rb b/db/migrate/20200420172752_add_sprints_foreign_key_to_projects.rb
new file mode 100644
index 00000000000..8d1af8f98c7
--- /dev/null
+++ b/db/migrate/20200420172752_add_sprints_foreign_key_to_projects.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddSprintsForeignKeyToProjects < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :sprints, :projects, column: :project_id, on_delete: :cascade
+ end
+
+ def down
+ with_lock_retries do # rubocop:disable Migration/WithLockRetriesWithoutDdlTransaction
+ remove_foreign_key :sprints, column: :project_id
+ end
+ end
+end
diff --git a/db/migrate/20200420172927_add_sprints_foreign_key_to_groups.rb b/db/migrate/20200420172927_add_sprints_foreign_key_to_groups.rb
new file mode 100644
index 00000000000..81b9805b874
--- /dev/null
+++ b/db/migrate/20200420172927_add_sprints_foreign_key_to_groups.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddSprintsForeignKeyToGroups < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :sprints, :namespaces, column: :group_id, on_delete: :cascade
+ end
+
+ def down
+ with_lock_retries do # rubocop:disable Migration/WithLockRetriesWithoutDdlTransaction
+ remove_foreign_key :sprints, column: :group_id
+ end
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index b3e605b97cf..982af99b2a0 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -6071,6 +6071,33 @@ CREATE SEQUENCE public.spam_logs_id_seq
ALTER SEQUENCE public.spam_logs_id_seq OWNED BY public.spam_logs.id;
+CREATE TABLE public.sprints (
+ id bigint NOT NULL,
+ created_at timestamp with time zone NOT NULL,
+ updated_at timestamp with time zone NOT NULL,
+ start_date date,
+ due_date date,
+ project_id bigint,
+ group_id bigint,
+ iid integer NOT NULL,
+ cached_markdown_version integer,
+ state smallint,
+ title text NOT NULL,
+ title_html text,
+ description text,
+ description_html text,
+ CONSTRAINT sprints_title CHECK ((char_length(title) <= 255))
+);
+
+CREATE SEQUENCE public.sprints_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE public.sprints_id_seq OWNED BY public.sprints.id;
+
CREATE TABLE public.status_page_settings (
project_id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
@@ -7589,6 +7616,8 @@ ALTER TABLE ONLY public.software_licenses ALTER COLUMN id SET DEFAULT nextval('p
ALTER TABLE ONLY public.spam_logs ALTER COLUMN id SET DEFAULT nextval('public.spam_logs_id_seq'::regclass);
+ALTER TABLE ONLY public.sprints ALTER COLUMN id SET DEFAULT nextval('public.sprints_id_seq'::regclass);
+
ALTER TABLE ONLY public.status_page_settings ALTER COLUMN project_id SET DEFAULT nextval('public.status_page_settings_project_id_seq'::regclass);
ALTER TABLE ONLY public.subscriptions ALTER COLUMN id SET DEFAULT nextval('public.subscriptions_id_seq'::regclass);
@@ -8515,6 +8544,9 @@ ALTER TABLE ONLY public.software_licenses
ALTER TABLE ONLY public.spam_logs
ADD CONSTRAINT spam_logs_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY public.sprints
+ ADD CONSTRAINT sprints_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY public.status_page_settings
ADD CONSTRAINT status_page_settings_pkey PRIMARY KEY (project_id);
@@ -10327,6 +10359,22 @@ CREATE INDEX index_software_licenses_on_spdx_identifier ON public.software_licen
CREATE UNIQUE INDEX index_software_licenses_on_unique_name ON public.software_licenses USING btree (name);
+CREATE INDEX index_sprints_on_description_trigram ON public.sprints USING gin (description public.gin_trgm_ops);
+
+CREATE INDEX index_sprints_on_due_date ON public.sprints USING btree (due_date);
+
+CREATE INDEX index_sprints_on_group_id ON public.sprints USING btree (group_id);
+
+CREATE UNIQUE INDEX index_sprints_on_group_id_and_title ON public.sprints USING btree (group_id, title) WHERE (group_id IS NOT NULL);
+
+CREATE UNIQUE INDEX index_sprints_on_project_id_and_iid ON public.sprints USING btree (project_id, iid);
+
+CREATE UNIQUE INDEX index_sprints_on_project_id_and_title ON public.sprints USING btree (project_id, title) WHERE (project_id IS NOT NULL);
+
+CREATE INDEX index_sprints_on_title ON public.sprints USING btree (title);
+
+CREATE INDEX index_sprints_on_title_trigram ON public.sprints USING gin (title public.gin_trgm_ops);
+
CREATE INDEX index_status_page_settings_on_project_id ON public.status_page_settings USING btree (project_id);
CREATE INDEX index_subscriptions_on_project_id ON public.subscriptions USING btree (project_id);
@@ -10894,6 +10942,9 @@ ALTER TABLE ONLY public.labels
ALTER TABLE ONLY public.merge_request_metrics
ADD CONSTRAINT fk_7f28d925f3 FOREIGN KEY (merged_by_id) REFERENCES public.users(id) ON DELETE SET NULL;
+ALTER TABLE ONLY public.sprints
+ ADD CONSTRAINT fk_80aa8a1f95 FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY public.import_export_uploads
ADD CONSTRAINT fk_83319d9721 FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE;
@@ -11134,6 +11185,9 @@ ALTER TABLE ONLY public.namespaces
ALTER TABLE ONLY public.fork_networks
ADD CONSTRAINT fk_e7b436b2b5 FOREIGN KEY (root_project_id) REFERENCES public.projects(id) ON DELETE SET NULL;
+ALTER TABLE ONLY public.sprints
+ ADD CONSTRAINT fk_e8206c9686 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY public.application_settings
ADD CONSTRAINT fk_e8a145f3a7 FOREIGN KEY (instance_administrators_group_id) REFERENCES public.namespaces(id) ON DELETE SET NULL;
@@ -13151,6 +13205,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200213204737
20200213220159
20200213220211
+20200213224220
20200214025454
20200214034836
20200214085940
@@ -13365,6 +13420,9 @@ COPY "schema_migrations" (version) FROM STDIN;
20200416120128
20200416120354
20200417044453
+20200420172113
+20200420172752
+20200420172927
20200421233150
20200423075720
20200423080334
diff --git a/doc/README.md b/doc/README.md
index 8d4be768d2d..9ab5f7526c9 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -118,6 +118,7 @@ The following documentation relates to the DevOps **Plan** stage:
| [Project Issue Board](user/project/issue_board.md) | Display issues on a Scrum or Kanban board. |
| [Quick Actions](user/project/quick_actions.md) | Shortcuts for common actions on issues or merge requests, replacing the need to click buttons or use dropdowns in GitLab's UI. |
| [Related Issues](user/project/issues/related_issues.md) **(STARTER)** | Create a relationship between issues. |
+| [Requirements Management](user/project/requirements/index.md) **(ULTIMATE)** | Check your products against a set of criteria. |
| [Roadmap](user/group/roadmap/index.md) **(ULTIMATE)** | Visualize epic timelines. |
| [Service Desk](user/project/service_desk.md) **(PREMIUM)** | A simple way to allow people to create issues in your GitLab instance without needing their own user account. |
| [Time Tracking](user/project/time_tracking.md) | Track time spent on issues and merge requests. |
diff --git a/doc/api/members.md b/doc/api/members.md
index e9131e2d4c3..afeda7780d7 100644
--- a/doc/api/members.md
+++ b/doc/api/members.md
@@ -282,6 +282,78 @@ Example response:
}
```
+### Set override flag for a member of a group
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/4875) in GitLab 12.10.
+
+By default, the access level of LDAP group members is set to the value specified
+by LDAP through Group Sync. You can allow access level overrides by calling this endpoint.
+
+```plaintext
+POST /groups/:id/members/:user_id/override
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `user_id` | integer | yes | The user ID of the member |
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/members/:user_id/override
+```
+
+Example response:
+
+```json
+{
+ "id": 1,
+ "username": "raymond_smith",
+ "name": "Raymond Smith",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
+ "web_url": "http://192.168.1.8:3000/root",
+ "expires_at": "2012-10-22T14:13:35Z",
+ "access_level": 40,
+ "override": true
+}
+```
+
+### Remove override for a member of a group
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/4875) in GitLab 12.10.
+
+Sets the override flag to false and allows LDAP Group Sync to reset the access
+level to the LDAP-prescribed value.
+
+```plaintext
+DELETE /groups/:id/members/:user_id/override
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `user_id` | integer | yes | The user ID of the member |
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/members/:user_id/override
+```
+
+Example response:
+
+```json
+{
+ "id": 1,
+ "username": "raymond_smith",
+ "name": "Raymond Smith",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
+ "web_url": "http://192.168.1.8:3000/root",
+ "expires_at": "2012-10-22T14:13:35Z",
+ "access_level": 40,
+ "override": false
+}
+```
+
## Remove a member from a group or project
Removes a user from a group or project.
diff --git a/doc/ci/img/metrics_reports.png b/doc/ci/img/metrics_reports.png
deleted file mode 100644
index ffd9f6830a2..00000000000
--- a/doc/ci/img/metrics_reports.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/img/metrics_reports_v13_0.png b/doc/ci/img/metrics_reports_v13_0.png
new file mode 100644
index 00000000000..1597031db0b
--- /dev/null
+++ b/doc/ci/img/metrics_reports_v13_0.png
Binary files differ
diff --git a/doc/ci/introduction/index.md b/doc/ci/introduction/index.md
index b16cde54b93..78317917c82 100644
--- a/doc/ci/introduction/index.md
+++ b/doc/ci/introduction/index.md
@@ -76,6 +76,9 @@ to apply all the continuous methods (Continuous Integration,
Delivery, and Deployment) to your software with no third-party
application or integration needed.
+<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+For an overview, see [Introduction to GitLab CI](https://www.youtube.com/watch?v=l5705U8s_nQ&t=397) from a recent GitLab meetup.
+
### How GitLab CI/CD works
To use GitLab CI/CD, all you need is an application codebase hosted in a
@@ -212,7 +215,7 @@ With GitLab CI/CD you can also:
To see all CI/CD features, navigate back to the [CI/CD index](../README.md).
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
-Watch the video [GitLab CI Live Demo](https://www.youtube.com/watch?v=pBe4t1CD8Fc) with a deeper overview of GitLab CI/CD.
+Watch the video [GitLab CI Live Demo](https://youtu.be/l5705U8s_nQ?t=369) with a deeper overview of GitLab CI/CD.
### Setting up GitLab CI/CD for the first time
diff --git a/doc/ci/metrics_reports.md b/doc/ci/metrics_reports.md
index 871f8c55e83..d5c76c1f3f9 100644
--- a/doc/ci/metrics_reports.md
+++ b/doc/ci/metrics_reports.md
@@ -12,7 +12,7 @@ GitLab provides a lot of great reporting tools for [merge requests](../user/proj
You can configure your job to use custom Metrics Reports, and GitLab will display a report on the merge request so that it's easier and faster to identify changes without having to check the entire log.
-![Metrics Reports](img/metrics_reports.png)
+![Metrics Reports](img/metrics_reports_v13_0.png)
## Use cases
diff --git a/doc/user/compliance/license_compliance/img/license_compliance_pipeline_tab_v12_3.png b/doc/user/compliance/license_compliance/img/license_compliance_pipeline_tab_v12_3.png
deleted file mode 100644
index fd519d63b3e..00000000000
--- a/doc/user/compliance/license_compliance/img/license_compliance_pipeline_tab_v12_3.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/license_compliance_pipeline_tab_v13_0.png b/doc/user/compliance/license_compliance/img/license_compliance_pipeline_tab_v13_0.png
new file mode 100644
index 00000000000..5dc46dbf979
--- /dev/null
+++ b/doc/user/compliance/license_compliance/img/license_compliance_pipeline_tab_v13_0.png
Binary files differ
diff --git a/doc/user/compliance/license_compliance/index.md b/doc/user/compliance/license_compliance/index.md
index f9b5a1e8d3c..22b0dfb2293 100644
--- a/doc/user/compliance/license_compliance/index.md
+++ b/doc/user/compliance/license_compliance/index.md
@@ -432,7 +432,7 @@ From your project's left sidebar, navigate to **CI/CD > Pipelines** and click on
pipeline ID that has a `license_management` job to see the Licenses tab with the listed
licenses (if any).
-![License Compliance Pipeline Tab](img/license_compliance_pipeline_tab_v12_3.png)
+![License Compliance Pipeline Tab](img/license_compliance_pipeline_tab_v13_0.png)
<!-- ## Troubleshooting
diff --git a/doc/user/project/file_lock.md b/doc/user/project/file_lock.md
index d5f35051e9a..b5350515c30 100644
--- a/doc/user/project/file_lock.md
+++ b/doc/user/project/file_lock.md
@@ -2,9 +2,9 @@
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/440) in [GitLab Premium](https://about.gitlab.com/pricing/) 8.9.
-File Locking helps you avoid merge conflicts and better manage your binary files.
-Lock any file or directory, make your changes, and then unlock it so another
-member of the team can edit it.
+Working with multiple people on the same file can be a risk. Conflicts when merging a non-text file are hard to overcome and will require a lot of manual work to resolve. File Locking helps you avoid these merge conflicts and better manage your binary files.
+
+With File Locaking, you can lock any file or directory, make your changes, and then unlock it so another member of the team can edit it.
## Overview
diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md
index 55e9967a370..bc297d5891d 100644
--- a/doc/user/project/issues/index.md
+++ b/doc/user/project/issues/index.md
@@ -28,6 +28,11 @@ you can also view all the issues collectively at the group level.
See also [Always start a discussion with an issue](https://about.gitlab.com/blog/2016/03/03/start-with-an-issue/).
+<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+To learn how GitLab's Strategic Marketing department uses GitLab issues with [labels](../labels.md) and
+[issue boards](../issue_board.md), see the video on
+[Managing Commitments with Issues](https://www.youtube.com/watch?v=cuIHNintg1o&t=3).
+
## Parts of an issue
Issues contain a variety of content and metadata, enabling a large range of flexibility
diff --git a/doc/user/project/labels.md b/doc/user/project/labels.md
index 618ef9cfc47..f3b59147d5b 100644
--- a/doc/user/project/labels.md
+++ b/doc/user/project/labels.md
@@ -2,13 +2,18 @@
## Overview
-Labels allow you to categorize epics, issues, and merge requests using descriptive titles like
-`bug`, `feature request`, or `docs`, as well as customizable colors. They allow you to quickly
-and dynamically filter and manage epics, issues, and merge requests, and are a key
-part of [issue boards](issue_board.md).
-
-You can use labels to help [search](../search/index.md#issues-and-merge-requests) in
-lists of issues, merge requests, and epics, as well as [search in issue boards](../search/index.md#issue-boards).
+As your count of issues, merge requests, and epics grows in GitLab, it's more and more challenging
+to keep track of those items. Especially as your organization grows from just a few people to
+hundreds or thousands. This is where labels come in. They help you organize and tag your work
+so you can track and find the work items you're interested in.
+
+Labels are a key part of [issue boards](issue_board.md). With labels you can:
+
+- Categorize epics, issues, and merge requests using colors and descriptive titles like
+`bug`, `feature request`, or `docs`.
+- Dynamically filter and manage epics, issues, and merge requests.
+- [Search lists of issues, merge requests, and epics](../search/index.md#issues-and-merge-requests),
+ as well as [issue boards](../search/index.md#issue-boards).
## Project labels and group labels
diff --git a/lib/api/members.rb b/lib/api/members.rb
index 2e49b4be45c..37d4ca29b68 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -160,3 +160,5 @@ module API
end
end
end
+
+API::Members.prepend_if_ee('EE::API::Members')
diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb
index e696abcc51c..cb0a24c8864 100644
--- a/lib/gitlab/import_export.rb
+++ b/lib/gitlab/import_export.rb
@@ -91,6 +91,10 @@ module Gitlab
def legacy_group_config_file
Rails.root.join('lib/gitlab/import_export/group/legacy_import_export.yml')
end
+
+ def group_config_file
+ Rails.root.join('lib/gitlab/import_export/group/import_export.yml')
+ end
end
end
diff --git a/lib/gitlab/import_export/group/import_export.yml b/lib/gitlab/import_export/group/import_export.yml
new file mode 100644
index 00000000000..e30206dc509
--- /dev/null
+++ b/lib/gitlab/import_export/group/import_export.yml
@@ -0,0 +1,84 @@
+# Model relationships to be included in the group import/export
+#
+# This list _must_ only contain relationships that are available to both FOSS and
+# Enterprise editions. EE specific relationships must be defined in the `ee` section further
+# down below.
+tree:
+ group:
+ - :milestones
+ - :badges
+ - labels:
+ - :priorities
+ - boards:
+ - lists:
+ - label:
+ - :priorities
+ - :board
+ - members:
+ - :user
+
+included_attributes:
+ user:
+ - :id
+ - :email
+ - :username
+ author:
+ - :name
+
+excluded_attributes:
+ group:
+ - :owner_id
+ - :created_at
+ - :updated_at
+ - :runners_token
+ - :runners_token_encrypted
+ - :saml_discovery_token
+ - :visibility_level
+ - :trial_ends_on
+ - :shared_runners_minute_limit
+ - :extra_shared_runners_minutes_limit
+ epics:
+ - :state_id
+
+methods:
+ labels:
+ - :type
+ label:
+ - :type
+ badges:
+ - :type
+ notes:
+ - :type
+ events:
+ - :action
+ lists:
+ - :list_type
+ epics:
+ - :state
+
+preloads:
+
+# EE specific relationships and settings to include. All of this will be merged
+# into the previous structures if EE is used.
+ee:
+ tree:
+ group:
+ - epics:
+ - :parent
+ - :award_emoji
+ - events:
+ - :push_event_payload
+ - notes:
+ - :author
+ - :award_emoji
+ - events:
+ - :push_event_payload
+ - boards:
+ - :board_assignee
+ - :milestone
+ - labels:
+ - :priorities
+ - lists:
+ - milestone:
+ - events:
+ - :push_event_payload
diff --git a/lib/gitlab/import_export/group/tree_saver.rb b/lib/gitlab/import_export/group/tree_saver.rb
new file mode 100644
index 00000000000..d538de33c51
--- /dev/null
+++ b/lib/gitlab/import_export/group/tree_saver.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module ImportExport
+ module Group
+ class TreeSaver
+ attr_reader :full_path, :shared
+
+ def initialize(group:, current_user:, shared:, params: {})
+ @params = params
+ @current_user = current_user
+ @shared = shared
+ @group = group
+ @full_path = File.join(@shared.export_path, 'tree')
+ end
+
+ def save
+ all_groups = Enumerator.new do |group_ids|
+ groups.each do |group|
+ serialize(group)
+ group_ids << group.id
+ end
+ end
+
+ json_writer.write_relation_array('groups', '_all', all_groups)
+
+ true
+ rescue => e
+ @shared.error(e)
+ false
+ ensure
+ json_writer&.close
+ end
+
+ private
+
+ def groups
+ @groups ||= Gitlab::ObjectHierarchy
+ .new(::Group.where(id: @group.id))
+ .base_and_descendants(with_depth: true)
+ .order_by(:depth)
+ end
+
+ def serialize(group)
+ ImportExport::JSON::StreamingSerializer.new(
+ group,
+ group_tree,
+ json_writer,
+ exportable_path: "groups/#{group.id}"
+ ).execute
+ end
+
+ def group_tree
+ @group_tree ||= Gitlab::ImportExport::Reader.new(
+ shared: @shared,
+ config: group_config
+ ).group_tree
+ end
+
+ def group_config
+ Gitlab::ImportExport::Config.new(
+ config: Gitlab::ImportExport.group_config_file
+ ).to_h
+ end
+
+ def json_writer
+ @json_writer ||= ImportExport::JSON::NdjsonWriter.new(@full_path)
+ end
+ end
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 610c504bfee..1e55090bc8f 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -13057,6 +13057,9 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
+msgid "Metrics|Expand panel"
+msgstr ""
+
msgid "Metrics|For grouping similar metrics"
msgstr ""
diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js
index c05bf1a547d..7d5a08bc4a1 100644
--- a/spec/frontend/monitoring/components/charts/time_series_spec.js
+++ b/spec/frontend/monitoring/components/charts/time_series_spec.js
@@ -1,4 +1,4 @@
-import { mount } from '@vue/test-utils';
+import { mount, shallowMount } from '@vue/test-utils';
import { setTestTimeout } from 'helpers/timeout';
import { GlLink } from '@gitlab/ui';
import { TEST_HOST } from 'jest/helpers/test_constants';
@@ -11,7 +11,7 @@ import {
import { cloneDeep } from 'lodash';
import { shallowWrapperContainsSlotText } from 'helpers/vue_test_utils_helper';
import { createStore } from '~/monitoring/stores';
-import { panelTypes } from '~/monitoring/constants';
+import { panelTypes, chartHeight } from '~/monitoring/constants';
import TimeSeries from '~/monitoring/components/charts/time_series.vue';
import * as types from '~/monitoring/stores/mutation_types';
import { deploymentData, mockProjectDir, annotationsData } from '../../mock_data';
@@ -40,10 +40,10 @@ describe('Time series component', () => {
let mockGraphData;
let store;
- const makeTimeSeriesChart = (graphData, type) =>
- mount(TimeSeries, {
+ const createWrapper = (graphData = mockGraphData, mountingMethod = shallowMount) =>
+ mountingMethod(TimeSeries, {
propsData: {
- graphData: { ...graphData, type },
+ graphData,
deploymentData: store.state.monitoringDashboard.deploymentData,
annotations: store.state.monitoringDashboard.annotations,
projectPath: `${TEST_HOST}${mockProjectDir}`,
@@ -80,9 +80,9 @@ describe('Time series component', () => {
const findChart = () => timeSeriesChart.find({ ref: 'chart' });
- beforeEach(done => {
- timeSeriesChart = makeTimeSeriesChart(mockGraphData, 'area-chart');
- timeSeriesChart.vm.$nextTick(done);
+ beforeEach(() => {
+ timeSeriesChart = createWrapper(mockGraphData, mount);
+ return timeSeriesChart.vm.$nextTick();
});
it('allows user to override max value label text using prop', () => {
@@ -101,6 +101,21 @@ describe('Time series component', () => {
});
});
+ it('chart sets a default height', () => {
+ const wrapper = createWrapper();
+ expect(wrapper.props('height')).toBe(chartHeight);
+ });
+
+ it('chart has a configurable height', () => {
+ const mockHeight = 599;
+ const wrapper = createWrapper();
+
+ wrapper.setProps({ height: mockHeight });
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.props('height')).toBe(mockHeight);
+ });
+ });
+
describe('events', () => {
describe('datazoom', () => {
let eChartMock;
@@ -126,7 +141,7 @@ describe('Time series component', () => {
}),
};
- timeSeriesChart = makeTimeSeriesChart(mockGraphData);
+ timeSeriesChart = createWrapper(mockGraphData, mount);
timeSeriesChart.vm.$nextTick(() => {
findChart().vm.$emit('created', eChartMock);
done();
@@ -551,7 +566,10 @@ describe('Time series component', () => {
const findChartComponent = () => timeSeriesAreaChart.find(dynamicComponent.component);
beforeEach(done => {
- timeSeriesAreaChart = makeTimeSeriesChart(mockGraphData, dynamicComponent.chartType);
+ timeSeriesAreaChart = createWrapper(
+ { ...mockGraphData, type: dynamicComponent.chartType },
+ mount,
+ );
timeSeriesAreaChart.vm.$nextTick(done);
});
@@ -633,7 +651,7 @@ describe('Time series component', () => {
Object.assign(metric, { result: metricResultStatus.result }),
);
- timeSeriesChart = makeTimeSeriesChart(graphData, 'area-chart');
+ timeSeriesChart = createWrapper({ ...graphData, type: 'area-chart' }, mount);
timeSeriesChart.vm.$nextTick(done);
});
diff --git a/spec/frontend/monitoring/components/dashboard_panel_spec.js b/spec/frontend/monitoring/components/dashboard_panel_spec.js
index 3c0292e016d..d440c063dd4 100644
--- a/spec/frontend/monitoring/components/dashboard_panel_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_panel_spec.js
@@ -52,11 +52,11 @@ describe('Dashboard Panel', () => {
const exampleText = 'example_text';
const findCopyLink = () => wrapper.find({ ref: 'copyChartLink' });
- const findTimeChart = () => wrapper.find({ ref: 'timeChart' });
+ const findTimeChart = () => wrapper.find({ ref: 'timeSeriesChart' });
const findTitle = () => wrapper.find({ ref: 'graphTitle' });
const findContextualMenu = () => wrapper.find({ ref: 'contextualMenu' });
- const createWrapper = props => {
+ const createWrapper = (props, options = {}) => {
wrapper = shallowMount(DashboardPanel, {
propsData: {
graphData,
@@ -64,6 +64,7 @@ describe('Dashboard Panel', () => {
},
store,
mocks,
+ ...options,
});
};
@@ -80,6 +81,22 @@ describe('Dashboard Panel', () => {
axiosMock.reset();
});
+ describe('Renders slots', () => {
+ it('renders "topLeft" slot', () => {
+ createWrapper(
+ {},
+ {
+ slots: {
+ topLeft: `<div class="top-left-content">OK</div>`,
+ },
+ },
+ );
+
+ expect(wrapper.find('.top-left-content').exists()).toBe(true);
+ expect(wrapper.find('.top-left-content').text()).toBe('OK');
+ });
+ });
+
describe('When no graphData is available', () => {
beforeEach(() => {
createWrapper({
@@ -111,7 +128,7 @@ describe('Dashboard Panel', () => {
});
});
- describe('when graph data is available', () => {
+ describe('When graphData is available', () => {
beforeEach(() => {
createWrapper();
});
@@ -182,10 +199,13 @@ describe('Dashboard Panel', () => {
${singleStatMetricsResult} | ${MonitorSingleStatChart}
${graphDataPrometheusQueryRangeMultiTrack} | ${MonitorHeatmapChart}
${barMockData} | ${MonitorBarChart}
- `('type $data.type renders the expected component', ({ data, component }) => {
- createWrapper({ graphData: data });
+ `('wrapps a $data.type component binding attributes', ({ data, component }) => {
+ const attrs = { attr1: 'attr1Value', attr2: 'attr2Value' };
+ createWrapper({ graphData: data }, { attrs });
+
expect(wrapper.find(component).exists()).toBe(true);
expect(wrapper.find(component).isVueInstance()).toBe(true);
+ expect(wrapper.find(component).attributes()).toMatchObject(attrs);
});
});
});
@@ -436,6 +456,32 @@ describe('Dashboard Panel', () => {
});
});
+ describe('Expand to full screen', () => {
+ const findExpandBtn = () => wrapper.find({ ref: 'expandBtn' });
+
+ describe('when there is no @expand listener', () => {
+ it('does not show `View full screen` option', () => {
+ createWrapper();
+ expect(findExpandBtn().exists()).toBe(false);
+ });
+ });
+
+ describe('when there is an @expand listener', () => {
+ beforeEach(() => {
+ createWrapper({}, { listeners: { expand: () => {} } });
+ });
+
+ it('shows the `expand` option', () => {
+ expect(findExpandBtn().exists()).toBe(true);
+ });
+
+ it('emits the `expand` event', () => {
+ findExpandBtn().vm.$emit('click');
+ expect(wrapper.emitted('expand')).toHaveLength(1);
+ });
+ });
+ });
+
describe('panel alerts', () => {
const setMetricsSavedToDb = val =>
monitoringDashboard.getters.metricsSavedToDb.mockReturnValue(val);
diff --git a/spec/frontend/monitoring/store/utils_spec.js b/spec/frontend/monitoring/store/utils_spec.js
index 7ee2a16b4bd..fe5754e1216 100644
--- a/spec/frontend/monitoring/store/utils_spec.js
+++ b/spec/frontend/monitoring/store/utils_spec.js
@@ -27,6 +27,7 @@ describe('mapToDashboardViewModel', () => {
group: 'Group 1',
panels: [
{
+ id: 'ID_ABC',
title: 'Title A',
xLabel: '',
xAxis: {
@@ -49,6 +50,7 @@ describe('mapToDashboardViewModel', () => {
key: 'group-1-0',
panels: [
{
+ id: 'ID_ABC',
title: 'Title A',
type: 'chart-type',
xLabel: '',
@@ -127,11 +129,13 @@ describe('mapToDashboardViewModel', () => {
it('panel with x_label', () => {
setupWithPanel({
+ id: 'ID_123',
title: panelTitle,
x_label: 'x label',
});
expect(getMappedPanel()).toEqual({
+ id: 'ID_123',
title: panelTitle,
xLabel: 'x label',
xAxis: {
@@ -149,10 +153,12 @@ describe('mapToDashboardViewModel', () => {
it('group y_axis defaults', () => {
setupWithPanel({
+ id: 'ID_456',
title: panelTitle,
});
expect(getMappedPanel()).toEqual({
+ id: 'ID_456',
title: panelTitle,
xLabel: '',
y_label: '',
diff --git a/spec/frontend/reports/components/__snapshots__/issue_status_icon_spec.js.snap b/spec/frontend/reports/components/__snapshots__/issue_status_icon_spec.js.snap
new file mode 100644
index 00000000000..70e1ff01323
--- /dev/null
+++ b/spec/frontend/reports/components/__snapshots__/issue_status_icon_spec.js.snap
@@ -0,0 +1,37 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`IssueStatusIcon renders "failed" state correctly 1`] = `
+<div
+ class="report-block-list-icon failed"
+>
+ <icon-stub
+ data-qa-selector="status_failed_icon"
+ name="status_failed_borderless"
+ size="24"
+ />
+</div>
+`;
+
+exports[`IssueStatusIcon renders "neutral" state correctly 1`] = `
+<div
+ class="report-block-list-icon neutral"
+>
+ <icon-stub
+ data-qa-selector="status_neutral_icon"
+ name="dash"
+ size="24"
+ />
+</div>
+`;
+
+exports[`IssueStatusIcon renders "success" state correctly 1`] = `
+<div
+ class="report-block-list-icon success"
+>
+ <icon-stub
+ data-qa-selector="status_success_icon"
+ name="status_success_borderless"
+ size="24"
+ />
+</div>
+`;
diff --git a/spec/frontend/reports/components/issue_status_icon_spec.js b/spec/frontend/reports/components/issue_status_icon_spec.js
new file mode 100644
index 00000000000..3a55ff0a9e3
--- /dev/null
+++ b/spec/frontend/reports/components/issue_status_icon_spec.js
@@ -0,0 +1,29 @@
+import { shallowMount } from '@vue/test-utils';
+import ReportItem from '~/reports/components/issue_status_icon.vue';
+import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/reports/constants';
+
+describe('IssueStatusIcon', () => {
+ let wrapper;
+
+ const createComponent = ({ status }) => {
+ wrapper = shallowMount(ReportItem, {
+ propsData: {
+ status,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it.each([STATUS_SUCCESS, STATUS_NEUTRAL, STATUS_FAILED])(
+ 'renders "%s" state correctly',
+ status => {
+ createComponent({ status });
+
+ expect(wrapper.element).toMatchSnapshot();
+ },
+ );
+});
diff --git a/spec/helpers/explore_helper_spec.rb b/spec/helpers/explore_helper_spec.rb
index 5208d3bd656..f8240dd3a4c 100644
--- a/spec/helpers/explore_helper_spec.rb
+++ b/spec/helpers/explore_helper_spec.rb
@@ -17,4 +17,25 @@ describe ExploreHelper do
expect(helper.explore_nav_links).to contain_exactly(*menu_items)
end
end
+
+ describe '#public_visibility_restricted?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:visibility_levels, :expected_status) do
+ nil | nil
+ [Gitlab::VisibilityLevel::PRIVATE] | false
+ [Gitlab::VisibilityLevel::PRIVATE, Gitlab::VisibilityLevel::INTERNAL] | false
+ [Gitlab::VisibilityLevel::PUBLIC] | true
+ end
+
+ with_them do
+ before do
+ stub_application_setting(restricted_visibility_levels: visibility_levels)
+ end
+
+ it 'returns the expected status' do
+ expect(helper.public_visibility_restricted?).to eq(expected_status)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 9881bfd3229..10a424c0c11 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -345,6 +345,7 @@ project:
- labels
- events
- milestones
+- sprints
- notes
- snippets
- hooks
diff --git a/spec/lib/gitlab/import_export/group/tree_saver_spec.rb b/spec/lib/gitlab/import_export/group/tree_saver_spec.rb
new file mode 100644
index 00000000000..06e8484a3cb
--- /dev/null
+++ b/spec/lib/gitlab/import_export/group/tree_saver_spec.rb
@@ -0,0 +1,140 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::ImportExport::Group::TreeSaver do
+ describe 'saves the group tree into a json object' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { setup_groups }
+
+ let(:shared) { Gitlab::ImportExport::Shared.new(group) }
+ let(:export_path) { "#{Dir.tmpdir}/group_tree_saver_spec" }
+
+ subject(:group_tree_saver) { described_class.new(group: group, current_user: user, shared: shared) }
+
+ before_all do
+ group.add_maintainer(user)
+ end
+
+ before do
+ allow_next_instance_of(Gitlab::ImportExport) do |import_export|
+ allow(import_export).to receive(:storage_path).and_return(export_path)
+ end
+ end
+
+ after do
+ FileUtils.rm_rf(export_path)
+ end
+
+ it 'saves the group successfully' do
+ expect(group_tree_saver.save).to be true
+ end
+
+ it 'fails to export a group' do
+ allow_next_instance_of(Gitlab::ImportExport::JSON::NdjsonWriter) do |ndjson_writer|
+ allow(ndjson_writer).to receive(:write_relation_array).and_raise(RuntimeError, 'exception')
+ end
+
+ expect(shared).to receive(:error).with(RuntimeError).and_call_original
+
+ expect(group_tree_saver.save).to be false
+ end
+
+ context 'exported files' do
+ before do
+ group_tree_saver.save
+ end
+
+ it 'has one group per line' do
+ groups_catalog =
+ File.readlines(exported_path_for('_all.ndjson'))
+ .map { |line| Integer(line) }
+
+ expect(groups_catalog.size).to eq(3)
+ expect(groups_catalog).to eq([
+ group.id,
+ group.descendants.first.id,
+ group.descendants.first.descendants.first.id
+ ])
+ end
+
+ it 'has a file per group' do
+ group.self_and_descendants.pluck(:id).each do |id|
+ group_attributes_file = exported_path_for("#{id}.json")
+
+ expect(File.exist?(group_attributes_file)).to be(true)
+ end
+ end
+
+ context 'group attributes file' do
+ let(:group_attributes_file) { exported_path_for("#{group.id}.json") }
+ let(:group_attributes) { ::JSON.parse(File.read(group_attributes_file)) }
+
+ it 'has a file for each group with its attributes' do
+ expect(group_attributes['description']).to eq(group.description)
+ expect(group_attributes['parent_id']).to eq(group.parent_id)
+ end
+
+ shared_examples 'excluded attributes' do
+ excluded_attributes = %w[
+ owner_id
+ created_at
+ updated_at
+ runners_token
+ runners_token_encrypted
+ saml_discovery_token
+ ]
+
+ excluded_attributes.each do |excluded_attribute|
+ it 'does not contain excluded attribute' do
+ expect(group_attributes).not_to include(excluded_attribute => group.public_send(excluded_attribute))
+ end
+ end
+ end
+
+ include_examples 'excluded attributes'
+ end
+
+ it 'has a file for each group association' do
+ group.self_and_descendants do |g|
+ %w[
+ badges
+ boards
+ epics
+ labels
+ members
+ milestones
+ ].each do |association|
+ path = exported_path_for("#{g.id}", "#{association}.ndjson")
+ expect(File.exist?(path)).to eq(true), "#{path} does not exist"
+ end
+ end
+ end
+ end
+ end
+
+ def exported_path_for(*file)
+ File.join(group_tree_saver.full_path, 'groups', *file)
+ end
+
+ def setup_groups
+ root = setup_group
+ subgroup = setup_group(parent: root)
+ setup_group(parent: subgroup)
+
+ root
+ end
+
+ def setup_group(parent: nil)
+ group = create(:group, description: 'description', parent: parent)
+ create(:milestone, group: group)
+ create(:group_badge, group: group)
+ group_label = create(:group_label, group: group)
+ board = create(:board, group: group, milestone_id: Milestone::Upcoming.id)
+ create(:list, board: board, label: group_label)
+ create(:group_badge, group: group)
+ create(:label_priority, label: group_label, priority: 1)
+
+ group
+ end
+end
diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb
index 8dabe5a756b..50b045c6aad 100644
--- a/spec/lib/gitlab/path_regex_spec.rb
+++ b/spec/lib/gitlab/path_regex_spec.rb
@@ -170,6 +170,11 @@ describe Gitlab::PathRegex do
expect(described_class::TOP_LEVEL_ROUTES)
.to contain_exactly(*top_level_words), failure_block
end
+
+ # We ban new items in this list, see https://gitlab.com/gitlab-org/gitlab/-/issues/215362
+ it 'does not allow expansion' do
+ expect(described_class::TOP_LEVEL_ROUTES.size).to eq(41)
+ end
end
describe 'GROUP_ROUTES' do
@@ -184,6 +189,11 @@ describe Gitlab::PathRegex do
expect(described_class::GROUP_ROUTES)
.to contain_exactly(*paths_after_group_id), failure_block
end
+
+ # We ban new items in this list, see https://gitlab.com/gitlab-org/gitlab/-/issues/215362
+ it 'does not allow expansion' do
+ expect(described_class::GROUP_ROUTES.size).to eq(1)
+ end
end
describe 'PROJECT_WILDCARD_ROUTES' do
@@ -195,6 +205,11 @@ describe Gitlab::PathRegex do
end
end
end
+
+ # We ban new items in this list, see https://gitlab.com/gitlab-org/gitlab/-/issues/215362
+ it 'does not allow expansion' do
+ expect(described_class::PROJECT_WILDCARD_ROUTES.size).to eq(21)
+ end
end
describe '.root_namespace_route_regex' do
diff --git a/spec/services/groups/import_export/export_service_spec.rb b/spec/services/groups/import_export/export_service_spec.rb
index 56c7121cc34..f77b5a2e5b9 100644
--- a/spec/services/groups/import_export/export_service_spec.rb
+++ b/spec/services/groups/import_export/export_service_spec.rb
@@ -11,7 +11,7 @@ describe Groups::ImportExport::ExportService do
let(:export_service) { described_class.new(group: group, user: user) }
it 'enqueues an export job' do
- expect(GroupExportWorker).to receive(:perform_async).with(user.id, group.id, {})
+ allow(GroupExportWorker).to receive(:perform_async).with(user.id, group.id, {})
export_service.async_execute
end
@@ -49,7 +49,17 @@ describe Groups::ImportExport::ExportService do
FileUtils.rm_rf(archive_path)
end
- it 'saves the models' do
+ it 'saves the models using ndjson tree saver' do
+ stub_feature_flags(group_import_export_ndjson: true)
+
+ expect(Gitlab::ImportExport::Group::TreeSaver).to receive(:new).and_call_original
+
+ service.execute
+ end
+
+ it 'saves the models using legacy tree saver' do
+ stub_feature_flags(group_import_export_ndjson: false)
+
expect(Gitlab::ImportExport::Group::LegacyTreeSaver).to receive(:new).and_call_original
service.execute
diff --git a/spec/support/shared_examples/models/concerns/blob_replicator_strategy_shared_examples.rb b/spec/support/shared_examples/models/concerns/blob_replicator_strategy_shared_examples.rb
index 995cb66a849..76339837351 100644
--- a/spec/support/shared_examples/models/concerns/blob_replicator_strategy_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/blob_replicator_strategy_shared_examples.rb
@@ -76,7 +76,7 @@ RSpec.shared_examples 'a blob replicator' do
expect(service).to receive(:execute)
expect(::Geo::BlobDownloadService).to receive(:new).with(replicator: replicator).and_return(service)
- replicator.consume_created_event
+ replicator.consume_event_created
end
end