Commit 46b10c0f authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 3358e1fd
...@@ -6,8 +6,11 @@ import { ...@@ -6,8 +6,11 @@ import {
GlButton, GlButton,
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
GlDropdownHeader,
GlDropdownDivider,
GlFormGroup, GlFormGroup,
GlModal, GlModal,
GlLoadingIcon,
GlSearchBoxByType, GlSearchBoxByType,
GlModalDirective, GlModalDirective,
GlTooltipDirective, GlTooltipDirective,
...@@ -41,7 +44,10 @@ export default { ...@@ -41,7 +44,10 @@ export default {
Icon, Icon,
GlButton, GlButton,
GlDropdown, GlDropdown,
GlLoadingIcon,
GlDropdownItem, GlDropdownItem,
GlDropdownHeader,
GlDropdownDivider,
GlSearchBoxByType, GlSearchBoxByType,
GlFormGroup, GlFormGroup,
GlModal, GlModal,
...@@ -210,6 +216,7 @@ export default { ...@@ -210,6 +216,7 @@ export default {
'useDashboardEndpoint', 'useDashboardEndpoint',
'allDashboards', 'allDashboards',
'additionalPanelTypesEnabled', 'additionalPanelTypesEnabled',
'environmentsLoading',
]), ]),
...mapGetters('monitoringDashboard', ['getMetricStates', 'filteredEnvironments']), ...mapGetters('monitoringDashboard', ['getMetricStates', 'filteredEnvironments']),
firstDashboard() { firstDashboard() {
...@@ -235,6 +242,9 @@ export default { ...@@ -235,6 +242,9 @@ export default {
shouldRenderSearchableEnvironmentsDropdown() { shouldRenderSearchableEnvironmentsDropdown() {
return this.glFeatures.searchableEnvironmentsDropdown; return this.glFeatures.searchableEnvironmentsDropdown;
}, },
shouldShowEnvironmentsDropdownNoMatchedMsg() {
return !this.environmentsLoading && this.filteredEnvironments.length === 0;
},
}, },
created() { created() {
this.setEndpoints({ this.setEndpoints({
...@@ -262,7 +272,7 @@ export default { ...@@ -262,7 +272,7 @@ export default {
'setGettingStartedEmptyState', 'setGettingStartedEmptyState',
'setEndpoints', 'setEndpoints',
'setPanelGroupMetrics', 'setPanelGroupMetrics',
'setEnvironmentsSearchTerm', 'filterEnvironments',
]), ]),
updatePanels(key, panels) { updatePanels(key, panels) {
this.setPanelGroupMetrics({ this.setPanelGroupMetrics({
...@@ -305,7 +315,7 @@ export default { ...@@ -305,7 +315,7 @@ export default {
this.formIsValid = isValid; this.formIsValid = isValid;
}, },
debouncedEnvironmentsSearch: debounce(function environmentsSearchOnInput(searchTerm) { debouncedEnvironmentsSearch: debounce(function environmentsSearchOnInput(searchTerm) {
this.setEnvironmentsSearchTerm(searchTerm); this.filterEnvironments(searchTerm);
}, 500), }, 500),
submitCustomMetricsForm() { submitCustomMetricsForm() {
this.$refs.customMetricsForm.submit(); this.$refs.customMetricsForm.submit();
...@@ -390,16 +400,22 @@ export default { ...@@ -390,16 +400,22 @@ export default {
toggle-class="dropdown-menu-toggle" toggle-class="dropdown-menu-toggle"
menu-class="monitor-environment-dropdown-menu" menu-class="monitor-environment-dropdown-menu"
:text="currentEnvironmentName" :text="currentEnvironmentName"
:disabled="filteredEnvironments.length === 0"
> >
<div class="d-flex flex-column overflow-hidden"> <div class="d-flex flex-column overflow-hidden">
<gl-dropdown-header class="text-center">{{ __('Environment') }}</gl-dropdown-header>
<gl-dropdown-divider />
<gl-search-box-by-type <gl-search-box-by-type
v-if="shouldRenderSearchableEnvironmentsDropdown" v-if="shouldRenderSearchableEnvironmentsDropdown"
ref="monitorEnvironmentsDropdownSearch" ref="monitorEnvironmentsDropdownSearch"
class="m-2" class="m-2"
@input="debouncedEnvironmentsSearch" @input="debouncedEnvironmentsSearch"
/> />
<div class="flex-fill overflow-auto"> <gl-loading-icon
v-if="environmentsLoading"
ref="monitorEnvironmentsDropdownLoading"
:inline="true"
/>
<div v-else class="flex-fill overflow-auto">
<gl-dropdown-item <gl-dropdown-item
v-for="environment in filteredEnvironments" v-for="environment in filteredEnvironments"
:key="environment.id" :key="environment.id"
...@@ -411,11 +427,11 @@ export default { ...@@ -411,11 +427,11 @@ export default {
</div> </div>
<div <div
v-if="shouldRenderSearchableEnvironmentsDropdown" v-if="shouldRenderSearchableEnvironmentsDropdown"
v-show="filteredEnvironments.length === 0" v-show="shouldShowEnvironmentsDropdownNoMatchedMsg"
ref="monitorEnvironmentsDropdownMsg" ref="monitorEnvironmentsDropdownMsg"
class="text-secondary no-matches-message" class="text-secondary no-matches-message"
> >
{{ s__('No matching results') }} {{ __('No matching results') }}
</div> </div>
</div> </div>
</gl-dropdown> </gl-dropdown>
......
...@@ -32,8 +32,9 @@ export const setEndpoints = ({ commit }, endpoints) => { ...@@ -32,8 +32,9 @@ export const setEndpoints = ({ commit }, endpoints) => {
commit(types.SET_ENDPOINTS, endpoints); commit(types.SET_ENDPOINTS, endpoints);
}; };
export const setEnvironmentsSearchTerm = ({ commit }, searchTerm) => { export const filterEnvironments = ({ commit, dispatch }, searchTerm) => {
commit(types.SET_ENVIRONMENTS_SEARCH_TERM, searchTerm); commit(types.SET_ENVIRONMENTS_FILTER, searchTerm);
dispatch('fetchEnvironmentsData');
}; };
export const setShowErrorBanner = ({ commit }, enabled) => { export const setShowErrorBanner = ({ commit }, enabled) => {
...@@ -56,6 +57,7 @@ export const receiveDeploymentsDataSuccess = ({ commit }, data) => ...@@ -56,6 +57,7 @@ export const receiveDeploymentsDataSuccess = ({ commit }, data) =>
commit(types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS, data); commit(types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS, data);
export const receiveDeploymentsDataFailure = ({ commit }) => export const receiveDeploymentsDataFailure = ({ commit }) =>
commit(types.RECEIVE_DEPLOYMENTS_DATA_FAILURE); commit(types.RECEIVE_DEPLOYMENTS_DATA_FAILURE);
export const requestEnvironmentsData = ({ commit }) => commit(types.REQUEST_ENVIRONMENTS_DATA);
export const receiveEnvironmentsDataSuccess = ({ commit }, data) => export const receiveEnvironmentsDataSuccess = ({ commit }, data) =>
commit(types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS, data); commit(types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS, data);
export const receiveEnvironmentsDataFailure = ({ commit }) => export const receiveEnvironmentsDataFailure = ({ commit }) =>
...@@ -189,8 +191,9 @@ export const fetchDeploymentsData = ({ state, dispatch }) => { ...@@ -189,8 +191,9 @@ export const fetchDeploymentsData = ({ state, dispatch }) => {
}); });
}; };
export const fetchEnvironmentsData = ({ state, dispatch }) => export const fetchEnvironmentsData = ({ state, dispatch }) => {
gqClient dispatch('requestEnvironmentsData');
return gqClient
.mutate({ .mutate({
mutation: getEnvironments, mutation: getEnvironments,
variables: { variables: {
...@@ -207,12 +210,14 @@ export const fetchEnvironmentsData = ({ state, dispatch }) => ...@@ -207,12 +210,14 @@ export const fetchEnvironmentsData = ({ state, dispatch }) =>
s__('Metrics|There was an error fetching the environments data, please try again'), s__('Metrics|There was an error fetching the environments data, please try again'),
); );
} }
dispatch('receiveEnvironmentsDataSuccess', environments); dispatch('receiveEnvironmentsDataSuccess', environments);
}) })
.catch(() => { .catch(() => {
dispatch('receiveEnvironmentsDataFailure'); dispatch('receiveEnvironmentsDataFailure');
createFlash(s__('Metrics|There was an error getting environments information.')); createFlash(s__('Metrics|There was an error getting environments information.'));
}); });
};
/** /**
* Set a new array of metrics to a panel group * Set a new array of metrics to a panel group
......
...@@ -22,4 +22,4 @@ export const SET_NO_DATA_EMPTY_STATE = 'SET_NO_DATA_EMPTY_STATE'; ...@@ -22,4 +22,4 @@ export const SET_NO_DATA_EMPTY_STATE = 'SET_NO_DATA_EMPTY_STATE';
export const SET_SHOW_ERROR_BANNER = 'SET_SHOW_ERROR_BANNER'; export const SET_SHOW_ERROR_BANNER = 'SET_SHOW_ERROR_BANNER';
export const SET_PANEL_GROUP_METRICS = 'SET_PANEL_GROUP_METRICS'; export const SET_PANEL_GROUP_METRICS = 'SET_PANEL_GROUP_METRICS';
export const SET_ENVIRONMENTS_SEARCH_TERM = 'SET_ENVIRONMENTS_SEARCH_TERM'; export const SET_ENVIRONMENTS_FILTER = 'SET_ENVIRONMENTS_FILTER';
...@@ -123,10 +123,15 @@ export default { ...@@ -123,10 +123,15 @@ export default {
[types.RECEIVE_DEPLOYMENTS_DATA_FAILURE](state) { [types.RECEIVE_DEPLOYMENTS_DATA_FAILURE](state) {
state.deploymentData = []; state.deploymentData = [];
}, },
[types.REQUEST_ENVIRONMENTS_DATA](state) {
state.environmentsLoading = true;
},
[types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS](state, environments) { [types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS](state, environments) {
state.environmentsLoading = false;
state.environments = environments; state.environments = environments;
}, },
[types.RECEIVE_ENVIRONMENTS_DATA_FAILURE](state) { [types.RECEIVE_ENVIRONMENTS_DATA_FAILURE](state) {
state.environmentsLoading = false;
state.environments = []; state.environments = [];
}, },
...@@ -195,7 +200,7 @@ export default { ...@@ -195,7 +200,7 @@ export default {
const panelGroup = state.dashboard.panel_groups.find(pg => payload.key === pg.key); const panelGroup = state.dashboard.panel_groups.find(pg => payload.key === pg.key);
panelGroup.panels = payload.panels; panelGroup.panels = payload.panels;
}, },
[types.SET_ENVIRONMENTS_SEARCH_TERM](state, searchTerm) { [types.SET_ENVIRONMENTS_FILTER](state, searchTerm) {
state.environmentsSearchTerm = searchTerm; state.environmentsSearchTerm = searchTerm;
}, },
}; };
...@@ -15,6 +15,7 @@ export default () => ({ ...@@ -15,6 +15,7 @@ export default () => ({
deploymentData: [], deploymentData: [],
environments: [], environments: [],
environmentsSearchTerm: '', environmentsSearchTerm: '',
environmentsLoading: false,
allDashboards: [], allDashboards: [],
currentDashboard: null, currentDashboard: null,
projectPath: null, projectPath: null,
......
...@@ -46,6 +46,20 @@ ...@@ -46,6 +46,20 @@
} }
} }
.prometheus-graphs-header {
.monitor-environment-dropdown-menu {
&.show {
display: flex;
flex-direction: column;
overflow: hidden;
}
.no-matches-message {
padding: $gl-padding-8 $gl-padding-12;
}
}
}
.prometheus-panel { .prometheus-panel {
margin-top: 20px; margin-top: 20px;
} }
......
...@@ -20,6 +20,14 @@ class Groups::ApplicationController < ApplicationController ...@@ -20,6 +20,14 @@ class Groups::ApplicationController < ApplicationController
@projects ||= GroupProjectsFinder.new(group: group, current_user: current_user).execute @projects ||= GroupProjectsFinder.new(group: group, current_user: current_user).execute
end end
def group_projects_with_subgroups
@group_projects_with_subgroups ||= GroupProjectsFinder.new(
group: group,
current_user: current_user,
options: { include_subgroups: true }
).execute
end
def authorize_admin_group! def authorize_admin_group!
unless can?(current_user, :admin_group, group) unless can?(current_user, :admin_group, group)
return render_404 return render_404
......
...@@ -103,8 +103,15 @@ class Groups::MilestonesController < Groups::ApplicationController ...@@ -103,8 +103,15 @@ class Groups::MilestonesController < Groups::ApplicationController
end end
def group_projects_with_access def group_projects_with_access
group_projects.with_issues_available_for_user(current_user) group_projects_with_subgroups.with_issues_or_mrs_available_for_user(current_user)
.or(group_projects.with_merge_requests_available_for_user(current_user)) end
def group_ids(include_ancestors: false)
if include_ancestors
group.self_and_hierarchy.public_or_visible_to_user(current_user).select(:id)
else
group.self_and_descendants.public_or_visible_to_user(current_user).select(:id)
end
end end
def milestone def milestone
...@@ -119,7 +126,7 @@ class Groups::MilestonesController < Groups::ApplicationController ...@@ -119,7 +126,7 @@ class Groups::MilestonesController < Groups::ApplicationController
end end
def search_params def search_params
groups = request.format.json? ? group.self_and_ancestors.select(:id) : group.id groups = request.format.json? ? group_ids(include_ancestors: true) : group_ids
params.permit(:state, :search_title).merge(group_ids: groups) params.permit(:state, :search_title).merge(group_ids: groups)
end end
......
...@@ -453,6 +453,9 @@ class Project < ApplicationRecord ...@@ -453,6 +453,9 @@ class Project < ApplicationRecord
scope :with_issues_enabled, -> { with_feature_enabled(:issues) } scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) } scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) }
scope :with_merge_requests_available_for_user, ->(current_user) { with_feature_available_for_user(:merge_requests, current_user) } scope :with_merge_requests_available_for_user, ->(current_user) { with_feature_available_for_user(:merge_requests, current_user) }
scope :with_issues_or_mrs_available_for_user, -> (user) do
with_issues_available_for_user(user).or(with_merge_requests_available_for_user(user))
end
scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) } scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) }
scope :with_remote_mirrors, -> { joins(:remote_mirrors).where(remote_mirrors: { enabled: true }).distinct } scope :with_remote_mirrors, -> { joins(:remote_mirrors).where(remote_mirrors: { enabled: true }).distinct }
scope :with_limit, -> (maximum) { limit(maximum) } scope :with_limit, -> (maximum) { limit(maximum) }
......
---
title: Migrate epic, epic notes mentions to respective DB table
merge_request: 22333
author:
type: changed
---
title: Include milestones from subgroups in the list of Group Milestones.
merge_request: 22922
author:
type: fixed
...@@ -6,14 +6,17 @@ if defined?(Rails::Console) ...@@ -6,14 +6,17 @@ if defined?(Rails::Console)
puts '-' * 80 puts '-' * 80
puts " GitLab:".ljust(justify) + "#{Gitlab::VERSION} (#{Gitlab.revision})" puts " GitLab:".ljust(justify) + "#{Gitlab::VERSION} (#{Gitlab.revision})"
puts " GitLab Shell:".ljust(justify) + "#{Gitlab::VersionInfo.parse(Gitlab::Shell.new.version)}" puts " GitLab Shell:".ljust(justify) + "#{Gitlab::VersionInfo.parse(Gitlab::Shell.new.version)}"
if Gitlab::Database.exists?
puts " #{Gitlab::Database.human_adapter_name}:".ljust(justify) + Gitlab::Database.version puts " #{Gitlab::Database.human_adapter_name}:".ljust(justify) + Gitlab::Database.version
Gitlab.ee do Gitlab.ee do
if Gitlab::Geo.enabled? if Gitlab::Geo.connected? && Gitlab::Geo.enabled?
puts " Geo enabled:".ljust(justify) + 'yes' puts " Geo enabled:".ljust(justify) + 'yes'
puts " Geo server:".ljust(justify) + EE::GeoHelper.current_node_human_status puts " Geo server:".ljust(justify) + EE::GeoHelper.current_node_human_status
end end
end end
end
puts '-' * 80 puts '-' * 80
end end
...@@ -152,6 +152,8 @@ ...@@ -152,6 +152,8 @@
- 1 - 1
- - object_storage - - object_storage
- 1 - 1
- - package_repositories
- 1
- - pages - - pages
- 1 - 1
- - pages_domain_ssl_renewal - - pages_domain_ssl_renewal
......
# frozen_string_literal: true
class MigrateEpicMentionsToDb < ActiveRecord::Migration[5.2]
DOWNTIME = false
disable_ddl_transaction!
DELAY = 2.minutes.to_i
BATCH_SIZE = 10000
MIGRATION = 'UserMentions::CreateResourceUserMention'
JOIN = "LEFT JOIN epic_user_mentions on epics.id = epic_user_mentions.epic_id"
QUERY_CONDITIONS = "(description like '%@%' OR title like '%@%') AND epic_user_mentions.epic_id is null"
class Epic < ActiveRecord::Base
include EachBatch
self.table_name = 'epics'
end
def up
return unless Gitlab.ee?
Epic
.joins(JOIN)
.where(QUERY_CONDITIONS)
.each_batch(of: BATCH_SIZE) do |batch, index|
range = batch.pluck(Arel.sql('MIN(epics.id)'), Arel.sql('MAX(epics.id)')).first
BackgroundMigrationWorker.perform_in(index * DELAY, MIGRATION, ['Epic', JOIN, QUERY_CONDITIONS, false, *range])
end
end
def down
# no-op
end
end
# frozen_string_literal: true
class MigrateEpicNotesMentionsToDb < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
DELAY = 2.minutes.to_i
BATCH_SIZE = 10000
MIGRATION = 'UserMentions::CreateResourceUserMention'
INDEX_NAME = 'epic_mentions_temp_index'
INDEX_CONDITION = "note LIKE '%@%'::text AND notes.noteable_type = 'Epic'"
QUERY_CONDITIONS = "#{INDEX_CONDITION} AND epic_user_mentions.epic_id IS NULL"
JOIN = 'LEFT JOIN epic_user_mentions ON notes.id = epic_user_mentions.note_id'
class Note < ActiveRecord::Base
include EachBatch
self.table_name = 'notes'
end
def up
return unless Gitlab.ee?
# create temporary index for notes with mentions, may take well over 1h
add_concurrent_index(:notes, :id, where: INDEX_CONDITION, name: INDEX_NAME)
Note
.joins(JOIN)
.where(QUERY_CONDITIONS)
.each_batch(of: BATCH_SIZE) do |batch, index|
range = batch.pluck(Arel.sql('MIN(notes.id)'), Arel.sql('MAX(notes.id)')).first
BackgroundMigrationWorker.perform_in(index * DELAY, MIGRATION, ['Epic', JOIN, QUERY_CONDITIONS, true, *range])
end
end
def down
# no-op
# temporary index is to be dropped in a different migration in an upcoming release:
# https://gitlab.com/gitlab-org/gitlab/issues/196842
end
end
...@@ -2778,6 +2778,7 @@ ActiveRecord::Schema.define(version: 2020_01_27_090233) do ...@@ -2778,6 +2778,7 @@ ActiveRecord::Schema.define(version: 2020_01_27_090233) do
t.index ["commit_id"], name: "index_notes_on_commit_id" t.index ["commit_id"], name: "index_notes_on_commit_id"
t.index ["created_at"], name: "index_notes_on_created_at" t.index ["created_at"], name: "index_notes_on_created_at"
t.index ["discussion_id"], name: "index_notes_on_discussion_id" t.index ["discussion_id"], name: "index_notes_on_discussion_id"
t.index ["id"], name: "epic_mentions_temp_index", where: "((note ~~ '%@%'::text) AND ((noteable_type)::text = 'Epic'::text))"
t.index ["line_code"], name: "index_notes_on_line_code" t.index ["line_code"], name: "index_notes_on_line_code"
t.index ["note"], name: "index_notes_on_note_trigram", opclass: :gin_trgm_ops, using: :gin t.index ["note"], name: "index_notes_on_note_trigram", opclass: :gin_trgm_ops, using: :gin
t.index ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type" t.index ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type"
......
...@@ -137,6 +137,7 @@ Complementary reads: ...@@ -137,6 +137,7 @@ Complementary reads:
- [Database helper modules](database_helpers.md) - [Database helper modules](database_helpers.md)
- [Code comments](code_comments.md) - [Code comments](code_comments.md)
- [Creating enums](creating_enums.md) - [Creating enums](creating_enums.md)
- [Renaming features](renaming_features.md)
### Case studies ### Case studies
......
# Renaming features
Sometimes the business asks to change the name of a feature. Broadly speaking, there are 2 approaches to that task. They basically trade between immediate effort and future complexity/bug risk:
- Complete, rename everything in the repo.
- Pros: does not increase code complexity.
- Cons: more work to execute, and higher risk of immediate bugs.
- Façade, rename as little as possible; only the user-facing content like interfaces,
documentation, error messages, etc.
- Pros: less work to execute.
- Cons: increases code complexity, creating higher risk of future bugs.
## When to choose the façade approach
The more of the following that are true, the more likely you should choose the façade approach:
- You are not confident the new name is permanent.
- The feature is susceptible to bugs (large, complex, needing refactor, etc).
- The renaming will be difficult to review (feature spans many lines/files/repos).
- The renaming will be disruptive in some way (database table renaming).
## Consider a façade-first approach
The façade approach is not necessarily a final step. It can (and possibly *should*) be treated as the first step, where later iterations will accomplish the complete rename.
# frozen_string_literal: true
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
module UserMentions
class CreateResourceUserMention
# Resources that have mentions to be migrated:
# issue, merge_request, epic, commit, snippet, design
BULK_INSERT_SIZE = 5000
ISOLATION_MODULE = 'Gitlab::BackgroundMigration::UserMentions::Models'
def perform(resource_model, join, conditions, with_notes, start_id, end_id)
resource_model = "#{ISOLATION_MODULE}::#{resource_model}".constantize if resource_model.is_a?(String)
model = with_notes ? "#{ISOLATION_MODULE}::Note".constantize : resource_model
resource_user_mention_model = resource_model.user_mention_model
records = model.joins(join).where(conditions).where(id: start_id..end_id)
records.in_groups_of(BULK_INSERT_SIZE, false).each do |records|
mentions = []
records.each do |record|
mentions << record.build_mention_values
end
no_quote_columns = [:note_id]
no_quote_columns << resource_user_mention_model.resource_foreign_key
Gitlab::Database.bulk_insert(
resource_user_mention_model.table_name,
mentions,
return_ids: true,
disable_quote: no_quote_columns,
on_conflict: :do_nothing
)
end
end
end
end
end
end
# frozen_string_literal: true
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
module UserMentions
module Models
class Epic < ActiveRecord::Base
include IsolatedMentionable
include CacheMarkdownField
attr_mentionable :title, pipeline: :single_line
attr_mentionable :description
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description, issuable_state_filter_enabled: true
self.table_name = 'epics'
belongs_to :author, class_name: "User"
belongs_to :project
belongs_to :group
def self.user_mention_model
Gitlab::BackgroundMigration::UserMentions::Models::EpicUserMention
end
def user_mention_model
self.class.user_mention_model
end
def project
nil
end
def mentionable_params
{ group: group, label_url_method: :group_epics_url }
end
def user_mention_resource_id
id
end
def user_mention_note_id
'NULL'
end
end
end
end
end
end
# frozen_string_literal: true
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
module UserMentions
module Models
class EpicUserMention < ActiveRecord::Base
self.table_name = 'epic_user_mentions'
def self.resource_foreign_key
:epic_id
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
module UserMentions
module Models
# == IsolatedMentionable concern
#
# Shortcutted for isolation version of Mentionable to be used in mentions migrations
#
module IsolatedMentionable
extend ::ActiveSupport::Concern
class_methods do
# Indicate which attributes of the Mentionable to search for GFM references.
def attr_mentionable(attr, options = {})
attr = attr.to_s
mentionable_attrs << [attr, options]
end
end
included do
# Accessor for attributes marked mentionable.
cattr_accessor :mentionable_attrs, instance_accessor: false do
[]
end
if self < Participable
participant -> (user, ext) { all_references(user, extractor: ext) }
end
end
def all_references(current_user = nil, extractor: nil)
# Use custom extractor if it's passed in the function parameters.
if extractor
extractors[current_user] = extractor
else
extractor = extractors[current_user] ||= ::Gitlab::ReferenceExtractor.new(project, current_user)
extractor.reset_memoized_values
end
self.class.mentionable_attrs.each do |attr, options|
text = __send__(attr) # rubocop:disable GitlabSecurity/PublicSend
options = options.merge(
cache_key: [self, attr],
author: author,
skip_project_check: skip_project_check?
).merge(mentionable_params)
cached_html = self.try(:updated_cached_html_for, attr.to_sym)
options[:rendered] = cached_html if cached_html
extractor.analyze(text, options)
end
extractor
end
def extractors
@extractors ||= {}
end
def skip_project_check?
false
end
def build_mention_values
refs = all_references(author)
{
"#{self.user_mention_model.resource_foreign_key}": user_mention_resource_id,
note_id: user_mention_note_id,
mentioned_users_ids: array_to_sql(refs.mentioned_users.pluck(:id)),
mentioned_projects_ids: array_to_sql(refs.mentioned_projects.pluck(:id)),
mentioned_groups_ids: array_to_sql(refs.mentioned_groups.pluck(:id))
}
end
def array_to_sql(ids_array)
return unless ids_array.present?
'{' + ids_array.join(", ") + '}'
end
private
def mentionable_params
{}
end
end
end
end
end
end
# frozen_string_literal: true
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
module UserMentions
module Models
class Note < ActiveRecord::Base
include IsolatedMentionable
include CacheMarkdownField
self.table_name = 'notes'
self.inheritance_column = :_type_disabled
attr_mentionable :note, pipeline: :note
cache_markdown_field :note, pipeline: :note, issuable_state_filter_enabled: true
belongs_to :author, class_name: "User"
belongs_to :noteable, polymorphic: true
belongs_to :project
def user_mention_model
"#{CreateResourceUserMention::ISOLATION_MODULE}::#{noteable.class}".constantize.user_mention_model
end
def for_personal_snippet?
noteable.class.name == 'PersonalSnippet'
end
def for_project_noteable?
!for_personal_snippet?
end
def skip_project_check?
!for_project_noteable?
end
def for_epic?
noteable.class.name == 'Epic'
end
def user_mention_resource_id
noteable_id || commit_id
end
def user_mention_note_id
id
end
private
def mentionable_params
return super unless for_epic?
super.merge(banzai_context_params)
end
def banzai_context_params
{ group: noteable.group, label_url_method: :group_epics_url }
end
end
end
end
end
end
...@@ -130,6 +130,40 @@ describe Groups::MilestonesController do ...@@ -130,6 +130,40 @@ describe Groups::MilestonesController do
end end
end end
end end
context 'when subgroup milestones are present' do
let(:subgroup) { create(:group, :private, parent: group) }
let(:sub_project) { create(:project, :private, group: subgroup) }
let!(:group_milestone) { create(:milestone, group: group, title: 'Group milestone') }
let!(:sub_project_milestone) { create(:milestone, project: sub_project, title: 'Sub Project Milestone') }
let!(:subgroup_milestone) { create(:milestone, title: 'Subgroup Milestone', group: subgroup) }
it 'shows subgroup milestones that user has access to' do
get :index, params: { group_id: group.to_param }
expect(response).to have_gitlab_http_status(200)
expect(response.body).to include(group_milestone.title)
expect(response.body).to include(sub_project_milestone.title)
expect(response.body).to include(subgroup_milestone.title)
end
context 'when user has no access to subgroups' do
let(:non_member) { create(:user) }
before do
sign_in(non_member)
end
it 'does not show subgroup milestones' do
get :index, params: { group_id: group.to_param }
expect(response).to have_gitlab_http_status(200)
expect(response.body).to include(group_milestone.title)
expect(response.body).not_to include(sub_project_milestone.title)
expect(response.body).not_to include(subgroup_milestone.title)
end
end
end
end end
context 'as JSON' do context 'as JSON' do
...@@ -149,6 +183,19 @@ describe Groups::MilestonesController do ...@@ -149,6 +183,19 @@ describe Groups::MilestonesController do
expect(response.content_type).to eq 'application/json' expect(response.content_type).to eq 'application/json'
end end
context 'with subgroup milestones' do
it 'lists descendants group milestones' do
subgroup = create(:group, :public, parent: group)
create(:milestone, group: subgroup, title: 'subgroup milestone')
get :index, params: { group_id: group.to_param }, format: :json
milestones = json_response
expect(milestones.count).to eq(3)
expect(milestones.second["title"]).to eq("subgroup milestone")
end
end
context 'for a subgroup' do context 'for a subgroup' do
let(:subgroup) { create(:group, parent: group) } let(:subgroup) { create(:group, parent: group) }
......
...@@ -31,10 +31,7 @@ describe('Dashboard', () => { ...@@ -31,10 +31,7 @@ describe('Dashboard', () => {
const findEnvironmentsDropdown = () => wrapper.find({ ref: 'monitorEnvironmentsDropdown' }); const findEnvironmentsDropdown = () => wrapper.find({ ref: 'monitorEnvironmentsDropdown' });
const findAllEnvironmentsDropdownItems = () => findEnvironmentsDropdown().findAll(GlDropdownItem); const findAllEnvironmentsDropdownItems = () => findEnvironmentsDropdown().findAll(GlDropdownItem);
const setSearchTerm = searchTerm => { const setSearchTerm = searchTerm => {
wrapper.vm.$store.commit( wrapper.vm.$store.commit(`monitoringDashboard/${types.SET_ENVIRONMENTS_FILTER}`, searchTerm);
`monitoringDashboard/${types.SET_ENVIRONMENTS_SEARCH_TERM}`,
searchTerm,
);
}; };
const createShallowWrapper = (props = {}, options = {}) => { const createShallowWrapper = (props = {}, options = {}) => {
...@@ -313,6 +310,25 @@ describe('Dashboard', () => { ...@@ -313,6 +310,25 @@ describe('Dashboard', () => {
expect(wrapper.find({ ref: 'monitorEnvironmentsDropdownMsg' }).isVisible()).toBe(true); expect(wrapper.find({ ref: 'monitorEnvironmentsDropdownMsg' }).isVisible()).toBe(true);
}); });
}); });
it('shows loading element when environments fetch is still loading', () => {
wrapper.vm.$store.commit(`monitoringDashboard/${types.REQUEST_ENVIRONMENTS_DATA}`);
return wrapper.vm
.$nextTick()
.then(() => {
expect(wrapper.find({ ref: 'monitorEnvironmentsDropdownLoading' }).exists()).toBe(true);
})
.then(() => {
wrapper.vm.$store.commit(
`monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
environmentData,
);
})
.then(() => {
expect(wrapper.find({ ref: 'monitorEnvironmentsDropdownLoading' }).exists()).toBe(false);
});
});
}); });
describe('drag and drop function', () => { describe('drag and drop function', () => {
......
...@@ -17,10 +17,12 @@ import { ...@@ -17,10 +17,12 @@ import {
fetchPrometheusMetrics, fetchPrometheusMetrics,
fetchPrometheusMetric, fetchPrometheusMetric,
setEndpoints, setEndpoints,
filterEnvironments,
setGettingStartedEmptyState, setGettingStartedEmptyState,
duplicateSystemDashboard, duplicateSystemDashboard,
} from '~/monitoring/stores/actions'; } from '~/monitoring/stores/actions';
import { gqClient, parseEnvironmentsResponse } from '~/monitoring/stores/utils'; import { gqClient, parseEnvironmentsResponse } from '~/monitoring/stores/utils';
import getEnvironments from '~/monitoring/queries/getEnvironments.query.graphql';
import storeState from '~/monitoring/stores/state'; import storeState from '~/monitoring/stores/state';
import { import {
deploymentData, deploymentData,
...@@ -105,12 +107,70 @@ describe('Monitoring store actions', () => { ...@@ -105,12 +107,70 @@ describe('Monitoring store actions', () => {
.catch(done.fail); .catch(done.fail);
}); });
}); });
describe('fetchEnvironmentsData', () => { describe('fetchEnvironmentsData', () => {
it('commits RECEIVE_ENVIRONMENTS_DATA_SUCCESS on error', () => {
const dispatch = jest.fn(); const dispatch = jest.fn();
const { state } = store; const { state } = store;
state.projectPath = '/gitlab-org/gitlab-test'; state.projectPath = 'gitlab-org/gitlab-test';
afterEach(() => {
resetStore(store);
jest.restoreAllMocks();
});
it('setting SET_ENVIRONMENTS_FILTER should dispatch fetchEnvironmentsData', () => {
jest.spyOn(gqClient, 'mutate').mockReturnValue(
Promise.resolve({
data: {
project: {
data: {
environments: [],
},
},
},
}),
);
return testAction(
filterEnvironments,
{},
state,
[
{
type: 'SET_ENVIRONMENTS_FILTER',
payload: {},
},
],
[
{
type: 'fetchEnvironmentsData',
},
],
);
});
it('fetch environments data call takes in search param', () => {
const mockMutate = jest.spyOn(gqClient, 'mutate');
const searchTerm = 'Something';
const mutationVariables = {
mutation: getEnvironments,
variables: {
projectPath: state.projectPath,
search: searchTerm,
},
};
state.environmentsSearchTerm = searchTerm;
mockMutate.mockReturnValue(Promise.resolve());
return fetchEnvironmentsData({
state,
dispatch,
}).then(() => {
expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
});
});
it('commits RECEIVE_ENVIRONMENTS_DATA_SUCCESS on success', () => {
jest.spyOn(gqClient, 'mutate').mockReturnValue( jest.spyOn(gqClient, 'mutate').mockReturnValue(
Promise.resolve({ Promise.resolve({
data: { data: {
...@@ -135,9 +195,6 @@ describe('Monitoring store actions', () => { ...@@ -135,9 +195,6 @@ describe('Monitoring store actions', () => {
}); });
it('commits RECEIVE_ENVIRONMENTS_DATA_FAILURE on error', () => { it('commits RECEIVE_ENVIRONMENTS_DATA_FAILURE on error', () => {
const dispatch = jest.fn();
const { state } = store;
state.projectPath = '/gitlab-org/gitlab-test';
jest.spyOn(gqClient, 'mutate').mockReturnValue(Promise.reject()); jest.spyOn(gqClient, 'mutate').mockReturnValue(Promise.reject());
return fetchEnvironmentsData({ return fetchEnvironmentsData({
...@@ -148,6 +205,7 @@ describe('Monitoring store actions', () => { ...@@ -148,6 +205,7 @@ describe('Monitoring store actions', () => {
}); });
}); });
}); });
describe('Set endpoints', () => { describe('Set endpoints', () => {
let mockedState; let mockedState;
beforeEach(() => { beforeEach(() => {
......
...@@ -5587,6 +5587,25 @@ describe Project do ...@@ -5587,6 +5587,25 @@ describe Project do
end end
end end
describe 'with_issues_or_mrs_available_for_user' do
before do
Project.delete_all
end
it 'returns correct projects' do
user = create(:user)
project1 = create(:project, :public, :merge_requests_disabled, :issues_enabled)
project2 = create(:project, :public, :merge_requests_disabled, :issues_disabled)
project3 = create(:project, :public, :issues_enabled, :merge_requests_enabled)
project4 = create(:project, :private, :issues_private, :merge_requests_private)
[project1, project2, project3, project4].each { |project| project.add_developer(user) }
expect(described_class.with_issues_or_mrs_available_for_user(user))
.to contain_exactly(project1, project3, project4)
end
end
def rugged_config def rugged_config
rugged_repo(project.repository).config rugged_repo(project.repository).config
end end
......
...@@ -229,16 +229,17 @@ RSpec.shared_examples 'mentions in description' do |mentionable_type| ...@@ -229,16 +229,17 @@ RSpec.shared_examples 'mentions in description' do |mentionable_type|
context 'when mentionable description contains mentions' do context 'when mentionable description contains mentions' do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:group) { create(:group) } let(:group) { create(:group) }
let(:mentionable_desc) { "#{user.to_reference} some description #{group.to_reference(full: true)} and @all" } let(:mentionable_desc) { "#{user.to_reference} #{user2.to_reference} #{user.to_reference} some description #{group.to_reference(full: true)} and #{user2.to_reference} @all" }
let(:mentionable) { create(mentionable_type, description: mentionable_desc) } let(:mentionable) { create(mentionable_type, description: mentionable_desc) }
it 'stores mentions' do it 'stores mentions' do
add_member(user) add_member(user)
expect(mentionable.user_mentions.count).to eq 1 expect(mentionable.user_mentions.count).to eq 1
expect(mentionable.referenced_users).to match_array([user]) expect(mentionable.referenced_users).to match_array([user, user2])
expect(mentionable.referenced_projects(user)).to match_array([mentionable.project].compact) # epic.project is nil, and we want empty [] expect(mentionable.referenced_projects(user)).to match_array([mentionable.project].compact) # epic.project is nil, and we want empty []
expect(mentionable.referenced_groups(user)).to match_array([group]) expect(mentionable.referenced_groups(user)).to match_array([group])
end end
...@@ -249,8 +250,9 @@ end ...@@ -249,8 +250,9 @@ end
RSpec.shared_examples 'mentions in notes' do |mentionable_type| RSpec.shared_examples 'mentions in notes' do |mentionable_type|
context 'when mentionable notes contain mentions' do context 'when mentionable notes contain mentions' do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:group) { create(:group) } let(:group) { create(:group) }
let(:note_desc) { "#{user.to_reference} and #{group.to_reference(full: true)} and @all" } let(:note_desc) { "#{user.to_reference} #{user2.to_reference} #{user.to_reference} and #{group.to_reference(full: true)} and #{user2.to_reference} @all" }
let!(:mentionable) { note.noteable } let!(:mentionable) { note.noteable }
before do before do
...@@ -261,7 +263,7 @@ RSpec.shared_examples 'mentions in notes' do |mentionable_type| ...@@ -261,7 +263,7 @@ RSpec.shared_examples 'mentions in notes' do |mentionable_type|
it 'returns all mentionable mentions' do it 'returns all mentionable mentions' do
expect(mentionable.user_mentions.count).to eq 1 expect(mentionable.user_mentions.count).to eq 1
expect(mentionable.referenced_users).to eq [user] expect(mentionable.referenced_users).to eq [user, user2]
expect(mentionable.referenced_projects(user)).to eq [mentionable.project].compact # epic.project is nil, and we want empty [] expect(mentionable.referenced_projects(user)).to eq [mentionable.project].compact # epic.project is nil, and we want empty []
expect(mentionable.referenced_groups(user)).to eq [group] expect(mentionable.referenced_groups(user)).to eq [group]
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment