Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-06-01 15:10:15 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-06-01 15:10:15 +0300
commitcf19a51fc5711144b26f7123c14f9b64a7597195 (patch)
tree09c151fd3655213e87b1c25beb842a99510122cb
parent3b1df712c7a15c9b6abadd61e9c8894fdeb0442a (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/qa-common/main.gitlab-ci.yml2
-rw-r--r--.rubocop_todo/gitlab/strong_memoize_attr.yml2
-rw-r--r--app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue2
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_create_form.vue4
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_form_fields.vue210
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_update_form.vue137
-rw-r--r--app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue2
-rw-r--r--app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue2
-rw-r--r--app/assets/javascripts/contextual_sidebar.js22
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_agent_info.vue52
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_overview.vue23
-rw-r--r--app/assets/javascripts/environments/components/new_environment_item.vue41
-rw-r--r--app/assets/javascripts/environments/graphql/queries/environment_cluster_agent.query.graphql19
-rw-r--r--app/assets/javascripts/environments/graphql/queries/k8s_cluster_agent.query.graphql15
-rw-r--r--app/assets/javascripts/layout_nav.js32
-rw-r--r--app/assets/javascripts/pages/groups/group_members/index.js4
-rw-r--r--app/assets/javascripts/pages/groups/shared/group_details.js2
-rw-r--r--app/assets/javascripts/pages/projects/project_members/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/show/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/work_items/index.js2
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js4
-rw-r--r--app/assets/stylesheets/notify_enhanced.scss4
-rw-r--r--app/helpers/application_settings_helper.rb4
-rw-r--r--app/helpers/form_helper.rb2
-rw-r--r--app/models/analytics/cycle_analytics/value_stream.rb1
-rw-r--r--app/models/packages/nuget/metadatum.rb12
-rw-r--r--app/policies/project_policy.rb10
-rw-r--r--app/presenters/packages/nuget/presenter_helpers.rb10
-rw-r--r--app/presenters/packages/nuget/search_results_presenter.rb4
-rw-r--r--app/services/ci/runners/assign_runner_service.rb4
-rw-r--r--app/services/issues/base_service.rb4
-rw-r--r--app/services/issues/close_service.rb5
-rw-r--r--app/services/issues/reopen_service.rb6
-rw-r--r--app/services/packages/nuget/metadata_extraction_service.rb26
-rw-r--r--app/services/packages/nuget/sync_metadatum_service.rb47
-rw-r--r--app/services/packages/nuget/update_package_from_metadata_service.rb36
-rw-r--r--app/views/projects/mirrors/_branch_filter.html.haml4
-rw-r--r--app/views/projects/mirrors/_mirror_repos_push.html.haml4
-rw-r--r--app/views/projects/network/show.html.haml2
-rw-r--r--app/views/shared/_new_merge_request_checkbox.html.haml2
-rw-r--r--app/views/shared/issuable/form/_metadata.html.haml4
-rw-r--r--app/workers/new_issue_worker.rb10
-rw-r--r--config/feature_flags/development/expired_storage_check.yml8
-rw-r--r--config/feature_flags/development/gitlab_duo.yml8
-rw-r--r--db/migrate/20230505115558_add_authors_and_description_to_nuget_metadatum.rb22
-rw-r--r--db/schema_migrations/202305051155581
-rw-r--r--db/structure.sql4
-rw-r--r--doc/api/graphql/reference/index.md10
-rw-r--r--doc/api/packages/nuget.md14
-rw-r--r--doc/development/migration_style_guide.md2
-rw-r--r--doc/user/group/saml_sso/index.md2
-rw-r--r--lib/api/entities/nuget/metadatum.rb6
-rw-r--r--lib/api/entities/nuget/package_metadata_catalog_entry.rb3
-rw-r--r--lib/api/entities/nuget/search_result.rb2
-rw-r--r--lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb46
-rw-r--r--lib/gitlab/project_authorizations.rb4
-rw-r--r--locale/gitlab.pot18
-rw-r--r--package.json4
-rw-r--r--spec/components/previews/layouts/horizontal_section_component_preview.rb6
-rw-r--r--spec/components/previews/pajamas/banner_component_preview.rb4
-rw-r--r--spec/factories/packages/nuget/metadata.rb2
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/packages/nuget/package_metadata.json71
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/packages/nuget/packages_metadata.json99
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/packages/nuget/search.json85
-rw-r--r--spec/frontend/ci/runner/components/runner_create_form_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_form_fields_spec.js117
-rw-r--r--spec/frontend/ci/runner/components/runner_update_form_spec.js180
-rw-r--r--spec/frontend/environments/environment_folder_spec.js2
-rw-r--r--spec/frontend/environments/graphql/mock_data.js6
-rw-r--r--spec/frontend/environments/kubernetes_agent_info_spec.js71
-rw-r--r--spec/frontend/environments/kubernetes_overview_spec.js18
-rw-r--r--spec/frontend/environments/new_environment_item_spec.js90
-rw-r--r--spec/lib/api/entities/nuget/metadatum_spec.rb30
-rw-r--r--spec/lib/api/entities/nuget/package_metadata_catalog_entry_spec.rb10
-rw-r--r--spec/lib/api/entities/nuget/search_result_spec.rb8
-rw-r--r--spec/lib/gitlab/database/query_analyzers/prevent_cross_database_modification_spec.rb49
-rw-r--r--spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb8
-rw-r--r--spec/migrations/20230505115558_add_authors_and_description_to_nuget_metadatum_spec.rb24
-rw-r--r--spec/models/analytics/cycle_analytics/value_stream_spec.rb14
-rw-r--r--spec/models/packages/nuget/metadatum_spec.rb18
-rw-r--r--spec/policies/project_policy_spec.rb26
-rw-r--r--spec/presenters/packages/nuget/package_metadata_presenter_spec.rb5
-rw-r--r--spec/presenters/packages/nuget/search_results_presenter_spec.rb12
-rw-r--r--spec/services/ci/runners/assign_runner_service_spec.rb55
-rw-r--r--spec/services/packages/nuget/metadata_extraction_service_spec.rb33
-rw-r--r--spec/services/packages/nuget/sync_metadatum_service_spec.rb36
-rw-r--r--spec/services/packages/nuget/update_package_from_metadata_service_spec.rb36
-rw-r--r--spec/support/shared_examples/features/runners_shared_examples.rb2
-rw-r--r--yarn.lock18
89 files changed, 1186 insertions, 893 deletions
diff --git a/.gitlab/ci/qa-common/main.gitlab-ci.yml b/.gitlab/ci/qa-common/main.gitlab-ci.yml
index 9a9e918e83d..72f49123190 100644
--- a/.gitlab/ci/qa-common/main.gitlab-ci.yml
+++ b/.gitlab/ci/qa-common/main.gitlab-ci.yml
@@ -6,7 +6,7 @@ workflow:
include:
- project: gitlab-org/quality/pipeline-common
- ref: 6.3.0
+ ref: 7.1.0
file:
- /ci/base.gitlab-ci.yml
- /ci/allure-report.yml
diff --git a/.rubocop_todo/gitlab/strong_memoize_attr.yml b/.rubocop_todo/gitlab/strong_memoize_attr.yml
index 4e220f22800..fc48a6f92ec 100644
--- a/.rubocop_todo/gitlab/strong_memoize_attr.yml
+++ b/.rubocop_todo/gitlab/strong_memoize_attr.yml
@@ -235,8 +235,6 @@ Gitlab/StrongMemoizeAttr:
- 'app/services/packages/npm/create_tag_service.rb'
- 'app/services/packages/nuget/metadata_extraction_service.rb'
- 'app/services/packages/nuget/search_service.rb'
- - 'app/services/packages/nuget/sync_metadatum_service.rb'
- - 'app/services/packages/nuget/update_package_from_metadata_service.rb'
- 'app/services/packages/pypi/create_package_service.rb'
- 'app/services/packages/rpm/parse_package_service.rb'
- 'app/services/packages/rubygems/dependency_resolver_service.rb'
diff --git a/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue b/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue
index 4ec41381045..f0a41a5949e 100644
--- a/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue
+++ b/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue
@@ -60,7 +60,7 @@ export default {
<hr aria-hidden="true" />
- <h2 class="gl-font-weight-normal gl-font-lg gl-my-5">
+ <h2 class="gl-font-size-h2 gl-my-5">
{{ s__('Runners|Platform') }}
</h2>
<runner-platforms-radio-group v-model="platform" />
diff --git a/app/assets/javascripts/ci/runner/components/runner_create_form.vue b/app/assets/javascripts/ci/runner/components/runner_create_form.vue
index d6db15a1996..040e42fa938 100644
--- a/app/assets/javascripts/ci/runner/components/runner_create_form.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_create_form.vue
@@ -83,13 +83,13 @@ export default {
if (errors?.length) {
this.$emit('error', new Error(errors.join(' ')));
+ this.saving = false;
} else {
this.onSuccess(runner);
}
} catch (error) {
captureException({ error, component: this.$options.name });
this.$emit('error', error);
- } finally {
this.saving = false;
}
},
@@ -103,7 +103,7 @@ export default {
<gl-form @submit.prevent="onSubmit">
<runner-form-fields v-model="runner" />
- <div class="gl-display-flex">
+ <div class="gl-display-flex gl-mt-6">
<gl-button type="submit" variant="confirm" class="js-no-auto-disable" :loading="saving">
{{ s__('Runners|Create runner') }}
</gl-button>
diff --git a/app/assets/javascripts/ci/runner/components/runner_form_fields.vue b/app/assets/javascripts/ci/runner/components/runner_form_fields.vue
index 880d217e344..180c41e7ed6 100644
--- a/app/assets/javascripts/ci/runner/components/runner_form_fields.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_form_fields.vue
@@ -1,5 +1,14 @@
<script>
-import { GlFormGroup, GlFormCheckbox, GlFormInput, GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
+import { isEqual } from 'lodash';
+import {
+ GlFormGroup,
+ GlFormCheckbox,
+ GlFormInput,
+ GlIcon,
+ GlLink,
+ GlSprintf,
+ GlSkeletonLoader,
+} from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { ACCESS_LEVEL_NOT_PROTECTED, ACCESS_LEVEL_REF_PROTECTED, PROJECT_TYPE } from '../constants';
@@ -12,6 +21,7 @@ export default {
GlIcon,
GlLink,
GlSprintf,
+ GlSkeletonLoader,
RunnerMaintenanceNoteField: () =>
import('ee_component/ci/runner/components/runner_maintenance_note_field.vue'),
},
@@ -21,12 +31,15 @@ export default {
default: null,
required: false,
},
+ loading: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
},
data() {
return {
- model: {
- ...this.value,
- },
+ model: null,
};
},
computed: {
@@ -35,6 +48,15 @@ export default {
},
},
watch: {
+ value: {
+ handler(newVal, oldVal) {
+ // update only when values change, avoids infinite loop
+ if (!isEqual(newVal, oldVal)) {
+ this.model = { ...newVal };
+ }
+ },
+ immediate: true,
+ },
model: {
handler() {
this.$emit('input', this.model);
@@ -51,107 +73,115 @@ export default {
</script>
<template>
<div>
- <h2 class="gl-font-weight-normal gl-font-lg gl-my-5">
+ <h2 class="gl-font-size-h2 gl-my-5">
{{ s__('Runners|Details') }}
{{ __('(optional)') }}
</h2>
- <gl-form-group :label="s__('Runners|Runner description')" label-for="runner-description">
- <gl-form-input id="runner-description" v-model="model.description" name="description" />
- </gl-form-group>
- <runner-maintenance-note-field v-model="model.maintenanceNote" class="gl-mt-5" />
+ <gl-skeleton-loader v-if="loading" :lines="9" />
+ <template v-else-if="model">
+ <gl-form-group :label="s__('Runners|Runner description')" label-for="runner-description">
+ <gl-form-input id="runner-description" v-model="model.description" name="description" />
+ </gl-form-group>
+ <runner-maintenance-note-field v-model="model.maintenanceNote" class="gl-mt-5" />
+ </template>
<hr aria-hidden="true" />
- <h2 class="gl-font-weight-normal gl-font-lg gl-my-5">
+ <h2 class="gl-font-size-h2 gl-my-5">
{{ s__('Runners|Configuration') }}
{{ __('(optional)') }}
</h2>
- <div class="gl-mb-5">
- <gl-form-checkbox v-model="model.paused" name="paused">
- {{ __('Paused') }}
- <template #help>
- {{ s__('Runners|Stop the runner from accepting new jobs.') }}
- </template>
- </gl-form-checkbox>
-
- <gl-form-checkbox
- v-model="model.accessLevel"
- name="protected"
- :value="$options.ACCESS_LEVEL_REF_PROTECTED"
- :unchecked-value="$options.ACCESS_LEVEL_NOT_PROTECTED"
- >
- {{ __('Protected') }}
- <template #help>
- {{ s__('Runners|Use the runner on pipelines for protected branches only.') }}
- </template>
- </gl-form-checkbox>
-
- <gl-form-checkbox v-model="model.runUntagged" name="run-untagged">
- {{ __('Run untagged jobs') }}
- <template #help>
- {{ s__('Runners|Use the runner for jobs without tags in addition to tagged jobs.') }}
- </template>
- </gl-form-checkbox>
-
- <gl-form-checkbox v-if="canBeLockedToProject" v-model="model.locked" name="locked">
- {{ __('Lock to current projects') }} <gl-icon name="lock" />
- <template #help>
- {{
- s__(
- 'Runners|Use the runner for the currently assigned projects only. Only administrators can change the assigned projects.',
- )
- }}
- </template>
- </gl-form-checkbox>
- </div>
+ <gl-skeleton-loader v-if="loading" :lines="27" />
+ <template v-else-if="model">
+ <div class="gl-mb-5">
+ <gl-form-checkbox v-model="model.paused" name="paused">
+ {{ __('Paused') }}
+ <template #help>
+ {{ s__('Runners|Stop the runner from accepting new jobs.') }}
+ </template>
+ </gl-form-checkbox>
- <gl-form-group :label="__('Tags')" label-for="runner-tags">
- <template #description>
- <gl-sprintf
- :message="
- s__('Runners|Multiple tags must be separated by a comma. For example, %{example}.')
- "
+ <gl-form-checkbox
+ v-model="model.accessLevel"
+ name="protected"
+ :value="$options.ACCESS_LEVEL_REF_PROTECTED"
+ :unchecked-value="$options.ACCESS_LEVEL_NOT_PROTECTED"
>
- <template #example>
- <!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
- <code>macos, shared</code>
+ {{ __('Protected') }}
+ <template #help>
+ {{ s__('Runners|Use the runner on pipelines for protected branches only.') }}
</template>
- </gl-sprintf>
- </template>
- <template #label-description>
- <gl-sprintf
- :message="
- s__(
- 'Runners|Add tags for the types of jobs the runner processes to ensure that the runner only runs jobs that you intend it to. %{helpLinkStart}Learn more.%{helpLinkEnd}',
- )
- "
- >
- <template #helpLink="{ content }">
- <gl-link :href="$options.HELP_LABELS_PAGE_PATH" target="_blank">{{ content }}</gl-link>
+ </gl-form-checkbox>
+
+ <gl-form-checkbox v-model="model.runUntagged" name="run-untagged">
+ {{ __('Run untagged jobs') }}
+ <template #help>
+ {{ s__('Runners|Use the runner for jobs without tags in addition to tagged jobs.') }}
</template>
- </gl-sprintf>
- </template>
- <gl-form-input id="runner-tags" v-model="model.tagList" name="tags" />
- </gl-form-group>
+ </gl-form-checkbox>
- <gl-form-group
- :label="__('Maximum job timeout')"
- :label-description="
- s__(
- 'Runners|Maximum amount of time the runner can run before it terminates. If a project has a shorter job timeout period, the job timeout period of the instance runner is used instead.',
- )
- "
- label-for="runner-max-timeout"
- :description="s__('Runners|Enter the number of seconds.')"
- >
- <gl-form-input
- id="runner-max-timeout"
- v-model.number="model.maximumTimeout"
- name="max-timeout"
- type="number"
- />
- </gl-form-group>
+ <gl-form-checkbox v-if="canBeLockedToProject" v-model="model.locked" name="locked">
+ {{ __('Lock to current projects') }} <gl-icon name="lock" />
+ <template #help>
+ {{
+ s__(
+ 'Runners|Use the runner for the currently assigned projects only. Only administrators can change the assigned projects.',
+ )
+ }}
+ </template>
+ </gl-form-checkbox>
+ </div>
+
+ <gl-form-group :label="__('Tags')" label-for="runner-tags">
+ <template #description>
+ <gl-sprintf
+ :message="
+ s__('Runners|Multiple tags must be separated by a comma. For example, %{example}.')
+ "
+ >
+ <template #example>
+ <!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
+ <code>macos, shared</code>
+ </template>
+ </gl-sprintf>
+ </template>
+ <template #label-description>
+ <gl-sprintf
+ :message="
+ s__(
+ 'Runners|Add tags for the types of jobs the runner processes to ensure that the runner only runs jobs that you intend it to. %{helpLinkStart}Learn more.%{helpLinkEnd}',
+ )
+ "
+ >
+ <template #helpLink="{ content }">
+ <gl-link :href="$options.HELP_LABELS_PAGE_PATH" target="_blank">{{
+ content
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </template>
+ <gl-form-input id="runner-tags" v-model="model.tagList" name="tags" />
+ </gl-form-group>
+
+ <gl-form-group
+ :label="__('Maximum job timeout')"
+ :label-description="
+ s__(
+ 'Runners|Maximum amount of time the runner can run before it terminates. If a project has a shorter job timeout period, the job timeout period of the instance runner is used instead.',
+ )
+ "
+ label-for="runner-max-timeout"
+ :description="s__('Runners|Enter the number of seconds.')"
+ >
+ <gl-form-input
+ id="runner-max-timeout"
+ v-model.number="model.maximumTimeout"
+ name="max-timeout"
+ type="number"
+ />
+ </gl-form-group>
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/ci/runner/components/runner_update_form.vue b/app/assets/javascripts/ci/runner/components/runner_update_form.vue
index aebddc70646..bc044b609a3 100644
--- a/app/assets/javascripts/ci/runner/components/runner_update_form.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_update_form.vue
@@ -1,23 +1,16 @@
<script>
-import {
- GlButton,
- GlIcon,
- GlForm,
- GlFormCheckbox,
- GlFormGroup,
- GlFormInputGroup,
- GlSkeletonLoader,
- GlTooltipDirective,
-} from '@gitlab/ui';
-import {
- modelToUpdateMutationVariables,
- runnerToModel,
-} from 'ee_else_ce/ci/runner/runner_update_form_utils';
+import { GlButton, GlForm } from '@gitlab/ui';
+import RunnerFormFields from '~/ci/runner/components/runner_form_fields.vue';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
import { visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import { captureException } from '~/ci/runner/sentry_utils';
-import { ACCESS_LEVEL_NOT_PROTECTED, ACCESS_LEVEL_REF_PROTECTED, PROJECT_TYPE } from '../constants';
+
+import {
+ modelToUpdateMutationVariables,
+ runnerToModel,
+} from 'ee_else_ce/ci/runner/runner_update_form_utils';
+import { ACCESS_LEVEL_NOT_PROTECTED, ACCESS_LEVEL_REF_PROTECTED } from '../constants';
import runnerUpdateMutation from '../graphql/edit/runner_update.mutation.graphql';
import { saveAlertToLocalStorage } from '../local_storage_alert/save_alert_to_local_storage';
@@ -25,20 +18,11 @@ export default {
name: 'RunnerUpdateForm',
components: {
GlButton,
- GlIcon,
GlForm,
- GlFormCheckbox,
- GlFormGroup,
- GlFormInputGroup,
- GlSkeletonLoader,
- RunnerMaintenanceNoteField: () =>
- import('ee_component/ci/runner/components/runner_maintenance_note_field.vue'),
+ RunnerFormFields,
RunnerUpdateCostFactorFields: () =>
import('ee_component/ci/runner/components/runner_update_cost_factor_fields.vue'),
},
- directives: {
- GlTooltip: GlTooltipDirective,
- },
props: {
runner: {
type: Object,
@@ -59,19 +43,12 @@ export default {
data() {
return {
saving: false,
- model: runnerToModel(this.runner),
+ model: null,
};
},
- computed: {
- canBeLockedToProject() {
- return this.runner?.runnerType === PROJECT_TYPE;
- },
- },
watch: {
- runner(newVal, oldVal) {
- if (oldVal === null) {
- this.model = runnerToModel(newVal);
- }
+ runner(val) {
+ this.model = runnerToModel(val);
},
},
methods: {
@@ -114,94 +91,8 @@ export default {
</script>
<template>
<gl-form @submit.prevent="onSubmit">
- <h4 class="gl-font-lg gl-my-5">{{ s__('Runners|Details') }}</h4>
-
- <gl-skeleton-loader v-if="loading" />
-
- <template v-else>
- <gl-form-group :label="__('Description')" data-testid="runner-field-description">
- <gl-form-input-group v-model="model.description" />
- </gl-form-group>
- <runner-maintenance-note-field v-model="model.maintenanceNote" />
- </template>
-
- <hr />
-
- <h4 class="gl-font-lg gl-my-5">{{ s__('Runners|Configuration') }}</h4>
-
- <template v-if="loading">
- <gl-skeleton-loader v-for="i in 3" :key="i" />
- </template>
- <template v-else>
- <div class="gl-mb-5">
- <gl-form-checkbox v-model="model.paused" data-testid="runner-field-paused">
- {{ __('Paused') }}
- <template #help>
- {{ s__('Runners|Stop the runner from accepting new jobs.') }}
- </template>
- </gl-form-checkbox>
-
- <gl-form-checkbox
- v-model="model.accessLevel"
- data-testid="runner-field-protected"
- :value="$options.ACCESS_LEVEL_REF_PROTECTED"
- :unchecked-value="$options.ACCESS_LEVEL_NOT_PROTECTED"
- >
- {{ __('Protected') }}
- <template #help>
- {{ s__('Runners|Use the runner on pipelines for protected branches only.') }}
- </template>
- </gl-form-checkbox>
-
- <gl-form-checkbox v-model="model.runUntagged" data-testid="runner-field-run-untagged">
- {{ __('Run untagged jobs') }}
- <template #help>
- {{ s__('Runners|Use the runner for jobs without tags, in addition to tagged jobs.') }}
- </template>
- </gl-form-checkbox>
-
- <gl-form-checkbox
- v-if="canBeLockedToProject"
- v-model="model.locked"
- data-testid="runner-field-locked"
- >
- {{ __('Lock to current projects') }} <gl-icon name="lock" />
- <template #help>
- {{
- s__(
- 'Runners|Use the runner for the currently assigned projects only. Only administrators can change the assigned projects.',
- )
- }}
- </template>
- </gl-form-checkbox>
- </div>
-
- <gl-form-group
- data-testid="runner-field-max-timeout"
- :label="__('Maximum job timeout')"
- :description="
- s__(
- 'Runners|Enter the number of seconds. This timeout takes precedence over lower timeouts set for the project.',
- )
- "
- >
- <gl-form-input-group v-model.number="model.maximumTimeout" type="number" />
- </gl-form-group>
-
- <gl-form-group
- data-testid="runner-field-tags"
- :label="__('Tags')"
- :description="
- __(
- 'You can set up jobs to only use runners with specific tags. Separate tags with commas.',
- )
- "
- >
- <gl-form-input-group v-model="model.tagList" />
- </gl-form-group>
-
- <runner-update-cost-factor-fields v-model="model" />
- </template>
+ <runner-form-fields v-model="model" :loading="loading" />
+ <runner-update-cost-factor-fields v-model="model" />
<div class="gl-mt-6">
<gl-button
diff --git a/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue b/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue
index 5965330c4eb..2e1706ddae9 100644
--- a/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue
+++ b/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue
@@ -66,7 +66,7 @@ export default {
<hr aria-hidden="true" />
- <h2 class="gl-font-weight-normal gl-font-lg gl-my-5">
+ <h2 class="gl-font-size-h2 gl-my-5">
{{ s__('Runners|Platform') }}
</h2>
<runner-platforms-radio-group v-model="platform" />
diff --git a/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue b/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue
index 715b0c28148..51f5a9ce8d9 100644
--- a/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue
+++ b/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue
@@ -66,7 +66,7 @@ export default {
<hr aria-hidden="true" />
- <h2 class="gl-font-weight-normal gl-font-lg gl-my-5">
+ <h2 class="gl-font-size-h2 gl-my-5">
{{ s__('Runners|Platform') }}
</h2>
<runner-platforms-radio-group v-model="platform" />
diff --git a/app/assets/javascripts/contextual_sidebar.js b/app/assets/javascripts/contextual_sidebar.js
index ea444b5c146..ab5f01227fb 100644
--- a/app/assets/javascripts/contextual_sidebar.js
+++ b/app/assets/javascripts/contextual_sidebar.js
@@ -108,27 +108,5 @@ export default class ContextualSidebar {
const collapse = parseBoolean(getCookie('sidebar_collapsed'));
this.toggleCollapsedSidebar(collapse, true);
}
-
- const modalEl = document.querySelector('.js-invite-members-modal');
- if (modalEl) {
- import(
- /* webpackChunkName: 'initInviteMembersModal' */ '~/invite_members/init_invite_members_modal'
- )
- .then(({ default: initInviteMembersModal }) => {
- initInviteMembersModal();
- })
- .catch(() => {});
-
- const inviteTriggers = document.querySelectorAll('.js-invite-members-trigger');
- if (inviteTriggers) {
- import(
- /* webpackChunkName: 'initInviteMembersTrigger' */ '~/invite_members/init_invite_members_trigger'
- )
- .then(({ default: initInviteMembersTrigger }) => {
- initInviteMembersTrigger();
- })
- .catch(() => {});
- }
- }
}
}
diff --git a/app/assets/javascripts/environments/components/kubernetes_agent_info.vue b/app/assets/javascripts/environments/components/kubernetes_agent_info.vue
index 7660912f93a..03bde8d64ac 100644
--- a/app/assets/javascripts/environments/components/kubernetes_agent_info.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_agent_info.vue
@@ -1,68 +1,37 @@
<script>
-import { GlIcon, GlLink, GlSprintf, GlLoadingIcon, GlAlert } from '@gitlab/ui';
+import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { getAgentLastContact, getAgentStatus } from '~/clusters_list/clusters_util';
import { AGENT_STATUSES } from '~/clusters_list/constants';
import { s__ } from '~/locale';
-import getK8sClusterAgentQuery from '../graphql/queries/k8s_cluster_agent.query.graphql';
export default {
components: {
GlIcon,
GlLink,
GlSprintf,
- GlLoadingIcon,
TimeAgoTooltip,
- GlAlert,
},
props: {
- agentName: {
- required: true,
- type: String,
- },
- agentId: {
- required: true,
- type: String,
- },
- agentProjectPath: {
- required: true,
- type: String,
- },
- },
- apollo: {
clusterAgent: {
- query: getK8sClusterAgentQuery,
- variables() {
- return {
- agentName: this.agentName,
- projectPath: this.agentProjectPath,
- };
- },
- update: (data) => data?.project?.clusterAgent,
- error() {
- this.clusterAgent = null;
- },
+ required: true,
+ type: Object,
},
},
- data() {
- return {
- clusterAgent: null,
- };
- },
computed: {
- isLoading() {
- return this.$apollo.queries.clusterAgent.loading;
- },
agentLastContact() {
return getAgentLastContact(this.clusterAgent.tokens.nodes);
},
agentStatus() {
return getAgentStatus(this.agentLastContact);
},
+ agentId() {
+ return getIdFromGraphQLId(this.clusterAgent.id);
+ },
},
methods: {},
i18n: {
- loadingError: s__('ClusterAgents|An error occurred while loading your agent'),
agentId: s__('ClusterAgents|Agent ID #%{agentId}'),
neverConnectedText: s__('ClusterAgents|Never'),
},
@@ -70,8 +39,7 @@ export default {
};
</script>
<template>
- <gl-loading-icon v-if="isLoading" inline />
- <div v-else-if="clusterAgent" class="gl-text-gray-900">
+ <div class="gl-text-gray-900">
<gl-icon name="kubernetes-agent" class="gl-text-gray-500" />
<gl-link :href="clusterAgent.webPath" class="gl-mr-3">
<gl-sprintf :message="$options.i18n.agentId"
@@ -92,8 +60,4 @@ export default {
<span v-else>{{ $options.i18n.neverConnectedText }}</span>
</span>
</div>
-
- <gl-alert v-else variant="danger" :dismissible="false">
- {{ $options.i18n.loadingError }}
- </gl-alert>
</template>
diff --git a/app/assets/javascripts/environments/components/kubernetes_overview.vue b/app/assets/javascripts/environments/components/kubernetes_overview.vue
index 1f15c4daa2f..a849adfc755 100644
--- a/app/assets/javascripts/environments/components/kubernetes_overview.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_overview.vue
@@ -2,7 +2,7 @@
import { GlCollapse, GlButton, GlAlert } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import csrf from '~/lib/utils/csrf';
-import { getIdFromGraphQLId, isGid } from '~/graphql_shared/utils';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import KubernetesAgentInfo from './kubernetes_agent_info.vue';
import KubernetesPods from './kubernetes_pods.vue';
import KubernetesTabs from './kubernetes_tabs.vue';
@@ -18,17 +18,9 @@ export default {
},
inject: ['kasTunnelUrl'],
props: {
- agentName: {
+ clusterAgent: {
required: true,
- type: String,
- },
- agentId: {
- required: true,
- type: String,
- },
- agentProjectPath: {
- required: true,
- type: String,
+ type: Object,
},
namespace: {
required: false,
@@ -50,8 +42,7 @@ export default {
return this.isVisible ? this.$options.i18n.collapse : this.$options.i18n.expand;
},
gitlabAgentId() {
- const id = isGid(this.agentId) ? getIdFromGraphQLId(this.agentId) : this.agentId;
- return id.toString();
+ return getIdFromGraphQLId(this.clusterAgent.id).toString();
},
k8sAccessConfiguration() {
return {
@@ -91,11 +82,7 @@ export default {
</p>
<gl-collapse :visible="isVisible" class="gl-md-pl-7 gl-md-pr-5 gl-mt-4">
<template v-if="isVisible">
- <kubernetes-agent-info
- :agent-name="agentName"
- :agent-id="agentId"
- :agent-project-path="agentProjectPath"
- class="gl-mb-5" />
+ <kubernetes-agent-info :cluster-agent="clusterAgent" class="gl-mb-5" />
<gl-alert v-if="error" variant="danger" :dismissible="false" class="gl-mb-5">
{{ error }}
diff --git a/app/assets/javascripts/environments/components/new_environment_item.vue b/app/assets/javascripts/environments/components/new_environment_item.vue
index 9ad31688329..72323c0e43e 100644
--- a/app/assets/javascripts/environments/components/new_environment_item.vue
+++ b/app/assets/javascripts/environments/components/new_environment_item.vue
@@ -13,6 +13,7 @@ import { truncate } from '~/lib/utils/text_utility';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import isLastDeployment from '../graphql/queries/is_last_deployment.query.graphql';
+import getEnvironmentClusterAgent from '../graphql/queries/environment_cluster_agent.query.graphql';
import ExternalUrl from './environment_external_url.vue';
import Actions from './environment_actions.vue';
import StopComponent from './environment_stop.vue';
@@ -51,7 +52,7 @@ export default {
GlTooltip,
},
mixins: [glFeatureFlagsMixin()],
- inject: ['helpPagePath'],
+ inject: ['helpPagePath', 'projectPath'],
props: {
environment: {
required: true,
@@ -81,7 +82,7 @@ export default {
tierTooltip: s__('Environment|Deployment tier'),
},
data() {
- return { visible: false };
+ return { visible: false, clusterAgent: null };
},
computed: {
icon() {
@@ -163,23 +164,33 @@ export default {
rolloutStatus() {
return this.environment?.rolloutStatus;
},
- agent() {
- return this.environment?.agent || {};
- },
isKubernetesOverviewAvailable() {
return this.glFeatures?.kasUserAccessProject;
},
- hasRequiredAgentData() {
- const { project, id, name } = this.agent || {};
- return project && id && name;
- },
showKubernetesOverview() {
- return this.isKubernetesOverviewAvailable && this.hasRequiredAgentData;
+ return Boolean(this.isKubernetesOverviewAvailable && this.clusterAgent);
},
},
methods: {
- toggleCollapse() {
+ toggleEnvironmentCollapse() {
this.visible = !this.visible;
+
+ if (this.visible) {
+ this.getClusterAgent();
+ }
+ },
+ getClusterAgent() {
+ if (!this.isKubernetesOverviewAvailable || this.clusterAgent) return;
+
+ this.$apollo.addSmartQuery('environmentClusterAgent', {
+ variables() {
+ return { environmentName: this.environment.name, projectFullPath: this.projectPath };
+ },
+ query: getEnvironmentClusterAgent,
+ update(data) {
+ this.clusterAgent = data?.project?.environment?.clusterAgent;
+ },
+ });
},
},
deploymentClasses: [
@@ -222,7 +233,7 @@ export default {
:aria-label="label"
size="small"
category="secondary"
- @click="toggleCollapse"
+ @click="toggleEnvironmentCollapse"
/>
<gl-link
v-gl-tooltip
@@ -359,10 +370,8 @@ export default {
</div>
<div v-if="showKubernetesOverview" :class="$options.kubernetesOverviewClasses">
<kubernetes-overview
- :agent-project-path="agent.project"
- :agent-name="agent.name"
- :agent-id="agent.id"
- :namespace="agent.kubernetesNamespace"
+ :cluster-agent="clusterAgent"
+ :namespace="environment.kubernetesNamespace"
/>
</div>
<div v-if="rolloutStatus" :class="$options.deployBoardClasses">
diff --git a/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent.query.graphql
new file mode 100644
index 00000000000..760f1fba897
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent.query.graphql
@@ -0,0 +1,19 @@
+query getEnvironmentClusterAgent($projectFullPath: ID!, $environmentName: String) {
+ project(fullPath: $projectFullPath) {
+ id
+ environment(name: $environmentName) {
+ id
+ clusterAgent {
+ id
+ name
+ webPath
+ tokens {
+ nodes {
+ id
+ lastUsedAt
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/environments/graphql/queries/k8s_cluster_agent.query.graphql b/app/assets/javascripts/environments/graphql/queries/k8s_cluster_agent.query.graphql
deleted file mode 100644
index bd45d2dba2f..00000000000
--- a/app/assets/javascripts/environments/graphql/queries/k8s_cluster_agent.query.graphql
+++ /dev/null
@@ -1,15 +0,0 @@
-query getK8sClusterAgentQuery($projectPath: ID!, $agentName: String!) {
- project(fullPath: $projectPath) {
- id
- clusterAgent(name: $agentName) {
- id
- webPath
- tokens {
- nodes {
- id
- lastUsedAt
- }
- }
- }
- }
-}
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
index 63a1ba89fff..42682d9b79f 100644
--- a/app/assets/javascripts/layout_nav.js
+++ b/app/assets/javascripts/layout_nav.js
@@ -64,9 +64,31 @@ export function initScrollingTabs() {
});
}
-function initDeferred() {
- initScrollingTabs();
+function initInviteMembers() {
+ const modalEl = document.querySelector('.js-invite-members-modal');
+ if (!modalEl) return;
+
+ import(
+ /* webpackChunkName: 'initInviteMembersModal' */ '~/invite_members/init_invite_members_modal'
+ )
+ .then(({ default: initInviteMembersModal }) => {
+ initInviteMembersModal();
+ })
+ .catch(() => {});
+ const inviteTriggers = document.querySelectorAll('.js-invite-members-trigger');
+ if (!inviteTriggers) return;
+
+ import(
+ /* webpackChunkName: 'initInviteMembersTrigger' */ '~/invite_members/init_invite_members_trigger'
+ )
+ .then(({ default: initInviteMembersTrigger }) => {
+ initInviteMembersTrigger();
+ })
+ .catch(() => {});
+}
+
+function initWhatsNewComponent() {
const appEl = document.getElementById('whats-new-app');
if (!appEl) return;
@@ -84,6 +106,12 @@ function initDeferred() {
});
}
+function initDeferred() {
+ initScrollingTabs();
+ initWhatsNewComponent();
+ initInviteMembers();
+}
+
export default function initLayoutNav() {
if (!gon.use_new_navigation) {
const contextualSidebar = new ContextualSidebar();
diff --git a/app/assets/javascripts/pages/groups/group_members/index.js b/app/assets/javascripts/pages/groups/group_members/index.js
index 2e71eced66f..df6ca8eab96 100644
--- a/app/assets/javascripts/pages/groups/group_members/index.js
+++ b/app/assets/javascripts/pages/groups/group_members/index.js
@@ -1,8 +1,6 @@
import { groupMemberRequestFormatter } from '~/groups/members/utils';
import initInviteGroupTrigger from '~/invite_members/init_invite_group_trigger';
import initInviteGroupsModal from '~/invite_members/init_invite_groups_modal';
-import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
-import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import { s__ } from '~/locale';
import { initMembersApp } from '~/members';
import { MEMBER_TYPES, EE_APP_OPTIONS } from 'ee_else_ce/members/constants';
@@ -60,7 +58,5 @@ const APP_OPTIONS = {
initMembersApp(document.querySelector('.js-group-members-list-app'), APP_OPTIONS);
-initInviteMembersModal();
initInviteGroupsModal();
-initInviteMembersTrigger();
initInviteGroupTrigger();
diff --git a/app/assets/javascripts/pages/groups/shared/group_details.js b/app/assets/javascripts/pages/groups/shared/group_details.js
index dba65c7e791..5d9eafe5672 100644
--- a/app/assets/javascripts/pages/groups/shared/group_details.js
+++ b/app/assets/javascripts/pages/groups/shared/group_details.js
@@ -1,6 +1,5 @@
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import initInviteMembersBanner from '~/groups/init_invite_members_banner';
-import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initNotificationsDropdown from '~/notifications';
import ProjectsList from '~/projects_list';
@@ -12,5 +11,4 @@ export default function initGroupDetails() {
new ProjectsList(); // eslint-disable-line no-new
initInviteMembersBanner();
- initInviteMembersModal();
}
diff --git a/app/assets/javascripts/pages/projects/project_members/index.js b/app/assets/javascripts/pages/projects/project_members/index.js
index 79a4ed0f9c3..1e9111a3cc6 100644
--- a/app/assets/javascripts/pages/projects/project_members/index.js
+++ b/app/assets/javascripts/pages/projects/project_members/index.js
@@ -1,9 +1,7 @@
import initImportProjectMembersTrigger from '~/invite_members/init_import_project_members_trigger';
import initImportProjectMembersModal from '~/invite_members/init_import_project_members_modal';
import initInviteGroupTrigger from '~/invite_members/init_invite_group_trigger';
-import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteGroupsModal from '~/invite_members/init_invite_groups_modal';
-import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import { s__ } from '~/locale';
import { initMembersApp } from '~/members';
import { MEMBER_TYPES } from '~/members/constants';
@@ -11,9 +9,7 @@ import { groupLinkRequestFormatter } from '~/members/utils';
import { projectMemberRequestFormatter } from '~/projects/members/utils';
initImportProjectMembersModal();
-initInviteMembersModal();
initInviteGroupsModal();
-initInviteMembersTrigger();
initInviteGroupTrigger();
initImportProjectMembersTrigger();
diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js
index 33d4090011f..e17f5255c54 100644
--- a/app/assets/javascripts/pages/projects/show/index.js
+++ b/app/assets/javascripts/pages/projects/show/index.js
@@ -1,7 +1,5 @@
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
-import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
-import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import initClustersDeprecationAlert from '~/projects/clusters_deprecation_alert';
import leaveByUrl from '~/namespaces/leave_by_url';
import initVueNotificationsDropdown from '~/notifications';
@@ -42,8 +40,6 @@ initVueNotificationsDropdown();
new ShortcutsNavigation(); // eslint-disable-line no-new
initUploadFileTrigger();
-initInviteMembersModal();
-initInviteMembersTrigger();
initClustersDeprecationAlert();
initTerraformNotification();
diff --git a/app/assets/javascripts/pages/projects/work_items/index.js b/app/assets/javascripts/pages/projects/work_items/index.js
index 6eef2352e2c..11c257611f0 100644
--- a/app/assets/javascripts/pages/projects/work_items/index.js
+++ b/app/assets/javascripts/pages/projects/work_items/index.js
@@ -1,5 +1,3 @@
import { initWorkItemsRoot } from '~/work_items/index';
-import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
initWorkItemsRoot();
-initInviteMembersModal();
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 74843bcc006..67e76b575e0 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -2,8 +2,6 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { TYPENAME_ISSUE, TYPENAME_MERGE_REQUEST } from '~/graphql_shared/constants';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
-import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
-import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import { TYPE_ISSUE, TYPE_MERGE_REQUEST, WORKSPACE_PROJECT } from '~/issues/constants';
import { gqlClient } from '~/issues/list/graphql';
import {
@@ -805,8 +803,6 @@ const isAssigneesWidgetShown =
(isInIssuePage() || isInDesignPage() || isInMRPage()) && gon.features.issueAssigneesWidget;
export function mountSidebar(mediator, store) {
- initInviteMembersModal();
- initInviteMembersTrigger();
mountSidebarTodoWidget();
if (isAssigneesWidgetShown) {
mountSidebarAssigneesWidget();
diff --git a/app/assets/stylesheets/notify_enhanced.scss b/app/assets/stylesheets/notify_enhanced.scss
index b331d997a97..a3e02dabe0e 100644
--- a/app/assets/stylesheets/notify_enhanced.scss
+++ b/app/assets/stylesheets/notify_enhanced.scss
@@ -32,6 +32,10 @@ body {
font-size: inherit;
}
+pre {
+ font-size: 14px;
+}
+
.gl-mb-5 {
@include gl-mb-5;
}
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 1c988b9767f..bf8dea15c58 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -99,11 +99,11 @@ module ApplicationSettingsHelper
checked_value: level,
unchecked_value: nil
) do |c|
- c.label do
+ c.with_label do
visibility_level_icon(level) + content_tag(:span, label, { class: 'gl-ml-2' })
end
- c.help_text do
+ c.with_help_text do
restricted_visibility_levels_help_text.fetch(level)
end
end
diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb
index ed8cca20241..3d0b899e867 100644
--- a/app/helpers/form_helper.rb
+++ b/app/helpers/form_helper.rb
@@ -37,7 +37,7 @@ module FormHelper
dismissible: false,
alert_options: { id: 'error_explanation', class: 'gl-mb-5' }
) do |c|
- c.body do
+ c.with_body do
tag.ul(class: 'gl-pl-5 gl-mb-0') do
messages
end
diff --git a/app/models/analytics/cycle_analytics/value_stream.rb b/app/models/analytics/cycle_analytics/value_stream.rb
index 59c68393d74..31e06075bcb 100644
--- a/app/models/analytics/cycle_analytics/value_stream.rb
+++ b/app/models/analytics/cycle_analytics/value_stream.rb
@@ -21,6 +21,7 @@ module Analytics
scope :preload_associated_models, -> {
includes(:namespace, stages: [:namespace, :end_event_label, :start_event_label])
}
+ scope :order_by_name_asc, -> { order(arel_table[:name].lower.asc) }
after_save :ensure_aggregation_record_presence
diff --git a/app/models/packages/nuget/metadatum.rb b/app/models/packages/nuget/metadatum.rb
index 1db8c0eddbf..08276f87568 100644
--- a/app/models/packages/nuget/metadatum.rb
+++ b/app/models/packages/nuget/metadatum.rb
@@ -1,24 +1,22 @@
# frozen_string_literal: true
class Packages::Nuget::Metadatum < ApplicationRecord
+ MAX_AUTHORS_LENGTH = 255
+ MAX_DESCRIPTION_LENGTH = 4000
+
belongs_to :package, -> { where(package_type: :nuget) }, inverse_of: :nuget_metadatum
validates :package, presence: true
validates :license_url, public_url: { allow_blank: true }
validates :project_url, public_url: { allow_blank: true }
validates :icon_url, public_url: { allow_blank: true }
+ validates :authors, presence: true, length: { maximum: MAX_AUTHORS_LENGTH }
+ validates :description, presence: true, length: { maximum: MAX_DESCRIPTION_LENGTH }
- validate :ensure_at_least_one_field_supplied
validate :ensure_nuget_package_type
private
- def ensure_at_least_one_field_supplied
- return if license_url? || project_url? || icon_url?
-
- errors.add(:base, _('Nuget metadatum must have at least license_url, project_url or icon_url set'))
- end
-
def ensure_nuget_package_type
return if package&.nuget?
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 15f91cae86b..c70dc288710 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -163,6 +163,11 @@ class ProjectPolicy < BasePolicy
condition(:service_desk_enabled) { @subject.service_desk_enabled? }
with_scope :subject
+ condition(:model_experiments_enabled) do
+ Feature.enabled?(:ml_experiment_tracking, @subject) && @subject.feature_available?(:model_experiments, @user)
+ end
+
+ with_scope :subject
condition(:model_registry_enabled) { Feature.enabled?(:model_registry, @subject) }
with_scope :subject
@@ -223,6 +228,7 @@ class ProjectPolicy < BasePolicy
feature_flags
releases
infrastructure
+ model_experiments
]
features.each do |f|
@@ -899,6 +905,10 @@ class ProjectPolicy < BasePolicy
enable :read_model_registry
end
+ rule { model_experiments_enabled }.policy do
+ enable :read_model_experiments
+ end
+
private
def user_is_user?
diff --git a/app/presenters/packages/nuget/presenter_helpers.rb b/app/presenters/packages/nuget/presenter_helpers.rb
index 82ed80d8372..ea8558c54f4 100644
--- a/app/presenters/packages/nuget/presenter_helpers.rb
+++ b/app/presenters/packages/nuget/presenter_helpers.rb
@@ -5,7 +5,6 @@ module Packages
module PresenterHelpers
include ::API::Helpers::RelatedResourcesHelpers
- BLANK_STRING = ''
PACKAGE_DEPENDENCY_GROUP = 'PackageDependencyGroup'
PACKAGE_DEPENDENCY = 'PackageDependency'
@@ -45,14 +44,13 @@ module Packages
def catalog_entry_for(package)
{
json_url: json_url_for(package),
- authors: BLANK_STRING,
dependency_groups: dependency_groups_for(package),
package_name: package.name,
package_version: package.version,
archive_url: archive_url_for(package),
- summary: BLANK_STRING,
tags: tags_for(package),
- metadatum: metadatum_for(package)
+ metadatum: metadatum_for(package),
+ published: package.created_at.iso8601
}
end
@@ -98,8 +96,8 @@ module Packages
metadatum = package.nuget_metadatum
return {} unless metadatum
- metadatum.slice(:project_url, :license_url, :icon_url)
- .compact
+ metadatum.slice(:authors, :description, :project_url, :license_url, :icon_url)
+ .compact
end
def base_path_for(package)
diff --git a/app/presenters/packages/nuget/search_results_presenter.rb b/app/presenters/packages/nuget/search_results_presenter.rb
index 311296d576c..45c2c5170ae 100644
--- a/app/presenters/packages/nuget/search_results_presenter.rb
+++ b/app/presenters/packages/nuget/search_results_presenter.rb
@@ -20,11 +20,9 @@ module Packages
{
type: 'Package',
- authors: '',
name: package_name,
version: latest_version,
versions: build_package_versions(packages),
- summary: '',
total_downloads: 0,
verified: true,
tags: tags_for(latest_package),
@@ -48,7 +46,7 @@ module Packages
def latest_version(packages)
versions = packages.map(&:version).compact
- VersionSorter.sort(versions).last # rubocop: disable Style/RedundantSort
+ VersionSorter.sort(versions).last
end
end
end
diff --git a/app/services/ci/runners/assign_runner_service.rb b/app/services/ci/runners/assign_runner_service.rb
index 290f945cc72..4e7b08bdd7a 100644
--- a/app/services/ci/runners/assign_runner_service.rb
+++ b/app/services/ci/runners/assign_runner_service.rb
@@ -17,6 +17,10 @@ module Ci
return ServiceResponse.error(message: 'user not allowed to assign runner', http_status: :forbidden)
end
+ unless @user.can?(:register_project_runners, @project)
+ return ServiceResponse.error(message: 'user not allowed to add runners to project', http_status: :forbidden)
+ end
+
if @runner.assign_to(@project, @user)
ServiceResponse.success
else
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index efe42fb29d5..f982d66eb08 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -124,6 +124,10 @@ module Issues
def update_project_counter_caches?(issue)
super || issue.confidential_changed?
end
+
+ def log_audit_event(issue, user, event_type, message)
+ # defined in EE
+ end
end
end
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index e45033f2b91..f848a8db12a 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -28,6 +28,11 @@ module Issues
event_service.close_issue(issue, current_user)
create_note(issue, closed_via) if system_note
+ if current_user.project_bot?
+ log_audit_event(issue, current_user, "#{issue.issue_type}_closed_by_project_bot",
+ "Closed #{issue.issue_type.humanize(capitalize: false)} #{issue.title}")
+ end
+
closed_via = _("commit %{commit_id}") % { commit_id: closed_via.id } if closed_via.is_a?(Commit)
notification_service.async.close_issue(issue, current_user, { closed_via: closed_via }) if notifications
diff --git a/app/services/issues/reopen_service.rb b/app/services/issues/reopen_service.rb
index f4d229ecec7..d71ba4e3414 100644
--- a/app/services/issues/reopen_service.rb
+++ b/app/services/issues/reopen_service.rb
@@ -7,6 +7,12 @@ module Issues
if issue.reopen
event_service.reopen_issue(issue, current_user)
+
+ if current_user.project_bot?
+ log_audit_event(issue, current_user, "#{issue.issue_type}_reopened_by_project_bot",
+ "Reopened #{issue.issue_type.humanize(capitalize: false)} #{issue.title}")
+ end
+
create_note(issue, 'reopened')
notification_service.async.reopen_issue(issue, current_user)
perform_incident_management_actions(issue)
diff --git a/app/services/packages/nuget/metadata_extraction_service.rb b/app/services/packages/nuget/metadata_extraction_service.rb
index 02086b2a282..3b92d977c79 100644
--- a/app/services/packages/nuget/metadata_extraction_service.rb
+++ b/app/services/packages/nuget/metadata_extraction_service.rb
@@ -7,18 +7,22 @@ module Packages
ExtractionError = Class.new(StandardError)
+ ROOT_XPATH = '//xmlns:package/xmlns:metadata/xmlns'
+
XPATHS = {
- package_name: '//xmlns:package/xmlns:metadata/xmlns:id',
- package_version: '//xmlns:package/xmlns:metadata/xmlns:version',
- license_url: '//xmlns:package/xmlns:metadata/xmlns:licenseUrl',
- project_url: '//xmlns:package/xmlns:metadata/xmlns:projectUrl',
- icon_url: '//xmlns:package/xmlns:metadata/xmlns:iconUrl'
+ package_name: "#{ROOT_XPATH}:id",
+ package_version: "#{ROOT_XPATH}:version",
+ authors: "#{ROOT_XPATH}:authors",
+ description: "#{ROOT_XPATH}:description",
+ license_url: "#{ROOT_XPATH}:licenseUrl",
+ project_url: "#{ROOT_XPATH}:projectUrl",
+ icon_url: "#{ROOT_XPATH}:iconUrl"
}.freeze
- XPATH_DEPENDENCIES = '//xmlns:package/xmlns:metadata/xmlns:dependencies/xmlns:dependency'
- XPATH_DEPENDENCY_GROUPS = '//xmlns:package/xmlns:metadata/xmlns:dependencies/xmlns:group'
- XPATH_TAGS = '//xmlns:package/xmlns:metadata/xmlns:tags'
- XPATH_PACKAGE_TYPES = '//xmlns:package/xmlns:metadata/xmlns:packageTypes/xmlns:packageType'
+ XPATH_DEPENDENCIES = "#{ROOT_XPATH}:dependencies/xmlns:dependency".freeze
+ XPATH_DEPENDENCY_GROUPS = "#{ROOT_XPATH}:dependencies/xmlns:group".freeze
+ XPATH_TAGS = "#{ROOT_XPATH}:tags".freeze
+ XPATH_PACKAGE_TYPES = "#{ROOT_XPATH}:packageTypes/xmlns:packageType".freeze
MAX_FILE_SIZE = 4.megabytes.freeze
@@ -40,10 +44,6 @@ module Packages
end
end
- def project
- package_file.package.project
- end
-
def valid_package_file?
package_file &&
package_file.package&.nuget? &&
diff --git a/app/services/packages/nuget/sync_metadatum_service.rb b/app/services/packages/nuget/sync_metadatum_service.rb
index ca9cc4d5b78..189b972c156 100644
--- a/app/services/packages/nuget/sync_metadatum_service.rb
+++ b/app/services/packages/nuget/sync_metadatum_service.rb
@@ -15,6 +15,8 @@ module Packages
metadatum.destroy! if metadatum.persisted?
else
metadatum.update!(
+ authors: authors,
+ description: description,
license_url: license_url,
project_url: project_url,
icon_url: icon_url
@@ -24,26 +26,57 @@ module Packages
private
+ attr_reader :package, :metadata
+
def metadatum
- strong_memoize(:metadatum) do
- @package.nuget_metadatum || @package.build_nuget_metadatum
- end
+ package.nuget_metadatum || package.build_nuget_metadatum
end
+ strong_memoize_attr :metadatum
def blank_metadata?
- project_url.blank? && license_url.blank? && icon_url.blank?
+ [authors, description, project_url, license_url, icon_url].all?(&:blank?)
+ end
+
+ def authors
+ truncate_value(:authors, ::Packages::Nuget::Metadatum::MAX_AUTHORS_LENGTH)
end
+ strong_memoize_attr :authors
+
+ def description
+ truncate_value(:description, ::Packages::Nuget::Metadatum::MAX_DESCRIPTION_LENGTH)
+ end
+ strong_memoize_attr :description
def project_url
- @metadata[:project_url]
+ metadata[:project_url]
end
def license_url
- @metadata[:license_url]
+ metadata[:license_url]
end
def icon_url
- @metadata[:icon_url]
+ metadata[:icon_url]
+ end
+
+ def truncate_value(field, max_length)
+ return unless metadata[field]
+
+ if metadata[field].size > max_length
+ log_info("#{field.capitalize} is too long (maximum is #{max_length} characters)", field)
+ end
+
+ metadata[field].truncate(max_length)
+ end
+
+ def log_info(message, field)
+ Gitlab::AppLogger.info(
+ class: self.class.name,
+ message: message,
+ package_id: package.id,
+ project_id: package.project_id,
+ field => metadata[field]
+ )
end
end
end
diff --git a/app/services/packages/nuget/update_package_from_metadata_service.rb b/app/services/packages/nuget/update_package_from_metadata_service.rb
index 5456ad4cad7..7153a9035b8 100644
--- a/app/services/packages/nuget/update_package_from_metadata_service.rb
+++ b/app/services/packages/nuget/update_package_from_metadata_service.rb
@@ -17,7 +17,7 @@ module Packages
end
def execute
- raise InvalidMetadataError, 'package name and/or package version not found in metadata' unless valid_metadata?
+ raise InvalidMetadataError, 'package name, version, authors and/or description not found in metadata' unless valid_metadata?
try_obtain_lease do
@package_file.transaction do
@@ -55,17 +55,19 @@ module Packages
return if symbol_package?
::Packages::Nuget::SyncMetadatumService
- .new(package, metadata.slice(:project_url, :license_url, :icon_url))
+ .new(package, metadata.slice(:authors, :description, :project_url, :license_url, :icon_url))
.execute
+
::Packages::UpdateTagsService
.new(package, package_tags)
.execute
+
rescue StandardError => e
raise InvalidMetadataError, e.message
end
def valid_metadata?
- package_name.present? && package_version.present?
+ [package_name, package_version, package_authors, package_description].all?(&:present?)
end
def link_to_existing_package
@@ -93,15 +95,14 @@ module Packages
end
def existing_package
- strong_memoize(:existing_package) do
- @package_file.project.packages
- .nuget
- .with_name(package_name)
- .with_version(package_version)
- .not_pending_destruction
- .first
- end
+ @package_file.project.packages
+ .nuget
+ .with_name(package_name)
+ .with_version(package_version)
+ .not_pending_destruction
+ .first
end
+ strong_memoize_attr :existing_package
def package_name
metadata[:package_name]
@@ -123,15 +124,22 @@ module Packages
metadata.fetch(:package_types, [])
end
+ def package_authors
+ metadata[:authors]
+ end
+
+ def package_description
+ metadata[:description]
+ end
+
def symbol_package?
package_types.include?(SYMBOL_PACKAGE_IDENTIFIER)
end
def metadata
- strong_memoize(:metadata) do
- ::Packages::Nuget::MetadataExtractionService.new(@package_file.id).execute
- end
+ ::Packages::Nuget::MetadataExtractionService.new(@package_file.id).execute
end
+ strong_memoize_attr :metadata
def package_filename
"#{package_name.downcase}.#{package_version.downcase}.#{symbol_package? ? 'snupkg' : 'nupkg'}"
diff --git a/app/views/projects/mirrors/_branch_filter.html.haml b/app/views/projects/mirrors/_branch_filter.html.haml
index 49b0f8c39c8..7d90906bfe8 100644
--- a/app/views/projects/mirrors/_branch_filter.html.haml
+++ b/app/views/projects/mirrors/_branch_filter.html.haml
@@ -2,8 +2,8 @@
= render Pajamas::CheckboxTagComponent.new(name: :only_protected_branches,
checkbox_options: { class: 'js-mirror-protected' },
label_options: { class: 'gl-mb-0!' }) do |c|
- = c.label do
+ - c.with_label do
= _('Mirror only protected branches')
- = c.help_text do
+ - c.with_help_text do
= _('If enabled, only protected branches will be mirrored.')
= link_to _('Learn more.'), help_page_path('user/project/repository/mirror/index.md', anchor: 'mirror-only-protected-branches'), target: '_blank', rel: 'noopener noreferrer'
diff --git a/app/views/projects/mirrors/_mirror_repos_push.html.haml b/app/views/projects/mirrors/_mirror_repos_push.html.haml
index 0947359a753..5b02d650989 100644
--- a/app/views/projects/mirrors/_mirror_repos_push.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos_push.html.haml
@@ -12,8 +12,8 @@
= render Pajamas::CheckboxTagComponent.new(name: :keep_divergent_refs,
checkbox_options: { class: 'js-mirror-keep-divergent-refs' },
label_options: { class: 'gl-mb-0!' }) do |c|
- = c.label do
+ - c.with_label do
= _('Keep divergent refs')
- = c.help_text do
+ - c.with_help_text do
- link_opening_tag = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe
= html_escape(_('Do not force push over diverged refs. After the mirror is created, this setting can only be modified using the API. %{mirroring_docs_link_start}Learn more about this option%{link_closing_tag} and %{mirroring_api_docs_link_start}the API.%{link_closing_tag}')) % { mirroring_docs_link_start: link_opening_tag % {url: help_page_path('user/project/repository/mirror/push.md', anchor: 'keep-divergent-refs')}, mirroring_api_docs_link_start: link_opening_tag % {url: help_page_path('api/remote_mirrors')}, link_closing_tag: '</a>'.html_safe }
diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml
index b31e8919832..c4630eec168 100644
--- a/app/views/projects/network/show.html.haml
+++ b/app/views/projects/network/show.html.haml
@@ -10,7 +10,7 @@
= render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, icon: 'search')
.form-group{ class: 'gl-ml-5 gl-mb-n3!' }
= render Pajamas::CheckboxTagComponent.new(name: :filter_ref, checked: @options[:filter_ref]) do |c|
- = c.label do
+ - c.with_label do
= _("Begin with the selected commit")
- if @commit
diff --git a/app/views/shared/_new_merge_request_checkbox.html.haml b/app/views/shared/_new_merge_request_checkbox.html.haml
index 75289e2e6a5..fb3dfba2691 100644
--- a/app/views/shared/_new_merge_request_checkbox.html.haml
+++ b/app/views/shared/_new_merge_request_checkbox.html.haml
@@ -3,7 +3,7 @@
= render Pajamas::CheckboxTagComponent.new(name: 'create_merge_request',
checked: true,
checkbox_options: { class: 'js-create-merge-request', id: "create_merge_request-#{nonce}" }) do |c|
- = c.label do
+ - c.with_label do
- translation_variables = { new_merge_request: "<strong>#{_('new merge request')}</strong>" }
- translation = _('Start a %{new_merge_request} with these changes') % translation_variables
#{ translation.html_safe }
diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml
index 1dc24d205d0..1da0b82b634 100644
--- a/app/views/shared/issuable/form/_metadata.html.haml
+++ b/app/views/shared/issuable/form/_metadata.html.haml
@@ -7,10 +7,10 @@
- if @add_related_issue
.form-group
= render Pajamas::CheckboxTagComponent.new(name: :add_related_issue, value: @add_related_issue.iid, checked: true) do |c|
- = c.label do
+ - c.with_label do
- add_related_issue_link = link_to "\##{@add_related_issue.iid}", issue_path(@add_related_issue), class: ['has-tooltip'], title: @add_related_issue.title
#{_('Relate to %{issuable_type} %{add_related_issue_link}').html_safe % { issuable_type: @add_related_issue.issue_type, add_related_issue_link: add_related_issue_link }}
- = c.help_text do
+ - c.with_help_text do
= _('Adds this %{issuable_type} as related to the %{issuable_type} it was created from') % { issuable_type: @add_related_issue.issue_type }
- if issuable.respond_to?(:confidential) && can?(current_user, :set_confidentiality, issuable)
diff --git a/app/workers/new_issue_worker.rb b/app/workers/new_issue_worker.rb
index 07699a50e36..0e7f11debd2 100644
--- a/app/workers/new_issue_worker.rb
+++ b/app/workers/new_issue_worker.rb
@@ -28,5 +28,15 @@ class NewIssueWorker # rubocop:disable Scalability/IdempotentWorker
Issues::AfterCreateService
.new(container: issuable.project, current_user: user)
.execute(issuable)
+
+ log_audit_event if user.project_bot?
+ end
+
+ private
+
+ def log_audit_event
+ # defined in EE
end
end
+
+NewIssueWorker.prepend_mod
diff --git a/config/feature_flags/development/expired_storage_check.yml b/config/feature_flags/development/expired_storage_check.yml
new file mode 100644
index 00000000000..271cc951f1f
--- /dev/null
+++ b/config/feature_flags/development/expired_storage_check.yml
@@ -0,0 +1,8 @@
+---
+name: expired_storage_check
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121048
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/411919
+milestone: '16.1'
+type: development
+group: group::utilization
+default_enabled: false
diff --git a/config/feature_flags/development/gitlab_duo.yml b/config/feature_flags/development/gitlab_duo.yml
new file mode 100644
index 00000000000..5fce9acf12f
--- /dev/null
+++ b/config/feature_flags/development/gitlab_duo.yml
@@ -0,0 +1,8 @@
+---
+name: gitlab_duo
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122235
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/413688
+milestone: '16.1'
+type: development
+group: group::ai-enablement
+default_enabled: false
diff --git a/db/migrate/20230505115558_add_authors_and_description_to_nuget_metadatum.rb b/db/migrate/20230505115558_add_authors_and_description_to_nuget_metadatum.rb
new file mode 100644
index 00000000000..366cd2151fd
--- /dev/null
+++ b/db/migrate/20230505115558_add_authors_and_description_to_nuget_metadatum.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class AddAuthorsAndDescriptionToNugetMetadatum < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ def up
+ with_lock_retries do
+ add_column :packages_nuget_metadata, :authors, :text, if_not_exists: true
+ add_column :packages_nuget_metadata, :description, :text, if_not_exists: true
+ end
+
+ add_text_limit :packages_nuget_metadata, :authors, 255
+ add_text_limit :packages_nuget_metadata, :description, 4000
+ end
+
+ def down
+ with_lock_retries do
+ remove_column :packages_nuget_metadata, :authors, if_exists: true
+ remove_column :packages_nuget_metadata, :description, if_exists: true
+ end
+ end
+end
diff --git a/db/schema_migrations/20230505115558 b/db/schema_migrations/20230505115558
new file mode 100644
index 00000000000..7906938db93
--- /dev/null
+++ b/db/schema_migrations/20230505115558
@@ -0,0 +1 @@
+3e5c849215b0bac1a2e68bc815dc19583f4f5dbb7a205eceff6f5b7e80ed3246 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 0a510da3292..6a382cc533e 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -19695,6 +19695,10 @@ CREATE TABLE packages_nuget_metadata (
license_url text,
project_url text,
icon_url text,
+ authors text,
+ description text,
+ CONSTRAINT check_d39a5fe9ee CHECK ((char_length(description) <= 4000)),
+ CONSTRAINT check_e2fc129ebd CHECK ((char_length(authors) <= 255)),
CONSTRAINT packages_nuget_metadata_icon_url_constraint CHECK ((char_length(icon_url) <= 255)),
CONSTRAINT packages_nuget_metadata_license_url_constraint CHECK ((char_length(license_url) <= 255)),
CONSTRAINT packages_nuget_metadata_project_url_constraint CHECK ((char_length(project_url) <= 255))
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index cf3a5e3a541..83076cab73a 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -1038,6 +1038,7 @@ Input type: `AiActionInput`
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="mutationaiactionchat"></a>`chat` | [`AiChatInput`](#aichatinput) | Input for chat AI action. |
| <a id="mutationaiactionclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationaiactionexplaincode"></a>`explainCode` | [`AiExplainCodeInput`](#aiexplaincodeinput) | Input for explain_code AI action. |
| <a id="mutationaiactionexplainvulnerability"></a>`explainVulnerability` | [`AiExplainVulnerabilityInput`](#aiexplainvulnerabilityinput) | Input for explain_vulnerability AI action. |
@@ -27840,6 +27841,15 @@ be used as arguments).
Only general use input types are listed here. For mutation input types,
see the associated mutation type above.
+### `AiChatInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="aichatinputcontent"></a>`content` | [`String!`](#string) | Content of the message. |
+| <a id="aichatinputresourceid"></a>`resourceId` | [`AiModelID!`](#aimodelid) | Global ID of the resource to mutate. |
+
### `AiExplainCodeInput`
#### Arguments
diff --git a/doc/api/packages/nuget.md b/doc/api/packages/nuget.md
index a0761b56645..aa2b4586e9c 100644
--- a/doc/api/packages/nuget.md
+++ b/doc/api/packages/nuget.md
@@ -265,13 +265,14 @@ Example response:
"packageContent": "https://gitlab.example.com/api/v4/projects/1/packages/nuget/download/MyNuGetPkg/1.3.0.17/helloworld.1.3.0.17.nupkg",
"catalogEntry": {
"@id": "https://gitlab.example.com/api/v4/projects/1/packages/nuget/metadata/MyNuGetPkg/1.3.0.17.json",
- "authors": "",
+ "authors": "Author1, Author2",
"dependencyGroups": [],
"id": "MyNuGetPkg",
"version": "1.3.0.17",
"tags": "",
"packageContent": "https://gitlab.example.com/api/v4/projects/1/packages/nuget/download/MyNuGetPkg/1.3.0.17/helloworld.1.3.0.17.nupkg",
- "summary": ""
+ "summary": "Summary of the package",
+ "published": "2023-05-08T17:23:25Z",
}
}
]
@@ -307,13 +308,14 @@ Example response:
"packageContent": "https://gitlab.example.com/api/v4/projects/1/packages/nuget/download/MyNuGetPkg/1.3.0.17/helloworld.1.3.0.17.nupkg",
"catalogEntry": {
"@id": "https://gitlab.example.com/api/v4/projects/1/packages/nuget/metadata/MyNuGetPkg/1.3.0.17.json",
- "authors": "",
+ "authors": "Author1, Author2",
"dependencyGroups": [],
"id": "MyNuGetPkg",
"version": "1.3.0.17",
"tags": "",
"packageContent": "https://gitlab.example.com/api/v4/projects/1/packages/nuget/download/MyNuGetPkg/1.3.0.17/helloworld.1.3.0.17.nupkg",
- "summary": ""
+ "summary": "Summary of the package",
+ "published": "2023-05-08T17:23:25Z",
}
}
```
@@ -347,10 +349,10 @@ Example response:
"data": [
{
"@type": "Package",
- "authors": "",
+ "authors": "Author1, Author2",
"id": "MyNuGetPkg",
"title": "MyNuGetPkg",
- "summary": "",
+ "summary": "Summary of the package",
"totalDownloads": 0,
"verified": true,
"version": "1.3.0.17",
diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md
index 513659d0f68..33f51c20446 100644
--- a/doc/development/migration_style_guide.md
+++ b/doc/development/migration_style_guide.md
@@ -973,6 +973,8 @@ Under the hood, it works like this:
```ruby
class SwapPrimaryKey < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
TABLE_NAME = :table_name
PRIMARY_KEY = :table_name_pkey
OLD_INDEX_NAME = :old_index_name
diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md
index a54b3fea53e..cee393df680 100644
--- a/doc/user/group/saml_sso/index.md
+++ b/doc/user/group/saml_sso/index.md
@@ -208,7 +208,7 @@ To change identity providers:
To migrate users to a new email domain, tell users to:
-1. Add their new email as the primary email to their accounts and verify it.
+1. [Add their new email](../../profile/index.md#change-your-primary-email) as the primary email to their accounts and verify it.
1. Optional. Remove their old email from the account.
If the **NameID** is configured with the email address, [change the **NameID** for users](#manage-user-saml-identity).
diff --git a/lib/api/entities/nuget/metadatum.rb b/lib/api/entities/nuget/metadatum.rb
index 256b916cb64..c316dfce740 100644
--- a/lib/api/entities/nuget/metadatum.rb
+++ b/lib/api/entities/nuget/metadatum.rb
@@ -4,6 +4,12 @@ module API
module Entities
module Nuget
class Metadatum < Grape::Entity
+ expose :authors, documentation: { type: 'string', example: 'Authors' } do |metadatum|
+ metadatum[:authors] || ''
+ end
+ expose :description, as: :summary, documentation: { type: 'string', example: 'Description' } do |metadatum|
+ metadatum[:description] || ''
+ end
expose :project_url, as: :projectUrl, expose_nil: false, documentation: { type: 'string', example: 'http://sandbox.com/project' }
expose :license_url, as: :licenseUrl, expose_nil: false, documentation: { type: 'string', example: 'http://sandbox.com/license' }
expose :icon_url, as: :iconUrl, expose_nil: false, documentation: { type: 'string', example: 'http://sandbox.com/icon' }
diff --git a/lib/api/entities/nuget/package_metadata_catalog_entry.rb b/lib/api/entities/nuget/package_metadata_catalog_entry.rb
index ce328c5a5ca..b6e768e5083 100644
--- a/lib/api/entities/nuget/package_metadata_catalog_entry.rb
+++ b/lib/api/entities/nuget/package_metadata_catalog_entry.rb
@@ -5,16 +5,15 @@ module API
module Nuget
class PackageMetadataCatalogEntry < Grape::Entity
expose :json_url, as: :@id, documentation: { type: 'string', example: 'https://gitlab.example.com/api/v4/projects/1/packages/nuget/metadata/MyNuGetPkg/1.3.0.17.json' }
- expose :authors, documentation: { type: 'string', example: 'Author' }
expose :dependency_groups, as: :dependencyGroups, using: ::API::Entities::Nuget::DependencyGroup,
documentation: { is_array: true, type: 'API::Entities::Nuget::DependencyGroup' }
expose :package_name, as: :id, documentation: { type: 'string', example: 'MyNuGetPkg' }
expose :package_version, as: :version, documentation: { type: 'string', example: '1.3.0.17' }
expose :tags, documentation: { type: 'string', example: 'tag#1 tag#2' }
expose :archive_url, as: :packageContent, documentation: { type: 'string', example: 'https://gitlab.example.com/api/v4/projects/1/packages/nuget/download/MyNuGetPkg/1.3.0.17/helloworld.1.3.0.17.nupkg' }
- expose :summary, documentation: { type: 'string', example: 'Summary' }
expose :metadatum, using: ::API::Entities::Nuget::Metadatum, merge: true,
documentation: { type: 'API::Entities::Nuget::Metadatum' }
+ expose :published, documentation: { type: 'string', example: '2023-05-08T17:23:25Z' }
end
end
end
diff --git a/lib/api/entities/nuget/search_result.rb b/lib/api/entities/nuget/search_result.rb
index bb3698de30b..303efa7718e 100644
--- a/lib/api/entities/nuget/search_result.rb
+++ b/lib/api/entities/nuget/search_result.rb
@@ -5,10 +5,8 @@ module API
module Nuget
class SearchResult < Grape::Entity
expose :type, as: :@type, documentation: { type: 'string', example: 'Package' }
- expose :authors, documentation: { type: 'string', example: 'Author' }
expose :name, as: :id, documentation: { type: 'string', example: 'MyNuGetPkg' }
expose :name, as: :title, documentation: { type: 'string', example: 'MyNuGetPkg' }
- expose :summary, documentation: { type: 'string', example: 'Summary' }
expose :total_downloads, as: :totalDownloads, documentation: { type: 'integer', example: 1 }
expose :verified, documentation: { type: 'boolean' }
expose :version, documentation: { type: 'string', example: '1.3.0.17' }
diff --git a/lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb b/lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb
index d15a0eaa44c..8e4373a9784 100644
--- a/lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb
+++ b/lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb
@@ -5,6 +5,7 @@ module Gitlab
module QueryAnalyzers
class PreventCrossDatabaseModification < Database::QueryAnalyzers::Base
CrossDatabaseModificationAcrossUnsupportedTablesError = Class.new(QueryAnalyzerError)
+ QUERY_LIMIT = 10
# This method will allow cross database modifications within the block
# Example:
@@ -42,7 +43,8 @@ module Gitlab
context.merge!({
transaction_depth_by_db: Hash.new { |h, k| h[k] = 0 },
modified_tables_by_db: Hash.new { |h, k| h[k] = Set.new },
- ignored_tables: []
+ ignored_tables: [],
+ queries: []
})
end
@@ -71,6 +73,7 @@ module Gitlab
context[:transaction_depth_by_db][database] -= 1
if context[:transaction_depth_by_db][database] == 0
context[:modified_tables_by_db][database].clear
+ clear_queries
# Attempt to troubleshoot https://gitlab.com/gitlab-org/gitlab/-/issues/351531
::CrossDatabaseModification::TransactionStackTrackRecord.log_gitlab_transactions_stack(action: :end_of_transaction)
@@ -104,6 +107,7 @@ module Gitlab
# databases
return if tables == ['schema_migrations']
+ add_to_queries(sql)
context[:modified_tables_by_db][database].merge(tables)
all_tables = context[:modified_tables_by_db].values.flat_map(&:to_a)
schemas = ::Gitlab::Database::GitlabSchema.table_schemas!(all_tables)
@@ -111,15 +115,17 @@ module Gitlab
schemas += ApplicationRecord.gitlab_transactions_stack
if schemas.many?
- message = "Cross-database data modification of '#{schemas.to_a.join(", ")}' were detected within " \
- "a transaction modifying the '#{all_tables.to_a.join(", ")}' tables. " \
- "Please refer to https://docs.gitlab.com/ee/development/database/multiple_databases.html#removing-cross-database-transactions for details on how to resolve this exception."
+ messages = ["Cross-database data modification of '#{schemas.to_a.join(", ")}' were detected within " \
+ "a transaction modifying the '#{all_tables.to_a.join(", ")}' tables. " \
+ "Please refer to https://docs.gitlab.com/ee/development/database/multiple_databases.html#removing-cross-database-transactions for details on how to resolve this exception."]
- raise CrossDatabaseModificationAcrossUnsupportedTablesError, message
+ messages << cleaned_queries
+
+ raise CrossDatabaseModificationAcrossUnsupportedTablesError, messages.join("\n\n")
end
rescue CrossDatabaseModificationAcrossUnsupportedTablesError => e
::Gitlab::ErrorTracking.track_exception(e, { gitlab_schemas: schemas, tables: all_tables, query: parsed.sql })
- raise if raise_exception?
+ raise if dev_or_test_env?
end
# rubocop:enable Metrics/AbcSize
@@ -159,12 +165,28 @@ module Gitlab
end
end
- # We only raise in tests for now otherwise some features will be broken
- # in development. For now we've mostly only added allowlist based on
- # spec names. Until we have allowed all the violations inline we don't
- # want to raise in development.
- def self.raise_exception?
- Rails.env.test?
+ def self.dev_or_test_env?
+ Gitlab.dev_or_test_env?
+ end
+
+ def self.clear_queries
+ return unless dev_or_test_env?
+
+ context[:queries].clear
+ end
+
+ def self.add_to_queries(sql)
+ return unless dev_or_test_env?
+
+ context[:queries].push(sql)
+ end
+
+ def self.cleaned_queries
+ return '' unless dev_or_test_env?
+
+ context[:queries].last(QUERY_LIMIT).each_with_index.map do |sql, i|
+ "#{i}: #{sql}"
+ end.join("\n")
end
def self.in_transaction?
diff --git a/lib/gitlab/project_authorizations.rb b/lib/gitlab/project_authorizations.rb
index c770260a66e..eedea2f0997 100644
--- a/lib/gitlab/project_authorizations.rb
+++ b/lib/gitlab/project_authorizations.rb
@@ -120,7 +120,9 @@ module Gitlab
.merge(user.group_members)
.merge(GroupMember.active_state)
- union = Namespace.from_union([shared_groups, member_groups_with_ancestors])
+ union = Namespace
+ .select("namespaces.id, access_level")
+ .from_union([shared_groups, member_groups_with_ancestors])
Gitlab::SQL::CTE.new(:linear_namespaces_cte, union)
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index c08cf3fbb67..b0bd90ed3bf 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -31101,9 +31101,6 @@ msgstr ""
msgid "Now, personalize your GitLab experience"
msgstr ""
-msgid "Nuget metadatum must have at least license_url, project_url or icon_url set"
-msgstr ""
-
msgid "Number of Elasticsearch shards and replicas per index:"
msgstr ""
@@ -39255,9 +39252,6 @@ msgstr ""
msgid "Runners|Enter the number of seconds."
msgstr ""
-msgid "Runners|Enter the number of seconds. This timeout takes precedence over lower timeouts set for the project."
-msgstr ""
-
msgid "Runners|Environment"
msgstr ""
@@ -39787,9 +39781,6 @@ msgstr ""
msgid "Runners|Use the runner for jobs without tags in addition to tagged jobs."
msgstr ""
-msgid "Runners|Use the runner for jobs without tags, in addition to tagged jobs."
-msgstr ""
-
msgid "Runners|Use the runner for the currently assigned projects only. Only administrators can change the assigned projects."
msgstr ""
@@ -50381,9 +50372,6 @@ msgstr ""
msgid "Vulnerability|Evidence:"
msgstr ""
-msgid "Vulnerability|Experiment"
-msgstr ""
-
msgid "Vulnerability|Explain this vulnerability"
msgstr ""
@@ -50453,6 +50441,9 @@ msgstr ""
msgid "Vulnerability|Request/Response"
msgstr ""
+msgid "Vulnerability|Response generated by AI"
+msgstr ""
+
msgid "Vulnerability|Scanner Provider"
msgstr ""
@@ -50495,9 +50486,6 @@ msgstr ""
msgid "Vulnerability|This is an experimental feature that uses AI to explain the vulnerability and provide recommendations. Use this feature with caution as we continue to iterate. Please provide your feedback and ideas in %{linkStart}this issue%{linkEnd}."
msgstr ""
-msgid "Vulnerability|This response is generated by AI."
-msgstr ""
-
msgid "Vulnerability|Tool"
msgstr ""
diff --git a/package.json b/package.json
index 536d49680b5..1d4bd4ec48c 100644
--- a/package.json
+++ b/package.json
@@ -50,8 +50,8 @@
"@apollo/client": "^3.5.10",
"@babel/core": "^7.18.5",
"@babel/preset-env": "^7.18.2",
- "@cubejs-client/core": "^0.33.0",
- "@cubejs-client/vue": "^0.33.0",
+ "@cubejs-client/core": "^0.33.12",
+ "@cubejs-client/vue": "^0.33.12",
"@gitlab/at.js": "1.5.7",
"@gitlab/cluster-client": "^1.2.0",
"@gitlab/favicon-overlay": "2.0.0",
diff --git a/spec/components/previews/layouts/horizontal_section_component_preview.rb b/spec/components/previews/layouts/horizontal_section_component_preview.rb
index cc7e8c8c2b1..7393020077f 100644
--- a/spec/components/previews/layouts/horizontal_section_component_preview.rb
+++ b/spec/components/previews/layouts/horizontal_section_component_preview.rb
@@ -13,9 +13,9 @@ module Layouts
body: 'Settings fields here.'
)
render(::Layouts::HorizontalSectionComponent.new(border: border, options: { class: 'gl-mb-6 gl-pb-3' })) do |c|
- c.title { title }
- c.description { description }
- c.body { body }
+ c.with_title { title }
+ c.with_description { description }
+ c.with_body { body }
end
end
end
diff --git a/spec/components/previews/pajamas/banner_component_preview.rb b/spec/components/previews/pajamas/banner_component_preview.rb
index 19f4f5243c0..db9bf2c51d6 100644
--- a/spec/components/previews/pajamas/banner_component_preview.rb
+++ b/spec/components/previews/pajamas/banner_component_preview.rb
@@ -32,7 +32,7 @@ module Pajamas
# like rendering a partial that holds your button.
def with_primary_action_slot
render(Pajamas::BannerComponent.new) do |c|
- c.primary_action do
+ c.with_primary_action do
# You could also `render` another partial here.
tag.button "I'm special", class: "btn btn-md btn-confirm gl-button"
end
@@ -44,7 +44,7 @@ module Pajamas
# but for example, an inline SVG via `custom_icon`.
def with_illustration_slot
render(Pajamas::BannerComponent.new) do |c|
- c.illustration do
+ c.with_illustration do
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="white" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-thumbs-up"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"></path></svg>'.html_safe # rubocop:disable Layout/LineLength
end
content_tag :p, "This banner uses the illustration slot."
diff --git a/spec/factories/packages/nuget/metadata.rb b/spec/factories/packages/nuget/metadata.rb
index d2a2a666928..08a52997786 100644
--- a/spec/factories/packages/nuget/metadata.rb
+++ b/spec/factories/packages/nuget/metadata.rb
@@ -4,6 +4,8 @@ FactoryBot.define do
factory :nuget_metadatum, class: 'Packages::Nuget::Metadatum' do
package { association(:nuget_package) }
+ authors { 'Authors' }
+ description { 'Description' }
license_url { 'http://www.gitlab.com' }
project_url { 'http://www.gitlab.com' }
icon_url { 'http://www.gitlab.com' }
diff --git a/spec/fixtures/api/schemas/public_api/v4/packages/nuget/package_metadata.json b/spec/fixtures/api/schemas/public_api/v4/packages/nuget/package_metadata.json
index 1244cbe474e..9c80f1621ad 100644
--- a/spec/fixtures/api/schemas/public_api/v4/packages/nuget/package_metadata.json
+++ b/spec/fixtures/api/schemas/public_api/v4/packages/nuget/package_metadata.json
@@ -1,26 +1,67 @@
{
"type": "object",
- "required": ["@id", "packageContent", "catalogEntry"],
+ "required": [
+ "@id",
+ "packageContent",
+ "catalogEntry"
+ ],
"properties": {
- "@id": { "type": "string" },
- "packageContent": { "type": "string" },
+ "@id": {
+ "type": "string"
+ },
+ "packageContent": {
+ "type": "string"
+ },
"catalogEntry": {
"type": "object",
- "required": ["@id", "authors", "dependencyGroups", "id", "packageContent", "summary", "version"],
+ "required": [
+ "@id",
+ "authors",
+ "dependencyGroups",
+ "id",
+ "packageContent",
+ "summary",
+ "version"
+ ],
"properties": {
- "@id": { "type": "string" },
- "authors": { "const": "" },
- "id": { "type": "string" },
- "packageContent": { "type": "string" },
- "summary": { "const": "" },
- "tags": { "type": "string" },
- "projectUrl": { "type": "string" },
- "licenseUrl": { "type": "string" },
- "iconUrl": { "type": "string" },
- "version": { "type": "string" },
+ "@id": {
+ "type": "string"
+ },
+ "authors": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "packageContent": {
+ "type": "string"
+ },
+ "summary": {
+ "type": "string"
+ },
+ "tags": {
+ "type": "string"
+ },
+ "projectUrl": {
+ "type": "string"
+ },
+ "licenseUrl": {
+ "type": "string"
+ },
+ "iconUrl": {
+ "type": "string"
+ },
+ "version": {
+ "type": "string"
+ },
+ "published": {
+ "type": "string"
+ },
"dependencyGroups": {
"type": "array",
- "items": { "$ref": "./dependency_group.json" }
+ "items": {
+ "$ref": "./dependency_group.json"
+ }
}
}
}
diff --git a/spec/fixtures/api/schemas/public_api/v4/packages/nuget/packages_metadata.json b/spec/fixtures/api/schemas/public_api/v4/packages/nuget/packages_metadata.json
index 0fa59bc3bec..94b5ad48a1e 100644
--- a/spec/fixtures/api/schemas/public_api/v4/packages/nuget/packages_metadata.json
+++ b/spec/fixtures/api/schemas/public_api/v4/packages/nuget/packages_metadata.json
@@ -1,41 +1,96 @@
{
"type": "object",
- "required": ["count", "items"],
+ "required": [
+ "count",
+ "items"
+ ],
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
- "required": ["lower", "upper", "count", "items"],
+ "required": [
+ "lower",
+ "upper",
+ "count",
+ "items"
+ ],
"properties": {
- "lower": { "type": "string" },
- "upper": { "type": "string" },
- "count": { "type": "integer" },
+ "lower": {
+ "type": "string"
+ },
+ "upper": {
+ "type": "string"
+ },
+ "count": {
+ "type": "integer"
+ },
"items": {
"type": "array",
"items": {
"type": "object",
- "required": ["@id", "packageContent", "catalogEntry"],
+ "required": [
+ "@id",
+ "packageContent",
+ "catalogEntry"
+ ],
"properties": {
- "@id": { "type": "string" },
- "packageContent": { "type": "string" },
+ "@id": {
+ "type": "string"
+ },
+ "packageContent": {
+ "type": "string"
+ },
"catalogEntry": {
"type": "object",
- "required": ["@id", "authors", "dependencyGroups", "id", "packageContent", "summary", "version"],
+ "required": [
+ "@id",
+ "authors",
+ "dependencyGroups",
+ "id",
+ "packageContent",
+ "summary",
+ "version"
+ ],
"properties": {
- "@id": { "type": "string" },
- "authors": { "const": "" },
- "id": { "type": "string" },
- "packageContent": { "type": "string" },
- "summary": { "const": "" },
- "tags": { "type": "string" },
- "projectUrl": { "type": "string" },
- "licenseUrl": { "type": "string" },
- "iconUrl": { "type": "string" },
- "version": { "type": "string" },
+ "@id": {
+ "type": "string"
+ },
+ "authors": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "packageContent": {
+ "type": "string"
+ },
+ "summary": {
+ "type": "string"
+ },
+ "tags": {
+ "type": "string"
+ },
+ "projectUrl": {
+ "type": "string"
+ },
+ "licenseUrl": {
+ "type": "string"
+ },
+ "iconUrl": {
+ "type": "string"
+ },
+ "version": {
+ "type": "string"
+ },
+ "published": {
+ "type": "string"
+ },
"dependencyGroups": {
"type": "array",
- "items": { "$ref": "./dependency_group.json" }
+ "items": {
+ "$ref": "./dependency_group.json"
+ }
}
}
}
@@ -47,7 +102,3 @@
}
}
}
-
-
-
-
diff --git a/spec/fixtures/api/schemas/public_api/v4/packages/nuget/search.json b/spec/fixtures/api/schemas/public_api/v4/packages/nuget/search.json
index 73d0927e24c..41ad7379d73 100644
--- a/spec/fixtures/api/schemas/public_api/v4/packages/nuget/search.json
+++ b/spec/fixtures/api/schemas/public_api/v4/packages/nuget/search.json
@@ -1,34 +1,83 @@
{
"type": "object",
- "required": ["totalHits", "data"],
+ "required": [
+ "totalHits",
+ "data"
+ ],
"properties": {
- "totalHits": { "type": "integer" },
+ "totalHits": {
+ "type": "integer"
+ },
"data": {
"type": "array",
"items": {
"type": "object",
- "required": ["@type", "authors", "id", "summary", "title", "totalDownloads", "verified", "versions"],
+ "required": [
+ "@type",
+ "authors",
+ "id",
+ "summary",
+ "title",
+ "totalDownloads",
+ "verified",
+ "versions"
+ ],
"properties": {
- "@type": { "const": "Package" },
- "authors": { "const": "" },
- "id": { "type": "string" },
- "summary": { "const": "" },
- "title": { "type": "string" },
- "totalDownloads": { "const": 0 },
- "verified": { "const": true },
- "tags": { "type": "string" },
- "projectUrl": { "type": "string" },
- "licenseUrl": { "type": "string" },
- "iconUrl": { "type": "string" },
+ "@type": {
+ "const": "Package"
+ },
+ "authors": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "summary": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "totalDownloads": {
+ "const": 0
+ },
+ "verified": {
+ "const": true
+ },
+ "tags": {
+ "type": "string"
+ },
+ "projectUrl": {
+ "type": "string"
+ },
+ "licenseUrl": {
+ "type": "string"
+ },
+ "iconUrl": {
+ "type": "string"
+ },
"versions": {
"type": "array",
"items": {
"type": "object",
- "required": ["@id", "version", "downloads"],
+ "required": [
+ "@id",
+ "version",
+ "downloads"
+ ],
"properties": {
- "@id": { "type": "string" },
- "version": { "type": "string" },
- "downloads": { "const": 0 }
+ "@id": {
+ "type": "string"
+ },
+ "version": {
+ "type": "string"
+ },
+ "downloads": {
+ "const": 0
+ },
+ "published": {
+ "type": "string"
+ }
}
}
}
diff --git a/spec/frontend/ci/runner/components/runner_create_form_spec.js b/spec/frontend/ci/runner/components/runner_create_form_spec.js
index 243d23aeb38..f11667ee415 100644
--- a/spec/frontend/ci/runner/components/runner_create_form_spec.js
+++ b/spec/frontend/ci/runner/components/runner_create_form_spec.js
@@ -126,8 +126,8 @@ describe('RunnerCreateForm', () => {
expect(wrapper.emitted('saved')[0]).toEqual([mockCreatedRunner]);
});
- it('does not show a saving state', () => {
- expect(findSubmitBtn().props('loading')).toBe(false);
+ it('maintains a saving state before navigating away', () => {
+ expect(findSubmitBtn().props('loading')).toBe(true);
});
});
diff --git a/spec/frontend/ci/runner/components/runner_form_fields_spec.js b/spec/frontend/ci/runner/components/runner_form_fields_spec.js
index 0e2f2aa2e91..98f170d8f18 100644
--- a/spec/frontend/ci/runner/components/runner_form_fields_spec.js
+++ b/spec/frontend/ci/runner/components/runner_form_fields_spec.js
@@ -1,4 +1,6 @@
import { nextTick } from 'vue';
+import { GlSkeletonLoader } from '@gitlab/ui';
+import { s__ } from '~/locale';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import RunnerFormFields from '~/ci/runner/components/runner_form_fields.vue';
import {
@@ -8,46 +10,97 @@ import {
} from '~/ci/runner/constants';
const mockDescription = 'My description';
+const mockNewDescription = 'My new description';
const mockMaxTimeout = 60;
const mockTags = 'tag, tag2';
describe('RunnerFormFields', () => {
let wrapper;
+ const findInputByLabel = (label) => wrapper.findByLabelText(label);
const findInput = (name) => wrapper.find(`input[name="${name}"]`);
- const createComponent = ({ runner } = {}) => {
+ const expectRendersFields = () => {
+ expect(wrapper.text()).toContain(s__('Runners|Details'));
+ expect(wrapper.text()).toContain(s__('Runners|Configuration'));
+
+ expect(wrapper.findAllComponents(GlSkeletonLoader)).toHaveLength(0);
+ expect(wrapper.findAll('input')).toHaveLength(6);
+ };
+
+ const createComponent = ({ ...props } = {}) => {
wrapper = mountExtended(RunnerFormFields, {
propsData: {
- value: runner,
+ ...props,
},
});
};
+ describe('when runner is loading', () => {
+ beforeEach(() => {
+ createComponent({ loading: true });
+ });
+
+ it('renders a loading frame', () => {
+ expect(wrapper.text()).toContain(s__('Runners|Details'));
+ expect(wrapper.text()).toContain(s__('Runners|Configuration'));
+
+ expect(wrapper.findAllComponents(GlSkeletonLoader)).toHaveLength(2);
+ expect(wrapper.findAll('input')).toHaveLength(0);
+ });
+
+ describe('and then is loaded', () => {
+ beforeEach(() => {
+ wrapper.setProps({ loading: false, value: { description: mockDescription } });
+ });
+
+ it('renders fields', () => {
+ expectRendersFields();
+ });
+ });
+ });
+
+ it('when runner is loaded, renders fields', () => {
+ createComponent({
+ value: { description: mockDescription },
+ });
+
+ expectRendersFields();
+ });
+
+ it('when runner is updated with the same value, only emits when changed (avoids infinite loop)', async () => {
+ createComponent({ value: null, loading: true });
+ await wrapper.setProps({ value: { description: mockDescription }, loading: false });
+ await wrapper.setProps({ value: { description: mockDescription }, loading: false });
+
+ expect(wrapper.emitted('input')).toHaveLength(1);
+ });
+
it('updates runner fields', async () => {
- createComponent();
+ createComponent({
+ value: { description: mockDescription },
+ });
expect(wrapper.emitted('input')).toBe(undefined);
- findInput('description').setValue(mockDescription);
+ findInputByLabel(s__('Runners|Runner description')).setValue(mockNewDescription);
findInput('max-timeout').setValue(mockMaxTimeout);
- findInput('paused').setChecked(true);
- findInput('protected').setChecked(true);
- findInput('run-untagged').setChecked(true);
findInput('tags').setValue(mockTags);
await nextTick();
- expect(wrapper.emitted('input')[0][0]).toMatchObject({
- description: mockDescription,
- maximumTimeout: mockMaxTimeout,
- tagList: mockTags,
- });
+ expect(wrapper.emitted('input').at(-1)).toEqual([
+ {
+ description: mockNewDescription,
+ maximumTimeout: mockMaxTimeout,
+ tagList: mockTags,
+ },
+ ]);
});
it('checks checkbox fields', async () => {
createComponent({
- runner: {
+ value: {
paused: false,
accessLevel: ACCESS_LEVEL_NOT_PROTECTED,
runUntagged: false,
@@ -60,11 +113,13 @@ describe('RunnerFormFields', () => {
await nextTick();
- expect(wrapper.emitted('input')[0][0]).toEqual({
- paused: true,
- accessLevel: ACCESS_LEVEL_REF_PROTECTED,
- runUntagged: true,
- });
+ expect(wrapper.emitted('input').at(-1)).toEqual([
+ {
+ paused: true,
+ accessLevel: ACCESS_LEVEL_REF_PROTECTED,
+ runUntagged: true,
+ },
+ ]);
});
it('locked checkbox is not shown', () => {
@@ -75,7 +130,7 @@ describe('RunnerFormFields', () => {
it('when runner is of project type, locked checkbox can be checked', async () => {
createComponent({
- runner: {
+ value: {
runnerType: PROJECT_TYPE,
locked: false,
},
@@ -85,15 +140,17 @@ describe('RunnerFormFields', () => {
await nextTick();
- expect(wrapper.emitted('input')[0][0]).toEqual({
- runnerType: PROJECT_TYPE,
- locked: true,
- });
+ expect(wrapper.emitted('input').at(-1)).toEqual([
+ {
+ runnerType: PROJECT_TYPE,
+ locked: true,
+ },
+ ]);
});
it('unchecks checkbox fields', async () => {
createComponent({
- runner: {
+ value: {
paused: true,
accessLevel: ACCESS_LEVEL_REF_PROTECTED,
runUntagged: true,
@@ -106,10 +163,12 @@ describe('RunnerFormFields', () => {
await nextTick();
- expect(wrapper.emitted('input')[0][0]).toEqual({
- paused: false,
- accessLevel: ACCESS_LEVEL_NOT_PROTECTED,
- runUntagged: false,
- });
+ expect(wrapper.emitted('input').at(-1)).toEqual([
+ {
+ paused: false,
+ accessLevel: ACCESS_LEVEL_NOT_PROTECTED,
+ runUntagged: false,
+ },
+ ]);
});
});
diff --git a/spec/frontend/ci/runner/components/runner_update_form_spec.js b/spec/frontend/ci/runner/components/runner_update_form_spec.js
index d1d4e38f47c..5851078a8d3 100644
--- a/spec/frontend/ci/runner/components/runner_update_form_spec.js
+++ b/spec/frontend/ci/runner/components/runner_update_form_spec.js
@@ -1,20 +1,17 @@
-import Vue, { nextTick } from 'vue';
-import { GlForm, GlSkeletonLoader } from '@gitlab/ui';
+import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import { GlForm } from '@gitlab/ui';
import { __ } from '~/locale';
+import { createAlert, VARIANT_SUCCESS } from '~/alert';
+import { visitUrl } from '~/lib/utils/url_utility';
+
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert, VARIANT_SUCCESS } from '~/alert';
-import { visitUrl } from '~/lib/utils/url_utility';
+
+import { runnerToModel } from 'ee_else_ce/ci/runner/runner_update_form_utils';
+import RunnerFormFields from '~/ci/runner/components/runner_form_fields.vue';
import RunnerUpdateForm from '~/ci/runner/components/runner_update_form.vue';
-import {
- INSTANCE_TYPE,
- GROUP_TYPE,
- PROJECT_TYPE,
- ACCESS_LEVEL_REF_PROTECTED,
- ACCESS_LEVEL_NOT_PROTECTED,
-} from '~/ci/runner/constants';
import runnerUpdateMutation from '~/ci/runner/graphql/edit/runner_update.mutation.graphql';
import { captureException } from '~/ci/runner/sentry_utils';
import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_alert_to_local_storage';
@@ -38,16 +35,7 @@ describe('RunnerUpdateForm', () => {
let runnerUpdateHandler;
const findForm = () => wrapper.findComponent(GlForm);
- const findPausedCheckbox = () => wrapper.findByTestId('runner-field-paused');
- const findProtectedCheckbox = () => wrapper.findByTestId('runner-field-protected');
- const findRunUntaggedCheckbox = () => wrapper.findByTestId('runner-field-run-untagged');
- const findLockedCheckbox = () => wrapper.findByTestId('runner-field-locked');
- const findFields = () => wrapper.findAll('[data-testid^="runner-field"');
-
- const findDescriptionInput = () => wrapper.findByTestId('runner-field-description').find('input');
- const findMaxJobTimeoutInput = () =>
- wrapper.findByTestId('runner-field-max-timeout').find('input');
- const findTagsInput = () => wrapper.findByTestId('runner-field-tags').find('input');
+ const findRunnerFormFields = () => wrapper.findComponent(RunnerFormFields);
const findSubmit = () => wrapper.find('[type="submit"]');
const findSubmitDisabledAttr = () => findSubmit().attributes('disabled');
@@ -55,21 +43,10 @@ describe('RunnerUpdateForm', () => {
const submitForm = () => findForm().trigger('submit');
const submitFormAndWait = () => submitForm().then(waitForPromises);
- const getFieldsModel = () => ({
- paused: findPausedCheckbox().element.checked,
- accessLevel: findProtectedCheckbox().element.checked
- ? ACCESS_LEVEL_REF_PROTECTED
- : ACCESS_LEVEL_NOT_PROTECTED,
- runUntagged: findRunUntaggedCheckbox().element.checked,
- locked: findLockedCheckbox().element?.checked || false,
- maximumTimeout: findMaxJobTimeoutInput().element.value || null,
- tagList: findTagsInput().element.value.split(',').filter(Boolean),
- });
-
const createComponent = ({ props } = {}) => {
wrapper = mountExtended(RunnerUpdateForm, {
propsData: {
- runner: mockRunner,
+ runner: null,
runnerPath: mockRunnerPath,
...props,
},
@@ -106,141 +83,82 @@ describe('RunnerUpdateForm', () => {
},
});
});
+ });
+ it('form has fields, submit and cancel buttons', () => {
createComponent();
- });
- it('Form has a submit button', () => {
+ expect(findRunnerFormFields().exists()).toBe(true);
expect(findSubmit().exists()).toBe(true);
- });
-
- it('Form fields match data', () => {
- expect(mockRunner).toMatchObject(getFieldsModel());
- });
-
- it('Form shows a cancel button', () => {
- expect(runnerUpdateHandler).not.toHaveBeenCalled();
expect(findCancelBtn().attributes('href')).toBe(mockRunnerPath);
});
- it('Form prevent multiple submissions', async () => {
- await submitForm();
-
- expect(findSubmitDisabledAttr()).toBe('disabled');
- });
-
- it('Updates runner with no changes', async () => {
- await submitFormAndWait();
-
- // Some read-only fields are not submitted
- const { __typename, shortSha, runnerType, createdAt, status, ...submitted } = mockRunner;
-
- expectToHaveSubmittedRunnerContaining(submitted);
- });
-
describe('When data is being loaded', () => {
beforeEach(() => {
createComponent({ props: { loading: true } });
});
- it('Form skeleton is shown', () => {
- expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true);
- expect(findFields()).toHaveLength(0);
+ it('form has no runner', () => {
+ expect(findRunnerFormFields().props('value')).toBe(null);
});
- it('Form cannot be submitted', () => {
+ it('form cannot be submitted', () => {
expect(findSubmit().props('loading')).toBe(true);
});
+ });
+
+ describe('When runner has loaded', () => {
+ beforeEach(async () => {
+ createComponent({ props: { loading: true } });
- it('Form is updated when data loads', async () => {
- wrapper.setProps({
+ await wrapper.setProps({
loading: false,
+ runner: mockRunner,
});
-
- await nextTick();
-
- expect(findFields()).not.toHaveLength(0);
- expect(mockRunner).toMatchObject(getFieldsModel());
});
- });
- it.each`
- runnerType | exists | outcome
- ${INSTANCE_TYPE} | ${false} | ${'hidden'}
- ${GROUP_TYPE} | ${false} | ${'hidden'}
- ${PROJECT_TYPE} | ${true} | ${'shown'}
- `(`When runner is $runnerType, locked field is $outcome`, ({ runnerType, exists }) => {
- const runner = { ...mockRunner, runnerType };
- createComponent({ props: { runner } });
+ it('shows runner fields', () => {
+ expect(findRunnerFormFields().props('value')).toEqual(runnerToModel(mockRunner));
+ });
- expect(findLockedCheckbox().exists()).toBe(exists);
- });
+ it('form has not been submitted', () => {
+ expect(runnerUpdateHandler).not.toHaveBeenCalled();
+ });
- describe('On submit, runner gets updated', () => {
- it.each`
- test | initialValue | findCheckbox | checked | submitted
- ${'pauses'} | ${{ paused: false }} | ${findPausedCheckbox} | ${true} | ${{ paused: true }}
- ${'activates'} | ${{ paused: true }} | ${findPausedCheckbox} | ${false} | ${{ paused: false }}
- ${'unprotects'} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED }} | ${findProtectedCheckbox} | ${true} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED }}
- ${'protects'} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED }} | ${findProtectedCheckbox} | ${false} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED }}
- ${'"runs untagged jobs"'} | ${{ runUntagged: true }} | ${findRunUntaggedCheckbox} | ${false} | ${{ runUntagged: false }}
- ${'"runs tagged jobs"'} | ${{ runUntagged: false }} | ${findRunUntaggedCheckbox} | ${true} | ${{ runUntagged: true }}
- ${'locks'} | ${{ runnerType: PROJECT_TYPE, locked: true }} | ${findLockedCheckbox} | ${false} | ${{ locked: false }}
- ${'unlocks'} | ${{ runnerType: PROJECT_TYPE, locked: false }} | ${findLockedCheckbox} | ${true} | ${{ locked: true }}
- `('Checkbox $test runner', async ({ initialValue, findCheckbox, checked, submitted }) => {
- const runner = { ...mockRunner, ...initialValue };
- createComponent({ props: { runner } });
-
- await findCheckbox().setChecked(checked);
- await submitFormAndWait();
+ it('Form prevents multiple submissions', async () => {
+ await submitForm();
- expectToHaveSubmittedRunnerContaining({
- id: runner.id,
- ...submitted,
- });
+ expect(findSubmitDisabledAttr()).toBe('disabled');
});
- it.each`
- test | initialValue | findInput | value | submitted
- ${'description'} | ${{ description: 'Desc. 1' }} | ${findDescriptionInput} | ${'Desc. 2'} | ${{ description: 'Desc. 2' }}
- ${'max timeout'} | ${{ maximumTimeout: 36000 }} | ${findMaxJobTimeoutInput} | ${'40000'} | ${{ maximumTimeout: 40000 }}
- ${'tags'} | ${{ tagList: ['tag1'] }} | ${findTagsInput} | ${'tag2, tag3'} | ${{ tagList: ['tag2', 'tag3'] }}
- `("Field updates runner's $test", async ({ initialValue, findInput, value, submitted }) => {
- const runner = { ...mockRunner, ...initialValue };
- createComponent({ props: { runner } });
-
- await findInput().setValue(value);
+ it('Updates runner with no changes', async () => {
await submitFormAndWait();
- expectToHaveSubmittedRunnerContaining({
- id: runner.id,
- ...submitted,
- });
+ // Some read-only fields are not submitted
+ const { __typename, shortSha, runnerType, createdAt, status, ...submitted } = mockRunner;
+
+ expectToHaveSubmittedRunnerContaining(submitted);
});
- it.each`
- value | submitted
- ${''} | ${{ tagList: [] }}
- ${'tag1, tag2'} | ${{ tagList: ['tag1', 'tag2'] }}
- ${'with spaces'} | ${{ tagList: ['with spaces'] }}
- ${'more ,,,,, commas'} | ${{ tagList: ['more', 'commas'] }}
- `('Field updates runner\'s tags for "$value"', async ({ value, submitted }) => {
- const runner = { ...mockRunner, tagList: ['tag1'] };
- createComponent({ props: { runner } });
-
- await findTagsInput().setValue(value);
+ it('Updates runner with changes', async () => {
+ findRunnerFormFields().vm.$emit(
+ 'input',
+ runnerToModel({ ...mockRunner, description: 'A new description' }),
+ );
await submitFormAndWait();
- expectToHaveSubmittedRunnerContaining({
- id: runner.id,
- ...submitted,
- });
+ expectToHaveSubmittedRunnerContaining({ description: 'A new description' });
});
});
describe('On error', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createComponent();
+
+ await wrapper.setProps({
+ loading: false,
+ runner: mockRunner,
+ });
});
it('On network error, error message is shown', async () => {
diff --git a/spec/frontend/environments/environment_folder_spec.js b/spec/frontend/environments/environment_folder_spec.js
index 4716f807657..65c16697d44 100644
--- a/spec/frontend/environments/environment_folder_spec.js
+++ b/spec/frontend/environments/environment_folder_spec.js
@@ -35,7 +35,7 @@ describe('~/environments/components/environments_folder.vue', () => {
...propsData,
},
stubs: { transition: stubTransition() },
- provide: { helpPagePath: '/help', projectId: '1' },
+ provide: { helpPagePath: '/help', projectId: '1', projectPath: 'path/to/project' },
});
beforeEach(() => {
diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js
index addbf2c21dc..91268ade1e9 100644
--- a/spec/frontend/environments/graphql/mock_data.js
+++ b/spec/frontend/environments/graphql/mock_data.js
@@ -800,12 +800,14 @@ export const resolvedDeploymentDetails = {
};
export const agent = {
- project: 'agent-project',
id: 'gid://gitlab/ClusterAgent/1',
name: 'agent-name',
- kubernetesNamespace: 'agent-namespace',
+ webPath: 'path/to/agent-page',
+ tokens: { nodes: [] },
};
+export const kubernetesNamespace = 'agent-namespace';
+
const runningPod = { status: { phase: 'Running' } };
const pendingPod = { status: { phase: 'Pending' } };
const succeededPod = { status: { phase: 'Succeeded' } };
diff --git a/spec/frontend/environments/kubernetes_agent_info_spec.js b/spec/frontend/environments/kubernetes_agent_info_spec.js
index b1795065281..9169b9284f4 100644
--- a/spec/frontend/environments/kubernetes_agent_info_spec.js
+++ b/spec/frontend/environments/kubernetes_agent_info_spec.js
@@ -1,26 +1,14 @@
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import { GlIcon, GlLink, GlSprintf, GlLoadingIcon, GlAlert } from '@gitlab/ui';
+import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import KubernetesAgentInfo from '~/environments/components/kubernetes_agent_info.vue';
import { AGENT_STATUSES, ACTIVE_CONNECTION_TIME } from '~/clusters_list/constants';
-import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import getK8sClusterAgentQuery from '~/environments/graphql/queries/k8s_cluster_agent.query.graphql';
-Vue.use(VueApollo);
-
-const propsData = {
- agentName: 'my-agent',
- agentId: '1',
- agentProjectPath: 'path/to/agent-config-project',
-};
-
-const mockClusterAgent = {
- id: '1',
- name: 'token-1',
+const defaultClusterAgent = {
+ name: 'my-agent',
+ id: 'gid://gitlab/ClusterAgent/1',
webPath: 'path/to/agent-page',
};
@@ -29,27 +17,16 @@ const connectedTimeInactive = new Date(connectedTimeNow.getTime() - ACTIVE_CONNE
describe('~/environments/components/kubernetes_agent_info.vue', () => {
let wrapper;
- let agentQueryResponse;
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findAgentLink = () => wrapper.findComponent(GlLink);
const findAgentStatus = () => wrapper.findByTestId('agent-status');
const findAgentStatusIcon = () => findAgentStatus().findComponent(GlIcon);
const findAgentLastUsedDate = () => wrapper.findByTestId('agent-last-used-date');
- const findAlert = () => wrapper.findComponent(GlAlert);
-
- const createWrapper = ({ tokens = [], queryResponse = null } = {}) => {
- const clusterAgent = { ...mockClusterAgent, tokens: { nodes: tokens } };
-
- agentQueryResponse =
- queryResponse ||
- jest.fn().mockResolvedValue({ data: { project: { id: 'project-1', clusterAgent } } });
- const apolloProvider = createMockApollo([[getK8sClusterAgentQuery, agentQueryResponse]]);
+ const createWrapper = ({ tokens = [] } = {}) => {
wrapper = extendedWrapper(
shallowMount(KubernetesAgentInfo, {
- apolloProvider,
- propsData,
+ propsData: { clusterAgent: { ...defaultClusterAgent, tokens: { nodes: tokens } } },
stubs: { TimeAgoTooltip, GlSprintf },
}),
);
@@ -60,28 +37,9 @@ describe('~/environments/components/kubernetes_agent_info.vue', () => {
createWrapper();
});
- it('shows loading icon while fetching the agent details', async () => {
- expect(findLoadingIcon().exists()).toBe(true);
- await waitForPromises();
- expect(findLoadingIcon().exists()).toBe(false);
- });
-
- it('sends expected params', async () => {
- await waitForPromises();
-
- const variables = {
- agentName: propsData.agentName,
- projectPath: propsData.agentProjectPath,
- };
-
- expect(agentQueryResponse).toHaveBeenCalledWith(variables);
- });
-
- it('renders the agent name with the link', async () => {
- await waitForPromises();
-
- expect(findAgentLink().attributes('href')).toBe(mockClusterAgent.webPath);
- expect(findAgentLink().text()).toContain(mockClusterAgent.id);
+ it('renders the agent name with the link', () => {
+ expect(findAgentLink().attributes('href')).toBe(defaultClusterAgent.webPath);
+ expect(findAgentLink().text()).toContain('1');
});
});
@@ -110,15 +68,4 @@ describe('~/environments/components/kubernetes_agent_info.vue', () => {
expect(findAgentLastUsedDate().text()).toBe(lastUsedText);
});
});
-
- describe('when the agent query has errored', () => {
- beforeEach(() => {
- createWrapper({ clusterAgent: null, queryResponse: jest.fn().mockRejectedValue() });
- return waitForPromises();
- });
-
- it('displays an alert message', () => {
- expect(findAlert().text()).toBe(KubernetesAgentInfo.i18n.loadingError);
- });
- });
});
diff --git a/spec/frontend/environments/kubernetes_overview_spec.js b/spec/frontend/environments/kubernetes_overview_spec.js
index 394fd200edf..d4ba7323aaf 100644
--- a/spec/frontend/environments/kubernetes_overview_spec.js
+++ b/spec/frontend/environments/kubernetes_overview_spec.js
@@ -5,14 +5,12 @@ import KubernetesOverview from '~/environments/components/kubernetes_overview.vu
import KubernetesAgentInfo from '~/environments/components/kubernetes_agent_info.vue';
import KubernetesPods from '~/environments/components/kubernetes_pods.vue';
import KubernetesTabs from '~/environments/components/kubernetes_tabs.vue';
-import { agent } from './graphql/mock_data';
+import { agent, kubernetesNamespace } from './graphql/mock_data';
import { mockKasTunnelUrl } from './mock_data';
const propsData = {
- agentId: agent.id,
- agentName: agent.name,
- agentProjectPath: agent.project,
- namespace: agent.kubernetesNamespace,
+ clusterAgent: agent,
+ namespace: kubernetesNamespace,
};
const provide = {
@@ -91,23 +89,19 @@ describe('~/environments/components/kubernetes_overview.vue', () => {
});
it('renders kubernetes agent info', () => {
- expect(findAgentInfo().props()).toEqual({
- agentName: agent.name,
- agentId: agent.id,
- agentProjectPath: agent.project,
- });
+ expect(findAgentInfo().props('clusterAgent')).toEqual(agent);
});
it('renders kubernetes pods', () => {
expect(findKubernetesPods().props()).toEqual({
- namespace: agent.kubernetesNamespace,
+ namespace: kubernetesNamespace,
configuration,
});
});
it('renders kubernetes tabs', () => {
expect(findKubernetesTabs().props()).toEqual({
- namespace: agent.kubernetesNamespace,
+ namespace: kubernetesNamespace,
configuration,
});
});
diff --git a/spec/frontend/environments/new_environment_item_spec.js b/spec/frontend/environments/new_environment_item_spec.js
index 51d66043d0a..02100046167 100644
--- a/spec/frontend/environments/new_environment_item_spec.js
+++ b/spec/frontend/environments/new_environment_item_spec.js
@@ -3,6 +3,7 @@ import Vue from 'vue';
import { GlCollapse, GlIcon } from '@gitlab/ui';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import { stubTransition } from 'helpers/stub_transition';
import { formatDate, getTimeago } from '~/lib/utils/datetime_utility';
import { __, s__, sprintf } from '~/locale';
@@ -11,6 +12,7 @@ import EnvironmentActions from '~/environments/components/environment_actions.vu
import Deployment from '~/environments/components/deployment.vue';
import DeployBoardWrapper from '~/environments/components/deploy_board_wrapper.vue';
import KubernetesOverview from '~/environments/components/kubernetes_overview.vue';
+import getEnvironmentClusterAgent from '~/environments/graphql/queries/environment_cluster_agent.query.graphql';
import { resolvedEnvironment, rolloutStatus, agent } from './graphql/mock_data';
import { mockKasTunnelUrl } from './mock_data';
@@ -18,9 +20,24 @@ Vue.use(VueApollo);
describe('~/environments/components/new_environment_item.vue', () => {
let wrapper;
+ let queryResponseHandler;
- const createApolloProvider = () => {
- return createMockApollo();
+ const projectPath = '/1';
+
+ const createApolloProvider = (clusterAgent = null) => {
+ const response = {
+ data: {
+ project: {
+ id: '1',
+ environment: {
+ id: '1',
+ clusterAgent,
+ },
+ },
+ },
+ };
+ queryResponseHandler = jest.fn().mockResolvedValue(response);
+ return createMockApollo([[getEnvironmentClusterAgent, queryResponseHandler]]);
};
const createWrapper = ({ propsData = {}, provideData = {}, apolloProvider } = {}) =>
@@ -30,7 +47,7 @@ describe('~/environments/components/new_environment_item.vue', () => {
provide: {
helpPagePath: '/help',
projectId: '1',
- projectPath: '/1',
+ projectPath,
kasTunnelUrl: mockKasTunnelUrl,
...provideData,
},
@@ -501,68 +518,69 @@ describe('~/environments/components/new_environment_item.vue', () => {
});
describe('kubernetes overview', () => {
- const environmentWithAgent = {
- ...resolvedEnvironment,
- agent,
- };
-
- it('should render if the feature flag is enabled and the environment has an agent object with the required data specified', () => {
+ it('should request agent data when the environment is visible if the feature flag is enabled', async () => {
wrapper = createWrapper({
- propsData: { environment: environmentWithAgent },
+ propsData: { environment: resolvedEnvironment },
provideData: {
glFeatures: {
kasUserAccessProject: true,
},
},
- apolloProvider: createApolloProvider(),
+ apolloProvider: createApolloProvider(agent),
});
- expandCollapsedSection();
+ await expandCollapsedSection();
- expect(findKubernetesOverview().props()).toMatchObject({
- agentProjectPath: agent.project,
- agentName: agent.name,
- agentId: agent.id,
- namespace: agent.kubernetesNamespace,
+ expect(queryResponseHandler).toHaveBeenCalledWith({
+ environmentName: resolvedEnvironment.name,
+ projectFullPath: projectPath,
});
});
- it('should not render if the feature flag is not enabled', () => {
+ it('should render if the feature flag is enabled and the environment has an agent associated', async () => {
wrapper = createWrapper({
- propsData: { environment: environmentWithAgent },
- apolloProvider: createApolloProvider(),
+ propsData: { environment: resolvedEnvironment },
+ provideData: {
+ glFeatures: {
+ kasUserAccessProject: true,
+ },
+ },
+ apolloProvider: createApolloProvider(agent),
});
- expandCollapsedSection();
+ await expandCollapsedSection();
+ await waitForPromises();
- expect(findKubernetesOverview().exists()).toBe(false);
+ expect(findKubernetesOverview().props()).toMatchObject({
+ clusterAgent: agent,
+ });
});
- it('should not render if the environment has no agent object', () => {
+ it('should not render if the feature flag is not enabled', async () => {
wrapper = createWrapper({
- apolloProvider: createApolloProvider(),
+ propsData: { environment: resolvedEnvironment },
+ apolloProvider: createApolloProvider(agent),
});
- expandCollapsedSection();
+ await expandCollapsedSection();
+ expect(queryResponseHandler).not.toHaveBeenCalled();
expect(findKubernetesOverview().exists()).toBe(false);
});
- it('should not render if the environment has an agent object without agent id specified', () => {
- const environment = {
- ...resolvedEnvironment,
- agent: {
- project: agent.project,
- name: agent.name,
- },
- };
-
+ it('should not render if the environment has no agent object', async () => {
wrapper = createWrapper({
- propsData: { environment },
+ propsData: { environment: resolvedEnvironment },
+ provideData: {
+ glFeatures: {
+ kasUserAccessProject: true,
+ },
+ },
apolloProvider: createApolloProvider(),
});
- expandCollapsedSection();
+ await expandCollapsedSection();
+ await waitForPromises();
expect(findKubernetesOverview().exists()).toBe(false);
});
diff --git a/spec/lib/api/entities/nuget/metadatum_spec.rb b/spec/lib/api/entities/nuget/metadatum_spec.rb
index 210ff0abdd3..cb4e53a1960 100644
--- a/spec/lib/api/entities/nuget/metadatum_spec.rb
+++ b/spec/lib/api/entities/nuget/metadatum_spec.rb
@@ -2,9 +2,11 @@
require 'spec_helper'
-RSpec.describe API::Entities::Nuget::Metadatum do
+RSpec.describe API::Entities::Nuget::Metadatum, feature_category: :package_registry do
let(:metadatum) do
{
+ authors: 'Authors',
+ description: 'Description',
project_url: 'http://sandbox.com/project',
license_url: 'http://sandbox.com/license',
icon_url: 'http://sandbox.com/icon'
@@ -13,6 +15,8 @@ RSpec.describe API::Entities::Nuget::Metadatum do
let(:expected) do
{
+ 'authors': 'Authors',
+ 'summary': 'Description',
'projectUrl': 'http://sandbox.com/project',
'licenseUrl': 'http://sandbox.com/license',
'iconUrl': 'http://sandbox.com/icon'
@@ -27,11 +31,27 @@ RSpec.describe API::Entities::Nuget::Metadatum do
%i[project_url license_url icon_url].each do |optional_field|
context "metadatum without #{optional_field}" do
- let(:metadatum_without_a_field) { metadatum.except(optional_field) }
- let(:expected_without_a_field) { expected.except(optional_field.to_s.camelize(:lower).to_sym) }
- let(:entity) { described_class.new(metadatum_without_a_field) }
+ let(:metadatum) { super().merge(optional_field => nil) }
- it { is_expected.to eq(expected_without_a_field) }
+ it { is_expected.not_to have_key(optional_field.to_s.camelize(:lower).to_sym) }
+ end
+ end
+
+ describe 'authors' do
+ context 'with default value' do
+ let(:metadatum) { super().merge(authors: nil) }
+
+ it { is_expected.to have_key(:authors) }
+ it { is_expected.to eq(expected.merge(authors: '')) }
+ end
+ end
+
+ describe 'description' do
+ context 'with default value' do
+ let(:metadatum) { super().merge(description: nil) }
+
+ it { is_expected.to have_key(:summary) }
+ it { is_expected.to eq(expected.merge(summary: '')) }
end
end
end
diff --git a/spec/lib/api/entities/nuget/package_metadata_catalog_entry_spec.rb b/spec/lib/api/entities/nuget/package_metadata_catalog_entry_spec.rb
index c422b51bf3b..2fad42f907b 100644
--- a/spec/lib/api/entities/nuget/package_metadata_catalog_entry_spec.rb
+++ b/spec/lib/api/entities/nuget/package_metadata_catalog_entry_spec.rb
@@ -2,18 +2,19 @@
require 'spec_helper'
-RSpec.describe API::Entities::Nuget::PackageMetadataCatalogEntry do
+RSpec.describe API::Entities::Nuget::PackageMetadataCatalogEntry, feature_category: :package_registry do
let(:entry) do
{
json_url: 'http://sandbox.com/json/package',
- authors: 'Authors',
dependency_groups: [],
package_name: 'PackageTest',
package_version: '1.2.3',
tags: 'tag1 tag2 tag3',
archive_url: 'http://sandbox.com/archive/package',
- summary: 'Summary',
+ published: '2022-10-05T18:40:32.43+00:00',
metadatum: {
+ authors: 'Authors',
+ description: 'Summary',
project_url: 'http://sandbox.com/project',
license_url: 'http://sandbox.com/license',
icon_url: 'http://sandbox.com/icon'
@@ -33,7 +34,8 @@ RSpec.describe API::Entities::Nuget::PackageMetadataCatalogEntry do
'summary': 'Summary',
'projectUrl': 'http://sandbox.com/project',
'licenseUrl': 'http://sandbox.com/license',
- 'iconUrl': 'http://sandbox.com/icon'
+ 'iconUrl': 'http://sandbox.com/icon',
+ 'published': '2022-10-05T18:40:32.43+00:00'
}
end
diff --git a/spec/lib/api/entities/nuget/search_result_spec.rb b/spec/lib/api/entities/nuget/search_result_spec.rb
index a24cd44be9e..5edff28824f 100644
--- a/spec/lib/api/entities/nuget/search_result_spec.rb
+++ b/spec/lib/api/entities/nuget/search_result_spec.rb
@@ -2,11 +2,10 @@
require 'spec_helper'
-RSpec.describe API::Entities::Nuget::SearchResult do
+RSpec.describe API::Entities::Nuget::SearchResult, feature_category: :package_registry do
let(:search_result) do
{
type: 'Package',
- authors: 'Author',
name: 'PackageTest',
version: '1.2.3',
versions: [
@@ -16,11 +15,12 @@ RSpec.describe API::Entities::Nuget::SearchResult do
version: '1.2.3'
}
],
- summary: 'Summary',
total_downloads: 100,
verified: true,
tags: 'tag1 tag2 tag3',
metadatum: {
+ authors: 'Author',
+ description: 'Description',
project_url: 'http://sandbox.com/project',
license_url: 'http://sandbox.com/license',
icon_url: 'http://sandbox.com/icon'
@@ -34,7 +34,7 @@ RSpec.describe API::Entities::Nuget::SearchResult do
'authors': 'Author',
'id': 'PackageTest',
'title': 'PackageTest',
- 'summary': 'Summary',
+ 'summary': 'Description',
'totalDownloads': 100,
'verified': true,
'version': '1.2.3',
diff --git a/spec/lib/gitlab/database/query_analyzers/prevent_cross_database_modification_spec.rb b/spec/lib/gitlab/database/query_analyzers/prevent_cross_database_modification_spec.rb
index 02bd6b51463..3ccdb907cba 100644
--- a/spec/lib/gitlab/database/query_analyzers/prevent_cross_database_modification_spec.rb
+++ b/spec/lib/gitlab/database/query_analyzers/prevent_cross_database_modification_spec.rb
@@ -57,13 +57,19 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio
end
end
- shared_examples 'cross-database modification errors' do |model:|
+ shared_examples 'cross-database modification errors' do |model:, sql_log_contains:|
let(:model) { model }
context "within #{model} transaction" do
it 'raises error' do
model.transaction do
- expect { run_queries }.to raise_error /Cross-database data modification/
+ expect { run_queries }.to raise_error do |error|
+ expect(error.message).to include 'Cross-database data modification'
+
+ sql_log_contains.each do |sql_query|
+ expect(error.message).to match sql_query
+ end
+ end
end
end
end
@@ -87,7 +93,8 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio
include_examples 'successful examples', model: Ci::Pipeline
- include_examples 'cross-database modification errors', model: Project
+ include_examples 'cross-database modification errors', model: Project,
+ sql_log_contains: [/UPDATE "ci_pipelines"/]
end
context 'when other data is modified' do
@@ -98,7 +105,8 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio
include_examples 'successful examples', model: Project
- include_examples 'cross-database modification errors', model: Ci::Pipeline
+ include_examples 'cross-database modification errors', model: Ci::Pipeline,
+ sql_log_contains: [/UPDATE "projects"/]
end
context 'when both CI and other data is modified' do
@@ -112,11 +120,8 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio
end
context 'when data modification happens in a transaction' do
- it 'raises error' do
- Project.transaction do
- expect { run_queries }.to raise_error /Cross-database data modification/
- end
- end
+ include_examples 'cross-database modification errors', model: Project,
+ sql_log_contains: [/UPDATE "projects"/, /UPDATE "ci_pipelines"/]
context 'when ci_pipelines are ignored for cross modification' do
it 'does not raise error' do
@@ -131,11 +136,16 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio
end
context 'when data modification happens in nested transactions' do
- it 'raises error' do
+ it 'raises error, with the generated sql queries included' do
Project.transaction(requires_new: true) do
project.touch
Project.transaction(requires_new: true) do
- expect { pipeline.touch }.to raise_error /Cross-database data modification/
+ expect { pipeline.touch }.to raise_error do |error|
+ expect(error.message).to include('Cross-database data modification')
+
+ expect(error.message).to match(/UPDATE "projects"/)
+ expect(error.message).to match(/UPDATE "ci_pipelines"/)
+ end
end
end
end
@@ -151,11 +161,8 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio
Marginalia::Comment.prepend_comment = prepend_comment_was
end
- it 'raises error' do
- Project.transaction do
- expect { run_queries }.to raise_error /Cross-database data modification/
- end
- end
+ include_examples 'cross-database modification errors', model: Project,
+ sql_log_contains: [/UPDATE "projects"/, /UPDATE "ci_pipelines"/]
end
end
@@ -170,11 +177,8 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio
end
context 'when data modification happens in a transaction' do
- it 'raises error' do
- Project.transaction do
- expect { run_queries }.to raise_error /Cross-database data modification/
- end
- end
+ include_examples 'cross-database modification errors', model: Project,
+ sql_log_contains: [/UPDATE "projects"/, /SELECT "ci_pipelines"."id".*FOR UPDATE/]
context 'when the modification is inside a factory save! call' do
let(:runner) { create(:ci_runner, :project, projects: [build(:project)]) }
@@ -194,7 +198,8 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio
include_examples 'successful examples', model: Ci::Pipeline
- include_examples 'cross-database modification errors', model: Project
+ include_examples 'cross-database modification errors', model: Project,
+ sql_log_contains: [/INSERT INTO "ci_variables"/]
end
describe '.allow_cross_database_modification_within_transaction' do
diff --git a/spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb b/spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb
index 98fb154fb05..b8829cc794c 100644
--- a/spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb
+++ b/spec/lib/gitlab/form_builders/gitlab_ui_form_builder_spec.rb
@@ -127,8 +127,8 @@ RSpec.describe Gitlab::FormBuilders::GitlabUiFormBuilder do
form_builder.gitlab_ui_checkbox_component(
:view_diffs_file_by_file
) do |c|
- c.label { "Show one file at a time on merge request's Changes tab" }
- c.help_text { 'Instead of all the files changed, show only one file at a time.' }
+ c.with_label { "Show one file at a time on merge request's Changes tab" }
+ c.with_help_text { 'Instead of all the files changed, show only one file at a time.' }
end
end
@@ -208,8 +208,8 @@ RSpec.describe Gitlab::FormBuilders::GitlabUiFormBuilder do
:access_level,
:admin
) do |c|
- c.label { "Admin" }
- c.help_text { 'Administrators have access to all groups, projects, and users and can manage all features in this installation' }
+ c.with_label { "Admin" }
+ c.with_help_text { 'Administrators have access to all groups, projects, and users and can manage all features in this installation' }
end
end
diff --git a/spec/migrations/20230505115558_add_authors_and_description_to_nuget_metadatum_spec.rb b/spec/migrations/20230505115558_add_authors_and_description_to_nuget_metadatum_spec.rb
new file mode 100644
index 00000000000..11e8ec39476
--- /dev/null
+++ b/spec/migrations/20230505115558_add_authors_and_description_to_nuget_metadatum_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe AddAuthorsAndDescriptionToNugetMetadatum, feature_category: :package_registry do
+ let(:metadatum) { table(:packages_nuget_metadata) }
+
+ it 'correctly migrates up and down' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(metadatum.column_names).not_to include('authors')
+ expect(metadatum.column_names).not_to include('description')
+ }
+
+ migration.after -> {
+ metadatum.reset_column_information
+
+ expect(metadatum.column_names).to include('authors')
+ expect(metadatum.column_names).to include('description')
+ }
+ end
+ end
+end
diff --git a/spec/models/analytics/cycle_analytics/value_stream_spec.rb b/spec/models/analytics/cycle_analytics/value_stream_spec.rb
index e32fbef30ae..f290cf25ae6 100644
--- a/spec/models/analytics/cycle_analytics/value_stream_spec.rb
+++ b/spec/models/analytics/cycle_analytics/value_stream_spec.rb
@@ -27,6 +27,20 @@ RSpec.describe Analytics::CycleAnalytics::ValueStream, type: :model, feature_cat
end
end
+ describe 'scopes' do
+ let_it_be(:group) { create(:group) }
+
+ describe '.order_by_name_asc' do
+ let_it_be(:stream1) { create(:cycle_analytics_value_stream, namespace: group, name: 'Bbb') }
+ let_it_be(:stream2) { create(:cycle_analytics_value_stream, namespace: group, name: 'aaa') }
+ let_it_be(:stream3) { create(:cycle_analytics_value_stream, namespace: group, name: 'Aaa') }
+
+ it 'returns in case-insensitive alphabetical order' do
+ expect(described_class.order_by_name_asc).to eq [stream2, stream3, stream1]
+ end
+ end
+ end
+
describe 'ordering of stages' do
let(:group) { create(:group) }
let(:value_stream) do
diff --git a/spec/models/packages/nuget/metadatum_spec.rb b/spec/models/packages/nuget/metadatum_spec.rb
index c1bc5429500..6c652f78849 100644
--- a/spec/models/packages/nuget/metadatum_spec.rb
+++ b/spec/models/packages/nuget/metadatum_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Nuget::Metadatum, type: :model do
+RSpec.describe Packages::Nuget::Metadatum, type: :model, feature_category: :package_registry do
describe 'relationships' do
it { is_expected.to belong_to(:package).inverse_of(:nuget_metadatum) }
end
@@ -10,6 +10,11 @@ RSpec.describe Packages::Nuget::Metadatum, type: :model do
describe 'validations' do
it { is_expected.to validate_presence_of(:package) }
+ it { is_expected.to validate_presence_of(:authors) }
+ it { is_expected.to validate_length_of(:authors).is_at_most(described_class::MAX_AUTHORS_LENGTH) }
+ it { is_expected.to validate_presence_of(:description) }
+ it { is_expected.to validate_length_of(:description).is_at_most(described_class::MAX_DESCRIPTION_LENGTH) }
+
%i[license_url project_url icon_url].each do |url|
describe "##{url}" do
it { is_expected.to allow_value('http://sandbox.com').for(url) }
@@ -18,17 +23,6 @@ RSpec.describe Packages::Nuget::Metadatum, type: :model do
it { is_expected.not_to allow_value('sandbox.com').for(url) }
end
- describe '#ensure_at_least_one_field_supplied' do
- subject { build(:nuget_metadatum) }
-
- it 'rejects unfilled metadatum' do
- subject.attributes = { license_url: nil, project_url: nil, icon_url: nil }
-
- expect(subject).not_to be_valid
- expect(subject.errors).to contain_exactly('Nuget metadatum must have at least license_url, project_url or icon_url set')
- end
- end
-
describe '#ensure_nuget_package_type' do
subject { build(:nuget_metadatum) }
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index d07a4e9f207..ee8d811971a 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -3283,6 +3283,32 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
end
end
+ describe ':read_model_experiments' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:ff_ml_experiment_tracking, :current_user, :access_level, :allowed) do
+ false | ref(:owner) | Featurable::ENABLED | false
+ true | ref(:guest) | Featurable::ENABLED | true
+ true | ref(:guest) | Featurable::PRIVATE | true
+ true | ref(:guest) | Featurable::DISABLED | false
+ true | ref(:non_member) | Featurable::ENABLED | true
+ true | ref(:non_member) | Featurable::PRIVATE | false
+ true | ref(:non_member) | Featurable::DISABLED | false
+ end
+ with_them do
+ before do
+ stub_feature_flags(ml_experiment_tracking: ff_ml_experiment_tracking)
+ project.project_feature.update!(model_experiments_access_level: access_level)
+ end
+
+ if params[:allowed]
+ it { is_expected.to be_allowed(:read_model_experiments) }
+ else
+ it { is_expected.not_to be_allowed(:read_model_experiments) }
+ end
+ end
+ end
+
private
def project_subject(project_type)
diff --git a/spec/presenters/packages/nuget/package_metadata_presenter_spec.rb b/spec/presenters/packages/nuget/package_metadata_presenter_spec.rb
index 6c56763e719..616fb8e8e4e 100644
--- a/spec/presenters/packages/nuget/package_metadata_presenter_spec.rb
+++ b/spec/presenters/packages/nuget/package_metadata_presenter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Nuget::PackageMetadataPresenter do
+RSpec.describe Packages::Nuget::PackageMetadataPresenter, feature_category: :package_registry do
include_context 'with expected presenters dependency groups'
let_it_be(:package) { create(:nuget_package, :with_symbol_package, :with_metadatum) }
@@ -44,13 +44,12 @@ RSpec.describe Packages::Nuget::PackageMetadataPresenter do
expect(entry).to be_a Hash
%i[json_url archive_url].each { |field| expect(entry[field]).not_to be_blank }
- %i[authors summary].each { |field| expect(entry[field]).to be_blank }
expect(entry[:dependency_groups]).to eq expected_dependency_groups(package.project_id, package.name, package.version)
expect(entry[:package_name]).to eq package.name
expect(entry[:package_version]).to eq package.version
expect(entry[:tags].split(::Packages::Tag::NUGET_TAGS_SEPARATOR)).to contain_exactly('tag1', 'tag2')
- %i[project_url license_url icon_url].each do |field|
+ %i[authors description project_url license_url icon_url].each do |field|
expect(entry.dig(:metadatum, field)).to eq(package.nuget_metadatum.send(field))
end
end
diff --git a/spec/presenters/packages/nuget/search_results_presenter_spec.rb b/spec/presenters/packages/nuget/search_results_presenter_spec.rb
index 745914c6c43..08c7d71599f 100644
--- a/spec/presenters/packages/nuget/search_results_presenter_spec.rb
+++ b/spec/presenters/packages/nuget/search_results_presenter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Nuget::SearchResultsPresenter do
+RSpec.describe Packages::Nuget::SearchResultsPresenter, feature_category: :package_registry do
let_it_be(:project) { create(:project) }
let_it_be(:package_a) { create(:nuget_package, :with_metadatum, project: project, name: 'DummyPackageA') }
let_it_be(:tag1) { create(:packages_tag, package: package_a, name: 'tag1') }
@@ -30,15 +30,12 @@ RSpec.describe Packages::Nuget::SearchResultsPresenter do
expect_package_result(pkg_c, packages_c.first.name, packages_c.map(&:version))
end
- # rubocop:disable Metrics/AbcSize
def expect_package_result(package_json, name, versions, tags = [], with_metadatum: false)
expect(package_json[:type]).to eq 'Package'
- expect(package_json[:authors]).to be_blank
expect(package_json[:name]).to eq(name)
- expect(package_json[:summary]).to be_blank
expect(package_json[:total_downloads]).to eq 0
- expect(package_json[:verified]).to be
- expect(package_json[:version]).to eq VersionSorter.sort(versions).last # rubocop: disable Style/RedundantSort
+ expect(package_json[:verified]).to be_truthy
+ expect(package_json[:version]).to eq VersionSorter.sort(versions).last
versions.zip(package_json[:versions]).each do |version, version_json|
expect(version_json[:json_url]).to end_with("#{version}.json")
expect(version_json[:downloads]).to eq 0
@@ -51,10 +48,9 @@ RSpec.describe Packages::Nuget::SearchResultsPresenter do
expect(package_json[:tags]).to be_blank
end
- %i[project_url license_url icon_url].each do |field|
+ %i[authors description project_url license_url icon_url].each do |field|
expect(package_json.dig(:metadatum, field)).to with_metadatum ? be_present : be_blank
end
end
- # rubocop:enable Metrics/AbcSize
end
end
diff --git a/spec/services/ci/runners/assign_runner_service_spec.rb b/spec/services/ci/runners/assign_runner_service_spec.rb
index 92f6db2bdfb..00fbb5e2d26 100644
--- a/spec/services/ci/runners/assign_runner_service_spec.rb
+++ b/spec/services/ci/runners/assign_runner_service_spec.rb
@@ -3,10 +3,12 @@
require 'spec_helper'
RSpec.describe ::Ci::Runners::AssignRunnerService, '#execute', feature_category: :runner_fleet do
- subject(:execute) { described_class.new(runner, project, user).execute }
+ subject(:execute) { described_class.new(runner, new_project, user).execute }
- let_it_be(:runner) { create(:ci_runner, :project, projects: [project]) }
- let_it_be(:project) { create(:project) }
+ let_it_be(:owner_group) { create(:group) }
+ let_it_be(:owner_project) { create(:project, group: owner_group) }
+ let_it_be(:new_project) { create(:project) }
+ let_it_be(:runner) { create(:ci_runner, :project, projects: [owner_project]) }
context 'without user' do
let(:user) { nil }
@@ -30,11 +32,54 @@ RSpec.describe ::Ci::Runners::AssignRunnerService, '#execute', feature_category:
end
end
+ context 'with authorized user' do
+ let(:user) { create(:user) }
+
+ context 'with user owning runner and being maintainer of new project' do
+ before do
+ owner_project.group.add_owner(user)
+ new_project.add_maintainer(user)
+ end
+
+ it 'calls assign_to on runner and returns success response' do
+ expect(runner).to receive(:assign_to).with(new_project, user).once.and_call_original
+
+ is_expected.to be_success
+ end
+ end
+
+ context 'with user owning runner' do
+ before do
+ owner_project.add_maintainer(user)
+ end
+
+ it 'does not call assign_to on runner and returns error message', :aggregate_failures do
+ expect(runner).not_to receive(:assign_to)
+
+ is_expected.to be_error
+ expect(execute.message).to eq('user not allowed to add runners to project')
+ end
+ end
+
+ context 'with user being maintainer of new project', :aggregate_failures do
+ before do
+ new_project.add_maintainer(user)
+ end
+
+ it 'does not call assign_to on runner and returns error message' do
+ expect(runner).not_to receive(:assign_to)
+
+ is_expected.to be_error
+ expect(execute.message).to eq('user not allowed to assign runner')
+ end
+ end
+ end
+
context 'with admin user', :enable_admin_mode do
- let(:user) { create_default(:user, :admin) }
+ let(:user) { create(:user, :admin) }
it 'calls assign_to on runner and returns success response' do
- expect(runner).to receive(:assign_to).with(project, user).once.and_call_original
+ expect(runner).to receive(:assign_to).with(new_project, user).once.and_call_original
is_expected.to be_success
end
diff --git a/spec/services/packages/nuget/metadata_extraction_service_spec.rb b/spec/services/packages/nuget/metadata_extraction_service_spec.rb
index 9177a5379d9..8954b89971e 100644
--- a/spec/services/packages/nuget/metadata_extraction_service_spec.rb
+++ b/spec/services/packages/nuget/metadata_extraction_service_spec.rb
@@ -10,10 +10,16 @@ RSpec.describe Packages::Nuget::MetadataExtractionService, feature_category: :pa
describe '#execute' do
subject { service.execute }
+ shared_examples 'raises an error' do |error_message|
+ it { expect { subject }.to raise_error(described_class::ExtractionError, error_message) }
+ end
+
context 'with valid package file id' do
expected_metadata = {
package_name: 'DummyProject.DummyPackage',
package_version: '1.0.0',
+ authors: 'Test',
+ description: 'This is a dummy project',
package_dependencies: [
{
name: 'Newtonsoft.Json',
@@ -72,15 +78,23 @@ RSpec.describe Packages::Nuget::MetadataExtractionService, feature_category: :pa
allow(service).to receive(:nuspec_file_content).and_return(fixture_file(nuspec_filepath))
end
- it { expect(subject[:license_url]).to eq('https://opensource.org/licenses/MIT') }
- it { expect(subject[:project_url]).to eq('https://gitlab.com/gitlab-org/gitlab') }
- it { expect(subject[:icon_url]).to eq('https://opensource.org/files/osi_keyhole_300X300_90ppi_0.png') }
+ it 'returns the correct metadata' do
+ expected_metadata = {
+ authors: 'Author Test',
+ description: 'Description Test',
+ license_url: 'https://opensource.org/licenses/MIT',
+ project_url: 'https://gitlab.com/gitlab-org/gitlab',
+ icon_url: 'https://opensource.org/files/osi_keyhole_300X300_90ppi_0.png'
+ }
+
+ expect(subject.slice(*expected_metadata.keys)).to eq(expected_metadata)
+ end
end
context 'with invalid package file id' do
let(:package_file) { double('file', id: 555) }
- it { expect { subject }.to raise_error(::Packages::Nuget::MetadataExtractionService::ExtractionError, 'invalid package file') }
+ it_behaves_like 'raises an error', 'invalid package file'
end
context 'linked to a non nuget package' do
@@ -88,7 +102,7 @@ RSpec.describe Packages::Nuget::MetadataExtractionService, feature_category: :pa
package_file.package.maven!
end
- it { expect { subject }.to raise_error(::Packages::Nuget::MetadataExtractionService::ExtractionError, 'invalid package file') }
+ it_behaves_like 'raises an error', 'invalid package file'
end
context 'with a 0 byte package file id' do
@@ -96,7 +110,7 @@ RSpec.describe Packages::Nuget::MetadataExtractionService, feature_category: :pa
allow_any_instance_of(Packages::PackageFileUploader).to receive(:size).and_return(0)
end
- it { expect { subject }.to raise_error(::Packages::Nuget::MetadataExtractionService::ExtractionError, 'invalid package file') }
+ it_behaves_like 'raises an error', 'invalid package file'
end
context 'without the nuspec file' do
@@ -104,7 +118,7 @@ RSpec.describe Packages::Nuget::MetadataExtractionService, feature_category: :pa
allow_any_instance_of(Zip::File).to receive(:glob).and_return([])
end
- it { expect { subject }.to raise_error(::Packages::Nuget::MetadataExtractionService::ExtractionError, 'nuspec file not found') }
+ it_behaves_like 'raises an error', 'nuspec file not found'
end
context 'with a too big nuspec file' do
@@ -112,18 +126,17 @@ RSpec.describe Packages::Nuget::MetadataExtractionService, feature_category: :pa
allow_any_instance_of(Zip::File).to receive(:glob).and_return([double('file', size: 6.megabytes)])
end
- it { expect { subject }.to raise_error(::Packages::Nuget::MetadataExtractionService::ExtractionError, 'nuspec file too big') }
+ it_behaves_like 'raises an error', 'nuspec file too big'
end
context 'with a corrupted nupkg file with a wrong entry size' do
let(:nupkg_fixture_path) { expand_fixture_path('packages/nuget/corrupted_package.nupkg') }
- let(:expected_error) { "nuspec file has the wrong entry size: entry 'DummyProject.DummyPackage.nuspec' should be 255B, but is larger when inflated." }
before do
allow(Zip::File).to receive(:new).and_return(Zip::File.new(nupkg_fixture_path, false, false))
end
- it { expect { subject }.to raise_error(::Packages::Nuget::MetadataExtractionService::ExtractionError, expected_error) }
+ it_behaves_like 'raises an error', "nuspec file has the wrong entry size: entry 'DummyProject.DummyPackage.nuspec' should be 255B, but is larger when inflated."
end
end
end
diff --git a/spec/services/packages/nuget/sync_metadatum_service_spec.rb b/spec/services/packages/nuget/sync_metadatum_service_spec.rb
index ae07f312fcc..bf352d134c0 100644
--- a/spec/services/packages/nuget/sync_metadatum_service_spec.rb
+++ b/spec/services/packages/nuget/sync_metadatum_service_spec.rb
@@ -6,6 +6,8 @@ RSpec.describe Packages::Nuget::SyncMetadatumService, feature_category: :package
let_it_be(:package, reload: true) { create(:nuget_package) }
let_it_be(:metadata) do
{
+ authors: 'Package authors',
+ description: 'Package description',
project_url: 'https://test.org/test',
license_url: 'https://test.org/MIT',
icon_url: 'https://test.org/icon.png'
@@ -53,5 +55,39 @@ RSpec.describe Packages::Nuget::SyncMetadatumService, feature_category: :package
end
end
end
+
+ context 'with metadata containing only authors and description' do
+ let_it_be(:metadata) { { authors: 'Package authors 2', description: 'Package description 2' } }
+
+ it 'updates the nuget metadatum' do
+ subject
+
+ expect(nuget_metadatum.authors).to eq('Package authors 2')
+ expect(nuget_metadatum.description).to eq('Package description 2')
+ end
+ end
+
+ context 'with too long metadata' do
+ let(:metadata) { super().merge(authors: 'a' * 260, description: 'a' * 4010) }
+ let(:max_authors_length) { ::Packages::Nuget::Metadatum::MAX_AUTHORS_LENGTH }
+ let(:max_description_length) { ::Packages::Nuget::Metadatum::MAX_DESCRIPTION_LENGTH }
+
+ it 'truncates authors and description to the maximum length and logs its info' do
+ %i[authors description].each do |field|
+ expect(Gitlab::AppLogger).to receive(:info).with(
+ class: described_class.name,
+ package_id: package.id,
+ project_id: package.project_id,
+ message: "#{field.capitalize} is too long (maximum is #{send("max_#{field}_length")} characters)",
+ field => metadata[field]
+ )
+ end
+
+ subject
+
+ expect(nuget_metadatum.authors.size).to eq(max_authors_length)
+ expect(nuget_metadatum.description.size).to eq(max_description_length)
+ end
+ end
end
end
diff --git a/spec/services/packages/nuget/update_package_from_metadata_service_spec.rb b/spec/services/packages/nuget/update_package_from_metadata_service_spec.rb
index c35863030b0..aebd5457075 100644
--- a/spec/services/packages/nuget/update_package_from_metadata_service_spec.rb
+++ b/spec/services/packages/nuget/update_package_from_metadata_service_spec.rb
@@ -38,7 +38,7 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_
shared_examples 'not updating the package if the lease is taken' do
context 'without obtaining the exclusive lease' do
let(:lease_key) { "packages:nuget:update_package_from_metadata_service:package:#{package_id}" }
- let(:metadata) { { package_name: package_name, package_version: package_version } }
+ let(:metadata) { { package_name: package_name, package_version: package_version, authors: 'author1, author2', description: 'test description' } }
let(:package_from_package_file) { package_file.package }
before do
@@ -66,12 +66,12 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_
context 'with no existing package' do
let(:package_id) { package.id }
- it 'updates package and package file', :aggregate_failures do
+ it 'updates package and package file and creates metadatum', :aggregate_failures do
expect { subject }
.to not_change { ::Packages::Package.count }
.and change { Packages::Dependency.count }.by(1)
.and change { Packages::DependencyLink.count }.by(1)
- .and change { ::Packages::Nuget::Metadatum.count }.by(0)
+ .and change { ::Packages::Nuget::Metadatum.count }.by(1)
expect(package.reload.name).to eq(package_name)
expect(package.version).to eq(package_version)
@@ -98,7 +98,7 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_
.and change { Packages::Dependency.count }.by(0)
.and change { Packages::DependencyLink.count }.by(0)
.and change { Packages::Nuget::DependencyLinkMetadatum.count }.by(0)
- .and change { ::Packages::Nuget::Metadatum.count }.by(0)
+ .and change { ::Packages::Nuget::Metadatum.count }.by(1)
expect(package_file.reload.file_name).to eq(package_file_name)
expect(package_file.package).to eq(existing_package)
end
@@ -117,7 +117,7 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_
.to not_change { ::Packages::Package.count }
.and change { Packages::Dependency.count }.by(1)
.and change { Packages::DependencyLink.count }.by(1)
- .and change { ::Packages::Nuget::Metadatum.count }.by(0)
+ .and change { ::Packages::Nuget::Metadatum.count }.by(1)
end
end
end
@@ -158,6 +158,8 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_
.and change { ::Packages::Nuget::Metadatum.count }.by(1)
metadatum = package_file.reload.package.nuget_metadatum
+ expect(metadatum.authors).to eq('Author Test')
+ expect(metadatum.description).to eq('Description Test')
expect(metadatum.license_url).to eq('https://opensource.org/licenses/MIT')
expect(metadatum.project_url).to eq('https://gitlab.com/gitlab-org/gitlab')
expect(metadatum.icon_url).to eq('https://opensource.org/files/osi_keyhole_300X300_90ppi_0.png')
@@ -172,7 +174,19 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_
allow(service).to receive(:metadata).and_return(metadata)
end
- it_behaves_like 'raising an', ::Packages::Nuget::UpdatePackageFromMetadataService::InvalidMetadataError
+ it_behaves_like 'raising an', described_class::InvalidMetadataError
+ end
+
+ context 'without authors or description' do
+ %i[authors description].each do |property|
+ let(:metadata) { { package_name: package_name, package_version: package_version, license_url: 'http://localhost/', property => nil } }
+
+ before do
+ allow(service).to receive(:metadata).and_return(metadata)
+ end
+
+ it_behaves_like 'raising an', described_class::InvalidMetadataError
+ end
end
end
@@ -222,13 +236,17 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_
context 'with no existing package' do
let(:package_id) { package.id }
- it_behaves_like 'raising an', ::Packages::Nuget::UpdatePackageFromMetadataService::InvalidMetadataError
+ it_behaves_like 'raising an', described_class::InvalidMetadataError
end
context 'with existing package' do
let!(:existing_package) { create(:nuget_package, project: package.project, name: package_name, version: package_version) }
let(:package_id) { existing_package.id }
+ before do
+ allow(service).to receive(:metadata).and_return(service.send(:metadata).merge(authors: 'Author Test'))
+ end
+
it 'link existing package and updates package file', :aggregate_failures do
expect(service).to receive(:try_obtain_lease).and_call_original
expect(::Packages::Nuget::SyncMetadatumService).not_to receive(:new)
@@ -264,7 +282,7 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_
allow(service).to receive(:package_name).and_return(invalid_name)
end
- it_behaves_like 'raising an', ::Packages::Nuget::UpdatePackageFromMetadataService::InvalidMetadataError
+ it_behaves_like 'raising an', described_class::InvalidMetadataError
end
end
end
@@ -284,7 +302,7 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_
allow(service).to receive(:package_version).and_return(invalid_version)
end
- it_behaves_like 'raising an', ::Packages::Nuget::UpdatePackageFromMetadataService::InvalidMetadataError
+ it_behaves_like 'raising an', described_class::InvalidMetadataError
end
end
end
diff --git a/spec/support/shared_examples/features/runners_shared_examples.rb b/spec/support/shared_examples/features/runners_shared_examples.rb
index db685a4d0a1..94e5df8c7ae 100644
--- a/spec/support/shared_examples/features/runners_shared_examples.rb
+++ b/spec/support/shared_examples/features/runners_shared_examples.rb
@@ -207,7 +207,7 @@ RSpec.shared_examples 'submits edit runner form' do
context 'when a runner is updated', :js do
before do
- find('[data-testid="runner-field-description"] input').set('new-runner-description')
+ fill_in s_('Runners|Runner description'), with: 'new-runner-description'
click_on _('Save changes')
wait_for_requests
diff --git a/yarn.lock b/yarn.lock
index 207e554e458..863c16e7ac2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1005,10 +1005,10 @@
resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.0.1.tgz#b6b8d81780b9a9f6459f4bfe9226ac6aefaefe87"
integrity sha512-aG20vknL4/YjQF9BSV7ts4EWm/yrjagAN7OWBNmlbEOUiu0llj4OGrFoOKK3g2vey4/p2omKCoHrWtPxSwV3HA==
-"@cubejs-client/core@^0.33.0":
- version "0.33.0"
- resolved "https://registry.yarnpkg.com/@cubejs-client/core/-/core-0.33.0.tgz#f6edd051e75cd104ae0e7ceae36244ee5d391485"
- integrity sha512-481ENJFSYnq4cg2Z6VcTn0U6YKLSF4akMmLh8Pt8df2s8WTBXro0AamuINHYOhCfzmvPYl7e26x+wMYGG1ubMA==
+"@cubejs-client/core@^0.33.12":
+ version "0.33.12"
+ resolved "https://registry.yarnpkg.com/@cubejs-client/core/-/core-0.33.12.tgz#135044493e450087ab10f035ef6eb280c9066bf1"
+ integrity sha512-BY4aNEIjRTL7Yj9K7pF+fETPTpyoistUVBhjxOO5Yc3HSoQWoupetonl33FEw1CIq4PczYtVQVLvmkMuGi8gPA==
dependencies:
"@babel/runtime" "^7.1.2"
core-js "^3.6.5"
@@ -1018,12 +1018,12 @@
url-search-params-polyfill "^7.0.0"
uuid "^8.3.2"
-"@cubejs-client/vue@^0.33.0":
- version "0.33.0"
- resolved "https://registry.yarnpkg.com/@cubejs-client/vue/-/vue-0.33.0.tgz#57ed9a5466de912f8176acad022d36e33d0f49fd"
- integrity sha512-WPX2zQn5hYLwV0y3DaQy67AqOuRc0hA9taMFP9N6/cO3+N2DOg77ocRwgDt0/KmYRkaq4D37w87I9whAbocgtQ==
+"@cubejs-client/vue@^0.33.12":
+ version "0.33.12"
+ resolved "https://registry.yarnpkg.com/@cubejs-client/vue/-/vue-0.33.12.tgz#9b1618aebc7a310a9535b4a1011f715d33dc3410"
+ integrity sha512-YwMtc4/AyWaXJKEbd7rEizc5xZ0t9VcM7B77aIEa+rbdMNZEurEBvnCx+gidOgk1nQ75Y0mfKk4TrKRCWg09+Q==
dependencies:
- "@cubejs-client/core" "^0.33.0"
+ "@cubejs-client/core" "^0.33.12"
core-js "^3.6.5"
ramda "^0.27.2"