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

Add latest changes from gitlab-org/gitlab@master

parent 51a95129
No related branches found
No related tags found
No related merge requests found
Showing
with 601 additions and 436 deletions
---
title: Fix pipeline details page initialisation on invalid pipeline
merge_request: 25302
author: Fabio Huser
type: fixed
Loading
Loading
@@ -2770,6 +2770,16 @@ type Group {
"""
avatarUrl: String
 
"""
A single board of the group
"""
board(
"""
Find a board by its ID
"""
id: ID
): Board
"""
Boards of the group
"""
Loading
Loading
@@ -2789,6 +2799,11 @@ type Group {
"""
first: Int
 
"""
Find a board by its ID
"""
id: ID
"""
Returns the last _n_ elements from the list.
"""
Loading
Loading
@@ -5254,6 +5269,16 @@ type Project {
"""
avatarUrl: String
 
"""
A single board of the project
"""
board(
"""
Find a board by its ID
"""
id: ID
): Board
"""
Boards of the project
"""
Loading
Loading
@@ -5273,6 +5298,11 @@ type Project {
"""
first: Int
 
"""
Find a board by its ID
"""
id: ID
"""
Returns the last _n_ elements from the list.
"""
Loading
Loading
Loading
Loading
@@ -368,10 +368,43 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "board",
"description": "A single board of the project",
"args": [
{
"name": "id",
"description": "Find a board by its ID",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "Board",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "boards",
"description": "Boards of the project",
"args": [
{
"name": "id",
"description": "Find a board by its ID",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
},
"defaultValue": null
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
Loading
Loading
@@ -3175,10 +3208,43 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "board",
"description": "A single board of the group",
"args": [
{
"name": "id",
"description": "Find a board by its ID",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "Board",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "boards",
"description": "Boards of the group",
"args": [
{
"name": "id",
"description": "Find a board by its ID",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
},
"defaultValue": null
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
Loading
Loading
Loading
Loading
@@ -426,6 +426,7 @@ Autogenerated return type of EpicTreeReorder
| --- | ---- | ---------- |
| `autoDevopsEnabled` | Boolean | Indicates whether Auto DevOps is enabled for all projects within this group |
| `avatarUrl` | String | Avatar URL of the group |
| `board` | Board | A single board of the group |
| `description` | String | Description of the namespace |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
| `emailsDisabled` | Boolean | Indicates if a group has email notifications disabled |
Loading
Loading
@@ -801,6 +802,7 @@ Information about pagination in a connection.
| `archived` | Boolean | Indicates the archived status of the project |
| `autocloseReferencedIssues` | Boolean | Indicates if issues referenced by merge requests and commits within the default branch are closed automatically |
| `avatarUrl` | String | URL to avatar image file of the project |
| `board` | Board | A single board of the project |
| `containerRegistryEnabled` | Boolean | Indicates if the project stores Docker container images in a container registry |
| `createdAt` | Time | Timestamp of the project creation |
| `description` | String | Short description of the project |
Loading
Loading
Loading
Loading
@@ -2,7 +2,7 @@
 
## Issue tracker guidelines
 
**[Search the issue tracker](https://gitlab.com/gitlab-org/gitlab-foss/issues)** for similar entries before
**[Search the issue tracker](https://gitlab.com/gitlab-org/gitlab/issues)** for similar entries before
submitting your own, there's a good chance somebody else had the same issue or
feature proposal. Show your support with an award emoji and/or join the
discussion.
Loading
Loading
@@ -35,7 +35,7 @@ project.
## Labels
 
To allow for asynchronous issue handling, we use [milestones](https://gitlab.com/groups/gitlab-org/-/milestones)
and [labels](https://gitlab.com/gitlab-org/gitlab-foss/-/labels). Leads and product managers handle most of the
and [labels](https://gitlab.com/gitlab-org/gitlab/-/labels). Leads and product managers handle most of the
scheduling into milestones. Labelling is a task for everyone.
 
Most issues will have labels for at least one of the following:
Loading
Loading
@@ -53,7 +53,7 @@ Most issues will have labels for at least one of the following:
- Severity: ~`S1`, `~S2`, `~S3`, `~S4`
 
All labels, their meaning and priority are defined on the
[labels page](https://gitlab.com/gitlab-org/gitlab-foss/-/labels).
[labels page](https://gitlab.com/gitlab-org/gitlab/-/labels).
 
If you come across an issue that has none of these, and you're allowed to set
labels, you can _always_ add the team and type, and often also the subject.
Loading
Loading
@@ -372,14 +372,11 @@ A recent example of this was the issue for
 
## Feature proposals
 
To create a feature proposal for CE, open an issue on the
[issue tracker of CE](https://gitlab.com/gitlab-org/gitlab-foss/issues).
For feature proposals for EE, open an issue on the
[issue tracker of EE](https://gitlab.com/gitlab-org/gitlab/issues).
To create a feature proposal, open an issue on the
[issue tracker](https://gitlab.com/gitlab-org/gitlab/issues).
 
In order to help track the feature proposals, we have created a
[`feature`](https://gitlab.com/gitlab-org/gitlab-foss/issues?label_name=feature) label. For the time being, users that are not members
[`feature`](https://gitlab.com/gitlab-org/gitlab/issues?label_name=feature) label. For the time being, users that are not members
of the project cannot add labels. You can instead ask one of the [core team](https://about.gitlab.com/community/core-team/)
members to add the label ~feature to the issue or add the following
code snippet right after your description in a new line: `~feature`.
Loading
Loading
@@ -441,7 +438,7 @@ addressed.
## Technical and UX debt
 
In order to track things that can be improved in GitLab's codebase,
we use the ~"technical debt" label in [GitLab's issue tracker](https://gitlab.com/gitlab-org/gitlab-foss/issues).
we use the ~"technical debt" label in [GitLab's issue tracker](https://gitlab.com/gitlab-org/gitlab/issues).
For missed user experience requirements, we use the ~"UX debt" label.
 
These labels should be added to issues that describe things that can be improved,
Loading
Loading
Loading
Loading
@@ -472,7 +472,7 @@ end
```
 
If a computed update is needed, the value can be wrapped in `Arel.sql`, so Arel
treats it as an SQL literal. It's also a required deprecation for [Rails 6](https://gitlab.com/gitlab-org/gitlab-foss/issues/61451).
treats it as an SQL literal. It's also a required deprecation for [Rails 6](https://gitlab.com/gitlab-org/gitlab/issues/28497).
 
The below example is the same as the one above, but
the value is set to the product of the `bar` and `baz` columns:
Loading
Loading
Loading
Loading
@@ -30,11 +30,11 @@ People are saying multiple inheritance is bad. Mixing multiple modules with
multiple instance variables scattering everywhere suffer from the same issue.
The same applies to `ActiveSupport::Concern`. See:
[Consider replacing concerns with dedicated classes & composition](
https://gitlab.com/gitlab-org/gitlab-foss/issues/23786)
https://gitlab.com/gitlab-org/gitlab/issues/16270)
 
There's also a similar idea:
[Use decorators and interface segregation to solve overgrowing models problem](
https://gitlab.com/gitlab-org/gitlab-foss/issues/13484)
https://gitlab.com/gitlab-org/gitlab/issues/14235)
 
Note that `included` doesn't solve the whole issue. They define the
dependencies, but they still allow each modules to talk implicitly via the
Loading
Loading
Loading
Loading
@@ -25,7 +25,7 @@ by [`Namespaces#with_statistics`](https://gitlab.com/gitlab-org/gitlab/blob/4ab5
 
Additionally, the pattern that is currently used to update the project statistics
(the callback) doesn't scale adequately. It is currently one of the largest
[database queries transactions on production](https://gitlab.com/gitlab-org/gitlab-foss/issues/62488)
[database queries transactions on production](https://gitlab.com/gitlab-org/gitlab/issues/29070)
that takes the most time overall. We can't add one more query to it as
it will increase the transaction's length.
 
Loading
Loading
@@ -142,7 +142,7 @@ but we refresh them through Sidekiq jobs and in different transactions:
1. Create a second table (`namespace_aggregation_schedules`) with two columns `id` and `namespace_id`.
1. Whenever the statistics of a project changes, insert a row into `namespace_aggregation_schedules`
- We don't insert a new row if there's already one related to the root namespace.
- Keeping in mind the length of the transaction that involves updating `project_statistics`(<https://gitlab.com/gitlab-org/gitlab-foss/issues/62488>), the insertion should be done in a different transaction and through a Sidekiq Job.
- Keeping in mind the length of the transaction that involves updating `project_statistics`(<https://gitlab.com/gitlab-org/gitlab/issues/29070>), the insertion should be done in a different transaction and through a Sidekiq Job.
1. After inserting the row, we schedule another worker to be executed async at two different moments:
- One enqueued for immediate execution and another one scheduled in `1.5h` hours.
- We only schedule the jobs, if we can obtain a `1.5h` lease on Redis on a key based on the root namespace ID.
Loading
Loading
@@ -162,7 +162,7 @@ This implementation has the following benefits:
 
The only downside of this approach is that namespaces' statistics are updated up to `1.5` hours after the change is done,
which means there's a time window in which the statistics are inaccurate. Because we're still not
[enforcing storage limits](https://gitlab.com/gitlab-org/gitlab-foss/issues/30421), this is not a major problem.
[enforcing storage limits](https://gitlab.com/gitlab-org/gitlab/issues/17664), this is not a major problem.
 
## Conclusion
 
Loading
Loading
Loading
Loading
@@ -45,7 +45,7 @@ They are available **per project** for GitLab Community Edition,
and **per project and per group** for **GitLab Enterprise Edition**.
 
Navigate to the webhooks page by going to your project's
**Settings ➔ Integrations**.
**Settings ➔ Webhooks**.
 
## Maximum number of webhooks (per tier)
 
Loading
Loading
Loading
Loading
@@ -6,8 +6,8 @@ type: reference
 
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/15643) in GitLab 11.7.
 
GitLab supports using [Git push options](https://git-scm.com/docs/git-push#Documentation/git-push.txt--oltoptiongt)
to perform various actions at the same time as pushing changes.
GitLab supports using client-side [Git push options](https://git-scm.com/docs/git-push#Documentation/git-push.txt--oltoptiongt)
to perform various actions at the same time as pushing changes. Additionally, [Push Rules](https://docs.gitlab.com/ee/push_rules/push_rules.html) offer server-side control and enforcement options.
 
Currently, there are push options available for:
 
Loading
Loading
Loading
Loading
@@ -6997,9 +6997,6 @@ msgstr ""
msgid "Edit Pipeline Schedule %{id}"
msgstr ""
 
msgid "Edit Project Hook"
msgstr ""
msgid "Edit Release"
msgstr ""
 
Loading
Loading
@@ -9451,6 +9448,9 @@ msgstr ""
msgid "Go to %{link_to_google_takeout}."
msgstr ""
 
msgid "Go to Webhooks"
msgstr ""
msgid "Go to commits"
msgstr ""
 
Loading
Loading
@@ -10607,10 +10607,13 @@ msgstr ""
msgid "Instance license"
msgstr ""
 
msgid "Integration Settings"
msgstr ""
msgid "Integrations"
msgstr ""
 
msgid "Integrations Settings"
msgid "Integrations allow you to integrate GitLab with other applications"
msgstr ""
 
msgid "Interested parties can even contribute by pushing commits if they want to."
Loading
Loading
@@ -14780,9 +14783,6 @@ msgstr ""
msgid "Project Files"
msgstr ""
 
msgid "Project Hooks"
msgstr ""
msgid "Project ID"
msgstr ""
 
Loading
Loading
@@ -14945,30 +14945,18 @@ msgstr ""
msgid "ProjectService|Comment will be posted on each event"
msgstr ""
 
msgid "ProjectService|Integrations"
msgstr ""
msgid "ProjectService|Last edit"
msgstr ""
 
msgid "ProjectService|Perform common operations on GitLab project: %{project_name}"
msgstr ""
 
msgid "ProjectService|Project services"
msgstr ""
msgid "ProjectService|Project services allow you to integrate GitLab with other applications"
msgstr ""
msgid "ProjectService|Service"
msgstr ""
 
msgid "ProjectService|Services"
msgstr ""
 
msgid "ProjectService|Settings"
msgstr ""
msgid "ProjectService|To set up this service:"
msgstr ""
 
Loading
Loading
@@ -21811,6 +21799,15 @@ msgstr ""
msgid "WebIDE|Merge request"
msgstr ""
 
msgid "Webhook"
msgstr ""
msgid "Webhook Logs"
msgstr ""
msgid "Webhook Settings"
msgstr ""
msgid "Webhooks"
msgstr ""
 
Loading
Loading
@@ -21820,6 +21817,9 @@ msgstr ""
msgid "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group."
msgstr ""
 
msgid "Webhooks have moved. They can now be found under the Settings menu."
msgstr ""
msgid "Wednesday"
msgstr ""
 
Loading
Loading
Loading
Loading
@@ -12,12 +12,11 @@ describe Projects::HooksController do
end
 
describe '#index' do
it 'redirects to settings/integrations page' do
get(:index, params: { namespace_id: project.namespace, project_id: project })
it 'renders index with 200 status code' do
get :index, params: { namespace_id: project.namespace, project_id: project }
 
expect(response).to redirect_to(
project_settings_integrations_path(project)
)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index)
end
end
 
Loading
Loading
Loading
Loading
@@ -22,6 +22,7 @@ FactoryBot.define do
 
factory :ci_pipeline do
trait :invalid do
status { :failed }
yaml_errors { 'invalid YAML' }
failure_reason { :config_error }
end
Loading
Loading
Loading
Loading
@@ -88,6 +88,7 @@ describe 'Project navbar' do
_('General'),
_('Members'),
_('Integrations'),
_('Webhooks'),
_('Repository'),
_('CI / CD'),
_('Operations'),
Loading
Loading
Loading
Loading
@@ -1077,8 +1077,6 @@ describe 'Pipeline', :js do
end
 
context 'when pipeline has configuration errors' do
include_context 'pipeline builds'
let(:pipeline) do
create(:ci_pipeline,
:invalid,
Loading
Loading
@@ -1119,6 +1117,10 @@ describe 'Pipeline', :js do
%Q{span[title="#{pipeline.present.failure_reason}"]})
end
end
it 'contains a pipeline header with title' do
expect(page).to have_content "Pipeline ##{pipeline.id}"
end
end
 
context 'when pipeline is stuck' do
Loading
Loading
Loading
Loading
@@ -14,7 +14,7 @@ describe 'User views services' do
end
 
it 'shows the list of available services' do
expect(page).to have_content('Project services')
expect(page).to have_content('Integrations')
expect(page).to have_content('Campfire')
expect(page).to have_content('HipChat')
expect(page).to have_content('Assembla')
Loading
Loading
Loading
Loading
@@ -35,7 +35,7 @@ describe 'Projects > Settings > For a forked project', :js do
end
 
it 'renders form for incident management' do
expect(page).to have_selector('h4', text: 'Incidents')
expect(page).to have_selector('h3', text: 'Incidents')
end
 
it 'sets correct default values' do
Loading
Loading
Loading
Loading
@@ -2,11 +2,10 @@
 
require 'spec_helper'
 
describe 'Projects > Settings > Integration settings' do
describe 'Projects > Settings > Webhook Settings' do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:role) { :developer }
let(:integrations_path) { project_settings_integrations_path(project) }
let(:webhooks_path) { project_hooks_path(project) }
 
before do
sign_in(user)
Loading
Loading
@@ -17,7 +16,7 @@ describe 'Projects > Settings > Integration settings' do
let(:role) { :developer }
 
it 'to be disallowed to view' do
visit integrations_path
visit webhooks_path
 
expect(page.status_code).to eq(404)
end
Loading
Loading
@@ -33,7 +32,7 @@ describe 'Projects > Settings > Integration settings' do
it 'show list of webhooks' do
hook
 
visit integrations_path
visit webhooks_path
 
expect(page.status_code).to eq(200)
expect(page).to have_content(hook.url)
Loading
Loading
@@ -49,7 +48,7 @@ describe 'Projects > Settings > Integration settings' do
end
 
it 'create webhook' do
visit integrations_path
visit webhooks_path
 
fill_in 'hook_url', with: url
check 'Tag push events'
Loading
Loading
@@ -68,7 +67,7 @@ describe 'Projects > Settings > Integration settings' do
 
it 'edit existing webhook' do
hook
visit integrations_path
visit webhooks_path
 
click_link 'Edit'
fill_in 'hook_url', with: url
Loading
Loading
@@ -81,25 +80,25 @@ describe 'Projects > Settings > Integration settings' do
 
it 'test existing webhook', :js do
WebMock.stub_request(:post, hook.url)
visit integrations_path
visit webhooks_path
 
find('.hook-test-button.dropdown').click
click_link 'Push events'
 
expect(current_path).to eq(integrations_path)
expect(current_path).to eq(webhooks_path)
end
 
context 'delete existing webhook' do
it 'from webhooks list page' do
hook
visit integrations_path
visit webhooks_path
 
expect { click_link 'Delete' }.to change(ProjectHook, :count).by(-1)
end
 
it 'from webhook edit page' do
hook
visit integrations_path
visit webhooks_path
click_link 'Edit'
 
expect { click_link 'Delete' }.to change(ProjectHook, :count).by(-1)
Loading
Loading
Loading
Loading
@@ -8,13 +8,13 @@ exports[`grafana integration component default state to match the default snapsh
<div
class="settings-header"
>
<h4
class="js-section-header"
<h3
class="js-section-header h4"
>
Grafana Authentication
</h4>
</h3>
<gl-button-stub
class="js-settings-toggle"
Loading
Loading
Loading
Loading
@@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import { setTestTimeout } from 'helpers/timeout';
import { GlLink } from '@gitlab/ui';
import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import { cloneDeep } from 'lodash';
import { shallowWrapperContainsSlotText } from 'helpers/vue_test_utils_helper';
import { chartColorValues } from '~/monitoring/constants';
import { createStore } from '~/monitoring/stores';
Loading
Loading
@@ -32,501 +33,563 @@ jest.mock('~/lib/utils/icon_utils', () => ({
 
describe('Time series component', () => {
let mockGraphData;
let makeTimeSeriesChart;
let store;
 
beforeEach(() => {
setTestTimeout(1000);
store = createStore();
store.commit(
`monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`,
metricsDashboardPayload,
);
store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData);
const makeTimeSeriesChart = (graphData, type) =>
shallowMount(TimeSeries, {
propsData: {
graphData: { ...graphData, type },
deploymentData: store.state.monitoringDashboard.deploymentData,
projectPath: `${mockHost}${mockProjectDir}`,
},
store,
});
 
// Mock data contains 2 panel groups, with 1 and 2 panels respectively
store.commit(
`monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`,
mockedQueryResultPayload,
);
describe('With a single time series', () => {
beforeEach(() => {
setTestTimeout(1000);
 
// Pick the second panel group and the first panel in it
[mockGraphData] = store.state.monitoringDashboard.dashboard.panel_groups[1].panels;
store = createStore();
 
makeTimeSeriesChart = (graphData, type) =>
shallowMount(TimeSeries, {
propsData: {
graphData: { ...graphData, type },
deploymentData: store.state.monitoringDashboard.deploymentData,
projectPath: `${mockHost}${mockProjectDir}`,
},
store,
});
});
store.commit(
`monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`,
metricsDashboardPayload,
);
 
describe('general functions', () => {
let timeSeriesChart;
store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData);
 
const findChart = () => timeSeriesChart.find({ ref: 'chart' });
// Mock data contains 2 panel groups, with 1 and 2 panels respectively
store.commit(
`monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`,
mockedQueryResultPayload,
);
 
beforeEach(done => {
timeSeriesChart = makeTimeSeriesChart(mockGraphData, 'area-chart');
timeSeriesChart.vm.$nextTick(done);
// Pick the second panel group and the first panel in it
[mockGraphData] = store.state.monitoringDashboard.dashboard.panel_groups[1].panels;
});
 
it('allows user to override max value label text using prop', () => {
timeSeriesChart.setProps({ legendMaxText: 'legendMaxText' });
return timeSeriesChart.vm.$nextTick().then(() => {
expect(timeSeriesChart.props().legendMaxText).toBe('legendMaxText');
});
});
describe('general functions', () => {
let timeSeriesChart;
 
it('allows user to override average value label text using prop', () => {
timeSeriesChart.setProps({ legendAverageText: 'averageText' });
const findChart = () => timeSeriesChart.find({ ref: 'chart' });
 
return timeSeriesChart.vm.$nextTick().then(() => {
expect(timeSeriesChart.props().legendAverageText).toBe('averageText');
beforeEach(done => {
timeSeriesChart = makeTimeSeriesChart(mockGraphData, 'area-chart');
timeSeriesChart.vm.$nextTick(done);
});
});
 
describe('events', () => {
describe('datazoom', () => {
let eChartMock;
let startValue;
let endValue;
beforeEach(done => {
eChartMock = {
handlers: {},
getOption: () => ({
dataZoom: [
{
startValue,
endValue,
},
],
}),
off: jest.fn(eChartEvent => {
delete eChartMock.handlers[eChartEvent];
}),
on: jest.fn((eChartEvent, fn) => {
eChartMock.handlers[eChartEvent] = fn;
}),
};
timeSeriesChart = makeTimeSeriesChart(mockGraphData);
timeSeriesChart.vm.$nextTick(() => {
findChart().vm.$emit('created', eChartMock);
done();
});
});
it('allows user to override max value label text using prop', () => {
timeSeriesChart.setProps({ legendMaxText: 'legendMaxText' });
 
it('handles datazoom event from chart', () => {
startValue = 1577836800000; // 2020-01-01T00:00:00.000Z
endValue = 1577840400000; // 2020-01-01T01:00:00.000Z
eChartMock.handlers.datazoom();
expect(timeSeriesChart.emitted('datazoom')).toHaveLength(1);
expect(timeSeriesChart.emitted('datazoom')[0]).toEqual([
{
start: new Date(startValue).toISOString(),
end: new Date(endValue).toISOString(),
},
]);
return timeSeriesChart.vm.$nextTick().then(() => {
expect(timeSeriesChart.props().legendMaxText).toBe('legendMaxText');
});
});
});
 
describe('methods', () => {
describe('formatTooltipText', () => {
let mockDate;
let mockCommitUrl;
let generateSeriesData;
it('allows user to override average value label text using prop', () => {
timeSeriesChart.setProps({ legendAverageText: 'averageText' });
 
beforeEach(() => {
mockDate = deploymentData[0].created_at;
mockCommitUrl = deploymentData[0].commitUrl;
generateSeriesData = type => ({
seriesData: [
{
seriesName: timeSeriesChart.vm.chartData[0].name,
componentSubType: type,
value: [mockDate, 5.55555],
dataIndex: 0,
},
],
value: mockDate,
});
return timeSeriesChart.vm.$nextTick().then(() => {
expect(timeSeriesChart.props().legendAverageText).toBe('averageText');
});
});
 
it('does not throw error if data point is outside the zoom range', () => {
const seriesDataWithoutValue = generateSeriesData('line');
expect(
timeSeriesChart.vm.formatTooltipText({
...seriesDataWithoutValue,
seriesData: seriesDataWithoutValue.seriesData.map(data => ({
...data,
value: undefined,
})),
}),
).toBeUndefined();
});
describe('events', () => {
describe('datazoom', () => {
let eChartMock;
let startValue;
let endValue;
 
describe('when series is of line type', () => {
beforeEach(done => {
timeSeriesChart.vm.formatTooltipText(generateSeriesData('line'));
timeSeriesChart.vm.$nextTick(done);
});
eChartMock = {
handlers: {},
getOption: () => ({
dataZoom: [
{
startValue,
endValue,
},
],
}),
off: jest.fn(eChartEvent => {
delete eChartMock.handlers[eChartEvent];
}),
on: jest.fn((eChartEvent, fn) => {
eChartMock.handlers[eChartEvent] = fn;
}),
};
 
it('formats tooltip title', () => {
expect(timeSeriesChart.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM');
timeSeriesChart = makeTimeSeriesChart(mockGraphData);
timeSeriesChart.vm.$nextTick(() => {
findChart().vm.$emit('created', eChartMock);
done();
});
});
 
it('formats tooltip content', () => {
const name = 'Pod average';
const value = '5.556';
const dataIndex = 0;
const seriesLabel = timeSeriesChart.find(GlChartSeriesLabel);
it('handles datazoom event from chart', () => {
startValue = 1577836800000; // 2020-01-01T00:00:00.000Z
endValue = 1577840400000; // 2020-01-01T01:00:00.000Z
eChartMock.handlers.datazoom();
 
expect(seriesLabel.vm.color).toBe('');
expect(shallowWrapperContainsSlotText(seriesLabel, 'default', name)).toBe(true);
expect(timeSeriesChart.vm.tooltip.content).toEqual([
{ name, value, dataIndex, color: undefined },
expect(timeSeriesChart.emitted('datazoom')).toHaveLength(1);
expect(timeSeriesChart.emitted('datazoom')[0]).toEqual([
{
start: new Date(startValue).toISOString(),
end: new Date(endValue).toISOString(),
},
]);
expect(
shallowWrapperContainsSlotText(
timeSeriesChart.find(GlAreaChart),
'tooltipContent',
value,
),
).toBe(true);
});
});
});
describe('methods', () => {
describe('formatTooltipText', () => {
let mockDate;
let mockCommitUrl;
let generateSeriesData;
 
describe('when series is of scatter type, for deployments', () => {
beforeEach(() => {
timeSeriesChart.vm.formatTooltipText(generateSeriesData('scatter'));
mockDate = deploymentData[0].created_at;
mockCommitUrl = deploymentData[0].commitUrl;
generateSeriesData = type => ({
seriesData: [
{
seriesName: timeSeriesChart.vm.chartData[0].name,
componentSubType: type,
value: [mockDate, 5.55555],
dataIndex: 0,
},
],
value: mockDate,
});
});
 
it('formats tooltip title', () => {
expect(timeSeriesChart.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM');
it('does not throw error if data point is outside the zoom range', () => {
const seriesDataWithoutValue = generateSeriesData('line');
expect(
timeSeriesChart.vm.formatTooltipText({
...seriesDataWithoutValue,
seriesData: seriesDataWithoutValue.seriesData.map(data => ({
...data,
value: undefined,
})),
}),
).toBeUndefined();
});
 
it('formats tooltip sha', () => {
expect(timeSeriesChart.vm.tooltip.sha).toBe('f5bcd1d9');
describe('when series is of line type', () => {
beforeEach(done => {
timeSeriesChart.vm.formatTooltipText(generateSeriesData('line'));
timeSeriesChart.vm.$nextTick(done);
});
it('formats tooltip title', () => {
expect(timeSeriesChart.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM');
});
it('formats tooltip content', () => {
const name = 'Pod average';
const value = '5.556';
const dataIndex = 0;
const seriesLabel = timeSeriesChart.find(GlChartSeriesLabel);
expect(seriesLabel.vm.color).toBe('');
expect(shallowWrapperContainsSlotText(seriesLabel, 'default', name)).toBe(true);
expect(timeSeriesChart.vm.tooltip.content).toEqual([
{ name, value, dataIndex, color: undefined },
]);
expect(
shallowWrapperContainsSlotText(
timeSeriesChart.find(GlAreaChart),
'tooltipContent',
value,
),
).toBe(true);
});
});
 
it('formats tooltip commit url', () => {
expect(timeSeriesChart.vm.tooltip.commitUrl).toBe(mockCommitUrl);
describe('when series is of scatter type, for deployments', () => {
beforeEach(() => {
timeSeriesChart.vm.formatTooltipText(generateSeriesData('scatter'));
});
it('formats tooltip title', () => {
expect(timeSeriesChart.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM');
});
it('formats tooltip sha', () => {
expect(timeSeriesChart.vm.tooltip.sha).toBe('f5bcd1d9');
});
it('formats tooltip commit url', () => {
expect(timeSeriesChart.vm.tooltip.commitUrl).toBe(mockCommitUrl);
});
});
});
});
 
describe('setSvg', () => {
const mockSvgName = 'mockSvgName';
describe('setSvg', () => {
const mockSvgName = 'mockSvgName';
 
beforeEach(done => {
timeSeriesChart.vm.setSvg(mockSvgName);
timeSeriesChart.vm.$nextTick(done);
});
beforeEach(done => {
timeSeriesChart.vm.setSvg(mockSvgName);
timeSeriesChart.vm.$nextTick(done);
});
 
it('gets svg path content', () => {
expect(iconUtils.getSvgIconPathContent).toHaveBeenCalledWith(mockSvgName);
});
it('gets svg path content', () => {
expect(iconUtils.getSvgIconPathContent).toHaveBeenCalledWith(mockSvgName);
});
 
it('sets svg path content', () => {
timeSeriesChart.vm.$nextTick(() => {
expect(timeSeriesChart.vm.svgs[mockSvgName]).toBe(`path://${mockSvgPathContent}`);
it('sets svg path content', () => {
timeSeriesChart.vm.$nextTick(() => {
expect(timeSeriesChart.vm.svgs[mockSvgName]).toBe(`path://${mockSvgPathContent}`);
});
});
});
 
it('contains an svg object within an array to properly render icon', () => {
timeSeriesChart.vm.$nextTick(() => {
expect(timeSeriesChart.vm.chartOptions.dataZoom).toEqual([
{
handleIcon: `path://${mockSvgPathContent}`,
},
]);
it('contains an svg object within an array to properly render icon', () => {
timeSeriesChart.vm.$nextTick(() => {
expect(timeSeriesChart.vm.chartOptions.dataZoom).toEqual([
{
handleIcon: `path://${mockSvgPathContent}`,
},
]);
});
});
});
});
 
describe('onResize', () => {
const mockWidth = 233;
describe('onResize', () => {
const mockWidth = 233;
 
beforeEach(() => {
jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => ({
width: mockWidth,
}));
timeSeriesChart.vm.onResize();
});
beforeEach(() => {
jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => ({
width: mockWidth,
}));
timeSeriesChart.vm.onResize();
});
 
it('sets area chart width', () => {
expect(timeSeriesChart.vm.width).toBe(mockWidth);
it('sets area chart width', () => {
expect(timeSeriesChart.vm.width).toBe(mockWidth);
});
});
});
});
 
describe('computed', () => {
const getChartOptions = () => findChart().props('option');
describe('computed', () => {
const getChartOptions = () => findChart().props('option');
 
describe('chartData', () => {
let chartData;
const seriesData = () => chartData[0];
describe('chartData', () => {
let chartData;
const seriesData = () => chartData[0];
 
beforeEach(() => {
({ chartData } = timeSeriesChart.vm);
});
beforeEach(() => {
({ chartData } = timeSeriesChart.vm);
});
 
it('utilizes all data points', () => {
const { values } = mockGraphData.metrics[0].result[0];
it('utilizes all data points', () => {
const { values } = mockGraphData.metrics[0].result[0];
 
expect(chartData.length).toBe(1);
expect(seriesData().data.length).toBe(values.length);
});
expect(chartData.length).toBe(1);
expect(seriesData().data.length).toBe(values.length);
});
 
it('creates valid data', () => {
const { data } = seriesData();
it('creates valid data', () => {
const { data } = seriesData();
 
expect(
data.filter(
([time, value]) => new Date(time).getTime() > 0 && typeof value === 'number',
).length,
).toBe(data.length);
});
expect(
data.filter(
([time, value]) => new Date(time).getTime() > 0 && typeof value === 'number',
).length,
).toBe(data.length);
});
 
it('formats line width correctly', () => {
expect(chartData[0].lineStyle.width).toBe(2);
});
it('formats line width correctly', () => {
expect(chartData[0].lineStyle.width).toBe(2);
});
 
it('formats line color correctly', () => {
expect(chartData[0].lineStyle.color).toBe(chartColorValues[0]);
it('formats line color correctly', () => {
expect(chartData[0].lineStyle.color).toBe(chartColorValues[0]);
});
});
});
 
describe('chartOptions', () => {
describe('are extended by `option`', () => {
const mockSeriesName = 'Extra series 1';
const mockOption = {
option1: 'option1',
option2: 'option2',
};
it('arbitrary options', () => {
timeSeriesChart.setProps({
option: mockOption,
});
describe('chartOptions', () => {
describe('are extended by `option`', () => {
const mockSeriesName = 'Extra series 1';
const mockOption = {
option1: 'option1',
option2: 'option2',
};
 
return timeSeriesChart.vm.$nextTick().then(() => {
expect(getChartOptions()).toEqual(expect.objectContaining(mockOption));
});
});
it('arbitrary options', () => {
timeSeriesChart.setProps({
option: mockOption,
});
 
it('additional series', () => {
timeSeriesChart.setProps({
option: {
series: [
{
name: mockSeriesName,
},
],
},
return timeSeriesChart.vm.$nextTick().then(() => {
expect(getChartOptions()).toEqual(expect.objectContaining(mockOption));
});
});
 
return timeSeriesChart.vm.$nextTick().then(() => {
const optionSeries = getChartOptions().series;
it('additional series', () => {
timeSeriesChart.setProps({
option: {
series: [
{
name: mockSeriesName,
},
],
},
});
return timeSeriesChart.vm.$nextTick().then(() => {
const optionSeries = getChartOptions().series;
 
expect(optionSeries.length).toEqual(2);
expect(optionSeries[0].name).toEqual(mockSeriesName);
expect(optionSeries.length).toEqual(2);
expect(optionSeries[0].name).toEqual(mockSeriesName);
});
});
});
 
it('additional y axis data', () => {
const mockCustomYAxisOption = {
name: 'Custom y axis label',
axisLabel: {
formatter: jest.fn(),
},
};
it('additional y axis data', () => {
const mockCustomYAxisOption = {
name: 'Custom y axis label',
axisLabel: {
formatter: jest.fn(),
},
};
 
timeSeriesChart.setProps({
option: {
yAxis: mockCustomYAxisOption,
},
timeSeriesChart.setProps({
option: {
yAxis: mockCustomYAxisOption,
},
});
return timeSeriesChart.vm.$nextTick().then(() => {
const { yAxis } = getChartOptions();
expect(yAxis[0]).toMatchObject(mockCustomYAxisOption);
});
});
 
return timeSeriesChart.vm.$nextTick().then(() => {
const { yAxis } = getChartOptions();
it('additional x axis data', () => {
const mockCustomXAxisOption = {
name: 'Custom x axis label',
};
timeSeriesChart.setProps({
option: {
xAxis: mockCustomXAxisOption,
},
});
return timeSeriesChart.vm.$nextTick().then(() => {
const { xAxis } = getChartOptions();
 
expect(yAxis[0]).toMatchObject(mockCustomYAxisOption);
expect(xAxis).toMatchObject(mockCustomXAxisOption);
});
});
});
 
it('additional x axis data', () => {
const mockCustomXAxisOption = {
name: 'Custom x axis label',
};
describe('yAxis formatter', () => {
let dataFormatter;
let deploymentFormatter;
 
timeSeriesChart.setProps({
option: {
xAxis: mockCustomXAxisOption,
},
beforeEach(() => {
dataFormatter = getChartOptions().yAxis[0].axisLabel.formatter;
deploymentFormatter = getChartOptions().yAxis[1].axisLabel.formatter;
});
 
return timeSeriesChart.vm.$nextTick().then(() => {
const { xAxis } = getChartOptions();
it('rounds to 3 decimal places', () => {
expect(dataFormatter(0.88888)).toBe('0.889');
});
 
expect(xAxis).toMatchObject(mockCustomXAxisOption);
it('deployment formatter is set as is required to display a tooltip', () => {
expect(deploymentFormatter).toEqual(expect.any(Function));
});
});
});
 
describe('yAxis formatter', () => {
let dataFormatter;
let deploymentFormatter;
describe('deploymentSeries', () => {
it('utilizes deployment data', () => {
expect(timeSeriesChart.vm.deploymentSeries.yAxisIndex).toBe(1); // same as deployment y axis
expect(timeSeriesChart.vm.deploymentSeries.data).toEqual([
['2019-07-16T10:14:25.589Z', expect.any(Number)],
['2019-07-16T11:14:25.589Z', expect.any(Number)],
['2019-07-16T12:14:25.589Z', expect.any(Number)],
]);
 
beforeEach(() => {
dataFormatter = getChartOptions().yAxis[0].axisLabel.formatter;
deploymentFormatter = getChartOptions().yAxis[1].axisLabel.formatter;
expect(timeSeriesChart.vm.deploymentSeries.symbolSize).toBe(14);
});
});
 
it('rounds to 3 decimal places', () => {
expect(dataFormatter(0.88888)).toBe('0.889');
describe('yAxisLabel', () => {
it('y axis is configured correctly', () => {
const { yAxis } = getChartOptions();
expect(yAxis).toHaveLength(2);
const [dataAxis, deploymentAxis] = yAxis;
expect(dataAxis.boundaryGap).toHaveLength(2);
expect(dataAxis.scale).toBe(true);
expect(deploymentAxis.show).toBe(false);
expect(deploymentAxis.min).toEqual(expect.any(Number));
expect(deploymentAxis.max).toEqual(expect.any(Number));
expect(deploymentAxis.min).toBeLessThan(deploymentAxis.max);
});
 
it('deployment formatter is set as is required to display a tooltip', () => {
expect(deploymentFormatter).toEqual(expect.any(Function));
it('constructs a label for the chart y-axis', () => {
const { yAxis } = getChartOptions();
expect(yAxis[0].name).toBe('Memory Used per Pod');
});
});
});
 
describe('deploymentSeries', () => {
it('utilizes deployment data', () => {
expect(timeSeriesChart.vm.deploymentSeries.yAxisIndex).toBe(1); // same as deployment y axis
expect(timeSeriesChart.vm.deploymentSeries.data).toEqual([
['2019-07-16T10:14:25.589Z', expect.any(Number)],
['2019-07-16T11:14:25.589Z', expect.any(Number)],
['2019-07-16T12:14:25.589Z', expect.any(Number)],
]);
expect(timeSeriesChart.vm.deploymentSeries.symbolSize).toBe(14);
});
afterEach(() => {
timeSeriesChart.destroy();
});
});
 
describe('yAxisLabel', () => {
it('y axis is configured correctly', () => {
const { yAxis } = getChartOptions();
describe('wrapped components', () => {
const glChartComponents = [
{
chartType: 'area-chart',
component: GlAreaChart,
},
{
chartType: 'line-chart',
component: GlLineChart,
},
];
 
expect(yAxis).toHaveLength(2);
glChartComponents.forEach(dynamicComponent => {
describe(`GitLab UI: ${dynamicComponent.chartType}`, () => {
let timeSeriesAreaChart;
const findChartComponent = () => timeSeriesAreaChart.find(dynamicComponent.component);
 
const [dataAxis, deploymentAxis] = yAxis;
beforeEach(done => {
timeSeriesAreaChart = makeTimeSeriesChart(mockGraphData, dynamicComponent.chartType);
timeSeriesAreaChart.vm.$nextTick(done);
});
 
expect(dataAxis.boundaryGap).toHaveLength(2);
expect(dataAxis.scale).toBe(true);
afterEach(() => {
timeSeriesAreaChart.destroy();
});
 
expect(deploymentAxis.show).toBe(false);
expect(deploymentAxis.min).toEqual(expect.any(Number));
expect(deploymentAxis.max).toEqual(expect.any(Number));
expect(deploymentAxis.min).toBeLessThan(deploymentAxis.max);
});
it('is a Vue instance', () => {
expect(findChartComponent().exists()).toBe(true);
expect(findChartComponent().isVueInstance()).toBe(true);
});
 
it('constructs a label for the chart y-axis', () => {
const { yAxis } = getChartOptions();
it('receives data properties needed for proper chart render', () => {
const props = findChartComponent().props();
 
expect(yAxis[0].name).toBe('Memory Used per Pod');
});
});
});
expect(props.data).toBe(timeSeriesAreaChart.vm.chartData);
expect(props.option).toBe(timeSeriesAreaChart.vm.chartOptions);
expect(props.formatTooltipText).toBe(timeSeriesAreaChart.vm.formatTooltipText);
expect(props.thresholds).toBe(timeSeriesAreaChart.vm.thresholds);
});
 
afterEach(() => {
timeSeriesChart.destroy();
});
});
it('recieves a tooltip title', done => {
const mockTitle = 'mockTitle';
timeSeriesAreaChart.vm.tooltip.title = mockTitle;
 
describe('wrapped components', () => {
const glChartComponents = [
{
chartType: 'area-chart',
component: GlAreaChart,
},
{
chartType: 'line-chart',
component: GlLineChart,
},
];
timeSeriesAreaChart.vm.$nextTick(() => {
expect(
shallowWrapperContainsSlotText(findChartComponent(), 'tooltipTitle', mockTitle),
).toBe(true);
done();
});
});
 
glChartComponents.forEach(dynamicComponent => {
describe(`GitLab UI: ${dynamicComponent.chartType}`, () => {
let timeSeriesAreaChart;
const findChartComponent = () => timeSeriesAreaChart.find(dynamicComponent.component);
describe('when tooltip is showing deployment data', () => {
const mockSha = 'mockSha';
const commitUrl = `${mockProjectDir}/-/commit/${mockSha}`;
 
beforeEach(done => {
timeSeriesAreaChart = makeTimeSeriesChart(mockGraphData, dynamicComponent.chartType);
timeSeriesAreaChart.vm.$nextTick(done);
});
beforeEach(done => {
timeSeriesAreaChart.vm.tooltip.isDeployment = true;
timeSeriesAreaChart.vm.$nextTick(done);
});
 
afterEach(() => {
timeSeriesAreaChart.destroy();
});
it('uses deployment title', () => {
expect(
shallowWrapperContainsSlotText(findChartComponent(), 'tooltipTitle', 'Deployed'),
).toBe(true);
});
 
it('is a Vue instance', () => {
expect(findChartComponent().exists()).toBe(true);
expect(findChartComponent().isVueInstance()).toBe(true);
});
it('renders clickable commit sha in tooltip content', done => {
timeSeriesAreaChart.vm.tooltip.sha = mockSha;
timeSeriesAreaChart.vm.tooltip.commitUrl = commitUrl;
 
it('receives data properties needed for proper chart render', () => {
const props = findChartComponent().props();
timeSeriesAreaChart.vm.$nextTick(() => {
const commitLink = timeSeriesAreaChart.find(GlLink);
 
expect(props.data).toBe(timeSeriesAreaChart.vm.chartData);
expect(props.option).toBe(timeSeriesAreaChart.vm.chartOptions);
expect(props.formatTooltipText).toBe(timeSeriesAreaChart.vm.formatTooltipText);
expect(props.thresholds).toBe(timeSeriesAreaChart.vm.thresholds);
expect(shallowWrapperContainsSlotText(commitLink, 'default', mockSha)).toBe(true);
expect(commitLink.attributes('href')).toEqual(commitUrl);
done();
});
});
});
});
});
});
});
 
it('recieves a tooltip title', done => {
const mockTitle = 'mockTitle';
timeSeriesAreaChart.vm.tooltip.title = mockTitle;
describe('with multiple time series', () => {
const mockedResultMultipleSeries = [];
const [, , panelData] = metricsDashboardPayload.panel_groups[1].panels;
 
timeSeriesAreaChart.vm.$nextTick(() => {
expect(
shallowWrapperContainsSlotText(findChartComponent(), 'tooltipTitle', mockTitle),
).toBe(true);
done();
});
});
for (let i = 0; i < panelData.metrics.length; i += 1) {
mockedResultMultipleSeries.push(cloneDeep(mockedQueryResultPayload));
mockedResultMultipleSeries[
i
].metricId = `${panelData.metrics[i].metric_id}_${panelData.metrics[i].id}`;
}
 
describe('when tooltip is showing deployment data', () => {
const mockSha = 'mockSha';
const commitUrl = `${mockProjectDir}/-/commit/${mockSha}`;
beforeEach(() => {
setTestTimeout(1000);
 
beforeEach(done => {
timeSeriesAreaChart.vm.tooltip.isDeployment = true;
timeSeriesAreaChart.vm.$nextTick(done);
});
store = createStore();
 
it('uses deployment title', () => {
expect(
shallowWrapperContainsSlotText(findChartComponent(), 'tooltipTitle', 'Deployed'),
).toBe(true);
});
store.commit(
`monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`,
metricsDashboardPayload,
);
 
it('renders clickable commit sha in tooltip content', done => {
timeSeriesAreaChart.vm.tooltip.sha = mockSha;
timeSeriesAreaChart.vm.tooltip.commitUrl = commitUrl;
store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData);
 
timeSeriesAreaChart.vm.$nextTick(() => {
const commitLink = timeSeriesAreaChart.find(GlLink);
// Mock data contains the metric_id for a multiple time series panel
for (let i = 0; i < panelData.metrics.length; i += 1) {
store.commit(
`monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`,
mockedResultMultipleSeries[i],
);
}
 
expect(shallowWrapperContainsSlotText(commitLink, 'default', mockSha)).toBe(true);
expect(commitLink.attributes('href')).toEqual(commitUrl);
done();
});
});
// Pick the second panel group and the second panel in it
[, , mockGraphData] = store.state.monitoringDashboard.dashboard.panel_groups[1].panels;
});
describe('General functions', () => {
let timeSeriesChart;
beforeEach(done => {
timeSeriesChart = makeTimeSeriesChart(mockGraphData, 'area-chart');
timeSeriesChart.vm.$nextTick(done);
});
describe('computed', () => {
let chartData;
beforeEach(() => {
({ chartData } = timeSeriesChart.vm);
});
it('should contain different colors for each time series', () => {
expect(chartData[0].lineStyle.color).toBe('#1f78d1');
expect(chartData[1].lineStyle.color).toBe('#1aaa55');
expect(chartData[2].lineStyle.color).toBe('#fc9403');
expect(chartData[3].lineStyle.color).toBe('#6d49cb');
expect(chartData[4].lineStyle.color).toBe('#1f78d1');
});
});
});
Loading
Loading
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