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:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-10-25 21:15:16 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-10-25 21:15:16 +0300
commit62866a623e24242c6f7a1a93dc2aca1467d6a6ae (patch)
tree322ce00c652d376c949f1d45c39764eeb5bce620
parentf8888a274f6b095075fd8648d3732ad577a3a742 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_todo/layout/line_length.yml1
-rw-r--r--.rubocop_todo/style/if_unless_modifier.yml1
-rw-r--r--.rubocop_todo/style/redundant_self.yml1
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.checksum2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/javascripts/ci/common/pipelines_table.vue7
-rw-r--r--app/assets/javascripts/content_editor/content_editor.stories.js1
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue21
-rw-r--r--app/assets/javascripts/ml/model_registry/apps/index.js3
-rw-r--r--app/assets/javascripts/ml/model_registry/apps/index_ml_models.vue (renamed from app/assets/javascripts/ml/model_registry/routes/models/index/components/ml_models_index.vue)8
-rw-r--r--app/assets/javascripts/ml/model_registry/components/model_row.vue (renamed from app/assets/javascripts/ml/model_registry/routes/models/index/components/model_row.vue)0
-rw-r--r--app/assets/javascripts/ml/model_registry/components/search_bar.vue (renamed from app/assets/javascripts/ml/model_registry/routes/models/index/components/search_bar.vue)0
-rw-r--r--app/assets/javascripts/ml/model_registry/constants.js (renamed from app/assets/javascripts/ml/model_registry/routes/models/index/constants.js)0
-rw-r--r--app/assets/javascripts/ml/model_registry/routes/models/index/index.js3
-rw-r--r--app/assets/javascripts/ml/model_registry/routes/models/index/translations.js16
-rw-r--r--app/assets/javascripts/ml/model_registry/translations.js17
-rw-r--r--app/assets/javascripts/observability/components/loader/constants.js (renamed from app/assets/javascripts/observability/constants.js)7
-rw-r--r--app/assets/javascripts/observability/components/loader/index.vue (renamed from app/assets/javascripts/observability/components/skeleton/index.vue)80
-rw-r--r--app/assets/javascripts/observability/components/observability_container.vue19
-rw-r--r--app/assets/javascripts/pages/import/history/components/import_history_app.vue7
-rw-r--r--app/assets/javascripts/pages/projects/ml/models/index/index.js4
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/utils.js3
-rw-r--r--app/graphql/resolvers/ci/catalog/resources_resolver.rb10
-rw-r--r--app/graphql/types/ci/catalog/resource_sort_enum.rb4
-rw-r--r--app/helpers/dropdowns_helper.rb2
-rw-r--r--app/models/ci/catalog/listing.rb1
-rw-r--r--app/models/ci/catalog/resource.rb17
-rw-r--r--app/models/project.rb8
-rw-r--r--app/policies/issue_policy.rb5
-rw-r--r--app/policies/namespaces/group_project_namespace_shared_policy.rb1
-rw-r--r--app/views/dashboard/todos/_todo.html.haml2
-rw-r--r--app/views/dashboard/todos/index.html.haml2
-rw-r--r--app/views/projects/artifacts/browse.html.haml1
-rw-r--r--config/initializers/1_settings.rb3
-rw-r--r--danger/analytics_instrumentation/Dangerfile2
-rw-r--r--db/migrate/20231005151816_add_created_at_to_status_check_responses.rb7
-rw-r--r--db/migrate/20231017154804_add_index_to_status_check_responses_on_id_and_status.rb14
-rw-r--r--db/migrate/20231019180421_add_name_description_to_catalog_resources.rb28
-rw-r--r--db/post_migrate/20231019223224_backfill_catalog_resources_name_and_description.rb24
-rw-r--r--db/schema_migrations/202310051518161
-rw-r--r--db/schema_migrations/202310171548041
-rw-r--r--db/schema_migrations/202310191804211
-rw-r--r--db/schema_migrations/202310192232241
-rw-r--r--db/structure.sql13
-rw-r--r--doc/administration/settings/usage_statistics.md1
-rw-r--r--doc/api/graphql/reference/index.md13
-rw-r--r--doc/install/installation.md2
-rw-r--r--doc/update/versions/gitlab_16_changes.md4
-rw-r--r--doc/user/analytics/analytics_dashboards.md6
-rw-r--r--doc/user/infrastructure/iac/index.md1
-rw-r--r--doc/user/project/merge_requests/status_checks.md4
-rw-r--r--doc/user/project/repository/branches/index.md21
-rw-r--r--locale/gitlab.pot5
-rw-r--r--package.json2
-rw-r--r--patches/vue-apollo+3.0.7.patch38
-rw-r--r--qa/qa/page/dashboard/todos.rb12
-rwxr-xr-xscripts/regenerate-schema32
-rw-r--r--spec/factories/ci/catalog/resources/components.rb2
-rw-r--r--spec/frontend/ml/model_registry/apps/index_ml_models_spec.js (renamed from spec/frontend/ml/model_registry/routes/models/index/components/ml_models_index_spec.js)14
-rw-r--r--spec/frontend/ml/model_registry/components/model_row_spec.js (renamed from spec/frontend/ml/model_registry/routes/models/index/components/model_row_spec.js)7
-rw-r--r--spec/frontend/ml/model_registry/components/search_bar_spec.js (renamed from spec/frontend/ml/model_registry/routes/models/index/components/search_bar_spec.js)4
-rw-r--r--spec/frontend/ml/model_registry/mock_data.js30
-rw-r--r--spec/frontend/observability/loader_spec.js (renamed from spec/frontend/observability/skeleton_spec.js)67
-rw-r--r--spec/frontend/observability/observability_container_spec.js62
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/mock_data.js24
-rw-r--r--spec/graphql/resolvers/ci/catalog/resources_resolver_spec.rb51
-rw-r--r--spec/graphql/types/ci/catalog/resource_sort_enum_spec.rb2
-rw-r--r--spec/migrations/20231019223224_backfill_catalog_resources_name_and_description_spec.rb29
-rw-r--r--spec/models/ci/catalog/listing_spec.rb22
-rw-r--r--spec/models/ci/catalog/resource_spec.rb41
-rw-r--r--spec/models/project_spec.rb50
-rw-r--r--spec/policies/work_item_policy_spec.rb72
-rw-r--r--spec/requests/api/graphql/work_item_spec.rb34
-rw-r--r--spec/tooling/danger/analytics_instrumentation_spec.rb60
-rw-r--r--tooling/danger/analytics_instrumentation.rb23
-rw-r--r--yarn.lock8
79 files changed, 798 insertions, 303 deletions
diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml
index a2ea446d037..c44782751e8 100644
--- a/.rubocop_todo/layout/line_length.yml
+++ b/.rubocop_todo/layout/line_length.yml
@@ -884,7 +884,6 @@ Layout/LineLength:
- 'ee/app/models/ee/packages/package_file.rb'
- 'ee/app/models/ee/pages_deployment.rb'
- 'ee/app/models/ee/project.rb'
- - 'ee/app/models/ee/project_feature.rb'
- 'ee/app/models/ee/resource_label_event.rb'
- 'ee/app/models/ee/snippet_repository.rb'
- 'ee/app/models/ee/terraform/state_version.rb'
diff --git a/.rubocop_todo/style/if_unless_modifier.yml b/.rubocop_todo/style/if_unless_modifier.yml
index a4e7c55ce38..78322dc3555 100644
--- a/.rubocop_todo/style/if_unless_modifier.yml
+++ b/.rubocop_todo/style/if_unless_modifier.yml
@@ -436,7 +436,6 @@ Style/IfUnlessModifier:
- 'ee/app/models/ee/milestone_release.rb'
- 'ee/app/models/ee/namespace.rb'
- 'ee/app/models/ee/project.rb'
- - 'ee/app/models/ee/project_feature.rb'
- 'ee/app/models/ee/project_team.rb'
- 'ee/app/models/ee/user.rb'
- 'ee/app/models/geo/tracking_base.rb'
diff --git a/.rubocop_todo/style/redundant_self.yml b/.rubocop_todo/style/redundant_self.yml
index eb928a8f9a7..481b6b756c0 100644
--- a/.rubocop_todo/style/redundant_self.yml
+++ b/.rubocop_todo/style/redundant_self.yml
@@ -194,7 +194,6 @@ Style/RedundantSelf:
- 'ee/app/models/ee/namespace.rb'
- 'ee/app/models/ee/packages/package_file.rb'
- 'ee/app/models/ee/project.rb'
- - 'ee/app/models/ee/project_feature.rb'
- 'ee/app/models/ee/project_import_state.rb'
- 'ee/app/models/ee/snippet_repository.rb'
- 'ee/app/models/ee/user.rb'
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index a9179624d29..9a17cb41880 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-2838777108e7c081ddf7ef0932fe93087c560238
+1db9c6e65ecc942b4ba907003018829ccacabf4b
diff --git a/Gemfile b/Gemfile
index e825735e56d..8c144a96256 100644
--- a/Gemfile
+++ b/Gemfile
@@ -216,7 +216,7 @@ gem 'asciidoctor', '~> 2.0.18' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'asciidoctor-include-ext', '~> 0.4.0', require: false # rubocop:todo Gemfile/MissingFeatureCategory
gem 'asciidoctor-plantuml', '~> 0.0.16' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'asciidoctor-kroki', '~> 0.8.0', require: false # rubocop:todo Gemfile/MissingFeatureCategory
-gem 'rouge', '~> 4.1.3' # rubocop:todo Gemfile/MissingFeatureCategory
+gem 'rouge', '~> 4.2.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'truncato', '~> 0.7.12' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'nokogiri', '~> 1.15', '>= 1.15.4' # rubocop:todo Gemfile/MissingFeatureCategory
diff --git a/Gemfile.checksum b/Gemfile.checksum
index 3ad703729b7..5cc93ef0ee9 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -523,7 +523,7 @@
{"name":"rexml","version":"3.2.6","platform":"ruby","checksum":"e0669a2d4e9f109951cb1fde723d8acd285425d81594a2ea929304af50282816"},
{"name":"rinku","version":"2.0.0","platform":"ruby","checksum":"3e695aaf9f24baba3af45823b5c427b58a624582132f18482320e2737f9f8a85"},
{"name":"rotp","version":"6.3.0","platform":"ruby","checksum":"75d40087e65ed0d8022c33055a6306c1c400d1c12261932533b5d6cbcd868854"},
-{"name":"rouge","version":"4.1.3","platform":"ruby","checksum":"9c8663db26e05e52b3b0286daacae73ebb361c1bd31d7febd8c57087faa0b9a5"},
+{"name":"rouge","version":"4.2.0","platform":"ruby","checksum":"60dd666b3a223467dc72f5b7384764dfd7ad4e50b0df9eff072be58123506eba"},
{"name":"rqrcode","version":"2.2.0","platform":"ruby","checksum":"23eea88bb44c7ee6d6cab9354d08c287f7ebcdc6112e1fe7bcc2d010d1ffefc1"},
{"name":"rqrcode_core","version":"1.2.0","platform":"ruby","checksum":"cf4989dc82d24e2877984738c4ee569308625fed2a810960f1b02d68d0308d1a"},
{"name":"rspec","version":"3.12.0","platform":"ruby","checksum":"ccc41799a43509dc0be84070e3f0410ac95cbd480ae7b6c245543eb64162399c"},
diff --git a/Gemfile.lock b/Gemfile.lock
index ea42a93a3c6..68590f74505 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1338,7 +1338,7 @@ GEM
rexml (3.2.6)
rinku (2.0.0)
rotp (6.3.0)
- rouge (4.1.3)
+ rouge (4.2.0)
rqrcode (2.2.0)
chunky_png (~> 1.0)
rqrcode_core (~> 1.0)
@@ -1968,7 +1968,7 @@ DEPENDENCIES
responders (~> 3.0)
retriable (~> 3.1.2)
rexml (~> 3.2.6)
- rouge (~> 4.1.3)
+ rouge (~> 4.2.0)
rqrcode (~> 2.0)
rspec-benchmark (~> 0.6.0)
rspec-parameterized (~> 1.0)
diff --git a/app/assets/javascripts/ci/common/pipelines_table.vue b/app/assets/javascripts/ci/common/pipelines_table.vue
index 13b5120654a..1efde90015e 100644
--- a/app/assets/javascripts/ci/common/pipelines_table.vue
+++ b/app/assets/javascripts/ci/common/pipelines_table.vue
@@ -13,8 +13,6 @@ import PipelineUrl from '../pipelines_page/components/pipeline_url.vue';
import PipelineStatusBadge from '../pipelines_page/components/pipeline_status_badge.vue';
const HIDE_TD_ON_MOBILE = 'gl-display-none! gl-lg-display-table-cell!';
-const DEFAULT_TH_CLASSES =
- 'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1!';
/**
* Pipelines Table
@@ -77,7 +75,6 @@ export default {
{
key: 'status',
label: s__('Pipeline|Status'),
- thClass: DEFAULT_TH_CLASSES,
columnClass: 'gl-w-15p',
tdClass: this.tdClasses,
thAttr: { 'data-testid': 'status-th' },
@@ -85,7 +82,6 @@ export default {
{
key: 'pipeline',
label: __('Pipeline'),
- thClass: DEFAULT_TH_CLASSES,
tdClass: `${this.tdClasses}`,
columnClass: 'gl-w-30p',
thAttr: { 'data-testid': 'pipeline-th' },
@@ -93,7 +89,6 @@ export default {
{
key: 'triggerer',
label: s__('Pipeline|Created by'),
- thClass: DEFAULT_TH_CLASSES,
tdClass: `${this.tdClasses} ${HIDE_TD_ON_MOBILE}`,
columnClass: 'gl-w-15p',
thAttr: { 'data-testid': 'triggerer-th' },
@@ -101,14 +96,12 @@ export default {
{
key: 'stages',
label: s__('Pipeline|Stages'),
- thClass: DEFAULT_TH_CLASSES,
tdClass: this.tdClasses,
columnClass: 'gl-w-quarter',
thAttr: { 'data-testid': 'stages-th' },
},
{
key: 'actions',
- thClass: DEFAULT_TH_CLASSES,
tdClass: this.tdClasses,
columnClass: 'gl-w-20p',
thAttr: { 'data-testid': 'actions-th' },
diff --git a/app/assets/javascripts/content_editor/content_editor.stories.js b/app/assets/javascripts/content_editor/content_editor.stories.js
index 1aa6568848f..6f7e9653e6e 100644
--- a/app/assets/javascripts/content_editor/content_editor.stories.js
+++ b/app/assets/javascripts/content_editor/content_editor.stories.js
@@ -30,4 +30,5 @@ Default.args = {
serializerConfig: {},
extensions: [],
enableAutocomplete: false,
+ markdownDocsPath: 'fake/path',
};
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
index 24197c680eb..df1e50cb433 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
@@ -42,9 +42,6 @@ import ImportTargetCell from './import_target_cell.vue';
const VALIDATION_DEBOUNCE_TIME = DEFAULT_DEBOUNCE_AND_THROTTLE_MS;
const PAGE_SIZES = [20, 50, 100];
const DEFAULT_PAGE_SIZE = PAGE_SIZES[0];
-const DEFAULT_TH_CLASSES =
- 'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-200! gl-border-b-1! gl-p-5!';
-const DEFAULT_TD_CLASSES = 'gl-vertical-align-top!';
export default {
components: {
@@ -129,36 +126,28 @@ export default {
{
key: 'selected',
label: '',
- // eslint-disable-next-line @gitlab/require-i18n-strings
- thClass: `${DEFAULT_TH_CLASSES} gl-w-3 gl-pr-3!`,
- // eslint-disable-next-line @gitlab/require-i18n-strings
- tdClass: `${DEFAULT_TD_CLASSES} gl-pr-3!`,
+ thClass: 'gl-w-3 gl-pr-3!',
+ tdClass: 'gl-pr-3!',
},
{
key: 'webUrl',
label: s__('BulkImport|Source group'),
- thClass: `${DEFAULT_TH_CLASSES} gl-pl-0! gl-w-half`,
- // eslint-disable-next-line @gitlab/require-i18n-strings
- tdClass: `${DEFAULT_TD_CLASSES} gl-pl-0!`,
+ thClass: 'gl-pl-0! gl-w-half',
+ tdClass: 'gl-pl-0!',
},
{
key: 'importTarget',
label: s__('BulkImport|New group'),
- thClass: `${DEFAULT_TH_CLASSES} gl-w-half`,
- tdClass: DEFAULT_TD_CLASSES,
+ thClass: `gl-w-half`,
},
{
key: 'progress',
label: __('Status'),
- thClass: `${DEFAULT_TH_CLASSES}`,
- tdClass: DEFAULT_TD_CLASSES,
tdAttr: { 'data-qa-selector': 'import_status_indicator' },
},
{
key: 'actions',
label: '',
- thClass: `${DEFAULT_TH_CLASSES}`,
- tdClass: DEFAULT_TD_CLASSES,
},
],
diff --git a/app/assets/javascripts/ml/model_registry/apps/index.js b/app/assets/javascripts/ml/model_registry/apps/index.js
index a25052615a4..92d159f68be 100644
--- a/app/assets/javascripts/ml/model_registry/apps/index.js
+++ b/app/assets/javascripts/ml/model_registry/apps/index.js
@@ -1,4 +1,5 @@
import ShowMlModel from './show_ml_model.vue';
import ShowMlModelVersion from './show_ml_model_version.vue';
+import IndexMlModels from './index_ml_models.vue';
-export { ShowMlModel, ShowMlModelVersion };
+export { ShowMlModel, ShowMlModelVersion, IndexMlModels };
diff --git a/app/assets/javascripts/ml/model_registry/routes/models/index/components/ml_models_index.vue b/app/assets/javascripts/ml/model_registry/apps/index_ml_models.vue
index 6a8bcc242c7..daf09dbf5e4 100644
--- a/app/assets/javascripts/ml/model_registry/routes/models/index/components/ml_models_index.vue
+++ b/app/assets/javascripts/ml/model_registry/apps/index_ml_models.vue
@@ -1,13 +1,13 @@
<script>
import { isEmpty } from 'lodash';
-import * as translations from '~/ml/model_registry/routes/models/index/translations';
+import * as translations from '~/ml/model_registry/translations';
import Pagination from '~/vue_shared/components/incubation/pagination.vue';
import { BASE_SORT_FIELDS } from '../constants';
-import SearchBar from './search_bar.vue';
-import ModelRow from './model_row.vue';
+import SearchBar from '../components/search_bar.vue';
+import ModelRow from '../components/model_row.vue';
export default {
- name: 'MlModelRegistryApp',
+ name: 'IndexMlModels',
components: {
Pagination,
ModelRow,
diff --git a/app/assets/javascripts/ml/model_registry/routes/models/index/components/model_row.vue b/app/assets/javascripts/ml/model_registry/components/model_row.vue
index 4f91f0939a8..4f91f0939a8 100644
--- a/app/assets/javascripts/ml/model_registry/routes/models/index/components/model_row.vue
+++ b/app/assets/javascripts/ml/model_registry/components/model_row.vue
diff --git a/app/assets/javascripts/ml/model_registry/routes/models/index/components/search_bar.vue b/app/assets/javascripts/ml/model_registry/components/search_bar.vue
index 2bcdabc403f..2bcdabc403f 100644
--- a/app/assets/javascripts/ml/model_registry/routes/models/index/components/search_bar.vue
+++ b/app/assets/javascripts/ml/model_registry/components/search_bar.vue
diff --git a/app/assets/javascripts/ml/model_registry/routes/models/index/constants.js b/app/assets/javascripts/ml/model_registry/constants.js
index 10c21ec4f12..10c21ec4f12 100644
--- a/app/assets/javascripts/ml/model_registry/routes/models/index/constants.js
+++ b/app/assets/javascripts/ml/model_registry/constants.js
diff --git a/app/assets/javascripts/ml/model_registry/routes/models/index/index.js b/app/assets/javascripts/ml/model_registry/routes/models/index/index.js
deleted file mode 100644
index d303d9716af..00000000000
--- a/app/assets/javascripts/ml/model_registry/routes/models/index/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import MlModelsIndex from './components/ml_models_index.vue';
-
-export default MlModelsIndex;
diff --git a/app/assets/javascripts/ml/model_registry/routes/models/index/translations.js b/app/assets/javascripts/ml/model_registry/routes/models/index/translations.js
deleted file mode 100644
index 9210d816373..00000000000
--- a/app/assets/javascripts/ml/model_registry/routes/models/index/translations.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import { s__, n__, sprintf } from '~/locale';
-
-export const TITLE_LABEL = s__('MlModelRegistry|Model registry');
-export const NO_MODELS_LABEL = s__('MlModelRegistry|No models registered in this project');
-
-export const modelVersionCountMessage = (version, versionCount) => {
- if (!versionCount) return s__('MlModelRegistry|No registered versions');
-
- const message = n__(
- 'MlModelRegistry|%{version} · No other versions',
- 'MlModelRegistry|%{version} · %{versionCount} versions',
- versionCount,
- );
-
- return sprintf(message, { version, versionCount });
-};
diff --git a/app/assets/javascripts/ml/model_registry/translations.js b/app/assets/javascripts/ml/model_registry/translations.js
index 5305285b363..39f4cd22495 100644
--- a/app/assets/javascripts/ml/model_registry/translations.js
+++ b/app/assets/javascripts/ml/model_registry/translations.js
@@ -1,4 +1,4 @@
-import { s__, n__ } from '~/locale';
+import { s__, n__, sprintf } from '~/locale';
export const MODEL_DETAILS_TAB_LABEL = s__('MlModelRegistry|Details');
export const MODEL_OTHER_VERSIONS_TAB_LABEL = s__('MlModelRegistry|Versions');
@@ -8,3 +8,18 @@ export const NO_VERSIONS_LABEL = s__('MlModelRegistry|This model has no versions
export const versionsCountLabel = (versionCount) =>
n__('MlModelRegistry|%d version', 'MlModelRegistry|%d versions', versionCount);
+
+export const TITLE_LABEL = s__('MlModelRegistry|Model registry');
+export const NO_MODELS_LABEL = s__('MlModelRegistry|No models registered in this project');
+
+export const modelVersionCountMessage = (version, versionCount) => {
+ if (!versionCount) return s__('MlModelRegistry|No registered versions');
+
+ const message = n__(
+ 'MlModelRegistry|%{version} · No other versions',
+ 'MlModelRegistry|%{version} · %{versionCount} versions',
+ versionCount,
+ );
+
+ return sprintf(message, { version, versionCount });
+};
diff --git a/app/assets/javascripts/observability/constants.js b/app/assets/javascripts/observability/components/loader/constants.js
index 83eaea185e5..5c2d8ad0d1b 100644
--- a/app/assets/javascripts/observability/constants.js
+++ b/app/assets/javascripts/observability/components/loader/constants.js
@@ -1,8 +1,11 @@
import { __ } from '~/locale';
-export const SKELETON_SPINNER_VARIANT = 'spinner';
+export const CONTENT_STATE = Object.freeze({
+ ERROR: 'error',
+ LOADED: 'loaded',
+});
-export const SKELETON_STATE = Object.freeze({
+export const LOADER_STATE = Object.freeze({
ERROR: 'error',
VISIBLE: 'visible',
HIDDEN: 'hidden',
diff --git a/app/assets/javascripts/observability/components/skeleton/index.vue b/app/assets/javascripts/observability/components/loader/index.vue
index c3d0a7c90b1..6b92dc428d2 100644
--- a/app/assets/javascripts/observability/components/skeleton/index.vue
+++ b/app/assets/javascripts/observability/components/loader/index.vue
@@ -1,31 +1,30 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
-import { GlSkeletonLoader, GlAlert, GlLoadingIcon } from '@gitlab/ui';
+import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import {
- SKELETON_STATE,
+ LOADER_STATE,
+ CONTENT_STATE,
DEFAULT_TIMERS,
TIMEOUT_ERROR_LABEL,
TIMEOUT_ERROR_MESSAGE,
- SKELETON_SPINNER_VARIANT,
-} from '../../constants';
+} from './constants';
export default {
components: {
- GlSkeletonLoader,
GlAlert,
GlLoadingIcon,
},
- SKELETON_STATE,
+ LOADER_STATE,
i18n: {
TIMEOUT_ERROR_LABEL,
TIMEOUT_ERROR_MESSAGE,
},
props: {
- variant: {
+ contentState: {
type: String,
required: false,
- default: '',
+ default: null,
},
},
data() {
@@ -35,18 +34,25 @@ export default {
errorTimeout: null,
};
},
+
computed: {
- skeletonVisible() {
- return this.state === SKELETON_STATE.VISIBLE;
+ loaderVisible() {
+ return this.state === LOADER_STATE.VISIBLE;
},
- skeletonHidden() {
- return this.state === SKELETON_STATE.HIDDEN;
+ loaderHidden() {
+ return this.state === LOADER_STATE.HIDDEN;
},
errorVisible() {
- return this.state === SKELETON_STATE.ERROR;
+ return this.state === LOADER_STATE.ERROR;
},
- spinnerVariant() {
- return this.variant === SKELETON_SPINNER_VARIANT;
+ },
+ watch: {
+ contentState(newValue) {
+ if (newValue === CONTENT_STATE.LOADED) {
+ this.onContentLoaded();
+ } else if (newValue === CONTENT_STATE.ERROR) {
+ this.onError();
+ }
},
},
mounted() {
@@ -62,7 +68,7 @@ export default {
clearTimeout(this.errorTimeout);
clearTimeout(this.loadingTimeout);
- this.hideSkeleton();
+ this.hideLoader();
},
onError() {
clearTimeout(this.errorTimeout);
@@ -74,10 +80,10 @@ export default {
this.loadingTimeout = setTimeout(() => {
/**
* If content is not loaded within CONTENT_WAIT_MS,
- * show the skeleton
+ * show the loader
*/
- if (this.state !== SKELETON_STATE.HIDDEN) {
- this.showSkeleton();
+ if (this.state !== LOADER_STATE.HIDDEN) {
+ this.showLoader();
}
}, DEFAULT_TIMERS.CONTENT_WAIT_MS);
},
@@ -87,19 +93,19 @@ export default {
* If content is not loaded within TIMEOUT_MS,
* show the error dialog
*/
- if (this.state !== SKELETON_STATE.HIDDEN) {
+ if (this.state !== LOADER_STATE.HIDDEN) {
this.showError();
}
}, DEFAULT_TIMERS.TIMEOUT_MS);
},
- hideSkeleton() {
- this.state = SKELETON_STATE.HIDDEN;
+ hideLoader() {
+ this.state = LOADER_STATE.HIDDEN;
},
- showSkeleton() {
- this.state = SKELETON_STATE.VISIBLE;
+ showLoader() {
+ this.state = LOADER_STATE.VISIBLE;
},
showError() {
- this.state = SKELETON_STATE.ERROR;
+ this.state = LOADER_STATE.ERROR;
},
},
};
@@ -107,19 +113,12 @@ export default {
<template>
<div class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-flex-align-items-stretch">
<transition name="fade">
- <div v-if="skeletonVisible" class="gl-px-5 gl-my-5">
- <gl-loading-icon v-if="spinnerVariant" size="lg" />
- <gl-skeleton-loader v-else>
- <rect y="2" width="10" height="8" />
- <rect y="2" x="15" width="15" height="8" />
- <rect y="2" x="35" width="15" height="8" />
- <rect y="15" width="400" height="30" />
- </gl-skeleton-loader>
+ <div v-if="loaderVisible" class="gl-px-5 gl-my-5">
+ <gl-loading-icon size="lg" />
</div>
- <!-- The double condition is only here temporarily for back-compatibility reasons. Will be removed in next iteration https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2275 -->
<div
- v-else-if="spinnerVariant && skeletonHidden"
+ v-else-if="loaderHidden"
data-testid="content-wrapper"
class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-flex-align-items-stretch"
>
@@ -136,16 +135,5 @@ export default {
>
{{ $options.i18n.TIMEOUT_ERROR_MESSAGE }}
</gl-alert>
-
- <!-- This is only kept temporarily for back-compatibility reasons. Will be removed in next iteration https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2275 -->
- <transition v-if="!spinnerVariant">
- <div
- v-show="skeletonHidden"
- data-testid="content-wrapper"
- class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-flex-align-items-stretch"
- >
- <slot></slot>
- </div>
- </transition>
</div>
</template>
diff --git a/app/assets/javascripts/observability/components/observability_container.vue b/app/assets/javascripts/observability/components/observability_container.vue
index 3027f01ea1e..0a9dec11430 100644
--- a/app/assets/javascripts/observability/components/observability_container.vue
+++ b/app/assets/javascripts/observability/components/observability_container.vue
@@ -1,12 +1,11 @@
<script>
import { buildClient } from '../client';
-import { SKELETON_SPINNER_VARIANT } from '../constants';
-import ObservabilitySkeleton from './skeleton/index.vue';
+import ObservabilityLoader from './loader/index.vue';
+import { CONTENT_STATE } from './loader/constants';
export default {
- SKELETON_SPINNER_VARIANT,
components: {
- ObservabilitySkeleton,
+ ObservabilityLoader,
},
props: {
oauthUrl: {
@@ -34,6 +33,7 @@ export default {
return {
observabilityClient: null,
authCompleted: false,
+ loaderContentState: null,
};
},
mounted() {
@@ -69,12 +69,12 @@ export default {
servicesUrl: this.servicesUrl,
operationsUrl: this.operationsUrl,
});
- this.$refs.observabilitySkeleton?.onContentLoaded();
this.$emit('observability-client-ready', this.observabilityClient);
+ this.loaderContentState = CONTENT_STATE.LOADED;
} else if (status === 'error') {
// eslint-disable-next-line @gitlab/require-i18n-strings,no-console
console.error('GOB auth failed with error:', message, statusCode);
- this.$refs.observabilitySkeleton?.onError();
+ this.loaderContentState = CONTENT_STATE.ERROR;
}
this.authCompleted = true;
}
@@ -93,11 +93,8 @@ export default {
data-testid="observability-oauth-iframe"
></iframe>
- <observability-skeleton
- ref="observabilitySkeleton"
- :variant="$options.SKELETON_SPINNER_VARIANT"
- >
+ <observability-loader :content-state="loaderContentState">
<slot v-if="observabilityClient" :observability-client="observabilityClient"></slot>
- </observability-skeleton>
+ </observability-loader>
</div>
</template>
diff --git a/app/assets/javascripts/pages/import/history/components/import_history_app.vue b/app/assets/javascripts/pages/import/history/components/import_history_app.vue
index b2cd728c8b9..9c0f937fe0e 100644
--- a/app/assets/javascripts/pages/import/history/components/import_history_app.vue
+++ b/app/assets/javascripts/pages/import/history/components/import_history_app.vue
@@ -11,11 +11,8 @@ import { DEFAULT_ERROR } from '../utils/error_messages';
import ImportErrorDetails from './import_error_details.vue';
const DEFAULT_PER_PAGE = 20;
-const DEFAULT_TH_CLASSES =
- 'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-200! gl-border-b-1! gl-p-5!';
const tableCell = (config) => ({
- thClass: DEFAULT_TH_CLASSES,
tdClass: (value, key, item) => {
return {
// eslint-disable-next-line no-underscore-dangle
@@ -57,12 +54,12 @@ export default {
tableCell({
key: 'source',
label: s__('BulkImport|Source'),
- thClass: `${DEFAULT_TH_CLASSES} gl-w-30p`,
+ thClass: 'gl-w-30p',
}),
tableCell({
key: 'destination',
label: s__('BulkImport|Destination'),
- thClass: `${DEFAULT_TH_CLASSES} gl-w-40p`,
+ thClass: 'gl-w-40p',
}),
tableCell({
key: 'created_at',
diff --git a/app/assets/javascripts/pages/projects/ml/models/index/index.js b/app/assets/javascripts/pages/projects/ml/models/index/index.js
index 62d326f43a5..3f8ef4910a7 100644
--- a/app/assets/javascripts/pages/projects/ml/models/index/index.js
+++ b/app/assets/javascripts/pages/projects/ml/models/index/index.js
@@ -1,4 +1,4 @@
import { initSimpleApp } from '~/helpers/init_simple_app_helper';
-import MlModelsIndex from '~/ml/model_registry/routes/models/index';
+import { IndexMlModels } from '~/ml/model_registry/apps';
-initSimpleApp('#js-index-ml-models', MlModelsIndex);
+initSimpleApp('#js-index-ml-models', IndexMlModels);
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue
index 377200ab804..3d9a5893c67 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue
@@ -81,7 +81,6 @@ export default {
:value="searchKey"
:placeholder="__('Search labels')"
:disabled="labelsFetchInProgress"
- data-qa-selector="dropdown_input_field"
data-testid="dropdown-input-field"
@input="$emit('input', $event)"
@keydown.enter="$emit('searchEnter', $event)"
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/utils.js b/app/assets/javascripts/vue_shared/components/source_viewer/utils.js
index 165f06e9228..8de6a7841ec 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/utils.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/utils.js
@@ -1,6 +1,7 @@
const BLAME_INFO_CLASSLIST = ['gl-border-t', 'gl-border-gray-500', 'gl-pt-3!'];
const PADDING_BOTTOM_LARGE = 'gl-pb-6!';
const PADDING_BOTTOM_SMALL = 'gl-pb-3!';
+const VIEWER_SELECTOR = '.file-holder .blob-viewer';
const findLineNumberElement = (lineNumber) => document.getElementById(`L${lineNumber}`);
@@ -8,7 +9,7 @@ const findLineContentElement = (lineNumber) => document.getElementById(`LC${line
export const calculateBlameOffset = (lineNumber) => {
if (lineNumber === 1) return '0px';
- const blobViewerOffset = document.querySelector('.blob-viewer')?.getBoundingClientRect().top;
+ const blobViewerOffset = document.querySelector(VIEWER_SELECTOR)?.getBoundingClientRect().top;
const lineContentOffset = findLineContentElement(lineNumber).getBoundingClientRect().top;
return `${lineContentOffset - blobViewerOffset}px`;
};
diff --git a/app/graphql/resolvers/ci/catalog/resources_resolver.rb b/app/graphql/resolvers/ci/catalog/resources_resolver.rb
index 120efe7e01d..bec3d840fc0 100644
--- a/app/graphql/resolvers/ci/catalog/resources_resolver.rb
+++ b/app/graphql/resolvers/ci/catalog/resources_resolver.rb
@@ -10,19 +10,23 @@ module Resolvers
argument :sort, ::Types::Ci::Catalog::ResourceSortEnum,
required: false,
- description: 'Sort Catalog Resources by given criteria.'
+ description: 'Sort catalog resources by given criteria.'
argument :project_path, GraphQL::Types::ID,
required: false,
description: 'Project with the namespace catalog.'
- def resolve_with_lookahead(project_path:, sort: nil)
+ argument :search, GraphQL::Types::String,
+ required: false,
+ description: 'Search term to filter the catalog resources by name or description.'
+
+ def resolve_with_lookahead(project_path:, sort: nil, search: nil)
project = Project.find_by_full_path(project_path)
apply_lookahead(
::Ci::Catalog::Listing
.new(context[:current_user])
- .resources(namespace: project.root_namespace, sort: sort)
+ .resources(namespace: project.root_namespace, sort: sort, search: search)
)
end
diff --git a/app/graphql/types/ci/catalog/resource_sort_enum.rb b/app/graphql/types/ci/catalog/resource_sort_enum.rb
index 7aba7265d39..bb0b5a6e0eb 100644
--- a/app/graphql/types/ci/catalog/resource_sort_enum.rb
+++ b/app/graphql/types/ci/catalog/resource_sort_enum.rb
@@ -3,7 +3,7 @@
module Types
module Ci
module Catalog
- class ResourceSortEnum < SortEnum
+ class ResourceSortEnum < BaseEnum
graphql_name 'CiCatalogResourceSort'
description 'Values for sorting catalog resources'
@@ -11,6 +11,8 @@ module Types
value 'NAME_DESC', 'Name by descending order.', value: :name_desc
value 'LATEST_RELEASED_AT_ASC', 'Latest release date by ascending order.', value: :latest_released_at_asc
value 'LATEST_RELEASED_AT_DESC', 'Latest release date by descending order.', value: :latest_released_at_desc
+ value 'CREATED_ASC', 'Created date by ascending order.', value: :created_at_asc
+ value 'CREATED_DESC', 'Created date by descending order.', value: :created_at_desc
end
end
end
diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb
index cc91b70758f..b6e0b2d6b20 100644
--- a/app/helpers/dropdowns_helper.rb
+++ b/app/helpers/dropdowns_helper.rb
@@ -110,7 +110,7 @@ module DropdownsHelper
def dropdown_filter(placeholder, search_id: nil)
content_tag :div, class: "dropdown-input" do
- filter_output = search_field_tag search_id, nil, data: { qa_selector: "dropdown_input_field" }, id: nil, class: "dropdown-input-field", placeholder: placeholder, autocomplete: 'off'
+ filter_output = search_field_tag search_id, nil, data: { testid: "dropdown-input-field" }, id: nil, class: "dropdown-input-field", placeholder: placeholder, autocomplete: 'off'
filter_output << sprite_icon('search', css_class: 'dropdown-input-search')
filter_output << sprite_icon('close', size: 16, css_class: 'dropdown-input-clear js-dropdown-input-clear')
diff --git a/app/models/ci/catalog/listing.rb b/app/models/ci/catalog/listing.rb
index 17d17bc8f9f..a6f0caa7a40 100644
--- a/app/models/ci/catalog/listing.rb
+++ b/app/models/ci/catalog/listing.rb
@@ -22,6 +22,7 @@ module Ci
when 'name_asc' then relation.order_by_name_asc
when 'latest_released_at_desc' then relation.order_by_latest_released_at_desc
when 'latest_released_at_asc' then relation.order_by_latest_released_at_asc
+ when 'created_at_asc' then relation.order_by_created_at_asc
else
relation.order_by_created_at_desc
end
diff --git a/app/models/ci/catalog/resource.rb b/app/models/ci/catalog/resource.rb
index 3fcf69983d2..c3e8955d2a2 100644
--- a/app/models/ci/catalog/resource.rb
+++ b/app/models/ci/catalog/resource.rb
@@ -16,15 +16,18 @@ module Ci
scope :for_projects, ->(project_ids) { where(project_id: project_ids) }
scope :order_by_created_at_desc, -> { reorder(created_at: :desc) }
+ scope :order_by_created_at_asc, -> { reorder(created_at: :asc) }
scope :order_by_name_desc, -> { joins(:project).merge(Project.sorted_by_name_desc) }
scope :order_by_name_asc, -> { joins(:project).merge(Project.sorted_by_name_asc) }
scope :order_by_latest_released_at_desc, -> { reorder(arel_table[:latest_released_at].desc.nulls_last) }
scope :order_by_latest_released_at_asc, -> { reorder(arel_table[:latest_released_at].asc.nulls_last) }
- delegate :avatar_path, :description, :name, :star_count, :forks_count, to: :project
+ delegate :avatar_path, :star_count, :forks_count, to: :project
enum state: { draft: 0, published: 1 }
+ before_create :sync_with_project
+
def versions
project.releases.order_released_desc
end
@@ -36,6 +39,18 @@ module Ci
def unpublish!
update!(state: :draft)
end
+
+ def sync_with_project!
+ sync_with_project
+ save!
+ end
+
+ private
+
+ def sync_with_project
+ self.name = project.name
+ self.description = project.description
+ end
end
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 0587ff69d3a..f7e994d4beb 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -140,8 +140,12 @@ class Project < ApplicationRecord
after_create -> { create_or_load_association(:pages_metadatum) }
after_create :set_timestamps_for_create
after_create :check_repository_absence!
+
+ after_update :update_catalog_resource, if: -> { (saved_change_to_name? || saved_change_to_description?) && catalog_resource }
+
before_destroy :remove_private_deploy_keys
after_destroy :remove_exports
+
after_save :update_project_statistics, if: :saved_change_to_namespace_id?
after_save :schedule_sync_event_worker, if: -> { saved_change_to_id? || saved_change_to_namespace_id? }
@@ -3530,6 +3534,10 @@ class Project < ApplicationRecord
pool_repository_shard == repository_storage
end
+
+ def update_catalog_resource
+ catalog_resource.sync_with_project!
+ end
end
Project.prepend_mod_with('Project')
diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb
index 6114785a851..683c53d8d78 100644
--- a/app/policies/issue_policy.rb
+++ b/app/policies/issue_policy.rb
@@ -57,7 +57,10 @@ class IssuePolicy < IssuablePolicy
prevent :read_issue
end
- rule { ~can?(:read_issue) }.prevent :create_note
+ rule { ~can?(:read_issue) }.policy do
+ prevent :create_note
+ prevent :read_note
+ end
rule { locked }.policy do
prevent :reopen_issue
diff --git a/app/policies/namespaces/group_project_namespace_shared_policy.rb b/app/policies/namespaces/group_project_namespace_shared_policy.rb
index b24cb5be607..81bb5d6289e 100644
--- a/app/policies/namespaces/group_project_namespace_shared_policy.rb
+++ b/app/policies/namespaces/group_project_namespace_shared_policy.rb
@@ -22,6 +22,7 @@ module Namespaces
enable :create_work_item
enable :read_work_item
enable :read_issue
+ enable :read_note
enable :read_namespace
enable :read_namespace_via_membership
end
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
index 181c79e7bd0..6920ad9cd83 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -33,7 +33,7 @@
- if todo.note.present?
\:
- %span.action-name{ data: { qa_selector: "todo_action_name_content" } }<
+ %span.action-name{ data: { testid: "todo-action-name-content" } }<
- if !todo.note.present?
= todo_action_name(todo)
- unless todo.self_assigned?
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index ab97507b3c8..684231d3a37 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -78,7 +78,7 @@
.row.js-todos-all
- if @allowed_todos.any?
- .col.js-todos-list-container{ data: { qa_selector: "todos_list_container" } }
+ .col.js-todos-list-container{ data: { testid: "todos-list-container" } }
.js-todos-options{ data: { per_page: @allowed_todos.count, current_page: @todos.current_page, total_pages: @todos.total_pages } }
%ul.content-list.todos-list
= render @allowed_todos
diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml
index ebeeaed7ae9..20b849d7af9 100644
--- a/app/views/projects/artifacts/browse.html.haml
+++ b/app/views/projects/artifacts/browse.html.haml
@@ -2,6 +2,7 @@
- page_title @path.presence, _('Artifacts'), "#{@build.name} (##{@build.id})", _('Jobs')
- add_page_specific_style 'page_bundles/tree'
- add_page_specific_style 'page_bundles/ci_status'
+- add_page_specific_style 'page_bundles/projects'
= render "projects/jobs/header"
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index e2e5c37aa43..96a81c7be15 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -874,6 +874,9 @@ Gitlab.ee do
Settings.cron_jobs['ci_schedule_unlock_pipelines_in_queue_worker'] ||= {}
Settings.cron_jobs['ci_schedule_unlock_pipelines_in_queue_worker']['cron'] ||= '*/1 * * * *'
Settings.cron_jobs['ci_schedule_unlock_pipelines_in_queue_worker']['job_class'] = 'Ci::ScheduleUnlockPipelinesInQueueCronWorker'
+ Settings.cron_jobs['timeout_pending_status_check_responses_worker'] ||= {}
+ Settings.cron_jobs['timeout_pending_status_check_responses_worker']['cron'] ||= '*/1 * * * *'
+ Settings.cron_jobs['timeout_pending_status_check_responses_worker']['job_class'] = 'ComplianceManagement::TimeoutPendingStatusCheckResponsesWorker'
Gitlab.com do
Settings.cron_jobs['disable_legacy_open_source_license_for_inactive_projects'] ||= {}
diff --git a/danger/analytics_instrumentation/Dangerfile b/danger/analytics_instrumentation/Dangerfile
index bbb984939dc..6b2139adab9 100644
--- a/danger/analytics_instrumentation/Dangerfile
+++ b/danger/analytics_instrumentation/Dangerfile
@@ -7,3 +7,5 @@ analytics_instrumentation.check!
analytics_instrumentation.check_affected_scopes!
analytics_instrumentation.check_usage_data_insertions!
+
+analytics_instrumentation.check_deprecated_data_sources!
diff --git a/db/migrate/20231005151816_add_created_at_to_status_check_responses.rb b/db/migrate/20231005151816_add_created_at_to_status_check_responses.rb
new file mode 100644
index 00000000000..118586f61a8
--- /dev/null
+++ b/db/migrate/20231005151816_add_created_at_to_status_check_responses.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddCreatedAtToStatusCheckResponses < Gitlab::Database::Migration[2.1]
+ def change
+ add_column :status_check_responses, :created_at, :datetime_with_timezone, null: false, default: -> { 'NOW()' }
+ end
+end
diff --git a/db/migrate/20231017154804_add_index_to_status_check_responses_on_id_and_status.rb b/db/migrate/20231017154804_add_index_to_status_check_responses_on_id_and_status.rb
new file mode 100644
index 00000000000..77aa1a1bb0f
--- /dev/null
+++ b/db/migrate/20231017154804_add_index_to_status_check_responses_on_id_and_status.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class AddIndexToStatusCheckResponsesOnIdAndStatus < Gitlab::Database::Migration[2.1]
+ INDEX_NAME = 'idx_status_check_responses_on_id_and_status'
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :status_check_responses, [:id, :status], name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index_by_name :status_check_responses, name: INDEX_NAME
+ end
+end
diff --git a/db/migrate/20231019180421_add_name_description_to_catalog_resources.rb b/db/migrate/20231019180421_add_name_description_to_catalog_resources.rb
new file mode 100644
index 00000000000..391d56342be
--- /dev/null
+++ b/db/migrate/20231019180421_add_name_description_to_catalog_resources.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class AddNameDescriptionToCatalogResources < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ NAME_INDEX = 'index_catalog_resources_on_name_trigram'
+ DESCRIPTION_INDEX = 'index_catalog_resources_on_description_trigram'
+
+ def up
+ # These columns must match the settings for the corresponding columns in the `projects` table
+ add_column :catalog_resources, :name, :varchar, null: true
+ add_column :catalog_resources, :description, :text, null: true # rubocop: disable Migration/AddLimitToTextColumns
+
+ add_concurrent_index :catalog_resources, :name, name: NAME_INDEX,
+ using: :gin, opclass: { name: :gin_trgm_ops }
+
+ add_concurrent_index :catalog_resources, :description, name: DESCRIPTION_INDEX,
+ using: :gin, opclass: { description: :gin_trgm_ops }
+ end
+
+ def down
+ remove_column :catalog_resources, :name
+ remove_column :catalog_resources, :description
+
+ remove_concurrent_index_by_name :catalog_resources, NAME_INDEX
+ remove_concurrent_index_by_name :catalog_resources, DESCRIPTION_INDEX
+ end
+end
diff --git a/db/post_migrate/20231019223224_backfill_catalog_resources_name_and_description.rb b/db/post_migrate/20231019223224_backfill_catalog_resources_name_and_description.rb
new file mode 100644
index 00000000000..fd5db7621e3
--- /dev/null
+++ b/db/post_migrate/20231019223224_backfill_catalog_resources_name_and_description.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class BackfillCatalogResourcesNameAndDescription < Gitlab::Database::Migration[2.1]
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+ def up
+ sql = <<-SQL
+ UPDATE catalog_resources
+ SET name = projects.name,
+ description = projects.description
+ FROM projects
+ WHERE catalog_resources.project_id = projects.id
+ SQL
+
+ execute(sql)
+ end
+
+ def down
+ # no-op
+
+ # The `name` and `description` columns in `catalog_resources` are denormalized;
+ # they should always stay in sync with the corresponding data in `projects`.
+ end
+end
diff --git a/db/schema_migrations/20231005151816 b/db/schema_migrations/20231005151816
new file mode 100644
index 00000000000..93bf7686fab
--- /dev/null
+++ b/db/schema_migrations/20231005151816
@@ -0,0 +1 @@
+cc9ddab54a3e120e53e214c2d5cb689fda02810031c30da26d0fdc09921c1082 \ No newline at end of file
diff --git a/db/schema_migrations/20231017154804 b/db/schema_migrations/20231017154804
new file mode 100644
index 00000000000..61386d6ebf9
--- /dev/null
+++ b/db/schema_migrations/20231017154804
@@ -0,0 +1 @@
+999c4fefec34812883cb458fe70b89247e3808e53441739ccfec5862b687977a \ No newline at end of file
diff --git a/db/schema_migrations/20231019180421 b/db/schema_migrations/20231019180421
new file mode 100644
index 00000000000..f7cd8f99b10
--- /dev/null
+++ b/db/schema_migrations/20231019180421
@@ -0,0 +1 @@
+9098a39552648a1a2b6439bc26b3e987fc604c0b3bd149d08049b376a09f5ebb \ No newline at end of file
diff --git a/db/schema_migrations/20231019223224 b/db/schema_migrations/20231019223224
new file mode 100644
index 00000000000..0a35a48222b
--- /dev/null
+++ b/db/schema_migrations/20231019223224
@@ -0,0 +1 @@
+d2bd2f99340f7653cec908c4c41c0326d7bf4765fd4e4287ae914ed3025cd690 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index bbb00db88bb..da04d20ff84 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -13153,7 +13153,9 @@ CREATE TABLE catalog_resources (
project_id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
state smallint DEFAULT 0 NOT NULL,
- latest_released_at timestamp with time zone
+ latest_released_at timestamp with time zone,
+ name character varying,
+ description text
);
CREATE SEQUENCE catalog_resources_id_seq
@@ -23434,7 +23436,8 @@ CREATE TABLE status_check_responses (
sha bytea NOT NULL,
external_status_check_id bigint NOT NULL,
status smallint DEFAULT 0 NOT NULL,
- retried_at timestamp with time zone
+ retried_at timestamp with time zone,
+ created_at timestamp with time zone DEFAULT now() NOT NULL
);
CREATE SEQUENCE status_check_responses_id_seq
@@ -31181,6 +31184,8 @@ CREATE INDEX idx_security_scans_on_scan_type ON security_scans USING btree (scan
CREATE UNIQUE INDEX idx_software_license_policies_unique_on_project_and_scan_policy ON software_license_policies USING btree (project_id, software_license_id, scan_result_policy_id);
+CREATE INDEX idx_status_check_responses_on_id_and_status ON status_check_responses USING btree (id, status);
+
CREATE INDEX idx_streaming_headers_on_external_audit_event_destination_id ON audit_events_streaming_headers USING btree (external_audit_event_destination_id);
CREATE INDEX idx_test_reports_on_issue_id_created_at_and_id ON requirements_management_test_reports USING btree (issue_id, created_at, id);
@@ -31541,6 +31546,10 @@ CREATE INDEX index_catalog_resource_versions_on_project_id ON catalog_resource_v
CREATE UNIQUE INDEX index_catalog_resource_versions_on_release_id ON catalog_resource_versions USING btree (release_id);
+CREATE INDEX index_catalog_resources_on_description_trigram ON catalog_resources USING gin (description gin_trgm_ops);
+
+CREATE INDEX index_catalog_resources_on_name_trigram ON catalog_resources USING gin (name gin_trgm_ops);
+
CREATE UNIQUE INDEX index_catalog_resources_on_project_id ON catalog_resources USING btree (project_id);
CREATE INDEX index_chat_names_on_team_id_and_chat_id ON chat_names USING btree (team_id, chat_id);
diff --git a/doc/administration/settings/usage_statistics.md b/doc/administration/settings/usage_statistics.md
index 365f3935afc..2bfcdbde34e 100644
--- a/doc/administration/settings/usage_statistics.md
+++ b/doc/administration/settings/usage_statistics.md
@@ -66,6 +66,7 @@ In the following table, you can see:
| [Group file templates](../../user/group/manage.md#group-file-templates) | GitLab 16.6 and later |
| [Group webhooks](../../user/project/integrations/webhooks.md#group-webhooks) | GitLab 16.6 and later |
| [Service Level Agreement countdown timer](../../operations/incident_management/incidents.md#service-level-agreement-countdown-timer) | GitLab 16.6 and later |
+| [Lock project membership to group](../../user/group/access_and_permissions.md#prevent-members-from-being-added-to-projects-in-a-group) | GitLab 16.6 and later |
### Enable registration features
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 24ce86eb5c9..247a1c18992 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -157,7 +157,8 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="querycicatalogresourcesprojectpath"></a>`projectPath` | [`ID`](#id) | Project with the namespace catalog. |
-| <a id="querycicatalogresourcessort"></a>`sort` | [`CiCatalogResourceSort`](#cicatalogresourcesort) | Sort Catalog Resources by given criteria. |
+| <a id="querycicatalogresourcessearch"></a>`search` | [`String`](#string) | Search term to filter the catalog resources by name or description. |
+| <a id="querycicatalogresourcessort"></a>`sort` | [`CiCatalogResourceSort`](#cicatalogresourcesort) | Sort catalog resources by given criteria. |
### `Query.ciConfig`
@@ -27665,18 +27666,12 @@ Values for sorting catalog resources.
| Value | Description |
| ----- | ----------- |
-| <a id="cicatalogresourcesortcreated_asc"></a>`CREATED_ASC` | Created at ascending order. |
-| <a id="cicatalogresourcesortcreated_desc"></a>`CREATED_DESC` | Created at descending order. |
+| <a id="cicatalogresourcesortcreated_asc"></a>`CREATED_ASC` | Created date by ascending order. |
+| <a id="cicatalogresourcesortcreated_desc"></a>`CREATED_DESC` | Created date by descending order. |
| <a id="cicatalogresourcesortlatest_released_at_asc"></a>`LATEST_RELEASED_AT_ASC` | Latest release date by ascending order. |
| <a id="cicatalogresourcesortlatest_released_at_desc"></a>`LATEST_RELEASED_AT_DESC` | Latest release date by descending order. |
| <a id="cicatalogresourcesortname_asc"></a>`NAME_ASC` | Name by ascending order. |
| <a id="cicatalogresourcesortname_desc"></a>`NAME_DESC` | Name by descending order. |
-| <a id="cicatalogresourcesortupdated_asc"></a>`UPDATED_ASC` | Updated at ascending order. |
-| <a id="cicatalogresourcesortupdated_desc"></a>`UPDATED_DESC` | Updated at descending order. |
-| <a id="cicatalogresourcesortcreated_asc"></a>`created_asc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `CREATED_ASC`. |
-| <a id="cicatalogresourcesortcreated_desc"></a>`created_desc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `CREATED_DESC`. |
-| <a id="cicatalogresourcesortupdated_asc"></a>`updated_asc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `UPDATED_ASC`. |
-| <a id="cicatalogresourcesortupdated_desc"></a>`updated_desc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `UPDATED_DESC`. |
### `CiConfigIncludeType`
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 68e69316f46..c8682fc154f 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -48,7 +48,7 @@ If the highest number stable branch is unclear, check the [GitLab blog](https://
| [Ruby](#2-ruby) | `3.0.x` | From GitLab 15.10, Ruby 3.0 is required. You must use the standard MRI implementation of Ruby. We love [JRuby](https://www.jruby.org/) and [Rubinius](https://github.com/rubinius/rubinius#the-rubinius-language-platform), but GitLab needs several Gems that have native extensions. |
| [RubyGems](#3-rubygems) | `3.4.x` | A specific RubyGems version is not fully needed, but it's recommended to update so you can enjoy some known performance improvements. |
| [Go](#4-go) | `1.20.x` | From GitLab 16.4, Go 1.20 or later is required. |
-| [Git](#git) | `2.41.x` | From GitLab 16.2, Git 2.41.x and later is required. You should use the [Git version provided by Gitaly](#git). |
+| [Git](#git) | `2.42.x` | From GitLab 16.5, Git 2.42.x and later is required. You should use the [Git version provided by Gitaly](#git). |
| [Node.js](#5-node) | `18.17.x` | From GitLab 16.3, Node.js 18.17 or later is required. |
## GitLab directory structure
diff --git a/doc/update/versions/gitlab_16_changes.md b/doc/update/versions/gitlab_16_changes.md
index 48a60267bf0..39660a910a2 100644
--- a/doc/update/versions/gitlab_16_changes.md
+++ b/doc/update/versions/gitlab_16_changes.md
@@ -30,6 +30,10 @@ For more information about upgrading GitLab Helm Chart, see [the release notes f
- [Praefect configuration structure change](#praefect-configuration-structure-change).
- [Gitaly configuration structure change](#gitaly-configuration-structure-change).
+## 16.5.0
+
+- Git 2.42.0 and later is required by Gitaly. For self-compiled installations, you should use the [Git version provided by Gitaly](../../install/installation.md#git).
+
## 16.4.0
- Updating a group path [received a bug fix](https://gitlab.com/gitlab-org/gitlab/-/issues/419289) that uses a database index introduced in 16.3.
diff --git a/doc/user/analytics/analytics_dashboards.md b/doc/user/analytics/analytics_dashboards.md
index 448a46fdc26..8bed8018eb8 100644
--- a/doc/user/analytics/analytics_dashboards.md
+++ b/doc/user/analytics/analytics_dashboards.md
@@ -39,6 +39,12 @@ When [product analytics](../product_analytics/index.md) is enabled and onboarded
- **Audience** displays metrics related to traffic, such as the number of users and sessions.
- **Behavior** displays metrics related to user activity, such as the number of page views and events.
+For more information about the development of product analytics, see the [group direction page](https://about.gitlab.com/direction/analytics/product-analytics/). To leave feedback about bugs or functionality:
+
+- Comment on issue [391970](https://gitlab.com/gitlab-org/gitlab/-/issues/391970).
+- Create an issue with the `group::product analytics` label.
+- [Schedule a call](https://calendly.com/jheimbuck/30-minute-call) with the team.
+
### Value Stream Management
- **Value Streams Dashboard** displays metrics related to [DevOps performance, security exposure, and workstream optimization](../analytics/value_streams_dashboard.md#devsecops-metrics-comparison-panel).
diff --git a/doc/user/infrastructure/iac/index.md b/doc/user/infrastructure/iac/index.md
index 1e6c59c2253..65ec84652ef 100644
--- a/doc/user/infrastructure/iac/index.md
+++ b/doc/user/infrastructure/iac/index.md
@@ -85,7 +85,6 @@ To use a Terraform template:
```yaml
variables:
TF_STATE_NAME: default
- TF_CACHE_KEY: default
# If your terraform files are in a subdirectory, set TF_ROOT accordingly. For example:
# TF_ROOT: terraform/production
```
diff --git a/doc/user/project/merge_requests/status_checks.md b/doc/user/project/merge_requests/status_checks.md
index 698078351e2..9249af0f25c 100644
--- a/doc/user/project/merge_requests/status_checks.md
+++ b/doc/user/project/merge_requests/status_checks.md
@@ -10,6 +10,8 @@ type: reference, concepts
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3869) in GitLab 14.0, disabled behind the `:ff_external_status_checks` feature flag.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/320783) in GitLab 14.1.
> - `failed` status [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/329636) in GitLab 14.9.
+> - `pending` status [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/413723) in GitLab 16.5
+> - Timeout interval of two minutes for `pending` status checks [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/388725) in GitLab 16.5.
Status checks are API calls to external systems that request the status of an external requirement.
@@ -25,6 +27,8 @@ at the merge request level itself.
You can configure merge request status checks for each individual project. These are not shared between projects.
+Status checks fail if they stay in the pending state for more than two minutes.
+
For more information about use cases, feature discovery, and development timelines,
see [epic 3869](https://gitlab.com/groups/gitlab-org/-/epics/3869).
diff --git a/doc/user/project/repository/branches/index.md b/doc/user/project/repository/branches/index.md
index 2b6858ab4f7..84383c4bf49 100644
--- a/doc/user/project/repository/branches/index.md
+++ b/doc/user/project/repository/branches/index.md
@@ -313,6 +313,27 @@ To create a target branch rule:
1. Select the **Target branch** to use when the branch name matches the **Rule name**.
1. Select **Save**.
+### Example
+
+You could configure your project to have the following target branch rules:
+
+| Rule name | Target branch |
+|-------------|---------------|
+| `feature/*` | `develop` |
+| `bug/*` | `develop` |
+| `release/*` | `main` |
+
+These rules simplify the process of creating merge requests for a project that:
+
+- Uses `main` to represent the deployed state of your application.
+- Tracks current, unreleased development work in another long-running branch, like `develop`.
+
+If your workflow initially places new features in `develop` instead of `main`, these rules
+ensure all branches matching either `feature/*` or `bug/*` do not target `main` by mistake.
+
+When you're ready to release to `main`, create a branch named `release/*`, and the rules
+ensure this branch targets `main`.
+
## Delete a target branch rule
When you remove a target branch rule, existing merge requests remain unchanged.
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index a928d73e9a2..6f1a247ba6c 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -26817,7 +26817,7 @@ msgstr ""
msgid "JiraService|Basic"
msgstr ""
-msgid "JiraService|Define the type of Jira issue to create from a vulnerability."
+msgid "JiraService|Create Jira issues of this type from vulnerabilities."
msgstr ""
msgid "JiraService|Displaying Jira issues while leaving GitLab issues also enabled might be confusing. Consider %{gitlab_issues_link_start}disabling GitLab issues%{link_end} if they won't otherwise be used."
@@ -26880,6 +26880,9 @@ msgstr ""
msgid "JiraService|Jira issue regex"
msgstr ""
+msgid "JiraService|Jira issue type"
+msgstr ""
+
msgid "JiraService|Jira issues"
msgstr ""
diff --git a/package.json b/package.json
index b309716dc04..6ab5e20903f 100644
--- a/package.json
+++ b/package.json
@@ -211,7 +211,7 @@
"uuid": "8.1.0",
"visibilityjs": "^1.2.4",
"vue": "2.7.14",
- "vue-apollo": "^3.1.1",
+ "vue-apollo": "^3.0.7",
"vue-loader": "15.10.2",
"vue-observe-visibility": "^1.0.0",
"vue-resize": "^1.0.1",
diff --git a/patches/vue-apollo+3.0.7.patch b/patches/vue-apollo+3.0.7.patch
new file mode 100644
index 00000000000..c463150a4e3
--- /dev/null
+++ b/patches/vue-apollo+3.0.7.patch
@@ -0,0 +1,38 @@
+diff --git a/node_modules/vue-apollo/dist/vue-apollo.esm.js b/node_modules/vue-apollo/dist/vue-apollo.esm.js
+index e4b4b15..6d3040b 100644
+--- a/node_modules/vue-apollo/dist/vue-apollo.esm.js
++++ b/node_modules/vue-apollo/dist/vue-apollo.esm.js
+@@ -1933,14 +1933,6 @@ function initProvider() {
+ this.$apolloProvider = typeof optionValue === 'function' ? optionValue() : optionValue;
+ } else if (options.parent && options.parent.$apolloProvider) {
+ this.$apolloProvider = options.parent.$apolloProvider;
+- } else if (options.provide) {
+- // TODO remove
+- // Temporary retro-compatibility
+- var provided = typeof options.provide === 'function' ? options.provide.call(this) : options.provide;
+-
+- if (provided && provided.$apolloProvider) {
+- this.$apolloProvider = provided.$apolloProvider;
+- }
+ }
+ }
+
+diff --git a/node_modules/vue-apollo/dist/vue-apollo.umd.js b/node_modules/vue-apollo/dist/vue-apollo.umd.js
+index 2310455..895f996 100644
+--- a/node_modules/vue-apollo/dist/vue-apollo.umd.js
++++ b/node_modules/vue-apollo/dist/vue-apollo.umd.js
+@@ -1939,14 +1939,6 @@
+ this.$apolloProvider = typeof optionValue === 'function' ? optionValue() : optionValue;
+ } else if (options.parent && options.parent.$apolloProvider) {
+ this.$apolloProvider = options.parent.$apolloProvider;
+- } else if (options.provide) {
+- // TODO remove
+- // Temporary retro-compatibility
+- var provided = typeof options.provide === 'function' ? options.provide.call(this) : options.provide;
+-
+- if (provided && provided.$apolloProvider) {
+- this.$apolloProvider = provided.$apolloProvider;
+- }
+ }
+ }
+
diff --git a/qa/qa/page/dashboard/todos.rb b/qa/qa/page/dashboard/todos.rb
index 94fd20b80ab..40b8498d6bf 100644
--- a/qa/qa/page/dashboard/todos.rb
+++ b/qa/qa/page/dashboard/todos.rb
@@ -7,18 +7,18 @@ module QA
include Page::Component::Snippet
view 'app/views/dashboard/todos/index.html.haml' do
- element :todos_list_container, required: true
+ element 'todos-list-container', required: true
element 'group-dropdown'
end
view 'app/views/dashboard/todos/_todo.html.haml' do
element 'todo-item-container'
- element :todo_action_name_content
+ element 'todo-action-name-content'
element 'todo-author-name-content'
end
view 'app/helpers/dropdowns_helper.rb' do
- element :dropdown_input_field
+ element 'dropdown-input-field'
element 'dropdown-list-content'
end
@@ -33,7 +33,7 @@ module QA
def filter_todos_by_group(group)
click_element 'group-dropdown'
- fill_element(:dropdown_input_field, group.path)
+ fill_element('dropdown-input-field', group.path)
within_element('dropdown-list-content') do
click_on group.path
@@ -54,9 +54,9 @@ module QA
private
def has_latest_todo_with_content?(action, **kwargs)
- within_element(:todos_list_container) do
+ within_element('todos-list-container') do
within_element_by_index('todo-item-container', 0) do
- has_element?(:todo_action_name_content, text: action) &&
+ has_element?('todo-action-name-content', text: action) &&
has_element?(kwargs[:selector], text: kwargs[:text])
end
end
diff --git a/scripts/regenerate-schema b/scripts/regenerate-schema
index f1018403395..697177c0c0f 100755
--- a/scripts/regenerate-schema
+++ b/scripts/regenerate-schema
@@ -2,6 +2,7 @@
# frozen_string_literal: true
+require 'optparse'
require 'open3'
require 'fileutils'
require 'uri'
@@ -27,6 +28,10 @@ class SchemaRegenerator
# directory when it runs.
SCHEMA_MIGRATIONS_DIR = 'db/schema_migrations/'
+ def initialize(options)
+ @rollback_testing = options.delete(:rollback_testing)
+ end
+
def execute
Dir.chdir(File.expand_path('..', __dir__)) do
checkout_ref
@@ -37,6 +42,7 @@ class SchemaRegenerator
reset_db
unhide_migrations
migrate
+ rollback if @rollback_testing
ensure
unhide_migrations
end
@@ -168,6 +174,15 @@ class SchemaRegenerator
end
##
+ # Run rake task to rollback migrations.
+ def rollback
+ (untracked_schema_migrations + committed_schema_migrations).sort.reverse_each do |filename|
+ version = filename[/\d+\Z/]
+ run %(bin/rails db:rollback:main db:rollback:ci RAILS_ENV=test VERSION=#{version})
+ end
+ end
+
+ ##
# Run the given +cmd+.
#
# The command is colored green, and the output of the command is
@@ -224,4 +239,19 @@ class SchemaRegenerator
end
end
-SchemaRegenerator.new.execute
+if $PROGRAM_NAME == __FILE__
+ options = {}
+
+ OptionParser.new do |opts|
+ opts.on("-r", "--rollback-testing", String, "Enable rollback testing") do
+ options[:rollback_testing] = true
+ end
+
+ opts.on("-h", "--help", "Prints this help") do
+ puts opts
+ exit
+ end
+ end.parse!
+
+ SchemaRegenerator.new(options).execute
+end
diff --git a/spec/factories/ci/catalog/resources/components.rb b/spec/factories/ci/catalog/resources/components.rb
index 8feecc695bc..843ccb2b461 100644
--- a/spec/factories/ci/catalog/resources/components.rb
+++ b/spec/factories/ci/catalog/resources/components.rb
@@ -5,6 +5,6 @@ FactoryBot.define do
version factory: :ci_catalog_resource_version
catalog_resource { version.catalog_resource }
project { version.project }
- name { catalog_resource.name }
+ name { catalog_resource.project.name }
end
end
diff --git a/spec/frontend/ml/model_registry/routes/models/index/components/ml_models_index_spec.js b/spec/frontend/ml/model_registry/apps/index_ml_models_spec.js
index fc8e7aef73e..612b5ecbc40 100644
--- a/spec/frontend/ml/model_registry/routes/models/index/components/ml_models_index_spec.js
+++ b/spec/frontend/ml/model_registry/apps/index_ml_models_spec.js
@@ -1,15 +1,15 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import MlModelsIndexApp from '~/ml/model_registry/routes/models/index';
-import ModelRow from '~/ml/model_registry/routes/models/index/components/model_row.vue';
-import { TITLE_LABEL, NO_MODELS_LABEL } from '~/ml/model_registry/routes/models/index/translations';
+import { IndexMlModels } from '~/ml/model_registry/apps';
+import ModelRow from '~/ml/model_registry/components/model_row.vue';
+import { TITLE_LABEL, NO_MODELS_LABEL } from '~/ml/model_registry/translations';
import Pagination from '~/vue_shared/components/incubation/pagination.vue';
-import SearchBar from '~/ml/model_registry/routes/models/index/components/search_bar.vue';
-import { BASE_SORT_FIELDS } from '~/ml/model_registry/routes/models/index/constants';
-import { mockModels, startCursor, defaultPageInfo } from './mock_data';
+import SearchBar from '~/ml/model_registry/components/search_bar.vue';
+import { BASE_SORT_FIELDS } from '~/ml/model_registry/constants';
+import { mockModels, startCursor, defaultPageInfo } from '../mock_data';
let wrapper;
const createWrapper = (propsData = { models: mockModels, pageInfo: defaultPageInfo }) => {
- wrapper = shallowMountExtended(MlModelsIndexApp, { propsData });
+ wrapper = shallowMountExtended(IndexMlModels, { propsData });
};
const findModelRow = (index) => wrapper.findAllComponents(ModelRow).at(index);
diff --git a/spec/frontend/ml/model_registry/routes/models/index/components/model_row_spec.js b/spec/frontend/ml/model_registry/components/model_row_spec.js
index 7600288f560..037abab0ac4 100644
--- a/spec/frontend/ml/model_registry/routes/models/index/components/model_row_spec.js
+++ b/spec/frontend/ml/model_registry/components/model_row_spec.js
@@ -1,10 +1,7 @@
import { GlLink } from '@gitlab/ui';
-import {
- mockModels,
- modelWithoutVersion,
-} from 'jest/ml/model_registry/routes/models/index/components/mock_data';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import ModelRow from '~/ml/model_registry/routes/models/index/components/model_row.vue';
+import ModelRow from '~/ml/model_registry/components/model_row.vue';
+import { mockModels, modelWithoutVersion } from '../mock_data';
let wrapper;
const createWrapper = (model = mockModels[0]) => {
diff --git a/spec/frontend/ml/model_registry/routes/models/index/components/search_bar_spec.js b/spec/frontend/ml/model_registry/components/search_bar_spec.js
index ac8c74bce7f..f9e18486434 100644
--- a/spec/frontend/ml/model_registry/routes/models/index/components/search_bar_spec.js
+++ b/spec/frontend/ml/model_registry/components/search_bar_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import setWindowLocation from 'helpers/set_window_location_helper';
import * as urlHelpers from '~/lib/utils/url_utility';
-import SearchBar from '~/ml/model_registry/routes/models/index/components/search_bar.vue';
-import { BASE_SORT_FIELDS } from '~/ml/model_registry/routes/models/index/constants';
+import SearchBar from '~/ml/model_registry/components/search_bar.vue';
+import { BASE_SORT_FIELDS } from '~/ml/model_registry/constants';
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
let wrapper;
diff --git a/spec/frontend/ml/model_registry/mock_data.js b/spec/frontend/ml/model_registry/mock_data.js
index bed0de36e5d..1c606e79fa2 100644
--- a/spec/frontend/ml/model_registry/mock_data.js
+++ b/spec/frontend/ml/model_registry/mock_data.js
@@ -15,3 +15,33 @@ export const makeModel = ({ latestVersion } = { latestVersion: LATEST_VERSION })
export const MODEL = makeModel();
export const MODEL_VERSION = { version: '1.2.3', model: MODEL };
+
+export const mockModels = [
+ {
+ name: 'model_1',
+ version: '1.0',
+ path: 'path/to/model_1',
+ versionCount: 3,
+ },
+ {
+ name: 'model_2',
+ version: '1.1',
+ path: 'path/to/model_2',
+ versionCount: 1,
+ },
+];
+
+export const modelWithoutVersion = {
+ name: 'model_without_version',
+ path: 'path/to/model_without_version',
+ versionCount: 0,
+};
+
+export const startCursor = 'eyJpZCI6IjE2In0';
+
+export const defaultPageInfo = Object.freeze({
+ startCursor,
+ endCursor: 'eyJpZCI6IjIifQ',
+ hasNextPage: true,
+ hasPreviousPage: true,
+});
diff --git a/spec/frontend/observability/skeleton_spec.js b/spec/frontend/observability/loader_spec.js
index 5501fa117e0..abd1e6f3fe0 100644
--- a/spec/frontend/observability/skeleton_spec.js
+++ b/spec/frontend/observability/loader_spec.js
@@ -1,12 +1,10 @@
import { nextTick } from 'vue';
-import { GlSkeletonLoader, GlAlert, GlLoadingIcon } from '@gitlab/ui';
+import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import Loader from '~/observability/components/loader/index.vue';
+import { DEFAULT_TIMERS, CONTENT_STATE } from '~/observability/components/loader/constants';
-import Skeleton from '~/observability/components/skeleton/index.vue';
-
-import { DEFAULT_TIMERS } from '~/observability/constants';
-
-describe('Skeleton component', () => {
+describe('Loader component', () => {
let wrapper;
const findSpinner = () => wrapper.findComponent(GlLoadingIcon);
@@ -16,18 +14,18 @@ describe('Skeleton component', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
const mountComponent = ({ ...props } = {}) => {
- wrapper = shallowMountExtended(Skeleton, {
+ wrapper = shallowMountExtended(Loader, {
propsData: props,
});
};
describe('on mount', () => {
beforeEach(() => {
- mountComponent({ variant: 'spinner' });
+ mountComponent();
});
describe('showing content', () => {
- it('shows the skeleton if content is not loaded within CONTENT_WAIT_MS', async () => {
+ it('shows the loader if content is not loaded within CONTENT_WAIT_MS', async () => {
expect(findSpinner().exists()).toBe(false);
expect(findContentWrapper().exists()).toBe(false);
@@ -39,13 +37,11 @@ describe('Skeleton component', () => {
expect(findContentWrapper().exists()).toBe(false);
});
- it('does not show the skeleton if content loads within CONTENT_WAIT_MS', async () => {
+ it('does not show the loader if content loads within CONTENT_WAIT_MS', async () => {
expect(findSpinner().exists()).toBe(false);
expect(findContentWrapper().exists()).toBe(false);
- wrapper.vm.onContentLoaded();
-
- await nextTick();
+ await wrapper.setProps({ contentState: CONTENT_STATE.LOADED });
expect(findContentWrapper().exists()).toBe(true);
expect(findSpinner().exists()).toBe(false);
@@ -58,7 +54,7 @@ describe('Skeleton component', () => {
expect(findSpinner().exists()).toBe(false);
});
- it('hides the skeleton after content loads', async () => {
+ it('hides the loader after content loads', async () => {
jest.advanceTimersByTime(DEFAULT_TIMERS.CONTENT_WAIT_MS);
await nextTick();
@@ -66,9 +62,7 @@ describe('Skeleton component', () => {
expect(findSpinner().exists()).toBe(true);
expect(findContentWrapper().exists()).toBe(false);
- wrapper.vm.onContentLoaded();
-
- await nextTick();
+ await wrapper.setProps({ contentState: CONTENT_STATE.LOADED });
expect(findContentWrapper().exists()).toBe(true);
expect(findSpinner().exists()).toBe(false);
@@ -89,16 +83,14 @@ describe('Skeleton component', () => {
it('shows the error dialog if content fails to load', async () => {
expect(findAlert().exists()).toBe(false);
- wrapper.vm.onError();
-
- await nextTick();
+ await wrapper.setProps({ contentState: 'error' });
expect(findAlert().exists()).toBe(true);
expect(findContentWrapper().exists()).toBe(false);
});
it('does not show the error dialog if content has loaded within TIMEOUT_MS', async () => {
- wrapper.vm.onContentLoaded();
+ wrapper.setProps({ contentState: CONTENT_STATE.LOADED });
jest.advanceTimersByTime(DEFAULT_TIMERS.TIMEOUT_MS);
await nextTick();
@@ -108,37 +100,4 @@ describe('Skeleton component', () => {
});
});
});
-
- describe('skeleton variant', () => {
- it('shows only the spinner variant when variant is spinner', async () => {
- mountComponent({ variant: 'spinner' });
- jest.advanceTimersByTime(DEFAULT_TIMERS.CONTENT_WAIT_MS);
- await nextTick();
-
- expect(findSpinner().exists()).toBe(true);
- expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(false);
- });
-
- it('shows only the default variant when variant is not spinner', async () => {
- mountComponent({ variant: 'unknown' });
- jest.advanceTimersByTime(DEFAULT_TIMERS.CONTENT_WAIT_MS);
- await nextTick();
-
- expect(findSpinner().exists()).toBe(false);
- expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true);
- });
- });
-
- describe('on destroy', () => {
- it('should clear init timer and timeout timer', () => {
- jest.spyOn(global, 'clearTimeout');
- mountComponent();
- wrapper.destroy();
- expect(clearTimeout).toHaveBeenCalledTimes(2);
- expect(clearTimeout.mock.calls).toEqual([
- [wrapper.vm.loadingTimeout], // First call
- [wrapper.vm.errorTimeout], // Second call
- ]);
- });
- });
});
diff --git a/spec/frontend/observability/observability_container_spec.js b/spec/frontend/observability/observability_container_spec.js
index 1e954b7db0d..16b1c7b254e 100644
--- a/spec/frontend/observability/observability_container_spec.js
+++ b/spec/frontend/observability/observability_container_spec.js
@@ -1,8 +1,9 @@
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { stubComponent } from 'helpers/stub_component';
import ObservabilityContainer from '~/observability/components/observability_container.vue';
-import ObservabilitySkeleton from '~/observability/components/skeleton/index.vue';
+import ObservabilityLoader from '~/observability/components/loader/index.vue';
+import { CONTENT_STATE } from '~/observability/components/loader/constants';
+
import { buildClient } from '~/observability/client';
jest.mock('~/observability/client');
@@ -10,9 +11,6 @@ jest.mock('~/observability/client');
describe('ObservabilityContainer', () => {
let wrapper;
- const mockSkeletonOnContentLoaded = jest.fn();
- const mockSkeletonOnError = jest.fn();
-
const OAUTH_URL = 'https://example.com/oauth';
const TRACING_URL = 'https://example.com/tracing';
const PROVISIONING_URL = 'https://example.com/provisioning';
@@ -34,11 +32,6 @@ describe('ObservabilityContainer', () => {
servicesUrl: SERVICES_URL,
operationsUrl: OPERATIONS_URL,
},
- stubs: {
- ObservabilitySkeleton: stubComponent(ObservabilitySkeleton, {
- methods: { onContentLoaded: mockSkeletonOnContentLoaded, onError: mockSkeletonOnError },
- }),
- },
slots: {
default: {
render(h) {
@@ -63,6 +56,7 @@ describe('ObservabilityContainer', () => {
const findIframe = () => wrapper.findByTestId('observability-oauth-iframe');
const findSlotComponent = () => wrapper.findComponent({ name: 'MockComponent' });
+ const findLoader = () => wrapper.findComponent(ObservabilityLoader);
it('should render the oauth iframe', () => {
const iframe = findIframe();
@@ -72,9 +66,8 @@ describe('ObservabilityContainer', () => {
expect(iframe.attributes('sandbox')).toBe('allow-same-origin allow-forms allow-scripts');
});
- it('should render the ObservabilitySkeleton', () => {
- const skeleton = wrapper.findComponent(ObservabilitySkeleton);
- expect(skeleton.exists()).toBe(true);
+ it('should render the ObservabilityLoader', () => {
+ expect(findLoader().exists()).toBe(true);
});
it('should not render the default slot', () => {
@@ -92,8 +85,8 @@ describe('ObservabilityContainer', () => {
await nextTick();
});
- it('renders invoke onContentLoaded on the skeleton', () => {
- expect(mockSkeletonOnContentLoaded).toHaveBeenCalledTimes(1);
+ it('sets the loader contentState to LOADED', () => {
+ expect(findLoader().props('contentState')).toBe(CONTENT_STATE.LOADED);
});
it('renders the slot content', () => {
@@ -115,29 +108,44 @@ describe('ObservabilityContainer', () => {
});
});
- it('does not render the slot content and removes the iframe on oauth error message', async () => {
- dispatchMessageEvent('error');
+ describe('on oauth error message', () => {
+ beforeEach(async () => {
+ dispatchMessageEvent('error');
- await nextTick();
+ await nextTick();
+ });
- expect(mockSkeletonOnError).toHaveBeenCalledTimes(1);
+ it('set the loader contentState to ERROR', () => {
+ expect(findLoader().props('contentState')).toBe(CONTENT_STATE.ERROR);
+ });
- expect(findSlotComponent().exists()).toBe(false);
- expect(findIframe().exists()).toBe(false);
- expect(buildClient).not.toHaveBeenCalled();
+ it('does not renders the slot content', () => {
+ expect(findSlotComponent().exists()).toBe(false);
+ });
+
+ it('does not build the observability client', () => {
+ expect(buildClient).not.toHaveBeenCalled();
+ });
+
+ it('does not emit observability-client-ready', () => {
+ expect(wrapper.emitted('observability-client-ready')).toBeUndefined();
+ });
});
- it('handles oauth message only once', () => {
- dispatchMessageEvent('success');
+ it('handles oauth message only once', async () => {
dispatchMessageEvent('success');
+ dispatchMessageEvent('error');
+
+ await nextTick();
- expect(mockSkeletonOnContentLoaded).toHaveBeenCalledTimes(1);
+ expect(buildClient).toHaveBeenCalledTimes(1);
+ expect(findLoader().props('contentState')).toBe(CONTENT_STATE.LOADED);
});
it('only handles messages from the oauth url', () => {
dispatchMessageEvent('success', 'www.fake-url.com');
- expect(mockSkeletonOnContentLoaded).toHaveBeenCalledTimes(0);
+ expect(findLoader().props('contentState')).toBe(null);
expect(findSlotComponent().exists()).toBe(false);
expect(findIframe().exists()).toBe(true);
});
@@ -147,6 +155,6 @@ describe('ObservabilityContainer', () => {
dispatchMessageEvent('success');
- expect(mockSkeletonOnContentLoaded).toHaveBeenCalledTimes(0);
+ expect(findLoader().props('contentState')).toBe(null);
});
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/mock_data.js b/spec/frontend/vue_shared/components/source_viewer/mock_data.js
index f03d6ee8aee..ec859e3c5d9 100644
--- a/spec/frontend/vue_shared/components/source_viewer/mock_data.js
+++ b/spec/frontend/vue_shared/components/source_viewer/mock_data.js
@@ -24,18 +24,20 @@ export const CHUNK_2 = {
};
export const SOURCE_CODE_CONTENT_MOCK = `
-<div class="blob-viewer">
- <div class="content">
- <div>
- <div id="L1">1</div>
- <div id="L2">2</div>
- <div id="L3">3</div>
- </div>
+<div class="file-holder">
+ <div class="blob-viewer">
+ <div class="content">
+ <div>
+ <div id="L1">1</div>
+ <div id="L2">2</div>
+ <div id="L3">3</div>
+ </div>
- <div>
- <div id="LC1">Content 1</div>
- <div id="LC2">Content 2</div>
- <div id="LC3">Content 3</div>
+ <div>
+ <div id="LC1">Content 1</div>
+ <div id="LC2">Content 2</div>
+ <div id="LC3">Content 3</div>
+ </div>
</div>
</div>
</div>`;
diff --git a/spec/graphql/resolvers/ci/catalog/resources_resolver_spec.rb b/spec/graphql/resolvers/ci/catalog/resources_resolver_spec.rb
index 0860e8114bd..97105db686f 100644
--- a/spec/graphql/resolvers/ci/catalog/resources_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/catalog/resources_resolver_spec.rb
@@ -7,44 +7,63 @@ RSpec.describe Resolvers::Ci::Catalog::ResourcesResolver, feature_category: :pip
let_it_be(:namespace) { create(:group) }
let_it_be(:project_1) { create(:project, name: 'Z', namespace: namespace) }
- let_it_be(:project_2) { create(:project, name: 'A', namespace: namespace) }
- let_it_be(:project_3) { create(:project, name: 'L', namespace: namespace) }
+ let_it_be(:project_2) { create(:project, name: 'A_Test', namespace: namespace) }
+ let_it_be(:project_3) { create(:project, name: 'L', description: 'Test', namespace: namespace) }
let_it_be(:resource_1) { create(:ci_catalog_resource, project: project_1) }
let_it_be(:resource_2) { create(:ci_catalog_resource, project: project_2) }
let_it_be(:resource_3) { create(:ci_catalog_resource, project: project_3) }
let_it_be(:user) { create(:user) }
+ let(:ctx) { { current_user: user } }
+ let(:search) { nil }
+ let(:sort) { nil }
+
+ let(:args) do
+ {
+ project_path: project_1.full_path,
+ sort: sort,
+ search: search
+ }.compact
+ end
+
+ subject(:result) { resolve(described_class, ctx: ctx, args: args) }
+
describe '#resolve' do
context 'with an authorized user' do
before_all do
namespace.add_owner(user)
end
- it 'returns all CI Catalog resources visible to the current user in the namespace' do
- result = resolve(described_class, ctx: { current_user: user }, args: { project_path: project_1.full_path })
-
+ it 'returns all catalog resources visible to the current user in the namespace' do
expect(result.items.count).to be(3)
- expect(result.items.pluck(:name)).to contain_exactly('Z', 'A', 'L')
+ expect(result.items.pluck(:name)).to contain_exactly('Z', 'A_Test', 'L')
+ end
+
+ context 'when the sort parameter is not provided' do
+ it 'returns all catalog resources sorted by descending created date' do
+ expect(result.items.pluck(:name)).to eq(%w[L A_Test Z])
+ end
end
- it 'returns all resources sorted by descending created date when given no sort param' do
- result = resolve(described_class, ctx: { current_user: user }, args: { project_path: project_1.full_path })
+ context 'when the sort parameter is provided' do
+ let(:sort) { 'NAME_DESC' }
- expect(result.items.pluck(:name)).to eq(%w[L A Z])
+ it 'returns all catalog resources sorted by descending name' do
+ expect(result.items.pluck(:name)).to eq(%w[Z L A_Test])
+ end
end
- it 'returns all CI Catalog resources sorted by descending name when there is a sort parameter' do
- result = resolve(described_class, ctx: { current_user: user }, args: { project_path: project_1.full_path, sort:
- 'NAME_DESC' })
+ context 'when the search parameter is provided' do
+ let(:search) { 'test' }
- expect(result.items.pluck(:name)).to eq(%w[Z L A])
+ it 'returns the catalog resources that match the search term' do
+ expect(result.items.pluck(:name)).to contain_exactly('A_Test', 'L')
+ end
end
end
context 'when the current user cannot read the namespace catalog' do
- it 'raises ResourceNotAvailable' do
- result = resolve(described_class, ctx: { current_user: user }, args: { project_path: project_1.full_path })
-
+ it 'returns empty response' do
expect(result).to be_empty
end
end
diff --git a/spec/graphql/types/ci/catalog/resource_sort_enum_spec.rb b/spec/graphql/types/ci/catalog/resource_sort_enum_spec.rb
index e64b6745751..1de324b6652 100644
--- a/spec/graphql/types/ci/catalog/resource_sort_enum_spec.rb
+++ b/spec/graphql/types/ci/catalog/resource_sort_enum_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe GitlabSchema.types['CiCatalogResourceSort'], feature_category: :p
it 'exposes all the existing catalog resource sort orders' do
expect(described_class.values.keys).to include(
- *%w[NAME_ASC NAME_DESC LATEST_RELEASED_AT_ASC LATEST_RELEASED_AT_DESC]
+ *%w[NAME_ASC NAME_DESC LATEST_RELEASED_AT_ASC LATEST_RELEASED_AT_DESC CREATED_ASC CREATED_DESC]
)
end
end
diff --git a/spec/migrations/20231019223224_backfill_catalog_resources_name_and_description_spec.rb b/spec/migrations/20231019223224_backfill_catalog_resources_name_and_description_spec.rb
new file mode 100644
index 00000000000..2945b9fbf8e
--- /dev/null
+++ b/spec/migrations/20231019223224_backfill_catalog_resources_name_and_description_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe BackfillCatalogResourcesNameAndDescription, feature_category: :pipeline_composition do
+ let(:namespace) { table(:namespaces).create!(name: 'name', path: 'path') }
+
+ let(:project) do
+ table(:projects).create!(
+ name: 'My project name', description: 'My description',
+ namespace_id: namespace.id, project_namespace_id: namespace.id
+ )
+ end
+
+ let(:resource) { table(:catalog_resources).create!(project_id: project.id) }
+
+ describe '#up' do
+ it 'updates the name and description to match the project' do
+ expect(resource.name).to be_nil
+ expect(resource.description).to be_nil
+
+ migrate!
+
+ expect(resource.reload.name).to eq(project.name)
+ expect(resource.reload.description).to eq(project.description)
+ end
+ end
+end
diff --git a/spec/models/ci/catalog/listing_spec.rb b/spec/models/ci/catalog/listing_spec.rb
index c8e2fd17811..7a1e12165ac 100644
--- a/spec/models/ci/catalog/listing_spec.rb
+++ b/spec/models/ci/catalog/listing_spec.rb
@@ -87,15 +87,15 @@ RSpec.describe Ci::Catalog::Listing, feature_category: :pipeline_composition do
let_it_be(:tomorrow) { today + 1.day }
let_it_be(:resource_1) do
- create(:ci_catalog_resource, project: project_x, latest_released_at: yesterday)
+ create(:ci_catalog_resource, project: project_x, latest_released_at: yesterday, created_at: today)
end
let_it_be(:resource_2) do
- create(:ci_catalog_resource, project: project_b, latest_released_at: today)
+ create(:ci_catalog_resource, project: project_b, latest_released_at: today, created_at: yesterday)
end
let_it_be(:resource_3) do
- create(:ci_catalog_resource, project: project_a, latest_released_at: nil)
+ create(:ci_catalog_resource, project: project_a, latest_released_at: nil, created_at: tomorrow)
end
let_it_be(:other_namespace_resource) do
@@ -109,6 +109,22 @@ RSpec.describe Ci::Catalog::Listing, feature_category: :pipeline_composition do
context 'with a sort parameter' do
let(:params) { { namespace: namespace, sort: sort } }
+ context 'when the sort is created_at ascending' do
+ let_it_be(:sort) { :created_at_asc }
+
+ it 'contains catalog resources sorted by created_at ascending' do
+ is_expected.to eq([resource_2, resource_1, resource_3])
+ end
+ end
+
+ context 'when the sort is created_at descending' do
+ let_it_be(:sort) { :created_at_desc }
+
+ it 'contains catalog resources sorted by created_at descending' do
+ is_expected.to eq([resource_3, resource_1, resource_2])
+ end
+ end
+
context 'when the sort is name ascending' do
let_it_be(:sort) { :name_asc }
diff --git a/spec/models/ci/catalog/resource_spec.rb b/spec/models/ci/catalog/resource_spec.rb
index f9723385111..4e292fc0ec0 100644
--- a/spec/models/ci/catalog/resource_spec.rb
+++ b/spec/models/ci/catalog/resource_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do
let_it_be(:yesterday) { today - 1.day }
let_it_be(:tomorrow) { today + 1.day }
- let_it_be(:project) { create(:project, name: 'A') }
+ let_it_be_with_reload(:project) { create(:project, name: 'A') }
let_it_be(:project_2) { build(:project, name: 'Z') }
let_it_be(:project_3) { build(:project, name: 'L') }
let_it_be_with_reload(:resource) { create(:ci_catalog_resource, project: project, latest_released_at: tomorrow) }
@@ -23,8 +23,6 @@ RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do
it { is_expected.to have_many(:versions).class_name('Ci::Catalog::Resources::Version') }
it { is_expected.to delegate_method(:avatar_path).to(:project) }
- it { is_expected.to delegate_method(:description).to(:project) }
- it { is_expected.to delegate_method(:name).to(:project) }
it { is_expected.to delegate_method(:star_count).to(:project) }
it { is_expected.to delegate_method(:forks_count).to(:project) }
@@ -46,6 +44,14 @@ RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do
end
end
+ describe '.order_by_created_at_asc' do
+ it 'returns catalog resources sorted by ascending created at' do
+ ordered_resources = described_class.order_by_created_at_asc
+
+ expect(ordered_resources.to_a).to eq([resource, resource_2, resource_3])
+ end
+ end
+
describe '.order_by_name_desc' do
it 'returns catalog resources sorted by descending name' do
ordered_resources = described_class.order_by_name_desc
@@ -118,4 +124,33 @@ RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do
end
end
end
+
+ describe 'sync with project' do
+ shared_examples 'name and description of the catalog resource matches the project' do
+ it do
+ expect(resource.reload.name).to eq(project.name)
+ expect(resource.reload.description).to eq(project.description)
+ end
+ end
+
+ context 'when the catalog resource is created' do
+ it_behaves_like 'name and description of the catalog resource matches the project'
+ end
+
+ context 'when the project name is updated' do
+ before do
+ project.update!(name: 'My new project name')
+ end
+
+ it_behaves_like 'name and description of the catalog resource matches the project'
+ end
+
+ context 'when the project description is updated' do
+ before do
+ project.update!(description: 'My new description')
+ end
+
+ it_behaves_like 'name and description of the catalog resource matches the project'
+ end
+ end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index c27ed2cc82c..5acf03e09d7 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -9141,6 +9141,56 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
end
end
+ describe '#update_catalog_resource' do
+ let_it_be_with_reload(:project) { create(:project, name: 'My project name', description: 'My description') }
+ let_it_be(:resource) { create(:ci_catalog_resource, project: project) }
+
+ shared_examples 'name and description of the catalog resource matches the project' do
+ it do
+ expect(project).to receive(:update_catalog_resource).once.and_call_original
+
+ project.save!
+
+ expect(resource.reload.name).to eq(project.name)
+ expect(resource.reload.description).to eq(project.description)
+ end
+ end
+
+ context 'when the project name is updated' do
+ before do
+ project.name = 'My new project name'
+ end
+
+ it_behaves_like 'name and description of the catalog resource matches the project'
+ end
+
+ context 'when the project description is updated' do
+ before do
+ project.description = 'My new description'
+ end
+
+ it_behaves_like 'name and description of the catalog resource matches the project'
+ end
+
+ context 'when neither the project name nor description are updated' do
+ it 'does not call update_catalog_resource' do
+ expect(project).not_to receive(:update_catalog_resource)
+
+ project.update!(path: 'path')
+ end
+ end
+
+ context 'when the project does not have a catalog resource' do
+ let_it_be(:project2) { create(:project) }
+
+ it 'does not call update_catalog_resource' do
+ expect(project2).not_to receive(:update_catalog_resource)
+
+ project.update!(name: 'name')
+ end
+ end
+ end
+
private
def finish_job(export_job)
diff --git a/spec/policies/work_item_policy_spec.rb b/spec/policies/work_item_policy_spec.rb
index 568c375ce56..5b2eb8ec2e8 100644
--- a/spec/policies/work_item_policy_spec.rb
+++ b/spec/policies/work_item_policy_spec.rb
@@ -309,4 +309,76 @@ RSpec.describe WorkItemPolicy, feature_category: :team_planning do
end
end
end
+
+ describe 'read_note' do
+ context 'when work item is associated with a project' do
+ context 'when project is public' do
+ let(:work_item_subject) { public_work_item }
+
+ context 'when user is not a member of the project' do
+ let(:current_user) { non_member_user }
+
+ it { is_expected.to be_allowed(:read_note) }
+ end
+
+ context 'when user is a member of the project' do
+ let(:current_user) { guest_author }
+
+ it { is_expected.to be_allowed(:read_note) }
+
+ context 'when work_item is confidential' do
+ let(:work_item_subject) { create(:work_item, :confidential, project: project) }
+
+ it { is_expected.not_to be_allowed(:read_note) }
+ end
+ end
+ end
+ end
+
+ context 'when work item is associated with a group' do
+ context 'when group is public' do
+ let_it_be(:public_group) { create(:group, :public) }
+ let_it_be(:public_group_work_item) { create(:work_item, :group_level, namespace: public_group) }
+ let_it_be(:public_group_member) { create(:user).tap { |u| public_group.add_reporter(u) } }
+ let(:work_item_subject) { public_group_work_item }
+
+ context 'when user is not a member of the group' do
+ let(:current_user) { non_member_user }
+
+ it { is_expected.not_to be_allowed(:read_note) }
+ end
+
+ context 'when user is a member of the group' do
+ let(:current_user) { public_group_member }
+
+ it { is_expected.to be_allowed(:read_note) }
+ end
+ end
+
+ context 'when group is not public' do
+ let_it_be(:private_group) { create(:group, :private) }
+ let_it_be(:private_group_work_item) { create(:work_item, :group_level, namespace: private_group) }
+ let_it_be(:private_group_reporter) { create(:user).tap { |u| private_group.add_reporter(u) } }
+ let(:work_item_subject) { private_group_work_item }
+
+ context 'when user is not a member of the group' do
+ let(:current_user) { non_member_user }
+
+ it { is_expected.not_to be_allowed(:read_note) }
+ end
+
+ context 'when user is a member of the group' do
+ let(:current_user) { private_group_reporter }
+
+ it { is_expected.to be_allowed(:read_note) }
+
+ context 'when work_item is confidential' do
+ let(:work_item_subject) { create(:work_item, :group_level, :confidential, namespace: private_group) }
+
+ it { is_expected.to be_allowed(:read_note) }
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb
index b8575b25e0a..1571066fac2 100644
--- a/spec/requests/api/graphql/work_item_spec.rb
+++ b/spec/requests/api/graphql/work_item_spec.rb
@@ -664,20 +664,6 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do
end
describe 'notes widget' do
- let(:work_item_fields) do
- <<~GRAPHQL
- id
- widgets {
- type
- ... on WorkItemWidgetNotes {
- system: discussions(filter: ONLY_ACTIVITY, first: 10) { nodes { id notes { nodes { id system internal body } } } },
- comments: discussions(filter: ONLY_COMMENTS, first: 10) { nodes { id notes { nodes { id system internal body } } } },
- all_notes: discussions(filter: ALL_NOTES, first: 10) { nodes { id notes { nodes { id system internal body } } } }
- }
- }
- GRAPHQL
- end
-
context 'when fetching award emoji from notes' do
let(:work_item_fields) do
<<~GRAPHQL
@@ -768,6 +754,26 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do
expect { post_graphql(query, current_user: developer) }.not_to exceed_query_limit(control).with_threshold(4)
expect_graphql_errors_to_be_empty
end
+
+ context 'when work item is associated with a group' do
+ let_it_be(:group_work_item) { create(:work_item, :group_level, namespace: group) }
+ let_it_be(:group_work_item_note) { create(:note, noteable: group_work_item, author: developer, project: nil) }
+ let(:global_id) { group_work_item.to_gid.to_s }
+
+ before_all do
+ create(:award_emoji, awardable: group_work_item_note, name: 'rocket', user: developer)
+ end
+
+ it 'returns notes for the group work item' do
+ all_widgets = graphql_dig_at(work_item_data, :widgets)
+ notes_widget = all_widgets.find { |x| x['type'] == 'NOTES' }
+ notes = graphql_dig_at(notes_widget['discussions'], :nodes).flat_map { |d| d['notes']['nodes'] }
+
+ expect(notes).to contain_exactly(
+ hash_including('body' => group_work_item_note.note)
+ )
+ end
+ end
end
end
diff --git a/spec/tooling/danger/analytics_instrumentation_spec.rb b/spec/tooling/danger/analytics_instrumentation_spec.rb
index 5dfde44081d..79c75b2e89c 100644
--- a/spec/tooling/danger/analytics_instrumentation_spec.rb
+++ b/spec/tooling/danger/analytics_instrumentation_spec.rb
@@ -231,4 +231,64 @@ RSpec.describe Tooling::Danger::AnalyticsInstrumentation, feature_category: :ser
end
end
end
+
+ describe '#check_deprecated_data_sources!' do
+ let(:fake_project_helper) { instance_double(Tooling::Danger::ProjectHelper) }
+
+ subject(:check_data_source) { analytics_instrumentation.check_deprecated_data_sources! }
+
+ before do
+ allow(fake_helper).to receive(:added_files).and_return([added_file])
+ allow(fake_helper).to receive(:changed_lines).with(added_file).and_return(changed_lines)
+ allow(analytics_instrumentation).to receive(:project_helper).and_return(fake_project_helper)
+ allow(analytics_instrumentation.project_helper).to receive(:file_lines).and_return(changed_lines.map { |line| line.delete_prefix('+') })
+ end
+
+ context 'when no metric definitions were modified' do
+ let(:added_file) { 'app/models/user.rb' }
+ let(:changed_lines) { ['+ data_source: redis,'] }
+
+ it 'does not trigger warning' do
+ expect(analytics_instrumentation).not_to receive(:markdown)
+
+ check_data_source
+ end
+ end
+
+ context 'when metrics fields were modified' do
+ let(:added_file) { 'config/metrics/count7_d/example_metric.yml' }
+
+ [:redis, :redis_hll].each do |source|
+ context "when source is #{source}" do
+ let(:changed_lines) { ["+ data_source: #{source}"] }
+ let(:template) do
+ <<~SUGGEST_COMMENT
+ ```suggestion
+ data_source: internal_events
+ ```
+
+ %<message>s
+ SUGGEST_COMMENT
+ end
+
+ it 'issues a warning' do
+ expected_comment = format(template, message: Tooling::Danger::AnalyticsInstrumentation::CHANGE_DEPRECATED_DATA_SOURCE_MESSAGE)
+ expect(analytics_instrumentation).to receive(:markdown).with(expected_comment.strip, file: added_file, line: 1)
+
+ check_data_source
+ end
+ end
+ end
+
+ context 'when neither redis nor redis_hll used as a data_source' do
+ let(:changed_lines) { ['+ data_source: database,'] }
+
+ it 'does not issue a warning' do
+ expect(analytics_instrumentation).not_to receive(:markdown)
+
+ check_data_source
+ end
+ end
+ end
+ end
end
diff --git a/tooling/danger/analytics_instrumentation.rb b/tooling/danger/analytics_instrumentation.rb
index 2f8066f4421..d49c0f9e6ba 100644
--- a/tooling/danger/analytics_instrumentation.rb
+++ b/tooling/danger/analytics_instrumentation.rb
@@ -1,8 +1,12 @@
# frozen_string_literal: true
+require_relative 'suggestor'
+
module Tooling
module Danger
module AnalyticsInstrumentation
+ include ::Tooling::Danger::Suggestor
+
METRIC_DIRS = %w[lib/gitlab/usage/metrics/instrumentations ee/lib/gitlab/usage/metrics/instrumentations].freeze
APPROVED_LABEL = 'analytics instrumentation::approved'
REVIEW_LABEL = 'analytics instrumentation::review pending'
@@ -26,6 +30,10 @@ module Tooling
Please use [Instrumentation Classes](https://docs.gitlab.com/ee/development/service_ping/metrics_instrumentation.html) instead.
MSG
+ CHANGE_DEPRECATED_DATA_SOURCE_MESSAGE = <<~MSG
+ Redis and RedisHLL tracking is deprecated, consider using Internal Events tracking instead https://docs.gitlab.com/ee/development/internal_analytics/internal_event_instrumentation/quick_start.html#defining-event-and-metrics
+ MSG
+
WORKFLOW_LABELS = [
APPROVED_LABEL,
REVIEW_LABEL
@@ -58,6 +66,17 @@ module Tooling
warn format(CHANGED_USAGE_DATA_MESSAGE)
end
+ def check_deprecated_data_sources!
+ new_metric_files.each do |filename|
+ add_suggestion(
+ filename: filename,
+ regex: /^\+?\s+data_source: redis\w*/,
+ replacement: 'data_source: internal_events',
+ comment_text: CHANGE_DEPRECATED_DATA_SOURCE_MESSAGE
+ )
+ end
+ end
+
private
def convert_to_table(items)
@@ -99,6 +118,10 @@ module Tooling
end
end
+ def new_metric_files
+ helper.added_files.select { |f| f.include?('config/metrics') && f.end_with?('.yml') }
+ end
+
def each_metric(&block)
METRIC_DIRS.each do |dir|
Dir.glob(File.join(dir, '*.rb')).each(&block)
diff --git a/yarn.lock b/yarn.lock
index a3e0a2d8251..31156439fea 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -13528,10 +13528,10 @@ vscode-uri@^3.0.0:
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.3.tgz#a95c1ce2e6f41b7549f86279d19f47951e4f4d84"
integrity sha512-EcswR2S8bpR7fD0YPeS7r2xXExrScVMxg4MedACaWHEtx9ftCF/qHG1xGkolzTPcEmjTavCQgbVzHUIdTMzFGA==
-vue-apollo@^3.1.1:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/vue-apollo/-/vue-apollo-3.1.1.tgz#304c436d8e39e43df86d898f637f6581437665cc"
- integrity sha512-rvRH6MIjkZffJi4Mfzek3jh4pAXgOTP3EaaUkAkA10yUxvFjw+NfAnzL14xkV3r0mczuLe1vetxz47pByZ137g==
+vue-apollo@^3.0.7:
+ version "3.0.7"
+ resolved "https://registry.yarnpkg.com/vue-apollo/-/vue-apollo-3.0.7.tgz#97a031d45641faa4888a6d5a7f71c40834359704"
+ integrity sha512-EUfIn4cJmoflnDJiSNP8gH4fofIEzd0I2AWnd9nhHB8mddmzIfgSNjIRihDcRB10wypYG1OG0GcU335CFgZRfA==
dependencies:
chalk "^2.4.2"
serialize-javascript "^4.0.0"