diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-10-25 21:15:16 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-10-25 21:15:16 +0300 |
commit | 62866a623e24242c6f7a1a93dc2aca1467d6a6ae (patch) | |
tree | 322ce00c652d376c949f1d45c39764eeb5bce620 | |
parent | f8888a274f6b095075fd8648d3732ad577a3a742 (diff) |
Add latest changes from gitlab-org/gitlab@master
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 @@ -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" |