Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
c568cb4dbc
commit
d089b5729e
|
@ -119,7 +119,6 @@ overrides:
|
||||||
- '@graphql-eslint'
|
- '@graphql-eslint'
|
||||||
parserOptions:
|
parserOptions:
|
||||||
parser: '@graphql-eslint/eslint-plugin'
|
parser: '@graphql-eslint/eslint-plugin'
|
||||||
schema: './tmp/tests/graphql/gitlab_schema_apollo.graphql'
|
|
||||||
operations:
|
operations:
|
||||||
- '{,ee/,jh/}app/**/*.graphql'
|
- '{,ee/,jh/}app/**/*.graphql'
|
||||||
# You can run `bundle exec rake gitlab:graphql:schema:dump` and then uncomment this line
|
# You can run `bundle exec rake gitlab:graphql:schema:dump` and then uncomment this line
|
||||||
|
|
|
@ -95,7 +95,7 @@ jsdoc/
|
||||||
webpack-dev-server.json
|
webpack-dev-server.json
|
||||||
/.nvimrc
|
/.nvimrc
|
||||||
.solargraph.yml
|
.solargraph.yml
|
||||||
./apollo.config.js
|
apollo.config.js
|
||||||
/tmp/matching_foss_tests.txt
|
/tmp/matching_foss_tests.txt
|
||||||
/tmp/matching_tests.txt
|
/tmp/matching_tests.txt
|
||||||
ee/changelogs/unreleased-ee
|
ee/changelogs/unreleased-ee
|
||||||
|
|
|
@ -51,18 +51,13 @@ eslint:
|
||||||
extends:
|
extends:
|
||||||
- .static-analysis-base
|
- .static-analysis-base
|
||||||
- .yarn-cache
|
- .yarn-cache
|
||||||
- .frontend:rules:default-frontend-jobs
|
- .static-analysis:rules:ee
|
||||||
needs: ['graphql-schema-dump']
|
needs: []
|
||||||
variables:
|
variables:
|
||||||
USE_BUNDLE_INSTALL: "false"
|
USE_BUNDLE_INSTALL: "false"
|
||||||
script:
|
script:
|
||||||
- run_timed_command "retry yarn install --frozen-lockfile"
|
- run_timed_command "retry yarn install --frozen-lockfile"
|
||||||
- yarn run apollo client:download-schema --config=config/apollo.config.js tmp/tests/graphql/gitlab_schema_apollo.graphql
|
|
||||||
- run_timed_command "yarn run lint:eslint:all"
|
- run_timed_command "yarn run lint:eslint:all"
|
||||||
artifacts:
|
|
||||||
name: graphql-schema-apollo
|
|
||||||
paths:
|
|
||||||
- tmp/tests/graphql/gitlab_schema_apollo.graphql
|
|
||||||
|
|
||||||
eslint as-if-foss:
|
eslint as-if-foss:
|
||||||
extends:
|
extends:
|
||||||
|
|
|
@ -176,7 +176,6 @@ Rails/TimeZone:
|
||||||
- 'ee/spec/lib/gitlab/geo/log_cursor/events/repository_renamed_event_spec.rb'
|
- 'ee/spec/lib/gitlab/geo/log_cursor/events/repository_renamed_event_spec.rb'
|
||||||
- 'ee/spec/lib/gitlab/geo/log_cursor/events/repository_updated_event_spec.rb'
|
- 'ee/spec/lib/gitlab/geo/log_cursor/events/repository_updated_event_spec.rb'
|
||||||
- 'ee/spec/lib/gitlab/geo/log_cursor/events/reset_checksum_event_spec.rb'
|
- 'ee/spec/lib/gitlab/geo/log_cursor/events/reset_checksum_event_spec.rb'
|
||||||
- 'ee/spec/lib/gitlab/geo/log_cursor/events/upload_deleted_event_spec.rb'
|
|
||||||
- 'ee/spec/lib/gitlab/geo/log_cursor/logger_spec.rb'
|
- 'ee/spec/lib/gitlab/geo/log_cursor/logger_spec.rb'
|
||||||
- 'ee/spec/lib/gitlab/git_access_spec.rb'
|
- 'ee/spec/lib/gitlab/git_access_spec.rb'
|
||||||
- 'ee/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb'
|
- 'ee/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb'
|
||||||
|
|
|
@ -1,7 +1,71 @@
|
||||||
<script>
|
<script>
|
||||||
export default {};
|
import { GlLoadingIcon, GlTable } from '@gitlab/ui';
|
||||||
|
import createFlash from '~/flash';
|
||||||
|
import { s__, __ } from '~/locale';
|
||||||
|
import getGroupOrganizationsQuery from './queries/get_group_organizations.query.graphql';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
GlLoadingIcon,
|
||||||
|
GlTable,
|
||||||
|
},
|
||||||
|
inject: ['groupFullPath'],
|
||||||
|
data() {
|
||||||
|
return { organizations: [] };
|
||||||
|
},
|
||||||
|
apollo: {
|
||||||
|
organizations: {
|
||||||
|
query() {
|
||||||
|
return getGroupOrganizationsQuery;
|
||||||
|
},
|
||||||
|
variables() {
|
||||||
|
return {
|
||||||
|
groupFullPath: this.groupFullPath,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
update(data) {
|
||||||
|
return this.extractOrganizations(data);
|
||||||
|
},
|
||||||
|
error(error) {
|
||||||
|
createFlash({
|
||||||
|
message: __('Something went wrong. Please try again.'),
|
||||||
|
error,
|
||||||
|
captureError: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isLoading() {
|
||||||
|
return this.$apollo.queries.organizations.loading;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
extractOrganizations(data) {
|
||||||
|
const organizations = data?.group?.organizations?.nodes || [];
|
||||||
|
return organizations.slice().sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{ key: 'name', sortable: true },
|
||||||
|
{ key: 'defaultRate', sortable: true },
|
||||||
|
{ key: 'description', sortable: true },
|
||||||
|
],
|
||||||
|
i18n: {
|
||||||
|
emptyText: s__('Crm|No organizations found'),
|
||||||
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div></div>
|
<div>
|
||||||
|
<gl-loading-icon v-if="isLoading" class="gl-mt-5" size="lg" />
|
||||||
|
<gl-table
|
||||||
|
v-else
|
||||||
|
:items="organizations"
|
||||||
|
:fields="$options.fields"
|
||||||
|
:empty-text="$options.i18n.emptyText"
|
||||||
|
show-empty
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
query organizations($groupFullPath: ID!) {
|
||||||
|
group(fullPath: $groupFullPath) {
|
||||||
|
__typename
|
||||||
|
id
|
||||||
|
organizations {
|
||||||
|
nodes {
|
||||||
|
__typename
|
||||||
|
id
|
||||||
|
name
|
||||||
|
defaultRate
|
||||||
|
description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,15 +1,25 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
import VueApollo from 'vue-apollo';
|
||||||
|
import createDefaultClient from '~/lib/graphql';
|
||||||
import CrmOrganizationsRoot from './components/organizations_root.vue';
|
import CrmOrganizationsRoot from './components/organizations_root.vue';
|
||||||
|
|
||||||
|
Vue.use(VueApollo);
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const el = document.getElementById('js-crm-organizations-app');
|
const el = document.getElementById('js-crm-organizations-app');
|
||||||
|
|
||||||
|
const apolloProvider = new VueApollo({
|
||||||
|
defaultClient: createDefaultClient(),
|
||||||
|
});
|
||||||
|
|
||||||
if (!el) {
|
if (!el) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Vue({
|
return new Vue({
|
||||||
el,
|
el,
|
||||||
|
apolloProvider,
|
||||||
|
provide: { groupFullPath: el.dataset.groupFullPath },
|
||||||
render(createElement) {
|
render(createElement) {
|
||||||
return createElement(CrmOrganizationsRoot);
|
return createElement(CrmOrganizationsRoot);
|
||||||
},
|
},
|
||||||
|
|
|
@ -63,9 +63,9 @@
|
||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"if": {
|
"if": { "$ref": "#/definitions/if" },
|
||||||
"type": "string"
|
"changes": { "$ref": "#/definitions/changes" },
|
||||||
},
|
"exists": { "$ref": "#/definitions/exists" },
|
||||||
"variables": { "$ref": "#/definitions/variables" },
|
"variables": { "$ref": "#/definitions/variables" },
|
||||||
"when": {
|
"when": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
@ -497,24 +497,9 @@
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
"if": {
|
"if": { "$ref": "#/definitions/if" },
|
||||||
"type": "string",
|
"changes": { "$ref": "#/definitions/changes" },
|
||||||
"description": "Expression to evaluate whether additional attributes should be provided to the job"
|
"exists": { "$ref": "#/definitions/exists" },
|
||||||
},
|
|
||||||
"changes": {
|
|
||||||
"type": "array",
|
|
||||||
"description": "Additional attributes will be provided to job if any of the provided paths matches a modified file",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"exists": {
|
|
||||||
"type": "array",
|
|
||||||
"description": "Additional attributes will be provided to job if any of the provided paths matches an existing file in the repository",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"variables": { "$ref": "#/definitions/variables" },
|
"variables": { "$ref": "#/definitions/variables" },
|
||||||
"when": { "$ref": "#/definitions/when" },
|
"when": { "$ref": "#/definitions/when" },
|
||||||
"start_in": { "$ref": "#/definitions/start_in" },
|
"start_in": { "$ref": "#/definitions/start_in" },
|
||||||
|
@ -541,6 +526,24 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"if": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Expression to evaluate whether additional attributes should be provided to the job"
|
||||||
|
},
|
||||||
|
"changes": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Additional attributes will be provided to job if any of the provided paths matches a modified file",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exists": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Additional attributes will be provided to job if any of the provided paths matches an existing file in the repository",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
"variables": {
|
"variables": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "Defines environment variables for specific jobs. Job level property overrides global variables. If a job sets `variables: {}`, all global variables are turned off.",
|
"description": "Defines environment variables for specific jobs. Job level property overrides global variables. If a job sets `variables: {}`, all global variables are turned off.",
|
||||||
|
|
|
@ -20,7 +20,7 @@ module Resolvers
|
||||||
description: 'Filter by permissions the user has on groups.'
|
description: 'Filter by permissions the user has on groups.'
|
||||||
|
|
||||||
before_connection_authorization do |nodes, current_user|
|
before_connection_authorization do |nodes, current_user|
|
||||||
Preloaders::UserMaxAccessLevelInGroupsPreloader.new(nodes, current_user).execute
|
Preloaders::GroupPolicyPreloader.new(nodes, current_user).execute
|
||||||
end
|
end
|
||||||
|
|
||||||
def ready?(**args)
|
def ready?(**args)
|
||||||
|
|
|
@ -30,6 +30,8 @@ class AuditEvent < ApplicationRecord
|
||||||
scope :by_entity_type, -> (entity_type) { where(entity_type: entity_type) }
|
scope :by_entity_type, -> (entity_type) { where(entity_type: entity_type) }
|
||||||
scope :by_entity_id, -> (entity_id) { where(entity_id: entity_id) }
|
scope :by_entity_id, -> (entity_id) { where(entity_id: entity_id) }
|
||||||
scope :by_author_id, -> (author_id) { where(author_id: author_id) }
|
scope :by_author_id, -> (author_id) { where(author_id: author_id) }
|
||||||
|
scope :by_entity_username, -> (username) { where(entity_id: find_user_id(username)) }
|
||||||
|
scope :by_author_username, -> (username) { where(author_id: find_user_id(username)) }
|
||||||
|
|
||||||
after_initialize :initialize_details
|
after_initialize :initialize_details
|
||||||
|
|
||||||
|
@ -106,6 +108,10 @@ class AuditEvent < ApplicationRecord
|
||||||
self[name] = self.details[name] = original
|
self[name] = self.details[name] = original
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.find_user_id(username)
|
||||||
|
User.find_by_username(username)&.id
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
AuditEvent.prepend_mod_with('AuditEvent')
|
AuditEvent.prepend_mod_with('AuditEvent')
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Preloaders
|
||||||
|
class GroupPolicyPreloader
|
||||||
|
def initialize(groups, current_user)
|
||||||
|
@groups = groups
|
||||||
|
@current_user = current_user
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute
|
||||||
|
Preloaders::UserMaxAccessLevelInGroupsPreloader.new(@groups, @current_user).execute
|
||||||
|
Preloaders::GroupRootAncestorPreloader.new(@groups, root_ancestor_preloads).execute
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def root_ancestor_preloads
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Preloaders::GroupPolicyPreloader.prepend_mod_with('Preloaders::GroupPolicyPreloader')
|
|
@ -0,0 +1,32 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Preloaders
|
||||||
|
class GroupRootAncestorPreloader
|
||||||
|
def initialize(groups, root_ancestor_preloads = [])
|
||||||
|
@groups = groups
|
||||||
|
@root_ancestor_preloads = root_ancestor_preloads
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute
|
||||||
|
return unless ::Feature.enabled?(:use_traversal_ids, default_enabled: :yaml)
|
||||||
|
|
||||||
|
# type == 'Group' condition located on subquery to prevent a filter in the query
|
||||||
|
root_query = Namespace.joins("INNER JOIN (#{join_sql}) as root_query ON root_query.root_id = namespaces.id")
|
||||||
|
.select('namespaces.*, root_query.id as source_id')
|
||||||
|
|
||||||
|
root_query = root_query.preload(*@root_ancestor_preloads) if @root_ancestor_preloads.any?
|
||||||
|
|
||||||
|
root_ancestors_by_id = root_query.group_by(&:source_id)
|
||||||
|
|
||||||
|
@groups.each do |group|
|
||||||
|
group.root_ancestor = root_ancestors_by_id[group.id].first
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def join_sql
|
||||||
|
Group.select('id, traversal_ids[1] as root_id').where(id: @groups.map(&:id)).to_sql
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -3,7 +3,6 @@
|
||||||
module Preloaders
|
module Preloaders
|
||||||
# This class preloads the max access level (role) for the user within the given groups and
|
# This class preloads the max access level (role) for the user within the given groups and
|
||||||
# stores the values in requests store.
|
# stores the values in requests store.
|
||||||
# Will only be able to preload max access level for groups where the user is a direct member
|
|
||||||
class UserMaxAccessLevelInGroupsPreloader
|
class UserMaxAccessLevelInGroupsPreloader
|
||||||
include BulkMemberAccessLoad
|
include BulkMemberAccessLoad
|
||||||
|
|
||||||
|
@ -13,8 +12,17 @@ module Preloaders
|
||||||
end
|
end
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
|
if ::Feature.enabled?(:use_traversal_ids, default_enabled: :yaml)
|
||||||
|
preload_with_traversal_ids
|
||||||
|
else
|
||||||
|
preload_direct_memberships
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def preload_direct_memberships
|
||||||
group_memberships = GroupMember.active_without_invites_and_requests
|
group_memberships = GroupMember.active_without_invites_and_requests
|
||||||
.non_minimal_access
|
|
||||||
.where(user: @user, source_id: @groups)
|
.where(user: @user, source_id: @groups)
|
||||||
.group(:source_id)
|
.group(:source_id)
|
||||||
.maximum(:access_level)
|
.maximum(:access_level)
|
||||||
|
@ -23,5 +31,22 @@ module Preloaders
|
||||||
merge_value_to_request_store(User, @user.id, group_id, max_access_level)
|
merge_value_to_request_store(User, @user.id, group_id, max_access_level)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def preload_with_traversal_ids
|
||||||
|
max_access_levels = GroupMember.active_without_invites_and_requests
|
||||||
|
.where(user: @user)
|
||||||
|
.joins("INNER JOIN (#{traversal_join_sql}) as hierarchy ON members.source_id = hierarchy.traversal_id")
|
||||||
|
.group('hierarchy.id')
|
||||||
|
.maximum(:access_level)
|
||||||
|
|
||||||
|
@groups.each do |group|
|
||||||
|
max_access_level = max_access_levels[group.id] || Gitlab::Access::NO_ACCESS
|
||||||
|
merge_value_to_request_store(User, @user.id, group.id, max_access_level)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def traversal_join_sql
|
||||||
|
Namespace.select('id, unnest(traversal_ids) as traversal_id').where(id: @groups.map(&:id)).to_sql
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
- breadcrumb_title _('Customer Relations Organizations')
|
- breadcrumb_title _('Customer Relations Organizations')
|
||||||
- page_title _('Customer Relations Organizations')
|
- page_title _('Customer Relations Organizations')
|
||||||
|
|
||||||
#js-crm-organizations-app
|
#js-crm-organizations-app{ data: { group_full_path: @group.full_path } }
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
client: {
|
|
||||||
service: {
|
|
||||||
name: 'gitlab',
|
|
||||||
localSchemaFile: './tmp/tests/graphql/gitlab_schema.graphql',
|
|
||||||
},
|
|
||||||
includes: ['../{ee/,jh/,}app/assets/javascripts/**/*.{js,graphql}'],
|
|
||||||
excludes: ['../{ee/,jh/,}spec/{frontend,frontend_integration}/**/*'],
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -256,8 +256,8 @@
|
||||||
:when: 2019-09-11 13:08:28.431132000 Z
|
:when: 2019-09-11 13:08:28.431132000 Z
|
||||||
- - :permit
|
- - :permit
|
||||||
- "(MIT OR CC0-1.0)"
|
- "(MIT OR CC0-1.0)"
|
||||||
- :who:
|
- :who:
|
||||||
:why:
|
:why:
|
||||||
:versions: []
|
:versions: []
|
||||||
:when: 2019-11-08 10:03:31.787226000 Z
|
:when: 2019-11-08 10:03:31.787226000 Z
|
||||||
- - :permit
|
- - :permit
|
||||||
|
@ -362,9 +362,3 @@
|
||||||
- - :approve
|
- - :approve
|
||||||
- 0.0.62
|
- 0.0.62
|
||||||
- *2
|
- *2
|
||||||
- - :approve
|
|
||||||
- sha.js
|
|
||||||
- :who: Vitaly Slobodin
|
|
||||||
:why: Dual license BSD-3-Clause and MIT. See https://gitlab.com/gitlab-com/legal-and-compliance/-/issues/686
|
|
||||||
:versions: []
|
|
||||||
:when: 2021-11-12 18:08:27.036400090 Z
|
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class RemoveGeoUploadDeprecatedFields < Gitlab::Database::Migration[1.0]
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def up
|
||||||
|
with_lock_retries do
|
||||||
|
remove_column :geo_event_log, :upload_deleted_event_id, :bigint
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
with_lock_retries do
|
||||||
|
add_column(:geo_event_log, :upload_deleted_event_id, :bigint) unless column_exists?(:geo_event_log, :upload_deleted_event_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
add_concurrent_foreign_key :geo_event_log, :geo_upload_deleted_events,
|
||||||
|
column: :upload_deleted_event_id,
|
||||||
|
name: 'fk_c1f241c70d',
|
||||||
|
on_delete: :cascade
|
||||||
|
|
||||||
|
add_concurrent_index :geo_event_log,
|
||||||
|
:upload_deleted_event_id,
|
||||||
|
name: 'index_geo_event_log_on_upload_deleted_event_id',
|
||||||
|
where: "(upload_deleted_event_id IS NOT NULL)"
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,17 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class DropGeoUploadDeletedEventsTable < Gitlab::Database::Migration[1.0]
|
||||||
|
def up
|
||||||
|
drop_table :geo_upload_deleted_events
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
create_table :geo_upload_deleted_events, id: :bigserial do |t|
|
||||||
|
t.integer :upload_id, null: false, index: true
|
||||||
|
t.string :file_path, null: false
|
||||||
|
t.integer :model_id, null: false
|
||||||
|
t.string :model_type, null: false
|
||||||
|
t.string :uploader, null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,12 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class RemoveOutdatedFieldsFromGeoNodeStatus < Gitlab::Database::Migration[1.0]
|
||||||
|
enable_lock_retries!
|
||||||
|
|
||||||
|
def change
|
||||||
|
remove_column :geo_node_statuses, :attachments_count, :integer
|
||||||
|
remove_column :geo_node_statuses, :attachments_synced_count, :integer
|
||||||
|
remove_column :geo_node_statuses, :attachments_failed_count, :integer
|
||||||
|
remove_column :geo_node_statuses, :attachments_synced_missing_on_primary_count, :integer
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1 @@
|
||||||
|
bc7974917509bfbda47375299009295bc5a55970b92443dd5d7134075b161279
|
|
@ -0,0 +1 @@
|
||||||
|
483e4cbe2a0be2afbda511f2298e3715abaca29afafeeae26449fc862f49a08f
|
|
@ -0,0 +1 @@
|
||||||
|
c474870a626c909da772a1c9f459f369d50658ce8f585a35e7cc3c7ef64af657
|
|
@ -14170,7 +14170,6 @@ CREATE TABLE geo_event_log (
|
||||||
hashed_storage_migrated_event_id bigint,
|
hashed_storage_migrated_event_id bigint,
|
||||||
lfs_object_deleted_event_id bigint,
|
lfs_object_deleted_event_id bigint,
|
||||||
hashed_storage_attachments_event_id bigint,
|
hashed_storage_attachments_event_id bigint,
|
||||||
upload_deleted_event_id bigint,
|
|
||||||
job_artifact_deleted_event_id bigint,
|
job_artifact_deleted_event_id bigint,
|
||||||
reset_checksum_event_id bigint,
|
reset_checksum_event_id bigint,
|
||||||
cache_invalidation_event_id bigint,
|
cache_invalidation_event_id bigint,
|
||||||
|
@ -14300,9 +14299,6 @@ CREATE TABLE geo_node_statuses (
|
||||||
lfs_objects_count integer,
|
lfs_objects_count integer,
|
||||||
lfs_objects_synced_count integer,
|
lfs_objects_synced_count integer,
|
||||||
lfs_objects_failed_count integer,
|
lfs_objects_failed_count integer,
|
||||||
attachments_count integer,
|
|
||||||
attachments_synced_count integer,
|
|
||||||
attachments_failed_count integer,
|
|
||||||
last_event_id integer,
|
last_event_id integer,
|
||||||
last_event_date timestamp without time zone,
|
last_event_date timestamp without time zone,
|
||||||
cursor_last_event_id integer,
|
cursor_last_event_id integer,
|
||||||
|
@ -14327,7 +14323,6 @@ CREATE TABLE geo_node_statuses (
|
||||||
wikis_verification_failed_count integer,
|
wikis_verification_failed_count integer,
|
||||||
lfs_objects_synced_missing_on_primary_count integer,
|
lfs_objects_synced_missing_on_primary_count integer,
|
||||||
job_artifacts_synced_missing_on_primary_count integer,
|
job_artifacts_synced_missing_on_primary_count integer,
|
||||||
attachments_synced_missing_on_primary_count integer,
|
|
||||||
repositories_checksummed_count integer,
|
repositories_checksummed_count integer,
|
||||||
repositories_checksum_failed_count integer,
|
repositories_checksum_failed_count integer,
|
||||||
repositories_checksum_mismatch_count integer,
|
repositories_checksum_mismatch_count integer,
|
||||||
|
@ -14496,24 +14491,6 @@ CREATE SEQUENCE geo_reset_checksum_events_id_seq
|
||||||
|
|
||||||
ALTER SEQUENCE geo_reset_checksum_events_id_seq OWNED BY geo_reset_checksum_events.id;
|
ALTER SEQUENCE geo_reset_checksum_events_id_seq OWNED BY geo_reset_checksum_events.id;
|
||||||
|
|
||||||
CREATE TABLE geo_upload_deleted_events (
|
|
||||||
id bigint NOT NULL,
|
|
||||||
upload_id integer NOT NULL,
|
|
||||||
file_path character varying NOT NULL,
|
|
||||||
model_id integer NOT NULL,
|
|
||||||
model_type character varying NOT NULL,
|
|
||||||
uploader character varying NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE SEQUENCE geo_upload_deleted_events_id_seq
|
|
||||||
START WITH 1
|
|
||||||
INCREMENT BY 1
|
|
||||||
NO MINVALUE
|
|
||||||
NO MAXVALUE
|
|
||||||
CACHE 1;
|
|
||||||
|
|
||||||
ALTER SEQUENCE geo_upload_deleted_events_id_seq OWNED BY geo_upload_deleted_events.id;
|
|
||||||
|
|
||||||
CREATE TABLE gitlab_subscription_histories (
|
CREATE TABLE gitlab_subscription_histories (
|
||||||
id bigint NOT NULL,
|
id bigint NOT NULL,
|
||||||
gitlab_subscription_created_at timestamp with time zone,
|
gitlab_subscription_created_at timestamp with time zone,
|
||||||
|
@ -21510,8 +21487,6 @@ ALTER TABLE ONLY geo_repository_updated_events ALTER COLUMN id SET DEFAULT nextv
|
||||||
|
|
||||||
ALTER TABLE ONLY geo_reset_checksum_events ALTER COLUMN id SET DEFAULT nextval('geo_reset_checksum_events_id_seq'::regclass);
|
ALTER TABLE ONLY geo_reset_checksum_events ALTER COLUMN id SET DEFAULT nextval('geo_reset_checksum_events_id_seq'::regclass);
|
||||||
|
|
||||||
ALTER TABLE ONLY geo_upload_deleted_events ALTER COLUMN id SET DEFAULT nextval('geo_upload_deleted_events_id_seq'::regclass);
|
|
||||||
|
|
||||||
ALTER TABLE ONLY gitlab_subscription_histories ALTER COLUMN id SET DEFAULT nextval('gitlab_subscription_histories_id_seq'::regclass);
|
ALTER TABLE ONLY gitlab_subscription_histories ALTER COLUMN id SET DEFAULT nextval('gitlab_subscription_histories_id_seq'::regclass);
|
||||||
|
|
||||||
ALTER TABLE ONLY gitlab_subscriptions ALTER COLUMN id SET DEFAULT nextval('gitlab_subscriptions_id_seq'::regclass);
|
ALTER TABLE ONLY gitlab_subscriptions ALTER COLUMN id SET DEFAULT nextval('gitlab_subscriptions_id_seq'::regclass);
|
||||||
|
@ -23129,9 +23104,6 @@ ALTER TABLE ONLY geo_repository_updated_events
|
||||||
ALTER TABLE ONLY geo_reset_checksum_events
|
ALTER TABLE ONLY geo_reset_checksum_events
|
||||||
ADD CONSTRAINT geo_reset_checksum_events_pkey PRIMARY KEY (id);
|
ADD CONSTRAINT geo_reset_checksum_events_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
ALTER TABLE ONLY geo_upload_deleted_events
|
|
||||||
ADD CONSTRAINT geo_upload_deleted_events_pkey PRIMARY KEY (id);
|
|
||||||
|
|
||||||
ALTER TABLE ONLY gitlab_subscription_histories
|
ALTER TABLE ONLY gitlab_subscription_histories
|
||||||
ADD CONSTRAINT gitlab_subscription_histories_pkey PRIMARY KEY (id);
|
ADD CONSTRAINT gitlab_subscription_histories_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
@ -25950,8 +25922,6 @@ CREATE INDEX index_geo_event_log_on_repository_updated_event_id ON geo_event_log
|
||||||
|
|
||||||
CREATE INDEX index_geo_event_log_on_reset_checksum_event_id ON geo_event_log USING btree (reset_checksum_event_id) WHERE (reset_checksum_event_id IS NOT NULL);
|
CREATE INDEX index_geo_event_log_on_reset_checksum_event_id ON geo_event_log USING btree (reset_checksum_event_id) WHERE (reset_checksum_event_id IS NOT NULL);
|
||||||
|
|
||||||
CREATE INDEX index_geo_event_log_on_upload_deleted_event_id ON geo_event_log USING btree (upload_deleted_event_id) WHERE (upload_deleted_event_id IS NOT NULL);
|
|
||||||
|
|
||||||
CREATE INDEX index_geo_hashed_storage_attachments_events_on_project_id ON geo_hashed_storage_attachments_events USING btree (project_id);
|
CREATE INDEX index_geo_hashed_storage_attachments_events_on_project_id ON geo_hashed_storage_attachments_events USING btree (project_id);
|
||||||
|
|
||||||
CREATE INDEX index_geo_hashed_storage_migrated_events_on_project_id ON geo_hashed_storage_migrated_events USING btree (project_id);
|
CREATE INDEX index_geo_hashed_storage_migrated_events_on_project_id ON geo_hashed_storage_migrated_events USING btree (project_id);
|
||||||
|
@ -25988,8 +25958,6 @@ CREATE INDEX index_geo_repository_updated_events_on_source ON geo_repository_upd
|
||||||
|
|
||||||
CREATE INDEX index_geo_reset_checksum_events_on_project_id ON geo_reset_checksum_events USING btree (project_id);
|
CREATE INDEX index_geo_reset_checksum_events_on_project_id ON geo_reset_checksum_events USING btree (project_id);
|
||||||
|
|
||||||
CREATE INDEX index_geo_upload_deleted_events_on_upload_id ON geo_upload_deleted_events USING btree (upload_id);
|
|
||||||
|
|
||||||
CREATE INDEX index_gin_ci_pending_builds_on_namespace_traversal_ids ON ci_pending_builds USING gin (namespace_traversal_ids);
|
CREATE INDEX index_gin_ci_pending_builds_on_namespace_traversal_ids ON ci_pending_builds USING gin (namespace_traversal_ids);
|
||||||
|
|
||||||
CREATE INDEX index_gitlab_subscription_histories_on_gitlab_subscription_id ON gitlab_subscription_histories USING btree (gitlab_subscription_id);
|
CREATE INDEX index_gitlab_subscription_histories_on_gitlab_subscription_id ON gitlab_subscription_histories USING btree (gitlab_subscription_id);
|
||||||
|
@ -29352,9 +29320,6 @@ ALTER TABLE ONLY design_management_versions
|
||||||
ALTER TABLE ONLY packages_packages
|
ALTER TABLE ONLY packages_packages
|
||||||
ADD CONSTRAINT fk_c188f0dba4 FOREIGN KEY (creator_id) REFERENCES users(id) ON DELETE SET NULL;
|
ADD CONSTRAINT fk_c188f0dba4 FOREIGN KEY (creator_id) REFERENCES users(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
ALTER TABLE ONLY geo_event_log
|
|
||||||
ADD CONSTRAINT fk_c1f241c70d FOREIGN KEY (upload_deleted_event_id) REFERENCES geo_upload_deleted_events(id) ON DELETE CASCADE;
|
|
||||||
|
|
||||||
ALTER TABLE ONLY analytics_cycle_analytics_project_stages
|
ALTER TABLE ONLY analytics_cycle_analytics_project_stages
|
||||||
ADD CONSTRAINT fk_c3339bdfc9 FOREIGN KEY (stage_event_hash_id) REFERENCES analytics_cycle_analytics_stage_event_hashes(id) ON DELETE CASCADE;
|
ADD CONSTRAINT fk_c3339bdfc9 FOREIGN KEY (stage_event_hash_id) REFERENCES analytics_cycle_analytics_stage_event_hashes(id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
|
|
@ -190,9 +190,6 @@ configuration option in `gitlab.yml`. These metrics are served from the
|
||||||
| `geo_lfs_objects` | Gauge | 10.2 | Total number of LFS objects available on primary | `url` |
|
| `geo_lfs_objects` | Gauge | 10.2 | Total number of LFS objects available on primary | `url` |
|
||||||
| `geo_lfs_objects_synced` | Gauge | 10.2 | Number of LFS objects synced on secondary | `url` |
|
| `geo_lfs_objects_synced` | Gauge | 10.2 | Number of LFS objects synced on secondary | `url` |
|
||||||
| `geo_lfs_objects_failed` | Gauge | 10.2 | Number of LFS objects failed to sync on secondary | `url` |
|
| `geo_lfs_objects_failed` | Gauge | 10.2 | Number of LFS objects failed to sync on secondary | `url` |
|
||||||
| `geo_attachments` | Gauge | 10.2 | Total number of file attachments available on primary | `url` |
|
|
||||||
| `geo_attachments_synced` | Gauge | 10.2 | Number of attachments synced on secondary | `url` |
|
|
||||||
| `geo_attachments_failed` | Gauge | 10.2 | Number of attachments failed to sync on secondary | `url` |
|
|
||||||
| `geo_last_event_id` | Gauge | 10.2 | Database ID of the latest event log entry on the primary | `url` |
|
| `geo_last_event_id` | Gauge | 10.2 | Database ID of the latest event log entry on the primary | `url` |
|
||||||
| `geo_last_event_timestamp` | Gauge | 10.2 | UNIX timestamp of the latest event log entry on the primary | `url` |
|
| `geo_last_event_timestamp` | Gauge | 10.2 | UNIX timestamp of the latest event log entry on the primary | `url` |
|
||||||
| `geo_cursor_last_event_id` | Gauge | 10.2 | Last database ID of the event log processed by the secondary | `url` |
|
| `geo_cursor_last_event_id` | Gauge | 10.2 | Last database ID of the event log processed by the secondary | `url` |
|
||||||
|
@ -201,7 +198,6 @@ configuration option in `gitlab.yml`. These metrics are served from the
|
||||||
| `geo_last_successful_status_check_timestamp` | Gauge | 10.2 | Last timestamp when the status was successfully updated | `url` |
|
| `geo_last_successful_status_check_timestamp` | Gauge | 10.2 | Last timestamp when the status was successfully updated | `url` |
|
||||||
| `geo_lfs_objects_synced_missing_on_primary` | Gauge | 10.7 | Number of LFS objects marked as synced due to the file missing on the primary | `url` |
|
| `geo_lfs_objects_synced_missing_on_primary` | Gauge | 10.7 | Number of LFS objects marked as synced due to the file missing on the primary | `url` |
|
||||||
| `geo_job_artifacts_synced_missing_on_primary` | Gauge | 10.7 | Number of job artifacts marked as synced due to the file missing on the primary | `url` |
|
| `geo_job_artifacts_synced_missing_on_primary` | Gauge | 10.7 | Number of job artifacts marked as synced due to the file missing on the primary | `url` |
|
||||||
| `geo_attachments_synced_missing_on_primary` | Gauge | 10.7 | Number of attachments marked as synced due to the file missing on the primary | `url` |
|
|
||||||
| `geo_repositories_checksummed` | Gauge | 10.7 | Number of repositories checksummed on primary | `url` |
|
| `geo_repositories_checksummed` | Gauge | 10.7 | Number of repositories checksummed on primary | `url` |
|
||||||
| `geo_repositories_checksum_failed` | Gauge | 10.7 | Number of repositories failed to calculate the checksum on primary | `url` |
|
| `geo_repositories_checksum_failed` | Gauge | 10.7 | Number of repositories failed to calculate the checksum on primary | `url` |
|
||||||
| `geo_wikis_checksummed` | Gauge | 10.7 | Number of wikis checksummed on primary | `url` |
|
| `geo_wikis_checksummed` | Gauge | 10.7 | Number of wikis checksummed on primary | `url` |
|
||||||
|
|
|
@ -259,7 +259,8 @@ control over how the Pages daemon runs and serves content in your environment.
|
||||||
| `FF_ENABLE_REDIRECTS` | Feature flag to enable/disable redirects (enabled by default). Read the [redirects documentation](../../user/project/pages/redirects.md#feature-flag-for-redirects) for more information. |
|
| `FF_ENABLE_REDIRECTS` | Feature flag to enable/disable redirects (enabled by default). Read the [redirects documentation](../../user/project/pages/redirects.md#feature-flag-for-redirects) for more information. |
|
||||||
| `FF_ENABLE_PLACEHOLDERS` | Feature flag to enable/disable rewrites (disabled by default). Read the [redirects documentation](../../user/project/pages/redirects.md#feature-flag-for-rewrites) for more information. |
|
| `FF_ENABLE_PLACEHOLDERS` | Feature flag to enable/disable rewrites (disabled by default). Read the [redirects documentation](../../user/project/pages/redirects.md#feature-flag-for-rewrites) for more information. |
|
||||||
| `use_legacy_storage` | Temporarily-introduced parameter allowing to use legacy domain configuration source and storage. [Removed in 14.3](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/6166). |
|
| `use_legacy_storage` | Temporarily-introduced parameter allowing to use legacy domain configuration source and storage. [Removed in 14.3](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/6166). |
|
||||||
|
| `rate_limit_source_ip` | Rate limit per source IP in number of requests per second. Set to `0` to disable this feature. |
|
||||||
|
| `rate_limit_source_ip_burst` | Rate limit per source IP maximum burst allowed per second. |
|
||||||
---
|
---
|
||||||
|
|
||||||
## Advanced configuration
|
## Advanced configuration
|
||||||
|
@ -1032,6 +1033,38 @@ GitLab Pages are part of the [regular backup](../../raketasks/backup_restore.md)
|
||||||
You should strongly consider running GitLab Pages under a different hostname
|
You should strongly consider running GitLab Pages under a different hostname
|
||||||
than GitLab to prevent XSS attacks.
|
than GitLab to prevent XSS attacks.
|
||||||
|
|
||||||
|
### Rate limits
|
||||||
|
|
||||||
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab-pages/-/issues/631) in GitLab 14.5.
|
||||||
|
|
||||||
|
You can enforce source-IP rate limits to help minimize the risk of a Denial of Service (DoS) attack. GitLab Pages
|
||||||
|
uses a [token bucket algorithm](https://en.wikipedia.org/wiki/Token_bucket) to enforce rate limiting. By default,
|
||||||
|
requests that exceed the specified limits are reported but not rejected.
|
||||||
|
|
||||||
|
Source-IP rate limits are enforced using the following:
|
||||||
|
|
||||||
|
- `rate_limit_source_ip`: Set the maximum threshold in number of requests per second. Set to 0 to disable this feature.
|
||||||
|
- `rate_limit_source_ip_burst`: Sets the maximum threshold of number of requests allowed in an initial outburst of requests.
|
||||||
|
For example, when you load a web page that loads a number of resources at the same time.
|
||||||
|
|
||||||
|
#### Enable source-IP rate limits
|
||||||
|
|
||||||
|
1. Set rate limits in `/etc/gitlab/gitlab.rb`:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
gitlab_pages['rate_limit_source_ip'] = 20.0
|
||||||
|
gitlab_pages['rate_limit_source_ip_burst'] = 600
|
||||||
|
```
|
||||||
|
|
||||||
|
1. To reject requests that exceed the specified limits, enable the `FF_ENABLE_RATE_LIMITER` feature flag in
|
||||||
|
`/etc/gitlab/gitlab.rb`:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
gitlab_pages['env'] = {'FF_ENABLE_RATE_LIMITER' => 'true'}
|
||||||
|
```
|
||||||
|
|
||||||
|
1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
|
||||||
|
|
||||||
<!-- ## Troubleshooting
|
<!-- ## Troubleshooting
|
||||||
|
|
||||||
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
|
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
|
||||||
|
|
|
@ -306,11 +306,6 @@ Example response:
|
||||||
"health": "Healthy",
|
"health": "Healthy",
|
||||||
"health_status": "Healthy",
|
"health_status": "Healthy",
|
||||||
"missing_oauth_application": false,
|
"missing_oauth_application": false,
|
||||||
"attachments_count": 1,
|
|
||||||
"attachments_synced_count": null,
|
|
||||||
"attachments_failed_count": null,
|
|
||||||
"attachments_synced_missing_on_primary_count": 0,
|
|
||||||
"attachments_synced_in_percentage": "0.00%",
|
|
||||||
"db_replication_lag_seconds": null,
|
"db_replication_lag_seconds": null,
|
||||||
"lfs_objects_count": 0,
|
"lfs_objects_count": 0,
|
||||||
"lfs_objects_synced_count": null,
|
"lfs_objects_synced_count": null,
|
||||||
|
@ -465,11 +460,6 @@ Example response:
|
||||||
"health": "Healthy",
|
"health": "Healthy",
|
||||||
"health_status": "Healthy",
|
"health_status": "Healthy",
|
||||||
"missing_oauth_application": false,
|
"missing_oauth_application": false,
|
||||||
"attachments_count": 1,
|
|
||||||
"attachments_synced_count": 1,
|
|
||||||
"attachments_failed_count": 0,
|
|
||||||
"attachments_synced_missing_on_primary_count": 0,
|
|
||||||
"attachments_synced_in_percentage": "100.00%",
|
|
||||||
"db_replication_lag_seconds": 0,
|
"db_replication_lag_seconds": 0,
|
||||||
"lfs_objects_count": 0,
|
"lfs_objects_count": 0,
|
||||||
"lfs_objects_synced_count": 0,
|
"lfs_objects_synced_count": 0,
|
||||||
|
@ -628,11 +618,6 @@ Example response:
|
||||||
"health": "Healthy",
|
"health": "Healthy",
|
||||||
"health_status": "Healthy",
|
"health_status": "Healthy",
|
||||||
"missing_oauth_application": false,
|
"missing_oauth_application": false,
|
||||||
"attachments_count": 1,
|
|
||||||
"attachments_synced_count": 1,
|
|
||||||
"attachments_failed_count": 0,
|
|
||||||
"attachments_synced_missing_on_primary_count": 0,
|
|
||||||
"attachments_synced_in_percentage": "100.00%",
|
|
||||||
"db_replication_lag_seconds": 0,
|
"db_replication_lag_seconds": 0,
|
||||||
"lfs_objects_count": 0,
|
"lfs_objects_count": 0,
|
||||||
"lfs_objects_synced_count": 0,
|
"lfs_objects_synced_count": 0,
|
||||||
|
|
|
@ -231,10 +231,6 @@ We also collect metrics specific to [Geo](../../administration/geo/index.md) sec
|
||||||
"repositories_replication_enabled"=>true,
|
"repositories_replication_enabled"=>true,
|
||||||
"repositories_synced_count"=>24,
|
"repositories_synced_count"=>24,
|
||||||
"repositories_failed_count"=>0,
|
"repositories_failed_count"=>0,
|
||||||
"attachments_replication_enabled"=>true,
|
|
||||||
"attachments_count"=>1,
|
|
||||||
"attachments_synced_count"=>1,
|
|
||||||
"attachments_failed_count"=>0,
|
|
||||||
"git_fetch_event_count_weekly"=>nil,
|
"git_fetch_event_count_weekly"=>nil,
|
||||||
"git_push_event_count_weekly"=>nil,
|
"git_push_event_count_weekly"=>nil,
|
||||||
... other geo node status fields
|
... other geo node status fields
|
||||||
|
|
|
@ -10,6 +10,7 @@ type: reference, howto
|
||||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/210181) in GitLab 13.0.
|
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/210181) in GitLab 13.0.
|
||||||
> - [Became available on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/235765) in GitLab 13.5 for paid groups only.
|
> - [Became available on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/235765) in GitLab 13.5 for paid groups only.
|
||||||
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/235765) in GitLab 13.5.
|
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/235765) in GitLab 13.5.
|
||||||
|
> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/342327) in GitLab 14.5. Default prefix added.
|
||||||
|
|
||||||
Project access tokens are similar to [personal access tokens](../../profile/personal_access_tokens.md)
|
Project access tokens are similar to [personal access tokens](../../profile/personal_access_tokens.md)
|
||||||
except they are attached to a project rather than a user. They can be used to:
|
except they are attached to a project rather than a user. They can be used to:
|
||||||
|
@ -32,6 +33,9 @@ Project access tokens:
|
||||||
For examples of how you can use a project access token to authenticate with the API, see the
|
For examples of how you can use a project access token to authenticate with the API, see the
|
||||||
[relevant section from our API Docs](../../../api/index.md#personalproject-access-tokens).
|
[relevant section from our API Docs](../../../api/index.md#personalproject-access-tokens).
|
||||||
|
|
||||||
|
NOTE:
|
||||||
|
For GitLab.com and new self-managed instances, the default prefix is `glpat-`.
|
||||||
|
|
||||||
## Creating a project access token
|
## Creating a project access token
|
||||||
|
|
||||||
1. Log in to GitLab.
|
1. Log in to GitLab.
|
||||||
|
|
|
@ -220,7 +220,6 @@ geo_repository_deleted_events: :gitlab_main
|
||||||
geo_repository_renamed_events: :gitlab_main
|
geo_repository_renamed_events: :gitlab_main
|
||||||
geo_repository_updated_events: :gitlab_main
|
geo_repository_updated_events: :gitlab_main
|
||||||
geo_reset_checksum_events: :gitlab_main
|
geo_reset_checksum_events: :gitlab_main
|
||||||
geo_upload_deleted_events: :gitlab_main
|
|
||||||
gitlab_subscription_histories: :gitlab_main
|
gitlab_subscription_histories: :gitlab_main
|
||||||
gitlab_subscriptions: :gitlab_main
|
gitlab_subscriptions: :gitlab_main
|
||||||
gpg_keys: :gitlab_main
|
gpg_keys: :gitlab_main
|
||||||
|
|
|
@ -10095,6 +10095,9 @@ msgstr ""
|
||||||
msgid "Crm|No contacts found"
|
msgid "Crm|No contacts found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Crm|No organizations found"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Cron Timezone"
|
msgid "Cron Timezone"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -15337,9 +15340,6 @@ msgstr ""
|
||||||
msgid "Geo|Could not remove tracking entry for an existing project."
|
msgid "Geo|Could not remove tracking entry for an existing project."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Geo|Could not remove tracking entry for an existing upload."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Geo|Data replication lag"
|
msgid "Geo|Data replication lag"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -15613,9 +15613,6 @@ msgstr ""
|
||||||
msgid "Geo|Tracking entry for project (%{project_id}) was successfully removed."
|
msgid "Geo|Tracking entry for project (%{project_id}) was successfully removed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Geo|Tracking entry for upload (%{type}/%{id}) was successfully removed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Geo|URL can't be blank"
|
msgid "Geo|URL can't be blank"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -28891,9 +28888,6 @@ msgstr ""
|
||||||
msgid "Removed %{reviewer_text} %{reviewer_references}."
|
msgid "Removed %{reviewer_text} %{reviewer_references}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Removed %{type} with id %{id}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Removed all labels."
|
msgid "Removed all labels."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -28918,6 +28912,9 @@ msgstr ""
|
||||||
msgid "Removed time estimate."
|
msgid "Removed time estimate."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Removed upload with id %{id}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "RemovedProjects|Projects which are removed and are yet to be permanently removed are visible here."
|
msgid "RemovedProjects|Projects which are removed and are yet to be permanently removed are visible here."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -37206,9 +37203,6 @@ msgstr ""
|
||||||
msgid "Uploading changes to terminal"
|
msgid "Uploading changes to terminal"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Uploads"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Upon performing this action, the contents of this group, its subgroup and projects will be permanently deleted after %{deletion_adjourned_period} days on %{date}. Until that time:"
|
msgid "Upon performing this action, the contents of this group, its subgroup and projects will be permanently deleted after %{deletion_adjourned_period} days on %{date}. Until that time:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -209,7 +209,6 @@
|
||||||
"@testing-library/dom": "^7.16.2",
|
"@testing-library/dom": "^7.16.2",
|
||||||
"@vue/test-utils": "1.2.0",
|
"@vue/test-utils": "1.2.0",
|
||||||
"acorn": "^6.3.0",
|
"acorn": "^6.3.0",
|
||||||
"apollo": "^2.33.8",
|
|
||||||
"axios-mock-adapter": "^1.15.0",
|
"axios-mock-adapter": "^1.15.0",
|
||||||
"babel-jest": "^26.5.2",
|
"babel-jest": "^26.5.2",
|
||||||
"babel-plugin-dynamic-import-node": "^2.3.3",
|
"babel-plugin-dynamic-import-node": "^2.3.3",
|
||||||
|
@ -264,8 +263,7 @@
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"chokidar": "^3.4.0",
|
"chokidar": "^3.4.0",
|
||||||
"@types/node": "14.17.5",
|
"@types/node": "14.17.5"
|
||||||
"graphql": "^15.4.0"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.22.1",
|
"node": ">=12.22.1",
|
||||||
|
|
|
@ -7,6 +7,17 @@ module QA
|
||||||
# creating it if it doesn't yet exist.
|
# creating it if it doesn't yet exist.
|
||||||
#
|
#
|
||||||
class Sandbox < GroupBase
|
class Sandbox < GroupBase
|
||||||
|
class << self
|
||||||
|
# Force top level group creation via UI if test is executed on dot_com environment
|
||||||
|
def fabricate!(*args, &prepare_block)
|
||||||
|
return fabricate_via_browser_ui!(*args, &prepare_block) if Specs::Helpers::ContextSelector.dot_com?
|
||||||
|
|
||||||
|
fabricate_via_api!(*args, &prepare_block)
|
||||||
|
rescue NotImplementedError
|
||||||
|
fabricate_via_browser_ui!(*args, &prepare_block)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
@path = Runtime::Namespace.sandbox_name
|
@path = Runtime::Namespace.sandbox_name
|
||||||
end
|
end
|
||||||
|
|
|
@ -22,9 +22,7 @@ module QA
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:source_group) do
|
let(:source_group) do
|
||||||
# top level group can't be created on staging via api, create via UI
|
Resource::Sandbox.fabricate! do |group|
|
||||||
fabricate_method = staging? ? :fabricate_via_browser_ui! : :fabricate_via_api!
|
|
||||||
Resource::Sandbox.send(fabricate_method) do |group|
|
|
||||||
group.api_client = api_client
|
group.api_client = api_client
|
||||||
group.path = "source-group-for-import-#{SecureRandom.hex(4)}"
|
group.path = "source-group-for-import-#{SecureRandom.hex(4)}"
|
||||||
end
|
end
|
||||||
|
|
|
@ -48,7 +48,6 @@ RSpec.describe 'Database schema' do
|
||||||
geo_node_statuses: %w[last_event_id cursor_last_event_id],
|
geo_node_statuses: %w[last_event_id cursor_last_event_id],
|
||||||
geo_nodes: %w[oauth_application_id],
|
geo_nodes: %w[oauth_application_id],
|
||||||
geo_repository_deleted_events: %w[project_id],
|
geo_repository_deleted_events: %w[project_id],
|
||||||
geo_upload_deleted_events: %w[upload_id model_id],
|
|
||||||
gitlab_subscription_histories: %w[gitlab_subscription_id hosted_plan_id namespace_id],
|
gitlab_subscription_histories: %w[gitlab_subscription_id hosted_plan_id namespace_id],
|
||||||
identities: %w[user_id],
|
identities: %w[user_id],
|
||||||
import_failures: %w[project_id],
|
import_failures: %w[project_id],
|
||||||
|
|
|
@ -45,3 +45,37 @@ export const getGroupContactsQueryResponse = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getGroupOrganizationsQueryResponse = {
|
||||||
|
data: {
|
||||||
|
group: {
|
||||||
|
__typename: 'Group',
|
||||||
|
id: 'gid://gitlab/Group/26',
|
||||||
|
organizations: {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
__typename: 'CustomerRelationsOrganization',
|
||||||
|
id: 'gid://gitlab/CustomerRelations::Organization/1',
|
||||||
|
name: 'Test Inc',
|
||||||
|
defaultRate: 100,
|
||||||
|
description: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__typename: 'CustomerRelationsOrganization',
|
||||||
|
id: 'gid://gitlab/CustomerRelations::Organization/2',
|
||||||
|
name: 'ABC Company',
|
||||||
|
defaultRate: 110,
|
||||||
|
description: 'VIP',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__typename: 'CustomerRelationsOrganization',
|
||||||
|
id: 'gid://gitlab/CustomerRelations::Organization/3',
|
||||||
|
name: 'GitLab',
|
||||||
|
defaultRate: 120,
|
||||||
|
description: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { GlLoadingIcon } from '@gitlab/ui';
|
||||||
|
import Vue from 'vue';
|
||||||
|
import VueApollo from 'vue-apollo';
|
||||||
|
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||||
|
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||||
|
import waitForPromises from 'helpers/wait_for_promises';
|
||||||
|
import createFlash from '~/flash';
|
||||||
|
import OrganizationsRoot from '~/crm/components/organizations_root.vue';
|
||||||
|
import getGroupOrganizationsQuery from '~/crm/components/queries/get_group_organizations.query.graphql';
|
||||||
|
import { getGroupOrganizationsQueryResponse } from './mock_data';
|
||||||
|
|
||||||
|
jest.mock('~/flash');
|
||||||
|
|
||||||
|
describe('Customer relations organizations root app', () => {
|
||||||
|
Vue.use(VueApollo);
|
||||||
|
let wrapper;
|
||||||
|
let fakeApollo;
|
||||||
|
|
||||||
|
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
|
||||||
|
const findRowByName = (rowName) => wrapper.findAllByRole('row', { name: rowName });
|
||||||
|
const successQueryHandler = jest.fn().mockResolvedValue(getGroupOrganizationsQueryResponse);
|
||||||
|
|
||||||
|
const mountComponent = ({
|
||||||
|
queryHandler = successQueryHandler,
|
||||||
|
mountFunction = shallowMountExtended,
|
||||||
|
} = {}) => {
|
||||||
|
fakeApollo = createMockApollo([[getGroupOrganizationsQuery, queryHandler]]);
|
||||||
|
wrapper = mountFunction(OrganizationsRoot, {
|
||||||
|
provide: { groupFullPath: 'flightjs' },
|
||||||
|
apolloProvider: fakeApollo,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.destroy();
|
||||||
|
fakeApollo = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render loading spinner', () => {
|
||||||
|
mountComponent();
|
||||||
|
|
||||||
|
expect(findLoadingIcon().exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render error message on reject', async () => {
|
||||||
|
mountComponent({ queryHandler: jest.fn().mockRejectedValue('ERROR') });
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(createFlash).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders correct results', async () => {
|
||||||
|
mountComponent({ mountFunction: mountExtended });
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(findRowByName(/Test Inc/i)).toHaveLength(1);
|
||||||
|
expect(findRowByName(/VIP/i)).toHaveLength(1);
|
||||||
|
expect(findRowByName(/120/i)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,45 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Preloaders::GroupPolicyPreloader do
|
||||||
|
let_it_be(:user) { create(:user) }
|
||||||
|
let_it_be(:root_parent) { create(:group, :private, name: 'root-1', path: 'root-1') }
|
||||||
|
let_it_be(:guest_group) { create(:group, name: 'public guest', path: 'public-guest') }
|
||||||
|
let_it_be(:private_maintainer_group) { create(:group, :private, name: 'b private maintainer', path: 'b-private-maintainer', parent: root_parent) }
|
||||||
|
let_it_be(:private_developer_group) { create(:group, :private, project_creation_level: nil, name: 'c public developer', path: 'c-public-developer') }
|
||||||
|
let_it_be(:public_maintainer_group) { create(:group, :private, name: 'a public maintainer', path: 'a-public-maintainer') }
|
||||||
|
|
||||||
|
let(:base_groups) { [guest_group, private_maintainer_group, private_developer_group, public_maintainer_group] }
|
||||||
|
|
||||||
|
before_all do
|
||||||
|
guest_group.add_guest(user)
|
||||||
|
private_maintainer_group.add_maintainer(user)
|
||||||
|
private_developer_group.add_developer(user)
|
||||||
|
public_maintainer_group.add_maintainer(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'avoids N+1 queries when authorizing a list of groups', :request_store do
|
||||||
|
preload_groups_for_policy(user)
|
||||||
|
control = ActiveRecord::QueryRecorder.new { authorize_all_groups(user) }
|
||||||
|
|
||||||
|
new_group1 = create(:group, :private).tap { |group| group.add_maintainer(user) }
|
||||||
|
new_group2 = create(:group, :private, parent: private_maintainer_group)
|
||||||
|
|
||||||
|
another_root = create(:group, :private, name: 'root-3', path: 'root-3')
|
||||||
|
new_group3 = create(:group, :private, parent: another_root).tap { |group| group.add_maintainer(user) }
|
||||||
|
|
||||||
|
pristine_groups = Group.where(id: base_groups + [new_group1, new_group2, new_group3])
|
||||||
|
|
||||||
|
preload_groups_for_policy(user, pristine_groups)
|
||||||
|
expect { authorize_all_groups(user, pristine_groups) }.not_to exceed_query_limit(control)
|
||||||
|
end
|
||||||
|
|
||||||
|
def authorize_all_groups(current_user, group_list = base_groups)
|
||||||
|
group_list.each { |group| current_user.can?(:read_group, group) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def preload_groups_for_policy(current_user, group_list = base_groups)
|
||||||
|
described_class.new(group_list, current_user).execute
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,63 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Preloaders::GroupRootAncestorPreloader do
|
||||||
|
let_it_be(:user) { create(:user) }
|
||||||
|
let_it_be(:root_parent1) { create(:group, :private, name: 'root-1', path: 'root-1') }
|
||||||
|
let_it_be(:root_parent2) { create(:group, :private, name: 'root-2', path: 'root-2') }
|
||||||
|
let_it_be(:guest_group) { create(:group, name: 'public guest', path: 'public-guest') }
|
||||||
|
let_it_be(:private_maintainer_group) { create(:group, :private, name: 'b private maintainer', path: 'b-private-maintainer', parent: root_parent1) }
|
||||||
|
let_it_be(:private_developer_group) { create(:group, :private, project_creation_level: nil, name: 'c public developer', path: 'c-public-developer') }
|
||||||
|
let_it_be(:public_maintainer_group) { create(:group, :private, name: 'a public maintainer', path: 'a-public-maintainer', parent: root_parent2) }
|
||||||
|
|
||||||
|
let(:root_query_regex) { /\ASELECT.+FROM "namespaces" WHERE "namespaces"."id" = \d+/ }
|
||||||
|
let(:additional_preloads) { [] }
|
||||||
|
let(:groups) { [guest_group, private_maintainer_group, private_developer_group, public_maintainer_group] }
|
||||||
|
let(:pristine_groups) { Group.where(id: groups) }
|
||||||
|
|
||||||
|
shared_examples 'executes N matching DB queries' do |expected_query_count, query_method = nil|
|
||||||
|
it 'executes the specified root_ancestor queries' do
|
||||||
|
expect do
|
||||||
|
pristine_groups.each do |group|
|
||||||
|
root_ancestor = group.root_ancestor
|
||||||
|
|
||||||
|
root_ancestor.public_send(query_method) if query_method.present?
|
||||||
|
end
|
||||||
|
end.to make_queries_matching(root_query_regex, expected_query_count)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'strong_memoizes the correct root_ancestor' do
|
||||||
|
pristine_groups.each do |group|
|
||||||
|
expected_parent_id = group.root_ancestor.id == group.id ? nil : group.root_ancestor.id
|
||||||
|
|
||||||
|
expect(group.parent_id).to eq(expected_parent_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the preloader is used' do
|
||||||
|
before do
|
||||||
|
preload_ancestors
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when no additional preloads are provided' do
|
||||||
|
it_behaves_like 'executes N matching DB queries', 0
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when additional preloads are provided' do
|
||||||
|
let(:additional_preloads) { [:route] }
|
||||||
|
let(:root_query_regex) { /\ASELECT.+FROM "routes" WHERE "routes"."source_id" = \d+/ }
|
||||||
|
|
||||||
|
it_behaves_like 'executes N matching DB queries', 0, :full_path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the preloader is not used' do
|
||||||
|
it_behaves_like 'executes N matching DB queries', 2
|
||||||
|
end
|
||||||
|
|
||||||
|
def preload_ancestors
|
||||||
|
described_class.new(pristine_groups, additional_preloads).execute
|
||||||
|
end
|
||||||
|
end
|
|
@ -13,32 +13,47 @@ RSpec.describe Preloaders::UserMaxAccessLevelInGroupsPreloader do
|
||||||
|
|
||||||
shared_examples 'executes N max member permission queries to the DB' do
|
shared_examples 'executes N max member permission queries to the DB' do
|
||||||
it 'executes the specified max membership queries' do
|
it 'executes the specified max membership queries' do
|
||||||
queries = ActiveRecord::QueryRecorder.new do
|
expect { groups.each { |group| user.can?(:read_group, group) } }.to make_queries_matching(max_query_regex, expected_query_count)
|
||||||
groups.each { |group| user.can?(:read_group, group) }
|
end
|
||||||
|
|
||||||
|
it 'caches the correct access_level for each group' do
|
||||||
|
groups.each do |group|
|
||||||
|
access_level_from_db = group.members_with_parents.where(user_id: user.id).group(:user_id).maximum(:access_level)[user.id] || Gitlab::Access::NO_ACCESS
|
||||||
|
cached_access_level = group.max_member_access_for_user(user)
|
||||||
|
|
||||||
|
expect(cached_access_level).to eq(access_level_from_db)
|
||||||
end
|
end
|
||||||
|
|
||||||
max_queries = queries.log.grep(max_query_regex)
|
|
||||||
|
|
||||||
expect(max_queries.count).to eq(expected_query_count)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the preloader is used', :request_store do
|
context 'when the preloader is used', :request_store do
|
||||||
before do
|
context 'when user has indirect access to groups' do
|
||||||
described_class.new(groups, user).execute
|
let_it_be(:child_maintainer) { create(:group, :private, parent: group1).tap {|g| g.add_maintainer(user)} }
|
||||||
end
|
let_it_be(:child_indirect_access) { create(:group, :private, parent: group1) }
|
||||||
|
|
||||||
it_behaves_like 'executes N max member permission queries to the DB' do
|
let(:groups) { [group1, group2, group3, child_maintainer, child_indirect_access] }
|
||||||
# Will query all groups where the user is not already a member
|
|
||||||
let(:expected_query_count) { 1 }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when user has access but is not a direct member of the group' do
|
context 'when traversal_ids feature flag is disabled' do
|
||||||
let(:groups) { [group1, group2, group3, create(:group, :private, parent: group1)] }
|
it_behaves_like 'executes N max member permission queries to the DB' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(use_traversal_ids: false)
|
||||||
|
described_class.new(groups, user).execute
|
||||||
|
end
|
||||||
|
|
||||||
it_behaves_like 'executes N max member permission queries to the DB' do
|
# One query for group with no access and another one per group where the user is not a direct member
|
||||||
# One query for group with no access and another one where the user is not a direct member
|
let(:expected_query_count) { 2 }
|
||||||
let(:expected_query_count) { 2 }
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when traversal_ids feature flag is enabled' do
|
||||||
|
it_behaves_like 'executes N max member permission queries to the DB' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(use_traversal_ids: true)
|
||||||
|
described_class.new(groups, user).execute
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:expected_query_count) { 0 }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue