Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-10-20 09:08:43 +00:00
parent 99551d4458
commit bc12365ae0
43 changed files with 598 additions and 107 deletions

View File

@ -4,7 +4,7 @@
- .docs:rules:review-docs
image: ruby:2.6-alpine
stage: review
dependencies: []
needs: []
variables:
# We're cloning the repo instead of downloading the script for now
# because some repos are private and CI_JOB_TOKEN cannot access files.

View File

@ -1274,14 +1274,12 @@ Graphql/IDType:
Exclude:
- 'ee/app/graphql/ee/mutations/issues/update.rb'
- 'ee/app/graphql/ee/types/boards/board_issue_input_base_type.rb'
- 'ee/app/graphql/mutations/instance_security_dashboard/remove_project.rb'
- 'ee/app/graphql/mutations/issues/set_epic.rb'
- 'ee/app/graphql/mutations/iterations/update.rb'
- 'ee/app/graphql/resolvers/iterations_resolver.rb'
- 'app/graphql/mutations/boards/create.rb'
- 'app/graphql/mutations/boards/issues/issue_move_list.rb'
- 'app/graphql/mutations/boards/lists/update.rb'
- 'app/graphql/mutations/discussions/toggle_resolve.rb'
- 'app/graphql/mutations/issues/update.rb'
- 'app/graphql/mutations/metrics/dashboard/annotations/delete.rb'
- 'app/graphql/mutations/snippets/destroy.rb'

View File

@ -1,19 +1,30 @@
<script>
import InstanceCounts from './instance_counts.vue';
import PipelinesChart from './pipelines_chart.vue';
import UsersChart from './users_chart.vue';
import { TODAY, TOTAL_DAYS_TO_SHOW, START_DATE } from '../constants';
export default {
name: 'InstanceStatisticsApp',
components: {
InstanceCounts,
PipelinesChart,
UsersChart,
},
TOTAL_DAYS_TO_SHOW,
START_DATE,
TODAY,
};
</script>
<template>
<div>
<instance-counts />
<users-chart
:start-date="$options.START_DATE"
:end-date="$options.TODAY"
:total-data-points="$options.TOTAL_DAYS_TO_SHOW"
/>
<pipelines-chart />
</div>
</template>

View File

@ -0,0 +1,143 @@
<script>
import { GlAlert } from '@gitlab/ui';
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import produce from 'immer';
import { sortBy } from 'lodash';
import * as Sentry from '~/sentry/wrapper';
import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
import { __ } from '~/locale';
import { formatDateAsMonth } from '~/lib/utils/datetime_utility';
import usersQuery from '../graphql/queries/users.query.graphql';
import { getAverageByMonth } from '../utils';
const sortByDate = data => sortBy(data, item => new Date(item[0]).getTime());
export default {
name: 'UsersChart',
components: { GlAlert, GlAreaChart, ChartSkeletonLoader },
props: {
startDate: {
type: Date,
required: true,
},
endDate: {
type: Date,
required: true,
},
totalDataPoints: {
type: Number,
required: true,
},
},
data() {
return {
loadingError: null,
users: [],
pageInfo: null,
};
},
apollo: {
users: {
query: usersQuery,
variables() {
return {
first: this.totalDataPoints,
after: null,
};
},
update(data) {
return data.users?.nodes || [];
},
result({ data }) {
const {
users: { pageInfo },
} = data;
this.pageInfo = pageInfo;
this.fetchNextPage();
},
error(error) {
this.handleError(error);
},
},
},
i18n: {
yAxisTitle: __('Total users'),
xAxisTitle: __('Month'),
loadUserChartError: __('Could not load the user chart. Please refresh the page to try again.'),
noDataMessage: __('There is no data available.'),
},
computed: {
isLoading() {
return this.$apollo.queries.users.loading || this.pageInfo?.hasNextPage;
},
chartUserData() {
const averaged = getAverageByMonth(
this.users.length > this.totalDataPoints
? this.users.slice(0, this.totalDataPoints)
: this.users,
{ shouldRound: true },
);
return sortByDate(averaged);
},
options() {
return {
xAxis: {
name: this.$options.i18n.xAxisTitle,
type: 'category',
axisLabel: {
formatter: formatDateAsMonth,
},
},
yAxis: {
name: this.$options.i18n.yAxisTitle,
},
};
},
},
methods: {
handleError(error) {
this.loadingError = true;
this.users = [];
Sentry.captureException(error);
},
fetchNextPage() {
if (this.pageInfo?.hasNextPage) {
this.$apollo.queries.users
.fetchMore({
variables: { first: this.totalDataPoints, after: this.pageInfo.endCursor },
updateQuery: (previousResult, { fetchMoreResult }) => {
return produce(fetchMoreResult, newUsers => {
// eslint-disable-next-line no-param-reassign
newUsers.users.nodes = [...previousResult.users.nodes, ...newUsers.users.nodes];
});
},
})
.catch(this.handleError);
}
},
},
};
</script>
<template>
<div>
<h3>{{ $options.i18n.yAxisTitle }}</h3>
<gl-alert v-if="loadingError" variant="danger" :dismissible="false" class="gl-mt-3">
{{ this.$options.i18n.loadUserChartError }}
</gl-alert>
<chart-skeleton-loader v-else-if="isLoading" />
<gl-alert v-else-if="!chartUserData.length" variant="info" :dismissible="false" class="gl-mt-3">
{{ $options.i18n.noDataMessage }}
</gl-alert>
<gl-area-chart
v-else
:option="options"
:include-legend-avg-max="true"
:data="[
{
name: $options.i18n.yAxisTitle,
data: chartUserData,
},
]"
/>
</div>
</template>

View File

@ -1,5 +1,5 @@
import { getDateInPast } from '~/lib/utils/datetime_utility';
const TOTAL_DAYS_TO_SHOW = 365;
export const TOTAL_DAYS_TO_SHOW = 365;
export const TODAY = new Date();
export const START_DATE = getDateInPast(TODAY, TOTAL_DAYS_TO_SHOW);

View File

@ -0,0 +1,4 @@
fragment Count on InstanceStatisticsMeasurement {
count
recordedAt
}

View File

@ -1,32 +1,34 @@
#import "../fragments/count.fragment.graphql"
query getInstanceCounts {
projects: instanceStatisticsMeasurements(identifier: PROJECTS, first: 1) {
nodes {
count
...Count
}
}
groups: instanceStatisticsMeasurements(identifier: GROUPS, first: 1) {
nodes {
count
...Count
}
}
users: instanceStatisticsMeasurements(identifier: USERS, first: 1) {
nodes {
count
...Count
}
}
issues: instanceStatisticsMeasurements(identifier: ISSUES, first: 1) {
nodes {
count
...Count
}
}
mergeRequests: instanceStatisticsMeasurements(identifier: MERGE_REQUESTS, first: 1) {
nodes {
count
...Count
}
}
pipelines: instanceStatisticsMeasurements(identifier: PIPELINES, first: 1) {
nodes {
count
...Count
}
}
}

View File

@ -0,0 +1,13 @@
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
#import "../fragments/count.fragment.graphql"
query getUsersCount($first: Int, $after: String) {
users: instanceStatisticsMeasurements(identifier: USERS, first: $first, after: $after) {
nodes {
...Count
}
pageInfo {
...PageInfo
}
}
}

View File

@ -125,10 +125,6 @@
content: '\f077';
}
.fa-github::before {
content: '\f09b';
}
.fa-paperclip::before {
content: '\f0c6';
}

View File

@ -8,7 +8,7 @@ module Mutations
description 'Toggles the resolved state of a discussion'
argument :id,
GraphQL::ID_TYPE,
Types::GlobalIDType[Discussion],
required: true,
description: 'The global id of the discussion'
@ -54,7 +54,10 @@ module Mutations
end
def find_object(id:)
GitlabSchema.object_from_id(id, expected_type: ::Discussion)
# TODO: remove explicit coercion once compatibility layer has been removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = Types::GlobalIDType[Discussion].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
def resolve!(discussion)

View File

@ -80,6 +80,7 @@ module Boards
set_scope
set_non_archived
set_attempt_search_optimizations
set_issue_types
params
end
@ -116,6 +117,10 @@ module Boards
end
end
def set_issue_types
params[:issue_types] = Issue::TYPES_FOR_LIST
end
# rubocop: disable CodeReuse/ActiveRecord
def board_label_ids
@board_label_ids ||= board.lists.movable.pluck(:label_id)

View File

@ -64,20 +64,26 @@ module Issues
private
def whitelisted_issue_params
base_params = [:title, :description, :confidential]
admin_params = [:milestone_id, :issue_type]
def allowed_issue_base_params
[:title, :description, :confidential, :issue_type]
end
def allowed_issue_admin_params
[:milestone_id]
end
def allowed_issue_params
if can?(current_user, :admin_issue, project)
params.slice(*(base_params + admin_params))
params.slice(*(allowed_issue_base_params + allowed_issue_admin_params))
else
params.slice(*base_params)
params.slice(*allowed_issue_base_params)
end
end
def build_issue_params
{ author: current_user }.merge(issue_params_with_info_from_discussions)
.merge(whitelisted_issue_params)
{ author: current_user }
.merge(issue_params_with_info_from_discussions)
.merge(allowed_issue_params)
end
end
end

View File

@ -10,7 +10,9 @@
= import_github_authorize_message
- if github_import_configured? && !has_ci_cd_only_params?
= link_to icon('github', text: title), status_import_github_path, class: 'btn btn-success'
= link_to status_import_github_path, class: 'btn btn-success gl-button' do
= sprite_icon('github', css_class: 'gl-mr-2')
= title
%hr

View File

@ -2,7 +2,9 @@
- page_title title
- breadcrumb_title title
- header_title _("Projects"), root_path
%h3.page-title.mb-0
= icon 'github', class: 'fa-2x', text: _('Import repositories from GitHub')
%h3.page-title.mb-0.gl-display-flex
.gl-display-flex.gl-align-items-center.gl-justify-content-center
= sprite_icon('github', css_class: 'gl-mr-2')
= _('Import repositories from GitHub')
= render 'import/githubish_status', provider: 'github'

View File

@ -15,7 +15,8 @@
- if github_import_enabled?
%div
= link_to new_import_github_path, class: 'btn js-import-github', **tracking_attrs(track_label, 'click_button', 'github') do
= icon('github', text: 'GitHub')
= sprite_icon('github')
GitHub
- if bitbucket_import_enabled?
%div

View File

@ -1,7 +1,8 @@
= form_for @project, url: project_pages_path(@project), html: { class: 'inline', title: pages_https_only_title } do |f|
- if Gitlab.config.pages.external_http || Gitlab.config.pages.external_https
= render_if_exists 'shared/pages/max_pages_size_input', form: f
- if Gitlab.config.pages.external_http || Gitlab.config.pages.external_https
.form-group
.form-check
= f.check_box :pages_https_only, class: 'form-check-input', disabled: pages_https_only_disabled?
@ -10,4 +11,4 @@
= s_('GitLabPages|Force HTTPS (requires valid certificates)')
.gl-mt-3
= f.submit s_('GitLabPages|Save'), class: 'btn btn-success'
= f.submit s_('GitLabPages|Save'), class: 'btn btn-success gl-button'

View File

@ -1,4 +1,4 @@
- return unless issuable.supports_issue_type? && can?(current_user, :admin_issue, @project)
- return unless issuable.supports_issue_type? && can?(current_user, :create_issue, @project)
.form-group.row.gl-mb-0
= form.label :type, 'Type', class: 'col-form-label col-sm-2'

View File

@ -0,0 +1,5 @@
---
title: Add LSIF to Go Auto DevOps gitlab-ci.yml
merge_request: 40072
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Update GraphQL discussionToggleResolve mutation input id to be more type-specific
merge_request: 45346
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Allow size limit to be available by default in the project pages settings form
merge_request: 45054
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Replace fa-github with GitLab SVG MERGE_REQUEST_ID
merge_request: 45533
author:
type: changed

View File

@ -91,8 +91,8 @@ The following metrics are available:
| `gitlab_transaction_rails_queue_duration_total` | Counter | 9.4 | Measures latency between GitLab Workhorse forwarding a request to Rails | `controller`, `action` |
| `gitlab_transaction_view_duration_total` | Counter | 9.4 | Duration for views | `controller`, `action`, `view` |
| `gitlab_view_rendering_duration_seconds` | Histogram | 10.2 | Duration for views (histogram) | `controller`, `action`, `view` |
| `http_requests_total` | Counter | 9.4 | Rack request count | `method` |
| `http_request_duration_seconds` | Histogram | 9.4 | HTTP response time from rack middleware | `method`, `status` |
| `http_requests_total` | Counter | 9.4 | Rack request count | `method`, `status` |
| `http_request_duration_seconds` | Histogram | 9.4 | HTTP response time from rack middleware | `method` |
| `gitlab_transaction_db_count_total` | Counter | 13.1 | Counter for total number of SQL calls | `controller`, `action` |
| `gitlab_transaction_db_write_count_total` | Counter | 13.1 | Counter for total number of write SQL calls | `controller`, `action` |
| `gitlab_transaction_db_cached_count_total` | Counter | 13.1 | Counter for total number of cached SQL calls | `controller`, `action` |

View File

@ -5870,7 +5870,7 @@ input DiscussionToggleResolveInput {
"""
The global id of the discussion
"""
id: ID!
id: DiscussionID!
"""
Will resolve the discussion when true, and unresolve the discussion when false
@ -16396,7 +16396,7 @@ input RemoveProjectFromSecurityDashboardInput {
"""
ID of the project to remove from the Instance Security Dashboard
"""
id: ID!
id: ProjectID!
}
"""

View File

@ -16105,7 +16105,7 @@
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"name": "DiscussionID",
"ofType": null
}
},
@ -47191,7 +47191,7 @@
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"name": "ProjectID",
"ofType": null
}
},

View File

@ -392,6 +392,7 @@ The following table lists variables used to disable jobs.
| `SAST_DISABLED` | From GitLab 11.0, used to disable the `sast` job. If the variable is present, the job won't be created. |
| `TEST_DISABLED` | From GitLab 11.0, used to disable the `test` job. If the variable is present, the job won't be created. |
| `SECRET_DETECTION_DISABLED` | From GitLab 13.1, used to disable the `secret_detection` job. If the variable is present, the job won't be created. |
| `CODE_INTELLIGENCE_DISABLED` | From GitLab 13.6, used to disable the `code_intelligence` job. If the variable is present, the job won't be created. |
### Application secret variables

View File

@ -95,6 +95,7 @@ project in a simple and automatic way:
1. [Auto Deploy](stages.md#auto-deploy)
1. [Auto Browser Performance Testing](stages.md#auto-browser-performance-testing) **(PREMIUM)**
1. [Auto Monitoring](stages.md#auto-monitoring)
1. [Auto Code Intelligence](stages.md#auto-code-intelligence)
As Auto DevOps relies on many different components, you should have a basic
knowledge of the following:

View File

@ -665,3 +665,7 @@ To use Auto Monitoring:
whole Kubernetes cluster, navigate to **Operations > Metrics**.
![Auto Metrics](img/auto_monitoring.png)
## Auto Code Intelligence
Code Intelligence is powered by [LSIF](https://lsif.dev/) and available for Go at this stage. We'll support more languages as they become available.

View File

@ -20,6 +20,7 @@
# * dast: DAST_DISABLED
# * review: REVIEW_DISABLED
# * stop_review: REVIEW_DISABLED
# * code_intelligence: CODE_INTELLIGENCE_DISABLED
#
# In order to deploy, you must have a Kubernetes cluster configured either
# via a project integration, or via group/project variables.
@ -159,6 +160,7 @@ include:
- template: Jobs/Build.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
- template: Jobs/Test.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml
- template: Jobs/Code-Quality.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
- template: Jobs/Code-Intelligence.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Code-Intelligence.gitlab-ci.yml
- template: Jobs/Deploy.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
- template: Jobs/Deploy/ECS.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Deploy/ECS.gitlab-ci.yml
- template: Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml

View File

@ -0,0 +1,16 @@
code_intelligence_go:
stage: test
needs: []
allow_failure: true
image: sourcegraph/lsif-go:v1
rules:
- if: $CODE_INTELLIGENCE_DISABLED
when: never
- if: $CI_COMMIT_BRANCH
exists:
- '**/*.go'
script:
- lsif-go
artifacts:
reports:
lsif: dump.lsif

View File

@ -3,15 +3,7 @@
module Gitlab
module Metrics
class RequestsRackMiddleware
HTTP_METHODS = {
"delete" => %w(200 202 204 303 400 401 403 404 500 503),
"get" => %w(200 204 301 302 303 304 307 400 401 403 404 410 422 429 500 503),
"head" => %w(200 204 301 302 303 401 403 404 410 500),
"options" => %w(200 404),
"patch" => %w(200 202 204 400 403 404 409 416 500),
"post" => %w(200 201 202 204 301 302 303 304 400 401 403 404 406 409 410 412 422 429 500 503),
"put" => %w(200 202 204 400 401 403 404 405 406 409 410 422 500)
}.freeze
HTTP_METHODS = %w(delete get head options patch post put).to_set.freeze
HEALTH_ENDPOINT = /^\/-\/(liveness|readiness|health|metrics)\/?$/.freeze
@ -40,16 +32,14 @@ module Gitlab
end
def self.initialize_http_request_duration_seconds
HTTP_METHODS.each do |method, statuses|
statuses.each do |status|
http_request_duration_seconds.get({ method: method, status: status.to_s })
end
HTTP_METHODS.each do |method|
http_request_duration_seconds.get({ method: method })
end
end
def call(env)
method = env['REQUEST_METHOD'].downcase
method = 'INVALID' unless HTTP_METHODS.key?(method)
method = 'INVALID' unless HTTP_METHODS.include?(method)
started = Time.now.to_f
health_endpoint = health_endpoint?(env['PATH_INFO'])
status = 'undefined'
@ -62,7 +52,7 @@ module Gitlab
feature_category = headers&.fetch(FEATURE_CATEGORY_HEADER, nil)
unless health_endpoint
RequestsRackMiddleware.http_request_duration_seconds.observe({ method: method, status: status.to_s }, elapsed)
RequestsRackMiddleware.http_request_duration_seconds.observe({ method: method }, elapsed)
end
[status, headers, body]

View File

@ -7513,6 +7513,9 @@ msgstr ""
msgid "Could not load instance counts. Please refresh the page to try again."
msgstr ""
msgid "Could not load the user chart. Please refresh the page to try again."
msgstr ""
msgid "Could not remove the trigger."
msgstr ""
@ -26510,6 +26513,9 @@ msgstr ""
msgid "There is no chart data available."
msgstr ""
msgid "There is no data available."
msgstr ""
msgid "There is no data available. Please change your selection."
msgstr ""
@ -27780,6 +27786,9 @@ msgstr ""
msgid "Total test time for all commits/merges"
msgstr ""
msgid "Total users"
msgstr ""
msgid "Total weight"
msgstr ""

View File

@ -23,10 +23,6 @@ module QA
element :visibility_radios, 'visibility_level:' # rubocop:disable QA/ElementWithPattern
end
view 'app/views/projects/_import_project_pane.html.haml' do
element :import_github, "icon('github', text: 'GitHub')" # rubocop:disable QA/ElementWithPattern
end
view 'app/views/projects/project_templates/_template.html.haml' do
element :use_template_button
element :template_option_row

View File

@ -387,7 +387,7 @@ module QA
end
def verify_storage_move_to_praefect(repo_path, virtual_storage)
wait_until_shell_command("docker exec #{@gitlab} bash -c 'tail -n 50 /var/log/gitlab/praefect/current'") do |line|
wait_until_shell_command("docker exec #{@praefect} bash -c 'tail -n 50 /var/log/gitlab/praefect/current'") do |line|
log = JSON.parse(line)
log['grpc.method'] == 'ReplicateRepository' && log['virtual_storage'] == virtual_storage && log['relative_path'] == repo_path

View File

@ -4,7 +4,6 @@ module QA
RSpec.describe 'Create' do
describe 'Changing Gitaly repository storage', :requires_admin do
praefect_manager = Service::PraefectManager.new
praefect_manager.gitlab = 'gitlab'
shared_examples 'repository storage move' do
it 'confirms a `finished` status after moving project repository storage' do
@ -28,7 +27,6 @@ module QA
context 'when moving from one Gitaly storage to another', :orchestrated, :repository_storage, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/973' do
let(:source_storage) { { type: :gitaly, name: 'default' } }
let(:destination_storage) { { type: :gitaly, name: QA::Runtime::Env.additional_repository_storage } }
let(:project) do
Resource::Project.fabricate_via_api! do |project|
project.name = 'repo-storage-move-status'
@ -37,6 +35,10 @@ module QA
end
end
before do
praefect_manager.gitlab = 'gitlab'
end
it_behaves_like 'repository storage move'
end
@ -46,7 +48,6 @@ module QA
context 'when moving from Gitaly to Gitaly Cluster', :requires_praefect, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/974' do
let(:source_storage) { { type: :gitaly, name: QA::Runtime::Env.non_cluster_repository_storage } }
let(:destination_storage) { { type: :praefect, name: QA::Runtime::Env.praefect_repository_storage } }
let(:project) do
Resource::Project.fabricate_via_api! do |project|
project.name = 'repo-storage-move'
@ -56,6 +57,10 @@ module QA
end
end
before do
praefect_manager.gitlab = 'gitlab-gitaly-cluster'
end
it_behaves_like 'repository storage move'
end
end

View File

@ -5,34 +5,51 @@ require 'spec_helper'
RSpec.describe 'Incident Management index', :js do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user) }
let_it_be(:guest) { create(:user) }
let_it_be(:incident) { create(:incident, project: project) }
before_all do
project.add_developer(developer)
project.add_guest(guest)
end
before do
sign_in(developer)
visit project_incidents_path(project)
wait_for_requests
end
context 'when a developer displays the incident list' do
shared_examples 'create incident form' do
it 'shows the create new issue button' do
expect(page).to have_selector('.create-incident-button')
end
it 'shows the create issue page with the Incident type pre-selected when clicked' do
it 'when clicked shows the create issue page with the Incident type pre-selected' do
find('.create-incident-button').click
wait_for_requests
wait_for_all_requests
expect(page).to have_selector(".dropdown-menu-toggle")
expect(page).to have_selector(".js-issuable-type-filter-dropdown-wrap")
expect(page).to have_selector('.dropdown-menu-toggle')
expect(page).to have_selector('.js-issuable-type-filter-dropdown-wrap')
page.within('.js-issuable-type-filter-dropdown-wrap') do
expect(page).to have_content('Incident')
end
end
end
context 'when a developer displays the incident list' do
before do
sign_in(developer)
visit project_incidents_path(project)
wait_for_all_requests
end
it_behaves_like 'create incident form'
end
context 'when a guest displays the incident list' do
before do
sign_in(guest)
visit project_incidents_path(project)
wait_for_all_requests
end
it_behaves_like 'create incident form'
end
end

View File

@ -336,7 +336,7 @@ RSpec.shared_examples 'pages settings editing' do
expect(page).not_to have_field(:project_pages_https_only)
expect(page).not_to have_content('Force HTTPS (requires valid certificates)')
expect(page).not_to have_button('Save')
expect(page).to have_button('Save')
end
end
end

View File

@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import InstanceStatisticsApp from '~/analytics/instance_statistics/components/app.vue';
import InstanceCounts from '~/analytics/instance_statistics/components//instance_counts.vue';
import PipelinesChart from '~/analytics/instance_statistics/components/pipelines_chart.vue';
import UsersChart from '~/analytics/instance_statistics/components/users_chart.vue';
describe('InstanceStatisticsApp', () => {
let wrapper;
@ -26,4 +27,8 @@ describe('InstanceStatisticsApp', () => {
it('displays the pipelines chart component', () => {
expect(wrapper.find(PipelinesChart).exists()).toBe(true);
});
it('displays the users chart component', () => {
expect(wrapper.find(UsersChart).exists()).toBe(true);
});
});

View File

@ -0,0 +1,200 @@
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import { GlAlert } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import createMockApollo from 'jest/helpers/mock_apollo_helper';
import { useFakeDate } from 'helpers/fake_date';
import UsersChart from '~/analytics/instance_statistics/components/users_chart.vue';
import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
import usersQuery from '~/analytics/instance_statistics/graphql/queries/users.query.graphql';
import { mockCountsData2, roundedSortedCountsMonthlyChartData2, mockPageInfo } from '../mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('UsersChart', () => {
let wrapper;
let queryHandler;
const mockApolloResponse = ({ loading = false, hasNextPage = false, users }) => ({
data: {
users: {
pageInfo: { ...mockPageInfo, hasNextPage },
nodes: users,
loading,
},
},
});
const mockQueryResponse = ({ users, loading = false, hasNextPage = false }) => {
const apolloQueryResponse = mockApolloResponse({ loading, hasNextPage, users });
if (loading) {
return jest.fn().mockReturnValue(new Promise(() => {}));
}
if (hasNextPage) {
return jest
.fn()
.mockResolvedValueOnce(apolloQueryResponse)
.mockResolvedValueOnce(
mockApolloResponse({
loading,
hasNextPage: false,
users: [{ recordedAt: '2020-07-21', count: 5 }],
}),
);
}
return jest.fn().mockResolvedValue(apolloQueryResponse);
};
const createComponent = ({
loadingError = false,
loading = false,
users = [],
hasNextPage = false,
} = {}) => {
queryHandler = mockQueryResponse({ users, loading, hasNextPage });
return shallowMount(UsersChart, {
props: {
startDate: useFakeDate(2020, 9, 26),
endDate: useFakeDate(2020, 10, 1),
totalDataPoints: mockCountsData2.length,
},
localVue,
apolloProvider: createMockApollo([[usersQuery, queryHandler]]),
data() {
return { loadingError };
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findLoader = () => wrapper.find(ChartSkeletonLoader);
const findAlert = () => wrapper.find(GlAlert);
const findChart = () => wrapper.find(GlAreaChart);
describe('while loading', () => {
beforeEach(() => {
wrapper = createComponent({ loading: true });
});
it('displays the skeleton loader', () => {
expect(findLoader().exists()).toBe(true);
});
it('hides the chart', () => {
expect(findChart().exists()).toBe(false);
});
});
describe('without data', () => {
beforeEach(async () => {
wrapper = createComponent({ users: [] });
await wrapper.vm.$nextTick();
});
it('renders an no data message', () => {
expect(findAlert().text()).toBe('There is no data available.');
});
it('hides the skeleton loader', () => {
expect(findLoader().exists()).toBe(false);
});
it('renders the chart', () => {
expect(findChart().exists()).toBe(false);
});
});
describe('with data', () => {
beforeEach(async () => {
wrapper = createComponent({ users: mockCountsData2 });
await wrapper.vm.$nextTick();
});
it('hides the skeleton loader', () => {
expect(findLoader().exists()).toBe(false);
});
it('renders the chart', () => {
expect(findChart().exists()).toBe(true);
});
it('passes the data to the line chart', () => {
expect(findChart().props('data')).toEqual([
{ data: roundedSortedCountsMonthlyChartData2, name: 'Total users' },
]);
});
});
describe('with errors', () => {
beforeEach(async () => {
wrapper = createComponent({ loadingError: true });
await wrapper.vm.$nextTick();
});
it('renders an error message', () => {
expect(findAlert().text()).toBe(
'Could not load the user chart. Please refresh the page to try again.',
);
});
it('hides the skeleton loader', () => {
expect(findLoader().exists()).toBe(false);
});
it('renders the chart', () => {
expect(findChart().exists()).toBe(false);
});
});
describe('when fetching more data', () => {
describe('when the fetchMore query returns data', () => {
beforeEach(async () => {
wrapper = createComponent({
users: mockCountsData2,
hasNextPage: true,
});
jest.spyOn(wrapper.vm.$apollo.queries.users, 'fetchMore');
await wrapper.vm.$nextTick();
});
it('requests data twice', () => {
expect(queryHandler).toBeCalledTimes(2);
});
it('calls fetchMore', () => {
expect(wrapper.vm.$apollo.queries.users.fetchMore).toHaveBeenCalledTimes(1);
});
});
describe('when the fetchMore query throws an error', () => {
beforeEach(() => {
wrapper = createComponent({
users: mockCountsData2,
hasNextPage: true,
});
jest
.spyOn(wrapper.vm.$apollo.queries.users, 'fetchMore')
.mockImplementation(jest.fn().mockRejectedValue());
return wrapper.vm.$nextTick();
});
it('calls fetchMore', () => {
expect(wrapper.vm.$apollo.queries.users.fetchMore).toHaveBeenCalledTimes(1);
});
it('renders an error message', () => {
expect(findAlert().text()).toBe(
'Could not load the user chart. Please refresh the page to try again.',
);
});
});
});
});

View File

@ -28,3 +28,15 @@ export const countsMonthlyChartData2 = [
['2020-07-01', 9.5], // average of 2020-07-x items
['2020-06-01', 20.666666666666668], // average of 2020-06-x items
];
export const roundedSortedCountsMonthlyChartData2 = [
['2020-06-01', 21], // average of 2020-06-x items
['2020-07-01', 10], // average of 2020-07-x items
];
export const mockPageInfo = {
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
endCursor: null,
};

View File

@ -50,8 +50,8 @@ RSpec.describe Mutations::Discussions::ToggleResolve do
it 'raises an error' do
expect { subject }.to raise_error(
Gitlab::Graphql::Errors::ArgumentError,
"#{discussion.to_global_id} is not a valid ID for Discussion."
GraphQL::CoercionError,
"\"#{discussion.to_global_id}\" does not represent an instance of Discussion"
)
end
end

View File

@ -32,7 +32,7 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware do
end
it 'measures execution time' do
expect(described_class).to receive_message_chain(:http_request_duration_seconds, :observe).with({ status: '200', method: 'get' }, a_positive_execution_time)
expect(described_class).to receive_message_chain(:http_request_duration_seconds, :observe).with({ method: 'get' }, a_positive_execution_time)
Timecop.scale(3600) { subject.call(env) }
end
@ -77,7 +77,7 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware do
it 'records the request duration' do
expect(described_class)
.to receive_message_chain(:http_request_duration_seconds, :observe)
.with({ method: 'get', status: '200' }, a_positive_execution_time)
.with({ method: 'get' }, a_positive_execution_time)
subject.call(env)
end
@ -137,10 +137,8 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware do
describe '.initialize_http_request_duration_seconds' do
it "sets labels" do
expected_labels = []
described_class::HTTP_METHODS.each do |method, statuses|
statuses.each do |status|
expected_labels << { method: method, status: status.to_s }
end
described_class::HTTP_METHODS.each do |method|
expected_labels << { method: method }
end
described_class.initialize_http_request_duration_seconds

View File

@ -3,11 +3,14 @@
require 'spec_helper.rb'
RSpec.describe Issues::BuildService do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:developer) { create(:user) }
let_it_be(:guest) { create(:user) }
let(:user) { developer }
before do
project.add_developer(user)
before_all do
project.add_developer(developer)
project.add_guest(guest)
end
def build_issue(issue_params = {})
@ -134,6 +137,7 @@ RSpec.describe Issues::BuildService do
end
describe '#execute' do
context 'as developer' do
it 'builds a new issues with given params' do
milestone = create(:milestone, project: project)
issue = build_issue(milestone_id: milestone.id)
@ -147,18 +151,42 @@ RSpec.describe Issues::BuildService do
expect(issue.milestone).to be_nil
end
context 'setting issue type' do
it 'sets the issue_type on the issue' do
issue = build_issue(issue_type: 'incident')
expect(issue.issue_type).to eq('incident')
end
context 'as guest' do
let(:user) { guest }
it 'cannot set milestone' do
milestone = create(:milestone, project: project)
issue = build_issue(milestone_id: milestone.id)
expect(issue.milestone).to be_nil
end
context 'setting issue type' do
it 'defaults to issue if issue_type not given' do
issue = build_issue
expect(issue.issue_type).to eq('issue')
expect(issue).to be_issue
end
it 'sets issue' do
issue = build_issue(issue_type: 'issue')
expect(issue).to be_issue
end
it 'sets incident' do
issue = build_issue(issue_type: 'incident')
expect(issue).to be_incident
end
it 'cannot set invalid type' do
expect do
build_issue(issue_type: 'invalid type')
end.to raise_error(ArgumentError, "'invalid type' is not a valid issue_type")
end
end
end
end