Skip to content
Snippets Groups Projects
Commit a53d2c37 authored by GitLab Bot's avatar GitLab Bot
Browse files

Add latest changes from gitlab-org/gitlab@master

parent 18b84353
No related branches found
No related tags found
No related merge requests found
Loading
Loading
@@ -4,13 +4,14 @@ type: reference
 
# Issues Analytics **(PREMIUM)**
 
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/7478) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.5.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/7478) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.5.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/196561) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.9 at the project level.
 
Issues Analytics is a bar graph which illustrates the number of issues created each month.
The default timespan is 13 months, which includes the current month, and the 12 months
prior.
 
To access the chart, navigate to a group's sidebar and select **Analytics > Issues Analytics**.
To access the chart, navigate to your group or project sidebar and select **{chart}** **Analytics > Issues Analytics**.
 
Hover over each bar to see the total number of issues.
 
Loading
Loading
# frozen_string_literal: true
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
class CleanupOptimisticLockingNulls
QUERY_ITEM_SIZE = 1_000
# table - The name of the table the migration is performed for.
# start_id - The ID of the object to start at
# stop_id - The ID of the object to end at
def perform(start_id, stop_id, table)
model = define_model_for(table)
# After analysis done, a batch size of 1,000 items per query was found to be
# the most optimal. Discussion in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18418#note_282285336
(start_id..stop_id).each_slice(QUERY_ITEM_SIZE).each do |range|
model
.where(lock_version: nil)
.where(id: range)
.update_all(lock_version: 0)
end
end
def define_model_for(table)
Class.new(ActiveRecord::Base) do
self.table_name = table
end
end
end
end
end
Loading
Loading
@@ -1042,6 +1042,7 @@ into similar problems in the future (e.g. when new tables are created).
# job_class_name - The background migration job class as a string
# delay_interval - The duration between each job's scheduled time (must respond to `to_f`)
# batch_size - The maximum number of rows per job
# other_arguments - Other arguments to send to the job
#
# Example:
#
Loading
Loading
@@ -1059,7 +1060,7 @@ into similar problems in the future (e.g. when new tables are created).
# # do something
# end
# end
def queue_background_migration_jobs_by_range_at_intervals(model_class, job_class_name, delay_interval, batch_size: BACKGROUND_MIGRATION_BATCH_SIZE)
def queue_background_migration_jobs_by_range_at_intervals(model_class, job_class_name, delay_interval, batch_size: BACKGROUND_MIGRATION_BATCH_SIZE, other_arguments: [])
raise "#{model_class} does not have an ID to use for batch ranges" unless model_class.column_names.include?('id')
 
# To not overload the worker too much we enforce a minimum interval both
Loading
Loading
@@ -1074,7 +1075,7 @@ into similar problems in the future (e.g. when new tables are created).
# `BackgroundMigrationWorker.bulk_perform_in` schedules all jobs for
# the same time, which is not helpful in most cases where we wish to
# spread the work over time.
migrate_in(delay_interval * index, job_class_name, [start_id, end_id])
migrate_in(delay_interval * index, job_class_name, [start_id, end_id] + other_arguments)
end
end
 
Loading
Loading
Loading
Loading
@@ -3,102 +3,123 @@
require 'spec_helper'
 
describe 'Project navbar' do
it_behaves_like 'verified navigation bar' do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
 
