summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-07-02 12:09:02 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-07-02 12:09:02 +0000
commitb0107e8756bf3287f8a6221252c800209a9c46f6 (patch)
treef9bd9f37b44a946deaa39fab82085538a5e12105
parent6aab18704a534ea04f7477146aaa02104e738fcb (diff)
downloadgitlab-ce-b0107e8756bf3287f8a6221252c800209a9c46f6.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop.yml3
-rw-r--r--app/assets/javascripts/jobs/components/log/collapsible_section.vue2
-rw-r--r--app/assets/javascripts/jobs/components/log/log.vue6
-rw-r--r--app/assets/javascripts/jobs/store/actions.js2
-rw-r--r--app/assets/javascripts/jobs/store/utils.js4
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list.vue91
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue147
-rw-r--r--app/models/concerns/routable.rb9
-rw-r--r--app/models/project_statistics.rb7
-rw-r--r--app/workers/all_queues.yml2
-rw-r--r--app/workers/project_update_repository_storage_worker.rb1
-rw-r--r--changelogs/unreleased/an-throttle-project-update-repository-storage-worker.yml5
-rw-r--r--changelogs/unreleased/fix-routes-for-internal-users.yml5
-rw-r--r--db/fixtures/development/24_forks.rb33
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql75
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json206
-rw-r--r--doc/api/graphql/reference/index.md9
-rw-r--r--doc/customization/issue_and_merge_request_template.md4
-rw-r--r--doc/development/telemetry/usage_ping.md8
-rw-r--r--doc/user/project/description_templates.md2
-rw-r--r--lib/gitlab/seeder.rb3
-rw-r--r--lib/gitlab/usage_data_concerns/topology.rb11
-rw-r--r--locale/gitlab.pot18
-rw-r--r--package.json2
-rw-r--r--qa/qa/resource/api_fabricator.rb14
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/jira_issue_import_spec.rb2
-rw-r--r--qa/spec/runtime/api/request_spec.rb6
-rw-r--r--rubocop/cop/graphql/authorize_types.rb9
-rw-r--r--spec/frontend/jobs/components/log/collapsible_section_spec.js4
-rw-r--r--spec/frontend/jobs/store/utils_spec.js4
-rw-r--r--spec/frontend/pages/projects/forks/new/components/fork_groups_list_item_spec.js78
-rw-r--r--spec/frontend/pages/projects/forks/new/components/fork_groups_list_spec.js133
-rw-r--r--spec/lib/gitlab/usage_data_concerns/topology_spec.rb14
-rw-r--r--spec/models/project_statistics_spec.rb20
-rw-r--r--spec/models/user_spec.rb6
-rw-r--r--spec/rubocop/cop/graphql/authorize_types_spec.rb109
-rw-r--r--yarn.lock8
37 files changed, 938 insertions, 124 deletions
diff --git a/.rubocop.yml b/.rubocop.yml
index 438f27b0c87..bff2b7a32b1 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -328,6 +328,9 @@ Cop/SidekiqOptionsQueue:
Graphql/AuthorizeTypes:
Enabled: true
+ Include:
+ - 'app/graphql/types/**/*'
+ - 'ee/app/graphql/types/**/*'
Exclude:
- 'spec/**/*.rb'
- 'ee/spec/**/*.rb'
diff --git a/app/assets/javascripts/jobs/components/log/collapsible_section.vue b/app/assets/javascripts/jobs/components/log/collapsible_section.vue
index 0c7b78a3da7..55cdfb691f4 100644
--- a/app/assets/javascripts/jobs/components/log/collapsible_section.vue
+++ b/app/assets/javascripts/jobs/components/log/collapsible_section.vue
@@ -3,7 +3,7 @@ import LogLine from './line.vue';
import LogLineHeader from './line_header.vue';
export default {
- name: 'CollpasibleLogSection',
+ name: 'CollapsibleLogSection',
components: {
LogLine,
LogLineHeader,
diff --git a/app/assets/javascripts/jobs/components/log/log.vue b/app/assets/javascripts/jobs/components/log/log.vue
index f0bdbde0602..0134e5dafe8 100644
--- a/app/assets/javascripts/jobs/components/log/log.vue
+++ b/app/assets/javascripts/jobs/components/log/log.vue
@@ -1,11 +1,11 @@
<script>
import { mapState, mapActions } from 'vuex';
-import CollpasibleLogSection from './collapsible_section.vue';
+import CollapsibleLogSection from './collapsible_section.vue';
import LogLine from './line.vue';
export default {
components: {
- CollpasibleLogSection,
+ CollapsibleLogSection,
LogLine,
},
computed: {
@@ -51,7 +51,7 @@ export default {
<template>
<code class="job-log d-block" data-qa-selector="job_log_content">
<template v-for="(section, index) in trace">
- <collpasible-log-section
+ <collapsible-log-section
v-if="section.isHeader"
:key="`collapsible-${index}`"
:section="section"
diff --git a/app/assets/javascripts/jobs/store/actions.js b/app/assets/javascripts/jobs/store/actions.js
index 0ce8dfe4442..4bd8d6f58a6 100644
--- a/app/assets/javascripts/jobs/store/actions.js
+++ b/app/assets/javascripts/jobs/store/actions.js
@@ -195,7 +195,7 @@ export const receiveTraceError = ({ dispatch }) => {
flash(__('An error occurred while fetching the job log.'));
};
/**
- * When the user clicks a collpasible line in the job
+ * When the user clicks a collapsible line in the job
* log, we commit a mutation to update the state
*
* @param {Object} section
diff --git a/app/assets/javascripts/jobs/store/utils.js b/app/assets/javascripts/jobs/store/utils.js
index 3b6b8a2c851..8d6e5aac566 100644
--- a/app/assets/javascripts/jobs/store/utils.js
+++ b/app/assets/javascripts/jobs/store/utils.js
@@ -11,7 +11,7 @@ export const parseLine = (line = {}, lineNumber) => ({
/**
* When a line has `section_header` set to true, we create a new
* structure to allow to nest the lines that belong to the
- * collpasible section
+ * collapsible section
*
* @param Object line
* @param Number lineNumber
@@ -91,7 +91,7 @@ export const getIncrementalLineNumber = acc => {
* Parses the job log content into a structure usable by the template
*
* For collaspible lines (section_header = true):
- * - creates a new array to hold the lines that are collpasible,
+ * - creates a new array to hold the lines that are collapsible,
* - adds a isClosed property to handle toggle
* - adds a isHeader property to handle template logic
* - adds the section_duration
diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list.vue
new file mode 100644
index 00000000000..77753521342
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list.vue
@@ -0,0 +1,91 @@
+<script>
+import { GlTabs, GlTab, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
+import axios from '~/lib/utils/axios_utils';
+import { __ } from '~/locale';
+import createFlash from '~/flash';
+import ForkGroupsListItem from './fork_groups_list_item.vue';
+
+export default {
+ components: {
+ GlTabs,
+ GlTab,
+ GlLoadingIcon,
+ GlSearchBoxByType,
+ ForkGroupsListItem,
+ },
+ props: {
+ hasReachedProjectLimit: {
+ type: Boolean,
+ required: true,
+ },
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ namespaces: null,
+ filter: '',
+ };
+ },
+ computed: {
+ filteredNamespaces() {
+ return this.namespaces.filter(n => n.name.toLowerCase().includes(this.filter.toLowerCase()));
+ },
+ },
+
+ mounted() {
+ this.loadGroups();
+ },
+
+ methods: {
+ loadGroups() {
+ axios
+ .get(this.endpoint)
+ .then(response => {
+ this.namespaces = response.data.namespaces;
+ })
+ .catch(() => createFlash(__('There was a problem fetching groups.')));
+ },
+ },
+
+ i18n: {
+ searchPlaceholder: __('Search by name'),
+ },
+};
+</script>
+<template>
+ <gl-tabs class="fork-groups">
+ <gl-tab :title="__('Groups and subgroups')">
+ <gl-loading-icon v-if="!namespaces" size="md" class="gl-mt-3" />
+ <template v-else-if="namespaces.length === 0">
+ <div class="gl-text-center">
+ <div class="h5">{{ __('No available groups to fork the project.') }}</div>
+ <p class="gl-mt-5">
+ {{ __('You must have permission to create a project in a group before forking.') }}
+ </p>
+ </div>
+ </template>
+ <div v-else-if="filteredNamespaces.length === 0" class="gl-text-center gl-mt-3">
+ {{ s__('GroupsTree|No groups matched your search') }}
+ </div>
+ <ul v-else class="groups-list group-list-tree">
+ <fork-groups-list-item
+ v-for="(namespace, index) in filteredNamespaces"
+ :key="index"
+ :group="namespace"
+ :has-reached-project-limit="hasReachedProjectLimit"
+ />
+ </ul>
+ </gl-tab>
+ <template #tabs-end>
+ <gl-search-box-by-type
+ v-if="namespaces && namespaces.length"
+ v-model="filter"
+ :placeholder="$options.i18n.searchPlaceholder"
+ class="gl-align-self-center gl-ml-auto fork-filtered-search"
+ />
+ </template>
+ </gl-tabs>
+</template>
diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue
new file mode 100644
index 00000000000..792c2f3db34
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue
@@ -0,0 +1,147 @@
+<script>
+import {
+ GlLink,
+ GlButton,
+ GlIcon,
+ GlAvatar,
+ GlTooltipDirective,
+ GlTooltip,
+ GlBadge,
+} from '@gitlab/ui';
+import { VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE } from '~/groups/constants';
+import { __ } from '~/locale';
+import csrf from '~/lib/utils/csrf';
+
+export default {
+ components: {
+ GlIcon,
+ GlAvatar,
+ GlBadge,
+ GlButton,
+ GlTooltip,
+ GlLink,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ group: {
+ type: Object,
+ required: true,
+ },
+ hasReachedProjectLimit: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ data() {
+ return { namespaces: null };
+ },
+
+ computed: {
+ rowClass() {
+ return {
+ 'has-description': this.group.description,
+ 'being-removed': this.isGroupPendingRemoval,
+ };
+ },
+ isGroupPendingRemoval() {
+ return this.group.marked_for_deletion;
+ },
+ hasForkedProject() {
+ return Boolean(this.group.forked_project_path);
+ },
+ visibilityIcon() {
+ return VISIBILITY_TYPE_ICON[this.group.visibility];
+ },
+ visibilityTooltip() {
+ return GROUP_VISIBILITY_TYPE[this.group.visibility];
+ },
+ isSelectButtonDisabled() {
+ return this.hasReachedProjectLimit || !this.group.can_create_project;
+ },
+ selectButtonDisabledTooltip() {
+ return this.hasReachedProjectLimit
+ ? this.$options.i18n.hasReachedProjectLimitMessage
+ : this.$options.i18n.insufficientPermissionsMessage;
+ },
+ },
+
+ i18n: {
+ hasReachedProjectLimitMessage: __('You have reached your project limit'),
+ insufficientPermissionsMessage: __(
+ 'You must have permission to create a project in a namespace before forking.',
+ ),
+ },
+
+ csrf,
+};
+</script>
+<template>
+ <li :class="rowClass" class="group-row">
+ <div class="group-row-contents gl-display-flex gl-align-items-center gl-py-3 gl-pr-5">
+ <div class="folder-toggle-wrap gl-mr-2 gl-display-flex gl-align-items-center">
+ <gl-icon name="folder-o" />
+ </div>
+ <gl-link
+ :href="group.relative_path"
+ class="gl-display-none gl-flex-shrink-0 gl-display-sm-flex gl-mr-3"
+ >
+ <gl-avatar :size="32" shape="rect" :entity-name="group.name" :src="group.avatarUrl" />
+ </gl-link>
+ <div class="gl-min-w-0 gl-display-flex gl-flex-grow-1 gl-flex-shrink-1 gl-align-items-center">
+ <div class="gl-min-w-0 gl-flex-grow-1 flex-shrink-1">
+ <div class="title gl-display-flex gl-align-items-center gl-flex-wrap gl-mr-3">
+ <gl-link :href="group.relative_path" class="gl-mt-3 gl-mr-3 gl-text-gray-900!">{{
+ group.full_name
+ }}</gl-link>
+ <gl-icon
+ v-gl-tooltip.hover.bottom
+ class="gl-mr-0 gl-inline-flex gl-mt-3 text-secondary"
+ :name="visibilityIcon"
+ :title="visibilityTooltip"
+ />
+ <gl-badge
+ v-if="isGroupPendingRemoval"
+ variant="warning"
+ class="gl-display-none gl-display-sm-flex gl-mt-3 gl-mr-1"
+ >{{ __('pending removal') }}</gl-badge
+ >
+ <span v-if="group.permission" class="user-access-role gl-mt-3">
+ {{ group.permission }}
+ </span>
+ </div>
+ <div v-if="group.description" class="description">
+ <span v-html="group.markdown_description"> </span>
+ </div>
+ </div>
+ <div class="gl-display-flex gl-flex-shrink-0">
+ <gl-button
+ v-if="hasForkedProject"
+ class="gl-h-7 gl-text-decoration-none!"
+ :href="group.forked_project_path"
+ >{{ __('Go to fork') }}</gl-button
+ >
+ <template v-else>
+ <div ref="selectButtonWrapper">
+ <form method="POST" :action="group.fork_path">
+ <input type="hidden" name="authenticity_token" :value="$options.csrf.token" />
+ <gl-button
+ type="submit"
+ class="gl-h-7 gl-text-decoration-none!"
+ :data-qa-name="group.full_name"
+ variant="success"
+ :disabled="isSelectButtonDisabled"
+ >{{ __('Select') }}</gl-button
+ >
+ </form>
+ </div>
+ <gl-tooltip v-if="isSelectButtonDisabled" :target="() => $refs.selectButtonWrapper">
+ {{ selectButtonDisabledTooltip }}
+ </gl-tooltip>
+ </template>
+ </div>
+ </div>
+ </div>
+ </li>
+</template>
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index 129d0fbb2c0..c70ce9bebcc 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -17,11 +17,8 @@ module Routable
after_validation :set_path_errors
- before_validation do
- if full_path_changed? || full_name_changed?
- prepare_route
- end
- end
+ before_validation :prepare_route
+ before_save :prepare_route # in case validation is skipped
end
class_methods do
@@ -118,6 +115,8 @@ module Routable
end
def prepare_route
+ return unless full_path_changed? || full_name_changed?
+
route || build_route(source: self)
route.path = build_full_path
route.name = build_full_name
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index 1d8e72c902a..5424abd069f 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -14,6 +14,7 @@ class ProjectStatistics < ApplicationRecord
COLUMNS_TO_REFRESH = [:repository_size, :wiki_size, :lfs_objects_size, :commit_count, :snippets_size].freeze
INCREMENTABLE_COLUMNS = { build_artifacts_size: %i[storage_size], packages_size: %i[storage_size] }.freeze
NAMESPACE_RELATABLE_COLUMNS = [:repository_size, :wiki_size, :lfs_objects_size].freeze
+ FLAGGED_NAMESPACE_RELATABLE_COLUMNS = [*NAMESPACE_RELATABLE_COLUMNS, :snippets_size].freeze
scope :for_project_ids, ->(project_ids) { where(project_id: project_ids) }
@@ -31,7 +32,7 @@ class ProjectStatistics < ApplicationRecord
end
end
- if only.empty? || only.any? { |column| NAMESPACE_RELATABLE_COLUMNS.include?(column) }
+ if only.empty? || only.any? { |column| namespace_relatable_columns.include?(column) }
schedule_namespace_aggregation_worker
end
@@ -110,6 +111,10 @@ class ProjectStatistics < ApplicationRecord
Namespaces::ScheduleAggregationWorker.perform_async(project.namespace_id)
end
end
+
+ def namespace_relatable_columns
+ Feature.enabled?(:namespace_snippets_size_stat) ? FLAGGED_NAMESPACE_RELATABLE_COLUMNS : NAMESPACE_RELATABLE_COLUMNS
+ end
end
ProjectStatistics.prepend_if_ee('EE::ProjectStatistics')
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index e3ef825a770..84c80335754 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -1527,7 +1527,7 @@
- :name: project_update_repository_storage
:feature_category: :gitaly
:has_external_dependencies:
- :urgency: :low
+ :urgency: :throttled
:resource_boundary: :unknown
:weight: 1
:idempotent: true
diff --git a/app/workers/project_update_repository_storage_worker.rb b/app/workers/project_update_repository_storage_worker.rb
index 4c44454fdcd..7c0b1ae07fa 100644
--- a/app/workers/project_update_repository_storage_worker.rb
+++ b/app/workers/project_update_repository_storage_worker.rb
@@ -5,6 +5,7 @@ class ProjectUpdateRepositoryStorageWorker
idempotent!
feature_category :gitaly
+ urgency :throttled
def perform(project_id, new_repository_storage_key, repository_storage_move_id = nil)
repository_storage_move =
diff --git a/changelogs/unreleased/an-throttle-project-update-repository-storage-worker.yml b/changelogs/unreleased/an-throttle-project-update-repository-storage-worker.yml
new file mode 100644
index 00000000000..f06c1e97b9e
--- /dev/null
+++ b/changelogs/unreleased/an-throttle-project-update-repository-storage-worker.yml
@@ -0,0 +1,5 @@
+---
+title: Throttle ProjectUpdateRepositoryStorageWorker Jobs
+merge_request: 35230
+author:
+type: other
diff --git a/changelogs/unreleased/fix-routes-for-internal-users.yml b/changelogs/unreleased/fix-routes-for-internal-users.yml
new file mode 100644
index 00000000000..5f96dd7f227
--- /dev/null
+++ b/changelogs/unreleased/fix-routes-for-internal-users.yml
@@ -0,0 +1,5 @@
+---
+title: Create associated routes when a new bot user is created
+merge_request: 35711
+author:
+type: fixed
diff --git a/db/fixtures/development/24_forks.rb b/db/fixtures/development/24_forks.rb
index cb6dbb7504d..536d9f9e2ba 100644
--- a/db/fixtures/development/24_forks.rb
+++ b/db/fixtures/development/24_forks.rb
@@ -10,17 +10,30 @@ Sidekiq::Testing.inline! do
# we use randomized approach (e.g. `Array#sample`).
return unless source_project
- fork_project = Projects::ForkService.new(
- source_project,
- user,
- namespace: user.namespace,
- skip_disk_validation: true
- ).execute
+ Sidekiq::Worker.skipping_transaction_check do
+ fork_project = Projects::ForkService.new(
+ source_project,
+ user,
+ namespace: user.namespace,
+ skip_disk_validation: true
+ ).execute
- if fork_project.valid?
- print '.'
- else
- print 'F'
+ # Seed-Fu runs this entire fixture in a transaction, so the `after_commit`
+ # hook won't run until after the fixture is loaded. That is too late
+ # since the Sidekiq::Testing block has already exited. Force clearing
+ # the `after_commit` queue to ensure the job is run now.
+ fork_project.send(:_run_after_commit_queue)
+ fork_project.import_state.send(:_run_after_commit_queue)
+
+ # Expire repository cache after import to ensure
+ # valid_repo? call below returns a correct answer
+ fork_project.repository.expire_all_method_caches
+
+ if fork_project.valid? && fork_project.valid_repo?
+ print '.'
+ else
+ print 'F'
+ end
end
end
end
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index 12b025e25b6..2429a0da8c6 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -11311,6 +11311,56 @@ type RunDASTScanPayload {
}
"""
+Represents a resource scanned by a security scan
+"""
+type ScannedResource {
+ """
+ The HTTP request method used to access the URL
+ """
+ requestMethod: String
+
+ """
+ The URL scanned by the scanner
+ """
+ url: String
+}
+
+"""
+The connection type for ScannedResource.
+"""
+type ScannedResourceConnection {
+ """
+ A list of edges.
+ """
+ edges: [ScannedResourceEdge]
+
+ """
+ A list of nodes.
+ """
+ nodes: [ScannedResource]
+
+ """
+ Information to aid in pagination.
+ """
+ pageInfo: PageInfo!
+}
+
+"""
+An edge in a connection.
+"""
+type ScannedResourceEdge {
+ """
+ A cursor for use in pagination.
+ """
+ cursor: String!
+
+ """
+ The item at the end of the edge.
+ """
+ node: ScannedResource
+}
+
+"""
Represents summary of a security report
"""
type SecurityReportSummary {
@@ -11350,6 +11400,31 @@ Represents a section of a summary of a security report
"""
type SecurityReportSummarySection {
"""
+ A list of the first 20 scanned resources
+ """
+ scannedResources(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+ ): ScannedResourceConnection
+
+ """
Total number of scanned resources
"""
scannedResourcesCount: Int
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index d91f0e74c2b..758be106669 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -33219,6 +33219,159 @@
},
{
"kind": "OBJECT",
+ "name": "ScannedResource",
+ "description": "Represents a resource scanned by a security scan",
+ "fields": [
+ {
+ "name": "requestMethod",
+ "description": "The HTTP request method used to access the URL",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "url",
+ "description": "The URL scanned by the scanner",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "ScannedResourceConnection",
+ "description": "The connection type for ScannedResource.",
+ "fields": [
+ {
+ "name": "edges",
+ "description": "A list of edges.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "ScannedResourceEdge",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nodes",
+ "description": "A list of nodes.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "ScannedResource",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "pageInfo",
+ "description": "Information to aid in pagination.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "PageInfo",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "ScannedResourceEdge",
+ "description": "An edge in a connection.",
+ "fields": [
+ {
+ "name": "cursor",
+ "description": "A cursor for use in pagination.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "node",
+ "description": "The item at the end of the edge.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "ScannedResource",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
"name": "SecurityReportSummary",
"description": "Represents summary of a security report",
"fields": [
@@ -33320,6 +33473,59 @@
"description": "Represents a section of a summary of a security report",
"fields": [
{
+ "name": "scannedResources",
+ "description": "A list of the first 20 scanned resources",
+ "args": [
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "ScannedResourceConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "scannedResourcesCount",
"description": "Total number of scanned resources",
"args": [
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 25d2ef9010f..5e1eafa0645 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -1637,6 +1637,15 @@ Autogenerated return type of RunDASTScan
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `pipelineUrl` | String | URL of the pipeline that was created. |
+## ScannedResource
+
+Represents a resource scanned by a security scan
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `requestMethod` | String | The HTTP request method used to access the URL |
+| `url` | String | The URL scanned by the scanner |
+
## SecurityReportSummary
Represents summary of a security report
diff --git a/doc/customization/issue_and_merge_request_template.md b/doc/customization/issue_and_merge_request_template.md
index ebf711e105b..bab81629221 100644
--- a/doc/customization/issue_and_merge_request_template.md
+++ b/doc/customization/issue_and_merge_request_template.md
@@ -1,5 +1,5 @@
---
-redirect_to: '../user/project/description_templates.md#setting-a-default-template-for-merge-requests-and-issues--starter'
+redirect_to: '../user/project/description_templates.md'
---
-This document was moved to [description_templates](../user/project/description_templates.md#setting-a-default-template-for-merge-requests-and-issues--starter).
+This document was moved to [description_templates](../user/project/description_templates.md).
diff --git a/doc/development/telemetry/usage_ping.md b/doc/development/telemetry/usage_ping.md
index 18e0f9e50fe..105bd915681 100644
--- a/doc/development/telemetry/usage_ping.md
+++ b/doc/development/telemetry/usage_ping.md
@@ -666,8 +666,9 @@ appear to be associated to any of the services running, since they all appear to
| `clusters_applications_runner` | `usage_activity_by_stage` | `verify` | | | Unique clusters with Runner enabled |
| `projects_reporting_ci_cd_back_to_github: 0` | `usage_activity_by_stage` | `verify` | | | Unique projects with a GitHub pipeline enabled |
| `merge_requests_users` | `usage_activity_by_stage_monthly` | `create` | | | Unique count of users who used a merge request |
-| `nodes` | `topology` | `enablement` | | | The list of server nodes on which GitLab components are running |
| `duration_s` | `topology` | `enablement` | | | Time it took to collect topology data |
+| `application_requests_per_hour` | `topology` | `enablement` | | | Number of requests to the web application per hour |
+| `nodes` | `topology` | `enablement` | | | The list of server nodes on which GitLab components are running |
| `node_memory_total_bytes` | `topology > nodes` | `enablement` | | | The total available memory of this node |
| `node_cpus` | `topology > nodes` | `enablement` | | | The number of CPU cores of this node |
| `node_services` | `topology > nodes` | `enablement` | | | The list of GitLab services running on this node |
@@ -873,6 +874,8 @@ The following is example content of the Usage Ping payload.
}
},
"topology": {
+ "duration_s": 0.013836685999194742,
+ "application_requests_per_hour": 4224,
"nodes": [
{
"node_memory_total_bytes": 33269903360,
@@ -897,8 +900,7 @@ The following is example content of the Usage Ping payload.
...
},
...
- ],
- "duration_s": 0.013836685999194742
+ ]
}
}
```
diff --git a/doc/user/project/description_templates.md b/doc/user/project/description_templates.md
index 0f90c321a14..aa5987bf5f9 100644
--- a/doc/user/project/description_templates.md
+++ b/doc/user/project/description_templates.md
@@ -81,7 +81,7 @@ changes you made after picking the template and return it to its initial status.
![Description templates](img/description_templates.png)
-## Setting a default template for merge requests and issues **(STARTER)**
+## Setting a default template for merge requests and issues **(STARTER)**
> - This feature was introduced before [description templates](#overview) and is available in [GitLab Starter](https://about.gitlab.com/pricing/). It can be enabled in the project's settings.
> - Templates for issues were [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28) in GitLab EE 8.1.
diff --git a/lib/gitlab/seeder.rb b/lib/gitlab/seeder.rb
index 53cbd5b21ea..d652719721e 100644
--- a/lib/gitlab/seeder.rb
+++ b/lib/gitlab/seeder.rb
@@ -18,6 +18,7 @@ module Gitlab
MASS_INSERT_PROJECT_START = 'mass_insert_project_'
MASS_INSERT_USER_START = 'mass_insert_user_'
+ REPORTED_USER_START = 'reported_user_'
ESTIMATED_INSERT_PER_MINUTE = 2_000_000
MASS_INSERT_ENV = 'MASS_INSERT'
@@ -36,7 +37,7 @@ module Gitlab
included do
scope :not_mass_generated, -> do
- where.not("username LIKE '#{MASS_INSERT_USER_START}%'")
+ where.not("username LIKE '#{MASS_INSERT_USER_START}%' OR username LIKE '#{REPORTED_USER_START}%'")
end
end
end
diff --git a/lib/gitlab/usage_data_concerns/topology.rb b/lib/gitlab/usage_data_concerns/topology.rb
index e05272d665f..d10fd56b5a2 100644
--- a/lib/gitlab/usage_data_concerns/topology.rb
+++ b/lib/gitlab/usage_data_concerns/topology.rb
@@ -28,11 +28,20 @@ module Gitlab
def topology_fetch_all_data
with_prometheus_client(fallback: {}) do |client|
{
+ application_requests_per_hour: topology_app_requests_per_hour(client),
nodes: topology_node_data(client)
- }
+ }.compact
end
end
+ def topology_app_requests_per_hour(client)
+ result = client.query(one_week_average('gitlab_usage_ping:ops:rate5m')).first
+ return unless result
+
+ # the metric is recorded as a per-second rate
+ (result['value'].last.to_f * 1.hour).to_i
+ end
+
def topology_node_data(client)
# node-level data
by_instance_mem = topology_node_memory(client)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 28f17414d15..5d081cc1266 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -10964,6 +10964,9 @@ msgstr ""
msgid "Go to find file"
msgstr ""
+msgid "Go to fork"
+msgstr ""
+
msgid "Go to issue boards"
msgstr ""
@@ -11531,6 +11534,9 @@ msgstr ""
msgid "Groups and projects"
msgstr ""
+msgid "Groups and subgroups"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -15131,6 +15137,9 @@ msgstr ""
msgid "No authentication methods configured."
msgstr ""
+msgid "No available groups to fork the project."
+msgstr ""
+
msgid "No available namespaces to fork the project."
msgstr ""
@@ -19831,6 +19840,9 @@ msgstr ""
msgid "Search by author"
msgstr ""
+msgid "Search by name"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -22979,6 +22991,9 @@ msgstr ""
msgid "There was a problem communicating with your device."
msgstr ""
+msgid "There was a problem fetching groups."
+msgstr ""
+
msgid "There was a problem fetching project branches."
msgstr ""
@@ -26256,6 +26271,9 @@ msgstr ""
msgid "You must have maintainer access to force delete a lock"
msgstr ""
+msgid "You must have permission to create a project in a group before forking."
+msgstr ""
+
msgid "You must have permission to create a project in a namespace before forking."
msgstr ""
diff --git a/package.json b/package.json
index 1d7471a1e05..680c850bd9d 100644
--- a/package.json
+++ b/package.json
@@ -40,7 +40,7 @@
"@babel/plugin-syntax-import-meta": "^7.10.1",
"@babel/preset-env": "^7.10.1",
"@gitlab/at.js": "1.5.5",
- "@gitlab/svgs": "1.146.0",
+ "@gitlab/svgs": "1.147.0",
"@gitlab/ui": "17.10.1",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "^6.0.3-1",
diff --git a/qa/qa/resource/api_fabricator.rb b/qa/qa/resource/api_fabricator.rb
index 591aa449219..babdfc96265 100644
--- a/qa/qa/resource/api_fabricator.rb
+++ b/qa/qa/resource/api_fabricator.rb
@@ -83,13 +83,13 @@ module QA
end
def api_get_from(get_path)
- url = Runtime::API::Request.new(api_client, get_path).url
- response = get(url)
+ request = Runtime::API::Request.new(api_client, get_path)
+ response = get(request.url)
if response.code == HTTP_STATUS_SERVER_ERROR
- raise InternalServerError, "Failed to GET #{url} - (#{response.code}): `#{response}`."
+ raise InternalServerError, "Failed to GET #{request.mask_url} - (#{response.code}): `#{response}`."
elsif response.code != HTTP_STATUS_OK
- raise ResourceNotFoundError, "Resource at #{url} could not be found (#{response.code}): `#{response}`."
+ raise ResourceNotFoundError, "Resource at #{request.mask_url} could not be found (#{response.code}): `#{response}`."
end
response
@@ -108,11 +108,11 @@ module QA
end
def api_delete
- url = Runtime::API::Request.new(api_client, api_delete_path).url
- response = delete(url)
+ request = Runtime::API::Request.new(api_client, api_delete_path)
+ response = delete(request.url)
unless [HTTP_STATUS_NO_CONTENT, HTTP_STATUS_ACCEPTED].include? response.code
- raise ResourceNotDeletedError, "Resource at #{url} could not be deleted (#{response.code}): `#{response}`."
+ raise ResourceNotDeletedError, "Resource at #{request.mask_url} could not be deleted (#{response.code}): `#{response}`."
end
response
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/jira_issue_import_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/jira_issue_import_spec.rb
index af09899100a..ba8e8635c87 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/jira_issue_import_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/jira_issue_import_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- context 'Plan', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/225409', type: :bug } do
+ context 'Plan' do
describe 'Jira issue import', :jira, :orchestrated, :requires_admin do
let(:jira_project_key) { "JITD" }
let(:jira_issue_title) { "[#{jira_project_key}-1] Jira to GitLab Test Issue" }
diff --git a/qa/spec/runtime/api/request_spec.rb b/qa/spec/runtime/api/request_spec.rb
index a20f1cf8559..8354eff6234 100644
--- a/qa/spec/runtime/api/request_spec.rb
+++ b/qa/spec/runtime/api/request_spec.rb
@@ -22,6 +22,12 @@ describe QA::Runtime::API::Request do
end
end
+ describe '#mask_url' do
+ it 'returns the full API request url with the token masked' do
+ expect(request.mask_url).to eq 'http://example.com/api/v4/users?private_token=[****]'
+ end
+ end
+
describe '#request_path' do
it 'prepends the api path' do
expect(request.request_path('/users')).to eq '/api/v4/users'
diff --git a/rubocop/cop/graphql/authorize_types.rb b/rubocop/cop/graphql/authorize_types.rb
index 7aaa9299362..c6dbe447b4a 100644
--- a/rubocop/cop/graphql/authorize_types.rb
+++ b/rubocop/cop/graphql/authorize_types.rb
@@ -7,8 +7,6 @@ module RuboCop
MSG = 'Add an `authorize :ability` call to the type: '\
'https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#type-authorization'
- TYPES_DIR = 'app/graphql/types'
-
# We want to exclude our own basetypes and scalars
WHITELISTED_TYPES = %w[BaseEnum BaseScalar BasePermissionType MutationType
QueryType GraphQL::Schema BaseUnion].freeze
@@ -18,7 +16,6 @@ module RuboCop
PATTERN
def on_class(node)
- return unless in_type?(node)
return if whitelisted?(class_constant(node))
return if whitelisted?(superclass_constant(node))
@@ -27,12 +24,6 @@ module RuboCop
private
- def in_type?(node)
- path = node.location.expression.source_buffer.name
-
- path.include? TYPES_DIR
- end
-
def whitelisted?(class_node)
class_const = class_node&.const_name
diff --git a/spec/frontend/jobs/components/log/collapsible_section_spec.js b/spec/frontend/jobs/components/log/collapsible_section_spec.js
index 3a16521a986..bf2f8c05806 100644
--- a/spec/frontend/jobs/components/log/collapsible_section_spec.js
+++ b/spec/frontend/jobs/components/log/collapsible_section_spec.js
@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
-import CollpasibleSection from '~/jobs/components/log/collapsible_section.vue';
+import CollapsibleSection from '~/jobs/components/log/collapsible_section.vue';
import { collapsibleSectionClosed, collapsibleSectionOpened } from './mock_data';
describe('Job Log Collapsible Section', () => {
@@ -11,7 +11,7 @@ describe('Job Log Collapsible Section', () => {
const findCollapsibleLineSvg = () => wrapper.find('.collapsible-line svg');
const createComponent = (props = {}) => {
- wrapper = mount(CollpasibleSection, {
+ wrapper = mount(CollapsibleSection, {
propsData: {
...props,
},
diff --git a/spec/frontend/jobs/store/utils_spec.js b/spec/frontend/jobs/store/utils_spec.js
index 8819f39dee0..294f88bbc74 100644
--- a/spec/frontend/jobs/store/utils_spec.js
+++ b/spec/frontend/jobs/store/utils_spec.js
@@ -181,7 +181,7 @@ describe('Jobs Store Utils', () => {
});
});
- describe('collpasible section', () => {
+ describe('collapsible section', () => {
it('adds a `isClosed` property', () => {
expect(result[1].isClosed).toEqual(false);
});
@@ -190,7 +190,7 @@ describe('Jobs Store Utils', () => {
expect(result[1].isHeader).toEqual(true);
});
- it('creates a lines array property with the content of the collpasible section', () => {
+ it('creates a lines array property with the content of the collapsible section', () => {
expect(result[1].lines.length).toEqual(2);
expect(result[1].lines[0].content).toEqual(utilsMockData[2].content);
expect(result[1].lines[1].content).toEqual(utilsMockData[3].content);
diff --git a/spec/frontend/pages/projects/forks/new/components/fork_groups_list_item_spec.js b/spec/frontend/pages/projects/forks/new/components/fork_groups_list_item_spec.js
new file mode 100644
index 00000000000..73e3c385d33
--- /dev/null
+++ b/spec/frontend/pages/projects/forks/new/components/fork_groups_list_item_spec.js
@@ -0,0 +1,78 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlBadge, GlButton, GlLink } from '@gitlab/ui';
+import ForkGroupsListItem from '~/pages/projects/forks/new/components/fork_groups_list_item.vue';
+
+describe('Fork groups list item component', () => {
+ let wrapper;
+
+ const DEFAULT_PROPS = {
+ hasReachedProjectLimit: false,
+ };
+
+ const DEFAULT_GROUP_DATA = {
+ id: 22,
+ name: 'Gitlab Org',
+ description: 'Ad et ipsam earum id aut nobis.',
+ visibility: 'public',
+ full_name: 'Gitlab Org',
+ created_at: '2020-06-22T03:32:05.664Z',
+ updated_at: '2020-06-22T03:32:05.664Z',
+ avatar_url: null,
+ fork_path: '/twitter/typeahead-js/-/forks?namespace_key=22',
+ forked_project_path: null,
+ permission: 'Owner',
+ relative_path: '/gitlab-org',
+ markdown_description:
+ '<p data-sourcepos="1:1-1:31" dir="auto">Ad et ipsam earum id aut nobis.</p>',
+ can_create_project: true,
+ marked_for_deletion: false,
+ };
+
+ const DUMMY_PATH = '/dummy/path';
+
+ const createWrapper = propsData => {
+ wrapper = shallowMount(ForkGroupsListItem, {
+ propsData: {
+ ...DEFAULT_PROPS,
+ ...propsData,
+ },
+ });
+ };
+
+ it('renders pending removal badge if applicable', () => {
+ createWrapper({ group: { ...DEFAULT_GROUP_DATA, marked_for_deletion: true } });
+
+ expect(wrapper.find(GlBadge).text()).toBe('pending removal');
+ });
+
+ it('renders go to fork button if has forked project', () => {
+ createWrapper({ group: { ...DEFAULT_GROUP_DATA, forked_project_path: DUMMY_PATH } });
+
+ expect(wrapper.find(GlButton).text()).toBe('Go to fork');
+ expect(wrapper.find(GlButton).attributes().href).toBe(DUMMY_PATH);
+ });
+
+ it('renders select button if has no forked project', () => {
+ createWrapper({
+ group: { ...DEFAULT_GROUP_DATA, forked_project_path: null, fork_path: DUMMY_PATH },
+ });
+
+ expect(wrapper.find(GlButton).text()).toBe('Select');
+ expect(wrapper.find('form').attributes().action).toBe(DUMMY_PATH);
+ });
+
+ it('renders link to current group', () => {
+ const DUMMY_FULL_NAME = 'dummy';
+ createWrapper({
+ group: { ...DEFAULT_GROUP_DATA, relative_path: DUMMY_PATH, full_name: DUMMY_FULL_NAME },
+ });
+
+ expect(
+ wrapper
+ .findAll(GlLink)
+ .filter(w => w.text() === DUMMY_FULL_NAME)
+ .at(0)
+ .attributes().href,
+ ).toBe(DUMMY_PATH);
+ });
+});
diff --git a/spec/frontend/pages/projects/forks/new/components/fork_groups_list_spec.js b/spec/frontend/pages/projects/forks/new/components/fork_groups_list_spec.js
new file mode 100644
index 00000000000..979dff78eba
--- /dev/null
+++ b/spec/frontend/pages/projects/forks/new/components/fork_groups_list_spec.js
@@ -0,0 +1,133 @@
+import AxiosMockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import { shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import createFlash from '~/flash';
+import ForkGroupsList from '~/pages/projects/forks/new/components/fork_groups_list.vue';
+import ForkGroupsListItem from '~/pages/projects/forks/new/components/fork_groups_list_item.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+
+jest.mock('~/flash', () => jest.fn());
+
+describe('Fork groups list component', () => {
+ let wrapper;
+ let axiosMock;
+
+ const DEFAULT_PROPS = {
+ endpoint: '/dummy',
+ hasReachedProjectLimit: false,
+ };
+
+ const replyWith = (...args) => axiosMock.onGet(DEFAULT_PROPS.endpoint).reply(...args);
+
+ const createWrapper = propsData => {
+ wrapper = shallowMount(ForkGroupsList, {
+ propsData: {
+ ...DEFAULT_PROPS,
+ ...propsData,
+ },
+ stubs: {
+ GlTabs: {
+ template: '<div><slot></slot><slot name="tabs-end"></slot></div>',
+ },
+ },
+ });
+ };
+
+ beforeEach(() => {
+ axiosMock = new AxiosMockAdapter(axios);
+ });
+
+ afterEach(() => {
+ axiosMock.reset();
+
+ if (wrapper) {
+ wrapper.destroy();
+ wrapper = null;
+ }
+ });
+
+ it('fires load groups request on mount', async () => {
+ replyWith(200, { namespaces: [] });
+ createWrapper();
+
+ await waitForPromises();
+
+ expect(axiosMock.history.get[0].url).toBe(DEFAULT_PROPS.endpoint);
+ });
+
+ it('displays flash if loading groups fails', async () => {
+ replyWith(500);
+ createWrapper();
+
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalled();
+ });
+
+ it('displays loading indicator while loading groups', () => {
+ replyWith(() => new Promise(() => {}));
+ createWrapper();
+
+ expect(wrapper.contains(GlLoadingIcon)).toBe(true);
+ });
+
+ it('displays empty text if no groups are available', async () => {
+ const EMPTY_TEXT = 'No available groups to fork the project.';
+ replyWith(200, { namespaces: [] });
+ createWrapper();
+
+ await waitForPromises();
+
+ expect(wrapper.text()).toContain(EMPTY_TEXT);
+ });
+
+ it('displays filter field when groups are available', async () => {
+ replyWith(200, { namespaces: [{ name: 'dummy1' }, { name: 'dummy2' }] });
+ createWrapper();
+
+ await waitForPromises();
+
+ expect(wrapper.contains(GlSearchBoxByType)).toBe(true);
+ });
+
+ it('renders list items for each available group', async () => {
+ const namespaces = [{ name: 'dummy1' }, { name: 'dummy2' }, { name: 'otherdummy' }];
+ const hasReachedProjectLimit = true;
+
+ replyWith(200, { namespaces });
+ createWrapper({ hasReachedProjectLimit });
+
+ await waitForPromises();
+
+ expect(wrapper.findAll(ForkGroupsListItem)).toHaveLength(namespaces.length);
+
+ namespaces.forEach((namespace, idx) => {
+ expect(
+ wrapper
+ .findAll(ForkGroupsListItem)
+ .at(idx)
+ .props(),
+ ).toStrictEqual({ group: namespace, hasReachedProjectLimit });
+ });
+ });
+
+ it('filters repositories on the fly', async () => {
+ replyWith(200, {
+ namespaces: [{ name: 'dummy1' }, { name: 'dummy2' }, { name: 'otherdummy' }],
+ });
+ createWrapper();
+ await waitForPromises();
+ wrapper.find(GlSearchBoxByType).vm.$emit('input', 'other');
+ await nextTick();
+
+ expect(wrapper.findAll(ForkGroupsListItem)).toHaveLength(1);
+ expect(
+ wrapper
+ .findAll(ForkGroupsListItem)
+ .at(0)
+ .props().group.name,
+ ).toBe('otherdummy');
+ });
+});
diff --git a/spec/lib/gitlab/usage_data_concerns/topology_spec.rb b/spec/lib/gitlab/usage_data_concerns/topology_spec.rb
index 5cc6f37b5c2..0aa3c5abe32 100644
--- a/spec/lib/gitlab/usage_data_concerns/topology_spec.rb
+++ b/spec/lib/gitlab/usage_data_concerns/topology_spec.rb
@@ -22,6 +22,7 @@ RSpec.describe Gitlab::UsageDataConcerns::Topology do
context 'tracking node metrics' do
it 'contains node level metrics for each instance' do
expect_prometheus_api_to(
+ receive_app_request_volume_query,
receive_node_memory_query,
receive_node_cpu_count_query,
receive_node_service_memory_rss_query,
@@ -32,6 +33,7 @@ RSpec.describe Gitlab::UsageDataConcerns::Topology do
expect(subject[:topology]).to eq({
duration_s: 0,
+ application_requests_per_hour: 36,
nodes: [
{
node_memory_total_bytes: 512,
@@ -76,6 +78,7 @@ RSpec.describe Gitlab::UsageDataConcerns::Topology do
context 'and some node memory metrics are missing' do
it 'removes the respective entries' do
expect_prometheus_api_to(
+ receive_app_request_volume_query(result: []),
receive_node_memory_query(result: []),
receive_node_cpu_count_query,
receive_node_service_memory_rss_query(result: []),
@@ -149,6 +152,17 @@ RSpec.describe Gitlab::UsageDataConcerns::Topology do
end
end
+ def receive_app_request_volume_query(result: nil)
+ receive(:query)
+ .with(/gitlab_usage_ping:ops:rate/)
+ .and_return(result || [
+ {
+ 'metric' => { 'component' => 'http_requests', 'service' => 'workhorse' },
+ 'value' => [1000, '0.01']
+ }
+ ])
+ end
+
def receive_node_memory_query(result: nil)
receive(:query)
.with(/node_memory_total_bytes/, an_instance_of(Hash))
diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb
index 3659e6b973e..52c64a6f0cb 100644
--- a/spec/models/project_statistics_spec.rb
+++ b/spec/models/project_statistics_spec.rb
@@ -189,6 +189,26 @@ RSpec.describe ProjectStatistics do
statistics.refresh!
end
end
+
+ context 'when snippets_size is updated' do
+ it 'schedules the aggregation worker' do
+ expect(Namespaces::ScheduleAggregationWorker)
+ .to receive(:perform_async)
+
+ statistics.refresh!(only: [:snippets_size])
+ end
+
+ context 'when feature flag :namespace_snippets_size_stat is disabled' do
+ it 'does not schedules an aggregation worker' do
+ stub_feature_flags(namespace_snippets_size_stat: false)
+
+ expect(Namespaces::ScheduleAggregationWorker)
+ .not_to receive(:perform_async)
+
+ statistics.refresh!(only: [:snippets_size])
+ end
+ end
+ end
end
context 'when the column is not namespace relatable' do
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 01a908a34d7..c0712755396 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -4766,6 +4766,12 @@ RSpec.describe User do
end.to change { User.where(user_type: bot_type).count }.by(1)
end
+ it 'creates a route for the namespace of the created user' do
+ bot_user = described_class.public_send(bot_type)
+
+ expect(bot_user.namespace.route).to be_present
+ end
+
it 'does not create a new user if it already exists' do
described_class.public_send(bot_type)
diff --git a/spec/rubocop/cop/graphql/authorize_types_spec.rb b/spec/rubocop/cop/graphql/authorize_types_spec.rb
index 5d35690c8b7..df637a26ec5 100644
--- a/spec/rubocop/cop/graphql/authorize_types_spec.rb
+++ b/spec/rubocop/cop/graphql/authorize_types_spec.rb
@@ -10,83 +10,60 @@ RSpec.describe RuboCop::Cop::Graphql::AuthorizeTypes, type: :rubocop do
subject(:cop) { described_class.new }
- context 'when NOT in a type folder' do
- before do
- allow(cop).to receive(:in_type?).and_return(false)
- end
-
- it 'does not add an offense even though there is no authorize call' do
- expect_no_offenses(<<~TYPE.strip)
- module Types
- class AType < BaseObject
- field :a_thing
- field :another_thing
- end
- end
- TYPE
- end
- end
-
- context 'when in a type folder' do
- before do
- allow(cop).to receive(:in_type?).and_return(true)
- end
-
- it 'adds an offense when there is no authorize call' do
- inspect_source(<<~TYPE)
- module Types
- class AType < BaseObject
- field :a_thing
- field :another_thing
- end
+ it 'adds an offense when there is no authorize call' do
+ inspect_source(<<~TYPE)
+ module Types
+ class AType < BaseObject
+ field :a_thing
+ field :another_thing
end
- TYPE
+ end
+ TYPE
- expect(cop.offenses.size).to eq 1
- end
+ expect(cop.offenses.size).to eq 1
+ end
- it 'does not add an offense for classes that have an authorize call' do
- expect_no_offenses(<<~TYPE.strip)
- module Types
- class AType < BaseObject
- graphql_name 'ATypeName'
+ it 'does not add an offense for classes that have an authorize call' do
+ expect_no_offenses(<<~TYPE.strip)
+ module Types
+ class AType < BaseObject
+ graphql_name 'ATypeName'
- authorize :an_ability, :second_ability
+ authorize :an_ability, :second_ability
- field :a_thing
- end
+ field :a_thing
end
- TYPE
- end
+ end
+ TYPE
+ end
- it 'does not add an offense for classes that only have an authorize call' do
- expect_no_offenses(<<~TYPE.strip)
- module Types
- class AType < SuperClassWithFields
- authorize :an_ability
- end
+ it 'does not add an offense for classes that only have an authorize call' do
+ expect_no_offenses(<<~TYPE.strip)
+ module Types
+ class AType < SuperClassWithFields
+ authorize :an_ability
end
- TYPE
- end
+ end
+ TYPE
+ end
- it 'does not add an offense for base types' do
- expect_no_offenses(<<~TYPE)
- module Types
- class AType < BaseEnum
- field :a_thing
- end
+ it 'does not add an offense for base types' do
+ expect_no_offenses(<<~TYPE)
+ module Types
+ class AType < BaseEnum
+ field :a_thing
end
- TYPE
- end
+ end
+ TYPE
+ end
- it 'does not add an offense for Enums' do
- expect_no_offenses(<<~TYPE)
- module Types
- class ATypeEnum < AnotherEnum
- field :a_thing
- end
+ it 'does not add an offense for Enums' do
+ expect_no_offenses(<<~TYPE)
+ module Types
+ class ATypeEnum < AnotherEnum
+ field :a_thing
end
- TYPE
- end
+ end
+ TYPE
end
end
diff --git a/yarn.lock b/yarn.lock
index 094949cbd18..a4de32ca84a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -843,10 +843,10 @@
eslint-plugin-vue "^6.2.1"
vue-eslint-parser "^7.0.0"
-"@gitlab/svgs@1.146.0":
- version "1.146.0"
- resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.146.0.tgz#c74118a3f1ab47ae77211d42597f553f395deb5d"
- integrity sha512-2/k9pAZPgHpZ5Ad0fz9i1109sWcShDE4XcjrjzltNNksbi86lqCKbsSe580ujtlG8KShgGMkDkmUa6AHZi64Xw==
+"@gitlab/svgs@1.147.0":
+ version "1.147.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.147.0.tgz#1b2cc986cb3219609136cab641e2c384d724700f"
+ integrity sha512-KnjN7ms7bEPajYl7q0nKv7HMKtqR/JxCVSBRGXH5ezkeGKy4wb4yEYtvRK8no7ix+Iw4rc0KTqOwKp9nkl/KdA==
"@gitlab/ui@17.10.1":
version "17.10.1"