Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-10-25 18:15:16 +00:00
parent f8888a274f
commit 62866a623e
79 changed files with 802 additions and 307 deletions

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -1 +1 @@
2838777108e7c081ddf7ef0932fe93087c560238
1db9c6e65ecc942b4ba907003018829ccacabf4b

View File

@ -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

View File

@ -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"},

View File

@ -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)

View File

@ -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' },

View File

@ -30,4 +30,5 @@ Default.args = {
serializerConfig: {},
extensions: [],
enableAutocomplete: false,
markdownDocsPath: 'fake/path',
};

View File

@ -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,
},
],

View File

@ -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 };

View File

@ -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,

View File

@ -1,3 +0,0 @@
import MlModelsIndex from './components/ml_models_index.vue';
export default MlModelsIndex;

View File

@ -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 });
};

View File

@ -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 });
};

View File

@ -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',

View File

@ -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>

View File

@ -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>

View File

@ -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',

View File

@ -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);

View File

@ -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)"

View File

@ -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`;
};

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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?

View File

@ -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

View File

@ -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"

View File

@ -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'] ||= {}

View File

@ -7,3 +7,5 @@ analytics_instrumentation.check!
analytics_instrumentation.check_affected_scopes!
analytics_instrumentation.check_usage_data_insertions!
analytics_instrumentation.check_deprecated_data_sources!

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
cc9ddab54a3e120e53e214c2d5cb689fda02810031c30da26d0fdc09921c1082

View File

@ -0,0 +1 @@
999c4fefec34812883cb458fe70b89247e3808e53441739ccfec5862b687977a

View File

@ -0,0 +1 @@
9098a39552648a1a2b6439bc26b3e987fc604c0b3bd149d08049b376a09f5ebb

View File

@ -0,0 +1 @@
d2bd2f99340f7653cec908c4c41c0326d7bf4765fd4e4287ae914ed3025cd690

View File

@ -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);

View File

@ -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

View File

@ -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`

View File

@ -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

View File

@ -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.

View File

@ -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).

View File

@ -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
```

View File

@ -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).

View File

@ -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.

View File

@ -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 ""

View File

@ -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",

View File

@ -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;
- }
}
}

View File

@ -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

View File

@ -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
@ -167,6 +173,15 @@ class SchemaRegenerator
run %q(bin/rails db:migrate RAILS_ENV=test)
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+.
#
@ -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

View File

@ -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

View File

@ -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);

View File

@ -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]) => {

View File

@ -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;

View File

@ -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,
});

View File

@ -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
]);
});
});
});

View File

@ -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 () => {
describe('on oauth error message', () => {
beforeEach(async () => {
dispatchMessageEvent('error');
await nextTick();
});
it('set the loader contentState to ERROR', () => {
expect(findLoader().props('contentState')).toBe(CONTENT_STATE.ERROR);
});
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', async () => {
dispatchMessageEvent('success');
dispatchMessageEvent('error');
await nextTick();
expect(mockSkeletonOnError).toHaveBeenCalledTimes(1);
expect(findSlotComponent().exists()).toBe(false);
expect(findIframe().exists()).toBe(false);
expect(buildClient).not.toHaveBeenCalled();
});
it('handles oauth message only once', () => {
dispatchMessageEvent('success');
dispatchMessageEvent('success');
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);
});
});

View File

@ -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>`;

View File

@ -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
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 })
expect(result.items.pluck(:name)).to eq(%w[L A Z])
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 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 sort parameter is provided' do
let(:sort) { 'NAME_DESC' }
expect(result.items.pluck(:name)).to eq(%w[Z L A])
it 'returns all catalog resources sorted by descending name' do
expect(result.items.pluck(:name)).to eq(%w[Z L A_Test])
end
end
context 'when the search parameter is provided' do
let(:search) { 'test' }
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

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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"