let(:structure) do
[
{
nav_item: _('Project overview'),
nav_sub_items: [
_('Details'),
_('Activity'),
_('Releases')
]
},
{
nav_item: _('Repository'),
nav_sub_items: [
_('Files'),
_('Commits'),
_('Branches'),
_('Tags'),
_('Contributors'),
_('Graph'),
_('Compare'),
(_('Locked Files') if Gitlab.ee?)
]
},
{
nav_item: _('Issues'),
nav_sub_items: [
_('List'),
_('Boards'),
_('Labels'),
_('Milestones')
]
},
{
nav_item: _('Merge Requests'),
nav_sub_items: []
},
{
nav_item: _('CI / CD'),
nav_sub_items: [
_('Pipelines'),
_('Jobs'),
_('Artifacts'),
_('Schedules')
]
},
{
nav_item: _('Operations'),
nav_sub_items: [
_('Metrics'),
_('Environments'),
_('Error Tracking'),
_('Serverless'),
_('Kubernetes')
]
},
{
nav_item: _('Analytics'),
nav_sub_items: [
_('CI / CD Analytics'),
(_('Code Review') if Gitlab.ee?),
_('Repository Analytics'),
_('Value Stream Analytics')
]
},
{
nav_item: _('Wiki'),
nav_sub_items: []
},
{
nav_item: _('Snippets'),
nav_sub_items: []
},
{
nav_item: _('Settings'),
nav_sub_items: [
_('General'),
_('Members'),
_('Integrations'),
_('Repository'),
_('CI / CD'),
_('Operations'),
(_('Audit Events') if Gitlab.ee?)
].compact
}
let(:analytics_nav_item) do
{
nav_item: _('Analytics'),
nav_sub_items: [
_('CI / CD Analytics'),
(_('Code Review') if Gitlab.ee?),
_('Repository Analytics'),
_('Value Stream Analytics')
]
end
}
end
 
before do
project.add_maintainer(user)
sign_in(user)
let(:structure) do
[
{
nav_item: _('Project overview'),
nav_sub_items: [
_('Details'),
_('Activity'),
_('Releases')
]
},
{
nav_item: _('Repository'),
nav_sub_items: [
_('Files'),
_('Commits'),
_('Branches'),
_('Tags'),
_('Contributors'),
_('Graph'),
_('Compare'),
(_('Locked Files') if Gitlab.ee?)
]
},
{
nav_item: _('Issues'),
nav_sub_items: [
_('List'),
_('Boards'),
_('Labels'),
_('Milestones')
]
},
{
nav_item: _('Merge Requests'),
nav_sub_items: []
},
{
nav_item: _('CI / CD'),
nav_sub_items: [
_('Pipelines'),
_('Jobs'),
_('Artifacts'),
_('Schedules')
]
},
{
nav_item: _('Operations'),
nav_sub_items: [
_('Metrics'),
_('Environments'),
_('Error Tracking'),
_('Serverless'),
_('Kubernetes')
]
},
analytics_nav_item,
{
nav_item: _('Wiki'),
nav_sub_items: []
},
{
nav_item: _('Snippets'),
nav_sub_items: []
},
{
nav_item: _('Settings'),
nav_sub_items: [
_('General'),
_('Members'),
_('Integrations'),
_('Repository'),
_('CI / CD'),
_('Operations'),
(_('Audit Events') if Gitlab.ee?)
].compact
}
]
end
before do
project.add_maintainer(user)
sign_in(user)
end
 
it_behaves_like 'verified navigation bar' do
before do
visit project_path(project)
end
end
if Gitlab.ee?
context 'when issues analytics is available' do
before do
stub_licensed_features(issues_analytics: true)
analytics_nav_item[:nav_sub_items] << _('Issues Analytics')
analytics_nav_item[:nav_sub_items].sort!
visit project_path(project)
end
it_behaves_like 'verified navigation bar'
end
end
end
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { trimText } from 'spec/helpers/text_helper';
import { shallowMount } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper';
import frequentItemsListItemComponent from '~/frequent_items/components/frequent_items_list_item.vue';
import { mockProject } from '../mock_data'; // can also use 'mockGroup', but not useful to test here
import mockData from '../mock_data'; // can also use 'mockGroup', but not useful to test here
 
const localVue = createLocalVue();
const mockProject = mockData();
 
describe('FrequentItemsListItemComponent', () => {
let wrapper;
 
const findTitle = () => wrapper.find({ ref: 'frequentItemsItemTitle' });
const findAvatar = () => wrapper.find({ ref: 'frequentItemsItemAvatar' });
const findAllTitles = () => wrapper.findAll({ ref: 'frequentItemsItemTitle' });
const findNamespace = () => wrapper.find({ ref: 'frequentItemsItemNamespace' });
const findAllAnchors = () => wrapper.findAll('a');
const findAllNamespace = () => wrapper.findAll({ ref: 'frequentItemsItemNamespace' });
const findAvatarContainer = () => wrapper.findAll({ ref: 'frequentItemsItemAvatarContainer' });
const findAllMetadataContainers = () =>
wrapper.findAll({ ref: 'frequentItemsItemMetadataContainer' });
const createComponent = (props = {}) => {
wrapper = shallowMount(localVue.extend(frequentItemsListItemComponent), {
wrapper = shallowMount(frequentItemsListItemComponent, {
propsData: {
itemId: mockProject.id,
itemName: mockProject.name,
Loading
Loading
@@ -18,7 +28,6 @@ describe('FrequentItemsListItemComponent', () => {
avatarUrl: mockProject.avatarUrl,
...props,
},
localVue,
});
};
 
Loading
Loading
@@ -28,35 +37,17 @@ describe('FrequentItemsListItemComponent', () => {
});
 
describe('computed', () => {
describe('hasAvatar', () => {
it('should return `true` or `false` if whether avatar is present or not', () => {
createComponent({ avatarUrl: 'path/to/avatar.png' });
expect(wrapper.vm.hasAvatar).toBe(true);
});
it('should return `false` if avatar is not present', () => {
createComponent({ avatarUrl: null });
expect(wrapper.vm.hasAvatar).toBe(false);
});
});
describe('highlightedItemName', () => {
it('should enclose part of project name in <b> & </b> which matches with `matcher` prop', () => {
createComponent({ matcher: 'lab' });
 
expect(wrapper.find('.js-frequent-items-item-title').html()).toContain(
'<b>L</b><b>a</b><b>b</b>',
);
expect(findTitle().element.innerHTML).toContain('<b>L</b><b>a</b><b>b</b>');
});
 
it('should return project name as it is if `matcher` is not available', () => {
createComponent({ matcher: null });
 
expect(trimText(wrapper.find('.js-frequent-items-item-title').text())).toBe(
mockProject.name,
);
expect(trimText(findTitle().text())).toBe(mockProject.name);
});
});
 
Loading
Loading
@@ -64,7 +55,7 @@ describe('FrequentItemsListItemComponent', () => {
it('should truncate project name from namespace string', () => {
createComponent({ namespace: 'platform / nokia-3310' });
 
expect(trimText(wrapper.find('.js-frequent-items-item-namespace').text())).toBe('platform');
expect(trimText(findNamespace().text())).toBe('platform');
});
 
it('should truncate namespace string from the middle if it includes more than two groups in path', () => {
Loading
Loading
@@ -72,23 +63,41 @@ describe('FrequentItemsListItemComponent', () => {
namespace: 'platform / hardware / broadcom / Wifi Group / Mobile Chipset / nokia-3310',
});
 
expect(trimText(wrapper.find('.js-frequent-items-item-namespace').text())).toBe(
'platform / ... / Mobile Chipset',
);
expect(trimText(findNamespace().text())).toBe('platform / ... / Mobile Chipset');
});
});
});
 
describe('template', () => {
it('should render component element', () => {
beforeEach(() => {
createComponent();
});
it('should render avatar if avatarUrl is present', () => {
wrapper.setProps({ avatarUrl: 'path/to/avatar.png' });
return wrapper.vm.$nextTick(() => {
expect(findAvatar().exists()).toBe(true);
});
});
it('should not render avatar if avatarUrl is not present', () => {
expect(findAvatar().exists()).toBe(false);
});
it('renders root element with the right classes', () => {
expect(wrapper.classes('frequent-items-list-item-container')).toBe(true);
});
 
expect(wrapper.classes()).toContain('frequent-items-list-item-container');
expect(wrapper.findAll('a').length).toBe(1);
expect(wrapper.findAll('.frequent-items-item-avatar-container').length).toBe(1);
expect(wrapper.findAll('.frequent-items-item-metadata-container').length).toBe(1);
expect(wrapper.findAll('.frequent-items-item-title').length).toBe(1);
expect(wrapper.findAll('.frequent-items-item-namespace').length).toBe(1);
it.each`
name | selector | expected
${'anchor'} | ${findAllAnchors} | ${1}
${'avatar container'} | ${findAvatarContainer} | ${1}
${'metadata container'} | ${findAllMetadataContainers} | ${1}
${'title'} | ${findAllTitles} | ${1}
${'namespace'} | ${findAllNamespace} | ${1}
`('should render $expected $name', ({ selector, expected }) => {
expect(selector()).toHaveLength(expected);
});
});
});
import { TEST_HOST } from 'helpers/test_constants';
export default () => ({
id: 1,
name: 'GitLab Community Edition',
namespace: 'gitlab-org / gitlab-ce',
webUrl: `${TEST_HOST}/gitlab-org/gitlab-foss`,
avatarUrl: null,
});
Loading
Loading
@@ -1332,6 +1332,15 @@ describe Gitlab::Database::MigrationHelpers do
end
end
end
context 'with other_arguments option' do
it 'queues jobs correctly' do
model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.minutes, other_arguments: [1, 2])
expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id3, 1, 2]])
expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.minutes.from_now.to_f)
end
end
end
 
context "when the model doesn't have an ID column" do
Loading
Loading
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20200128210353_cleanup_optimistic_locking_nulls')
describe CleanupOptimisticLockingNulls, :migration do
TABLES = %w(epics merge_requests issues).freeze
TABLES.each do |table|
let(table.to_sym) { table(table.to_sym) }
end
let(:tables) { TABLES.map { |t| method(t.to_sym).call } }
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:users) { table(:users)}
before do
namespaces.create!(id: 123, name: 'gitlab1', path: 'gitlab1')
projects.create!(id: 123, name: 'gitlab1', path: 'gitlab1', namespace_id: 123)
users.create!(id: 123, username: 'author', projects_limit: 1000)
# Create necessary rows
epics.create!(iid: 123, group_id: 123, author_id: 123, title: 'a', title_html: 'a')
merge_requests.create!(iid: 123, target_project_id: 123, source_project_id: 123, target_branch: 'master', source_branch: 'hmm', title: 'a', title_html: 'a')
issues.create!(iid: 123, project_id: 123, title: 'a', title_html: 'a')
# Nullify `lock_version` column for all rows
# Needs to be done with a SQL fragment, otherwise Rails will coerce it to 0
tables.each do |table|
table.update_all('lock_version = NULL')
end
end
it 'correctly migrates nullified lock_version column', :sidekiq_inline do
tables.each do |table|
expect(table.where(lock_version: nil).count).to eq(1)
end
tables.each do |table|
expect(table.where(lock_version: 0).count).to eq(0)
end
migrate!
tables.each do |table|
expect(table.where(lock_version: nil).count).to eq(0)
end
tables.each do |table|
expect(table.where(lock_version: 0).count).to eq(1)
end
end
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment