Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
f8888a274f
commit
62866a623e
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
2838777108e7c081ddf7ef0932fe93087c560238
|
||||
1db9c6e65ecc942b4ba907003018829ccacabf4b
|
||||
|
|
|
|||
2
Gemfile
2
Gemfile
|
|
@ -216,7 +216,7 @@ gem 'asciidoctor', '~> 2.0.18' # rubocop:todo Gemfile/MissingFeatureCategory
|
|||
gem 'asciidoctor-include-ext', '~> 0.4.0', require: false # rubocop:todo Gemfile/MissingFeatureCategory
|
||||
gem 'asciidoctor-plantuml', '~> 0.0.16' # rubocop:todo Gemfile/MissingFeatureCategory
|
||||
gem 'asciidoctor-kroki', '~> 0.8.0', require: false # rubocop:todo Gemfile/MissingFeatureCategory
|
||||
gem 'rouge', '~> 4.1.3' # rubocop:todo Gemfile/MissingFeatureCategory
|
||||
gem 'rouge', '~> 4.2.0' # rubocop:todo Gemfile/MissingFeatureCategory
|
||||
gem 'truncato', '~> 0.7.12' # rubocop:todo Gemfile/MissingFeatureCategory
|
||||
gem 'nokogiri', '~> 1.15', '>= 1.15.4' # rubocop:todo Gemfile/MissingFeatureCategory
|
||||
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
|
|
|
|||
|
|
@ -30,4 +30,5 @@ Default.args = {
|
|||
serializerConfig: {},
|
||||
extensions: [],
|
||||
enableAutocomplete: false,
|
||||
markdownDocsPath: 'fake/path',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
import MlModelsIndex from './components/ml_models_index.vue';
|
||||
|
||||
export default MlModelsIndex;
|
||||
|
|
@ -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 });
|
||||
};
|
||||
|
|
@ -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 });
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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'] ||= {}
|
||||
|
|
|
|||
|
|
@ -7,3 +7,5 @@ analytics_instrumentation.check!
|
|||
analytics_instrumentation.check_affected_scopes!
|
||||
|
||||
analytics_instrumentation.check_usage_data_insertions!
|
||||
|
||||
analytics_instrumentation.check_deprecated_data_sources!
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
cc9ddab54a3e120e53e214c2d5cb689fda02810031c30da26d0fdc09921c1082
|
||||
|
|
@ -0,0 +1 @@
|
|||
999c4fefec34812883cb458fe70b89247e3808e53441739ccfec5862b687977a
|
||||
|
|
@ -0,0 +1 @@
|
|||
9098a39552648a1a2b6439bc26b3e987fc604c0b3bd149d08049b376a09f5ebb
|
||||
|
|
@ -0,0 +1 @@
|
|||
d2bd2f99340f7653cec908c4c41c0326d7bf4765fd4e4287ae914ed3025cd690
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
- }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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]) => {
|
||||
|
|
@ -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;
|
||||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>`;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 }
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue