Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-07-22 18:09:27 +00:00
parent d1cb802bac
commit 3ce55b46df
72 changed files with 813 additions and 209 deletions

View File

@ -76,9 +76,9 @@ Dangerfile @gl-quality/eng-prod
/doc/user/project/code_owners.md @reprazent @kerrizor @garyh
[Telemetry]
/ee/lib/gitlab/usage_data_counters/ @gitlab-org/growth/telemetry
/ee/lib/ee/gitlab/usage_data.rb @gitlab-org/growth/telemetry
/lib/gitlab/grafana_embed_usage_data.rb @gitlab-org/growth/telemetry
/lib/gitlab/usage_data.rb @gitlab-org/growth/telemetry
/lib/gitlab/cycle_analytics/usage_data.rb @gitlab-org/growth/telemetry
/lib/gitlab/usage_data_counters/ @gitlab-org/growth/telemetry
/ee/lib/gitlab/usage_data_counters/ @gitlab-org/growth/telemetry/engineers
/ee/lib/ee/gitlab/usage_data.rb @gitlab-org/growth/telemetry/engineers
/lib/gitlab/grafana_embed_usage_data.rb @gitlab-org/growth/telemetry/engineers
/lib/gitlab/usage_data.rb @gitlab-org/growth/telemetry/engineers
/lib/gitlab/cycle_analytics/usage_data.rb @gitlab-org/growth/telemetry/engineers
/lib/gitlab/usage_data_counters/ @gitlab-org/growth/telemetry/engineers

View File

@ -153,23 +153,6 @@ Layout/SpaceInsideBlockBraces:
Layout/SpaceInsideParens:
Enabled: false
# Offense count: 19
Lint/DuplicateMethods:
Exclude:
- 'app/models/commit.rb'
- 'app/models/note.rb'
- 'lib/bitbucket/representation/repo.rb'
- 'lib/declarative_policy/base.rb'
- 'lib/gitlab/auth/ldap/person.rb'
- 'lib/gitlab/auth/o_auth/user.rb'
- 'lib/gitlab/ci/build/artifacts/metadata/entry.rb'
- 'lib/gitlab/cycle_analytics/base_event_fetcher.rb'
- 'lib/gitlab/diff/formatters/base_formatter.rb'
- 'lib/gitlab/git/blob.rb'
- 'lib/gitlab/git/repository.rb'
- 'lib/gitlab/git/tree.rb'
- 'lib/gitlab/git/wiki_page.rb'
# Offense count: 157
# Configuration parameters: MaximumRangeSize.
Lint/MissingCopEnableDirective:

View File

@ -1 +1 @@
d758ff48fbf8392e08626b60685d037373347d72
e3bedb3507c01fbe8395dd76589e095d7da14e66

View File

@ -328,7 +328,7 @@ group :metrics do
gem 'method_source', '~> 0.8', require: false
# Prometheus
gem 'prometheus-client-mmap', '~> 0.10.0'
gem 'prometheus-client-mmap', '~> 0.11.0'
gem 'raindrops', '~> 0.18'
end

View File

@ -787,7 +787,7 @@ GEM
parser
unparser
procto (0.0.3)
prometheus-client-mmap (0.10.0)
prometheus-client-mmap (0.11.0)
pry (0.11.3)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
@ -1345,7 +1345,7 @@ DEPENDENCIES
pg (~> 1.1)
png_quantizator (~> 0.2.1)
premailer-rails (~> 1.10.3)
prometheus-client-mmap (~> 0.10.0)
prometheus-client-mmap (~> 0.11.0)
pry-byebug (~> 3.5.1)
pry-rails (~> 0.3.9)
rack (~> 2.0.9)

View File

@ -49,7 +49,7 @@ const multiMetricLabel = metricAttributes => {
* @param {Object} metricAttributes - Default metric attribute values (e.g. method, instance)
* @returns {String} The formatted query label
*/
const getSeriesLabel = (queryLabel, metricAttributes) => {
export const getSeriesLabel = (queryLabel, metricAttributes) => {
return (
singleAttributeLabel(queryLabel, metricAttributes) ||
templatedLabel(queryLabel, metricAttributes) ||
@ -63,7 +63,6 @@ const getSeriesLabel = (queryLabel, metricAttributes) => {
* @param {Object} defaultConfig - Default chart config values (e.g. lineStyle, name)
* @returns {Array} The formatted values
*/
// eslint-disable-next-line import/prefer-default-export
export const makeDataSeries = (queryResults, defaultConfig) =>
queryResults.map(result => {
return {

View File

@ -5,6 +5,8 @@ mutation($input: JiraImportUsersInput!) {
jiraDisplayName
jiraEmail
gitlabId
gitlabName
gitlabUsername
}
errors
}

View File

@ -30,6 +30,7 @@ import MonitorStackedColumnChart from './charts/stacked_column.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import AlertWidget from './alert_widget.vue';
import { timeRangeToUrl, downloadCSVOptions, generateLinkToChartOptions } from '../utils';
import { graphDataToCsv } from '../csv_export';
const events = {
timeRangeZoom: 'timerangezoom',
@ -148,13 +149,10 @@ export default {
return null;
},
csvText() {
const chartData = this.graphData?.metrics[0].result[0].values || [];
const yLabel = this.graphData.y_label;
const header = `timestamp,${yLabel}\r\n`; // eslint-disable-line @gitlab/require-i18n-strings
return chartData.reduce((csv, data) => {
const row = data.join(',');
return `${csv}${row}\r\n`;
}, header);
if (this.graphData) {
return graphDataToCsv(this.graphData);
}
return null;
},
downloadCsv() {
const data = new Blob([this.csvText], { type: 'text/plain' });

View File

@ -0,0 +1,147 @@
import { getSeriesLabel } from '~/helpers/monitor_helper';
/**
* Returns a label for a header of the csv.
*
* Includes double quotes ("") in case the header includes commas or other separator.
*
* @param {String} axisLabel
* @param {String} metricLabel
* @param {Object} metricAttributes
*/
const csvHeader = (axisLabel, metricLabel, metricAttributes = {}) =>
`${axisLabel} > ${getSeriesLabel(metricLabel, metricAttributes)}`;
/**
* Returns an array with the header labels given a list of metrics
*
* ```
* metrics = [
* {
* label: "..." // user-defined label
* result: [
* {
* metric: { ... } // metricAttributes
* },
* ...
* ]
* },
* ...
* ]
* ```
*
* When metrics have a `label` or `metricAttributes`, they are
* used to generate the column name.
*
* @param {String} axisLabel - Main label
* @param {Array} metrics - Metrics with results
*/
const csvMetricHeaders = (axisLabel, metrics) =>
metrics.flatMap(({ label, result }) =>
// The `metric` in a `result` is a map of `metricAttributes`
// contains key-values to identify the series, rename it
// here for clarity.
result.map(({ metric: metricAttributes }) => {
return csvHeader(axisLabel, label, metricAttributes);
}),
);
/**
* Returns a (flat) array with all the values arrays in each
* metric and series
*
* ```
* metrics = [
* {
* result: [
* {
* values: [ ... ] // `values`
* },
* ...
* ]
* },
* ...
* ]
* ```
*
* @param {Array} metrics - Metrics with results
*/
const csvMetricValues = metrics =>
metrics.flatMap(({ result }) => result.map(res => res.values || []));
/**
* Returns headers and rows for csv, sorted by their timestamp.
*
* {
* headers: ["timestamp", "<col_1_name>", "col_2_name"],
* rows: [
* [ <timestamp>, <col_1_value>, <col_2_value> ],
* [ <timestamp>, <col_1_value>, <col_2_value> ]
* ...
* ]
* }
*
* @param {Array} metricHeaders
* @param {Array} metricValues
*/
const csvData = (metricHeaders, metricValues) => {
const rowsByTimestamp = {};
metricValues.forEach((values, colIndex) => {
values.forEach(([timestamp, value]) => {
if (!rowsByTimestamp[timestamp]) {
rowsByTimestamp[timestamp] = [];
}
// `value` should be in the right column
rowsByTimestamp[timestamp][colIndex] = value;
});
});
const rows = Object.keys(rowsByTimestamp)
.sort()
.map(timestamp => {
// force each row to have the same number of entries
rowsByTimestamp[timestamp].length = metricHeaders.length;
// add timestamp as the first entry
return [timestamp, ...rowsByTimestamp[timestamp]];
});
// Escape double quotes and enclose headers:
// "If double-quotes are used to enclose fields, then a double-quote
// appearing inside a field must be escaped by preceding it with
// another double quote."
// https://tools.ietf.org/html/rfc4180#page-2
const headers = metricHeaders.map(header => `"${header.replace(/"/g, '""')}"`);
return {
headers: ['timestamp', ...headers],
rows,
};
};
/**
* Returns dashboard panel's data in a string in CSV format
*
* @param {Object} graphData - Panel contents
* @returns {String}
*/
// eslint-disable-next-line import/prefer-default-export
export const graphDataToCsv = graphData => {
const delimiter = ',';
const br = '\r\n';
const { metrics = [], y_label: axisLabel } = graphData;
const metricsWithResults = metrics.filter(metric => metric.result);
const metricHeaders = csvMetricHeaders(axisLabel, metricsWithResults);
const metricValues = csvMetricValues(metricsWithResults);
const { headers, rows } = csvData(metricHeaders, metricValues);
if (rows.length === 0) {
return '';
}
const headerLine = headers.join(delimiter) + br;
const lines = rows.map(row => row.join(delimiter));
return headerLine + lines.join(br) + br;
};

View File

@ -506,6 +506,3 @@ export const fetchVariableMetricLabelValues = ({ state, commit }, { defaultQuery
return Promise.all(optionsRequests);
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};

View File

@ -170,6 +170,3 @@ export const getCustomVariablesParams = state =>
*/
export const fullDashboardPath = state =>
normalizeCustomDashboardPath(state.currentDashboard, state.customDashboardBasePath);
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};

View File

@ -287,7 +287,7 @@ export default {
</gl-tab>
<gl-tab
:title="__('Versions')"
:title="__('Other versions')"
title-item-class="js-versions-tab"
@click="getPackageVersions"
>

View File

@ -29,7 +29,7 @@ export default {
},
methods: {
...mapActions([
'fetchFullReport',
'fetchTestSuite',
'fetchSummary',
'setSelectedSuiteIndex',
'removeSelectedSuiteIndex',
@ -40,10 +40,8 @@ export default {
summaryTableRowClick(index) {
this.setSelectedSuiteIndex(index);
// Fetch the full report when the user clicks to see more details
if (!this.hasFullReport) {
this.fetchFullReport();
}
// Fetch the test suite when the user clicks to see more details
this.fetchTestSuite(index);
},
beforeEnterTransition() {
document.documentElement.style.overflowX = 'hidden';

View File

@ -122,11 +122,12 @@ const createTestDetails = () => {
}
const el = document.querySelector('#js-pipeline-tests-detail');
const { fullReportEndpoint, summaryEndpoint, countEndpoint } = el?.dataset || {};
const { fullReportEndpoint, summaryEndpoint, suiteEndpoint, countEndpoint } = el?.dataset || {};
const testReportsStore = createTestReportsStore({
fullReportEndpoint,
summaryEndpoint: summaryEndpoint || countEndpoint,
suiteEndpoint,
useBuildSummaryReport: window.gon?.features?.buildReportSummary,
});

View File

@ -33,6 +33,31 @@ export const fetchSummary = ({ state, commit, dispatch }) => {
});
};
export const fetchTestSuite = ({ state, commit, dispatch }, index) => {
const { hasFullSuite } = state.testReports?.test_suites?.[index] || {};
// We don't need to fetch the suite if we have the information already
if (state.hasFullReport || hasFullSuite) {
return Promise.resolve();
}
dispatch('toggleLoading');
const { name = '', build_ids = [] } = state.testReports?.test_suites?.[index] || {};
// Replacing `/:suite_name.json` with the name of the suite. Including the extra characters
// to ensure that we replace exactly the template part of the URL string
const endpoint = state.suiteEndpoint?.replace('/:suite_name.json', `/${name}.json`);
return axios
.get(endpoint, { params: { build_ids } })
.then(({ data }) => commit(types.SET_SUITE, { suite: data, index }))
.catch(() => {
createFlash(s__('TestReports|There was an error fetching the test suite.'));
})
.finally(() => {
dispatch('toggleLoading');
});
};
export const fetchFullReport = ({ state, commit, dispatch }) => {
dispatch('toggleLoading');

View File

@ -1,4 +1,5 @@
export const SET_REPORTS = 'SET_REPORTS';
export const SET_SELECTED_SUITE_INDEX = 'SET_SELECTED_SUITE_INDEX';
export const SET_SUMMARY = 'SET_SUMMARY';
export const SET_SUITE = 'SET_SUITE';
export const TOGGLE_LOADING = 'TOGGLE_LOADING';

View File

@ -5,6 +5,10 @@ export default {
Object.assign(state, { testReports, hasFullReport: true });
},
[types.SET_SUITE](state, { suite = {}, index = null }) {
state.testReports.test_suites[index] = { ...suite, hasFullSuite: true };
},
[types.SET_SELECTED_SUITE_INDEX](state, selectedSuiteIndex) {
Object.assign(state, { selectedSuiteIndex });
},

View File

@ -1,9 +1,11 @@
export default ({
fullReportEndpoint = '',
summaryEndpoint = '',
suiteEndpoint = '',
useBuildSummaryReport = false,
}) => ({
summaryEndpoint,
suiteEndpoint,
fullReportEndpoint,
testReports: {},
selectedSuiteIndex: null,

View File

@ -5,7 +5,7 @@ class AuditEvent < ApplicationRecord
include IgnorableColumns
include BulkInsertSafe
PARALLEL_PERSISTENCE_COLUMNS = [:author_name, :entity_path].freeze
PARALLEL_PERSISTENCE_COLUMNS = [:author_name, :entity_path, :target_details].freeze
ignore_column :updated_at, remove_with: '13.4', remove_after: '2020-09-22'

View File

@ -21,7 +21,6 @@ class Commit
participant :committer
participant :notes_with_associations
attr_accessor :author
attr_accessor :redacted_description_html
attr_accessor :redacted_title_html
attr_accessor :redacted_full_title_html

View File

@ -61,7 +61,7 @@ class Note < ApplicationRecord
attr_accessor :commands_changes
# A special role that may be displayed on issuable's discussions
attr_accessor :special_role
attr_reader :special_role
default_value_for :system, false

View File

@ -1,5 +1,5 @@
- page_title _("Blame"), @blob.path, @ref
- link_icon = icon("link")
- link_icon = sprite_icon("link", size: 12)
#blob-content-holder.tree-holder
= render "projects/blob/breadcrumb", blob: @blob, blame: true

View File

@ -88,5 +88,6 @@
#js-tab-tests.tab-pane
#js-pipeline-tests-detail{ data: { full_report_endpoint: test_report_project_pipeline_path(@project, @pipeline, format: :json),
summary_endpoint: Feature.enabled?(:build_report_summary, @project) ? summary_project_pipeline_tests_path(@project, @pipeline, format: :json) : '',
suite_endpoint: Feature.enabled?(:build_report_summary, @project) ? project_pipeline_test_path(@project, @pipeline, suite_name: ':suite_name', format: :json) : '',
count_endpoint: test_reports_count_project_pipeline_path(@project, @pipeline, format: :json) } }
= render_if_exists "projects/pipelines/tabs_content", pipeline: @pipeline, project: @project

View File

@ -1,7 +1,7 @@
.file-content.code.js-syntax-highlight
.line-numbers
- if blob.data.present?
- link_icon = icon('link')
- link_icon = sprite_icon('link', size: 12)
- link = blob_link if defined?(blob_link)
- blob.data.each_line.each_with_index do |_, index|
- offset = defined?(first_line_number) ? first_line_number : 1

View File

@ -35,11 +35,6 @@ if (BABEL_ENV === 'coverage') {
]);
}
// add rewire support when running tests
if (BABEL_ENV === 'karma' || BABEL_ENV === 'coverage') {
plugins.push('babel-plugin-rewire');
}
// Jest is running in node environment, so we need additional plugins
const isJest = Boolean(process.env.JEST_WORKER_ID);
if (isJest) {

View File

@ -0,0 +1,5 @@
---
title: Update versions tab to other versions
merge_request: 37513
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Show mapped user in Jira import form dropdown
merge_request: 37575
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Replace fa-link icons with GitLab SVG link icon
merge_request: 36973
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Show relevant error messages when failing to match a CI job entry
merge_request: 36536
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fix CSV downloads for multiple series in the same chart
merge_request: 37377
author:
type: fixed

View File

@ -68,6 +68,7 @@ exceptions:
- SSH
- SSL
- SSO
- SVG
- SVN
- TCP
- TIP

View File

@ -35,6 +35,20 @@ kernel version:
If you are using that kernel version, be sure to upgrade GitLab to avoid
errors.
## Fast lookup of authorized SSH keys
The [fast SSH key lookup](../operations/fast_ssh_key_lookup.md) feature can improve
performance of GitLab instances even if they're using block storage.
[Fast SSH key lookup](../operations/fast_ssh_key_lookup.md) is a replacement for
`authorized_keys` (in `/var/opt/gitlab/.ssh`) using the GitLab database.
NFS increases latency, so fast lookup is recommended if `/var/opt/gitlab`
is moved to NFS.
We are investigating the use of
[fast lookup as the default](https://gitlab.com/groups/gitlab-org/-/epics/3104).
## NFS Server features
### Required features

View File

@ -230,7 +230,7 @@ syntax and with its attributes.
This document [introduces the concepts of GitLab CI/CD in the scope of GitLab Pages](../../user/project/pages/getting_started/pages_from_scratch.md), for deploying static websites.
Although it's meant for users who want to write their own Pages
script from scratch, it also serves as an introduction to the setup process for GitLab CI/CD.
It covers the very first general steps of writing a CI/CD configuration
It covers the first general steps of writing a CI/CD configuration
file, so we recommend you read through it to understand GitLab's CI/CD
logic, and learn how to write your own script (or tweak an
existing one) for any application.

View File

@ -1204,3 +1204,7 @@ See the [schema reference](../api/graphql/reference/index.md) for details.
This generated GraphQL documentation needs to be updated when the schema changes.
For information on generating GraphQL documentation and schema files, see
[updating the schema documentation](rake_tasks.md#update-graphql-documentation-and-schema-definitions).
To help our readers, you should also add a new page to our [GraphQL API](../api/graphql/index.md) documentation.
For guidance, see the [GraphQL API](documentation/styleguide.md#graphql-api) section
of our documentation style guide.

View File

@ -13,7 +13,7 @@ Always use an [Entity](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/api/
## Documentation
API endpoints must come with [documentation](documentation/styleguide.md#api), unless it is internal or behind a feature flag.
API endpoints must come with [documentation](documentation/styleguide.md#restful-api), unless it is internal or behind a feature flag.
The docs should be in the same merge request, or, if strictly necessary,
in a follow-up with the same milestone as the original merge request.

View File

@ -1135,48 +1135,39 @@ Usage examples:
[Bootstrap utility class](https://getbootstrap.com/docs/4.4/utilities/float/):
`**{tanuki, 32, float-right}**` renders as: **{tanuki, 32, float-right}**
### Use GitLab SVGs to describe UI elements
### When to use icons
When using GitLab SVGs to describe screen elements, also include the name or tooltip of the element as text.
Icons should be used sparingly, and only in ways that aid and do not hinder the readability of the
text.
For example, for references to the Admin Area:
- Correct: `**{admin}** **Admin Area > Settings**` (**{admin}** **Admin Area > Settings**)
- Incorrect: `**{admin}** **> Settings**` (**{admin}** **> Settings**)
This will ensure that the source Markdown remains readable and should help with accessibility.
The following are examples of source Markdown for menu items with their published output:
For example, the following adds little to the accompanying text:
```markdown
1. Go to **{home}** **Project overview > Details**
1. Go to **{doc-text}** **Repository > Branches**
1. Go to **{issues}** **Issues > List**
1. Go to **{merge-request}** **Merge Requests**
1. Go to **{rocket}** **CI/CD > Pipelines**
1. Go to **{shield}** **Security & Compliance > Configuration**
1. Go to **{cloud-gear}** **Operations > Metrics**
1. Go to **{package}** **Packages > Container Registry**
1. Go to **{chart}** **Project Analytics > Code Review**
1. Go to **{book}** **Wiki**
1. Go to **{snippet}** **Snippets**
1. Go to **{users}** **Members**
1. Select the **More actions** **{ellipsis_v}** icon > **Hide stage**
```
1. Go to **{home}** **Project overview > Details**
1. Go to **{doc-text}** **Repository > Branches**
1. Go to **{issues}** **Issues > List**
1. Go to **{merge-request}** **Merge Requests**
1. Go to **{rocket}** **CI/CD > Pipelines**
1. Go to **{shield}** **Security & Compliance > Configuration**
1. Go to **{cloud-gear}** **Operations > Metrics**
1. Go to **{package}** **Packages > Container Registry**
1. Go to **{chart}** **Project Analytics > Code Review**
1. Go to **{book}** **Wiki**
1. Go to **{snippet}** **Snippets**
1. Go to **{users}** **Members**
1. Select the **More actions** **{ellipsis_v}** icon > **Hide stage**
However, the following might help the reader connect the text to the user interface:
```markdown
| Section | Description |
|:-------------------------|:----------------------------------------------------------------------------------------------------------------------------|
| **{overview}** Overview | View your GitLab Dashboard, and administer projects, users, groups, jobs, Runners, and Gitaly servers. |
| **{monitor}** Monitoring | View GitLab system information, and information on background jobs, logs, health checks, requests profiles, and audit logs. |
| **{messages}** Messages | Send and manage broadcast messages for your users. |
```
| Section | Description |
|:-------------------------|:----------------------------------------------------------------------------------------------------------------------------|
| **{overview}** Overview | View your GitLab Dashboard, and administer projects, users, groups, jobs, Runners, and Gitaly servers. |
| **{monitor}** Monitoring | View GitLab system information, and information on background jobs, logs, health checks, requests profiles, and audit logs. |
| **{messages}** Messages | Send and manage broadcast messages for your users. |
Use an icon when you find youself having to describe an interface element. For example:
- Do: Click the Admin Area icon ( **{admin}** ).
- Don't: Click the Admin Area icon (the wrench icon).
## Alert boxes
@ -1622,10 +1613,10 @@ Learn how to [document features deployed behind flags](feature_flags.md).
For guidance on developing GitLab with feature flags, see
[Feature flags in development of GitLab](../feature_flags/index.md).
## API
## RESTful API
Here is a list of must-have items. Use them in the exact order that appears
on this document. Further explanation is given below.
Here is a list of must-have items for RESTful API documentation. Use them in the
exact order that appears on this document. Further explanation is given below.
- Every method must have the REST API request. For example:
@ -1816,3 +1807,80 @@ exclude specific users when requesting a list of users for a project, you would
```shell
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --data "skip_users[]=<user_id>" --data "skip_users[]=<user_id>" https://gitlab.example.com/api/v4/projects/<project_id>/users
```
## GraphQL API
GraphQL APIs are different from [RESTful APIs](#restful-api). Reference information is
generated automatically in our [GraphQL reference](../../api/graphql/reference/index.md).
However, it's helpful to include examples on how to use GraphQL for different "use cases",
with samples that readers can use directly in the [GraphiQL explorer](../api_graphql_styleguide.md#graphiql).
This section describes the steps required to add your GraphQL examples to GitLab documentation.
### Add a dedicated GraphQL page
To create a dedicated GraphQL page, create a new `.md` file in the `doc/api/graphql/` directory.
Give that file a functional name, such as `import_from_specific_location.md`.
### Start the page with an explanation
Include a page title that describes the GraphQL functionality in a few words, such as:
```markdown
# Search for [substitute kind of data]
```
Describe the search. One sentence may be all you need. More information may help
readers learn how to use the example for their GitLab deployments.
### Include a procedure using the GraphiQL explorer
The GraphiQL explorer can help readers test queries with working deployments. Set up the section with the following:
- Use the following title:
```markdown
## Set up the GraphiQL explorer
```
- Include a code block with the query that anyone can include in their instance of
the GraphiQL explorer:
````markdown
```graphql
query {
<insert queries here>
}
```
````
- Tell the user what to do:
```markdown
1. Open the GraphiQL explorer tool in the following URL: `https://gitlab.com/-/graphql-explorer`.
1. Paste the `query` listed above into the left window of your GraphiQL explorer tool.
1. Click Play to get the result shown here:
```
- Include a screenshot of the result in the GraphiQL explorer. Follow the naming
convention described in the [Save the image](#save-the-image) section.
- Follow up with an example of what you can do with the output.
Make sure the example is something that readers can do on their own deployments.
- Include a link to the [GraphQL API resources](../../api/graphql/reference/index.md).
### Add the GraphQL example to the Table of Contents
You'll need to open a second MR, against the [GitLab Docs repository](https://gitlab.com/gitlab-org/gitlab-docs/).
We store our Table of Contents in the `default-nav.yaml` file, in the `content/_data`
subdirectory. You can find the GraphQL section under the following line:
```yaml
- category_title: GraphQL
```
Be aware that CI tests for that second MR will fail with a bad link until the main MR
that adds the new GraphQL page is merged.
And that's all you need!

View File

@ -360,11 +360,33 @@ Widgets should now be replicated by Geo!
DOWNTIME = false
def change
add_column :widgets, :verification_retry_at, :datetime_with_timezone
add_column :widgets, :verified_at, :datetime_with_timezone
add_column :widgets, :verification_checksum, :binary, using: 'verification_checksum::bytea'
add_column :widgets, :verification_failure, :string
add_column :widgets, :verification_retry_count, :integer
change_table(:widgets) do |t|
t.integer :verification_retry_count, limit: 2
t.column :verification_retry_at, :datetime_with_timezone
t.column :verified_at, :datetime_with_timezone
t.binary :verification_checksum, using: 'verification_checksum::bytea'
# rubocop:disable Migration/AddLimitToTextColumns
t.text :verification_failure
# rubocop:enable Migration/AddLimitToTextColumns
end
end
end
```
Adding a `text` column also [requires](../database/strings_and_the_text_data_type.md#add-a-text-column-to-an-existing-table)
setting a limit:
```ruby
# frozen_string_literal: true
class AddVerificationFailureLimitToWidgets < ActiveRecord::Migration[6.0]
DOWNTIME = false
disable_ddl_transaction!
def change
add_text_limit :widgets, :verification_failure, 255
end
end
```

View File

@ -311,7 +311,7 @@ This feature:
For example:
```shell
kubectl logs -n gitlab-managed-apps $(kubectl get pod -n gitlab-managed-apps -l app=nginx-ingress,component=controller --no-headers=true -o custom-columns=:metadata.name) modsecurity-log -f
kubectl -n gitlab-managed-apps logs -l app=nginx-ingress,component=controller -c modsecurity-log -f
```
To enable WAF, switch its respective toggle to the enabled position when installing or updating [Ingress application](#ingress).
@ -1004,7 +1004,7 @@ The Cilium monitor log for traffic is logged out by the
`cilium-monitor` sidecar container. You can check these logs with the following command:
```shell
kubectl -n gitlab-managed-apps logs cilium-XXXX cilium-monitor
kubectl -n gitlab-managed-apps logs -l k8s-app=cilium -c cilium-monitor
```
You can disable the monitor log in `.gitlab/managed-apps/cilium/values.yaml`:
@ -1127,7 +1127,7 @@ falco:
You can check these logs with the following command:
```shell
kubectl logs -l app=falco -n gitlab-managed-apps
kubectl -n gitlab-managed-apps logs -l app=falco
```
NOTE: **Note:**

View File

@ -43,7 +43,7 @@ To assign a label to an issue, merge request or epic:
click on them. You can search repeatedly and add more labels.
1. Click **X** or anywhere outside the label section and the labels are applied.
You can also assign a label with the [`/assign @username` quick action](quick_actions.md).
You can also assign a label with the [`/label ~label1 ~label2` quick action](quick_actions.md).
## Label management

View File

@ -3,8 +3,6 @@
module Bitbucket
module Representation
class Repo < Representation::Base
attr_reader :owner, :slug
def initialize(raw)
super(raw)
end

View File

@ -213,7 +213,7 @@ module DeclarativePolicy
#
# It also stores a reference to the cache, so it can be used
# to cache computations by e.g. ManifestCondition.
attr_reader :user, :subject, :cache
attr_reader :user, :subject
def initialize(user, subject, opts = {})
@user = user
@subject = subject

View File

@ -11,7 +11,7 @@ module Gitlab
InvalidEntryError = Class.new(StandardError)
attr_accessor :entry, :provider
attr_accessor :provider
def self.find_by_uid(uid, adapter)
uid = Net::LDAP::Filter.escape(uid)

View File

@ -12,7 +12,7 @@ module Gitlab
SignupDisabledError = Class.new(StandardError)
SigninDisabledForProviderError = Class.new(StandardError)
attr_accessor :auth_hash, :gl_user
attr_reader :auth_hash
def initialize(auth_hash)
self.auth_hash = auth_hash

View File

@ -16,7 +16,7 @@ module Gitlab
#
class Entry
attr_reader :entries
attr_accessor :name
attr_writer :name
def initialize(path, entries)
@entries = entries

View File

@ -11,7 +11,7 @@ module Gitlab
class Bridge < ::Gitlab::Config::Entry::Node
include ::Gitlab::Ci::Config::Entry::Processable
ALLOWED_KEYS = %i[trigger allow_failure when needs].freeze
ALLOWED_KEYS = %i[trigger].freeze
validations do
validates :config, allowed_keys: ALLOWED_KEYS + PROCESSABLE_ALLOWED_KEYS

View File

@ -11,9 +11,8 @@ module Gitlab
include ::Gitlab::Ci::Config::Entry::Processable
ALLOWED_WHEN = %w[on_success on_failure always manual delayed].freeze
ALLOWED_KEYS = %i[tags script type image services
allow_failure type when start_in artifacts cache
dependencies before_script needs after_script
ALLOWED_KEYS = %i[tags script type image services start_in artifacts
cache dependencies before_script after_script
environment coverage retry parallel interruptible timeout
resource_group release secrets].freeze
@ -129,9 +128,39 @@ module Gitlab
:needs, :retry, :parallel, :start_in,
:interruptible, :timeout, :resource_group, :release
Matcher = Struct.new(:name, :config) do
def applies?
job_is_not_hidden? &&
config_is_a_hash? &&
has_job_keys?
end
private
def job_is_not_hidden?
!name.to_s.start_with?('.')
end
def config_is_a_hash?
config.is_a?(Hash)
end
def has_job_keys?
if name == :default
config.key?(:script)
else
(ALLOWED_KEYS & config.keys).any?
end
end
end
def self.matching?(name, config)
!name.to_s.start_with?('.') &&
config.is_a?(Hash) && config.key?(:script)
if Gitlab::Ci::Features.job_entry_matches_all_keys?
Matcher.new(name, config).applies?
else
!name.to_s.start_with?('.') &&
config.is_a?(Hash) && config.key?(:script)
end
end
def self.visible?

View File

@ -14,7 +14,8 @@ module Gitlab
include ::Gitlab::Config::Entry::Attributable
include ::Gitlab::Config::Entry::Inheritable
PROCESSABLE_ALLOWED_KEYS = %i[extends stage only except rules variables inherit].freeze
PROCESSABLE_ALLOWED_KEYS = %i[extends stage only except rules variables
inherit allow_failure when needs].freeze
included do
validations do

View File

@ -77,6 +77,10 @@ module Gitlab
def self.allow_to_create_merge_request_pipelines_in_target_project?(target_project)
::Feature.enabled?(:ci_allow_to_create_merge_request_pipelines_in_target_project, target_project)
end
def self.job_entry_matches_all_keys?
::Feature.enabled?(:ci_job_entry_matches_all_keys)
end
end
end
end

View File

@ -6,7 +6,7 @@ module Gitlab
include BaseQuery
include GroupProjectsProvider
attr_reader :projections, :query, :stage, :order, :options
attr_reader :projections, :query, :stage, :options
MAX_EVENTS = 50

View File

@ -10,7 +10,6 @@ module Gitlab
attr_reader :base_sha
attr_reader :start_sha
attr_reader :head_sha
attr_reader :position_type
def initialize(attrs)
if diff_file = attrs[:diff_file]

View File

@ -25,7 +25,8 @@ module Gitlab
LFS_POINTER_MIN_SIZE = 120.bytes
LFS_POINTER_MAX_SIZE = 200.bytes
attr_accessor :name, :path, :size, :data, :mode, :id, :commit_id, :loaded_size, :binary
attr_accessor :size, :mode, :id, :commit_id, :loaded_size, :binary
attr_writer :name, :path, :data
define_counter :gitlab_blob_truncated_true do
docstring 'blob.truncated? == true'

View File

@ -44,7 +44,7 @@ module Gitlab
# Relative path of repo
attr_reader :relative_path
attr_reader :storage, :gl_repository, :relative_path, :gl_project_path
attr_reader :storage, :gl_repository, :gl_project_path
# This remote name has to be stable for all types of repositories that
# can join an object pool. If it's structure ever changes, a migration

View File

@ -6,8 +6,8 @@ module Gitlab
include Gitlab::EncodingHelper
extend Gitlab::Git::WrapsGitalyErrors
attr_accessor :id, :root_id, :name, :path, :flat_path, :type,
:mode, :commit_id, :submodule_url
attr_accessor :id, :root_id, :type, :mode, :commit_id, :submodule_url
attr_writer :name, :path, :flat_path
class << self
# Get list of tree objects

View File

@ -3,7 +3,7 @@
module Gitlab
module Git
class WikiPage
attr_reader :url_path, :title, :format, :path, :version, :raw_data, :name, :text_data, :historical, :formatted_data
attr_reader :url_path, :title, :format, :path, :version, :raw_data, :name, :historical, :formatted_data
# This class abstracts away Gitlab::GitalyClient::WikiPage
def initialize(gitaly_page, version)

View File

@ -8,11 +8,11 @@ module Gitlab
class << self
def enabled?
config.enabled && config.address
config.enabled && config.address.present?
end
def supports_wildcard?
config.address && config.address.include?(WILDCARD_PLACEHOLDER)
config.address.present? && config.address.include?(WILDCARD_PLACEHOLDER)
end
def supports_issue_creation?

View File

@ -16516,6 +16516,9 @@ msgstr ""
msgid "Other merge requests block this MR"
msgstr ""
msgid "Other versions"
msgstr ""
msgid "Other visibility settings have been disabled by the administrator."
msgstr ""
@ -23383,6 +23386,9 @@ msgstr ""
msgid "TestReports|There was an error fetching the test reports."
msgstr ""
msgid "TestReports|There was an error fetching the test suite."
msgstr ""
msgid "Tests"
msgstr ""
@ -26193,9 +26199,6 @@ msgstr ""
msgid "Version"
msgstr ""
msgid "Versions"
msgstr ""
msgid "View Documentation"
msgstr ""

View File

@ -165,7 +165,6 @@
"babel-jest": "^24.1.0",
"babel-plugin-dynamic-import-node": "^2.2.0",
"babel-plugin-istanbul": "^5.1.0",
"babel-plugin-rewire": "^1.2.0",
"chalk": "^2.4.1",
"commander": "^2.18.0",
"custom-jquery-matchers": "^2.1.0",

View File

@ -50,7 +50,7 @@ RSpec.describe 'Blob button line permalinks (BlobLinePermalinkUpdater)', :js do
visit_blob
find("##{ending_fragment}").hover
find("##{ending_fragment} i").click
find("##{ending_fragment} svg").click
expect(find('.js-data-file-blob-permalink-url')['href']).to eq(get_absolute_url(project_blob_path(project, tree_join(sha, path), anchor: ending_fragment)))
end
@ -100,7 +100,7 @@ RSpec.describe 'Blob button line permalinks (BlobLinePermalinkUpdater)', :js do
visit_blob
find("##{ending_fragment}").hover
find("##{ending_fragment} i").click
find("##{ending_fragment} svg").click
expect(find('.js-blob-blame-link')['href']).to eq(get_absolute_url(project_blame_path(project, tree_join('master', path), anchor: ending_fragment)))
end

View File

@ -1,12 +1,38 @@
import * as monitorHelper from '~/helpers/monitor_helper';
import { getSeriesLabel, makeDataSeries } from '~/helpers/monitor_helper';
describe('monitor helper', () => {
const defaultConfig = { default: true, name: 'default name' };
const name = 'data name';
const series = [[1, 1], [2, 2], [3, 3]];
const data = ({ metric = { default_name: name }, values = series } = {}) => [{ metric, values }];
describe('getSeriesLabel', () => {
const metricAttributes = { __name__: 'up', app: 'prometheus' };
it('gets a single attribute label', () => {
expect(getSeriesLabel('app', metricAttributes)).toBe('app: prometheus');
});
it('gets a templated label', () => {
expect(getSeriesLabel('{{__name__}}', metricAttributes)).toBe('up');
expect(getSeriesLabel('{{app}}', metricAttributes)).toBe('prometheus');
expect(getSeriesLabel('{{missing}}', metricAttributes)).toBe('{{missing}}');
});
it('gets a multiple label', () => {
expect(getSeriesLabel(null, metricAttributes)).toBe('__name__: up, app: prometheus');
expect(getSeriesLabel('', metricAttributes)).toBe('__name__: up, app: prometheus');
});
it('gets a simple label', () => {
expect(getSeriesLabel('A label', {})).toBe('A label');
});
});
describe('makeDataSeries', () => {
const data = ({ metric = { default_name: name }, values = series } = {}) => [
{ metric, values },
];
const expectedDataSeries = [
{
...defaultConfig,
@ -15,19 +41,17 @@ describe('monitor helper', () => {
];
it('converts query results to data series', () => {
expect(monitorHelper.makeDataSeries(data({ metric: {} }), defaultConfig)).toEqual(
expectedDataSeries,
);
expect(makeDataSeries(data({ metric: {} }), defaultConfig)).toEqual(expectedDataSeries);
});
it('returns an empty array if no query results exist', () => {
expect(monitorHelper.makeDataSeries([], defaultConfig)).toEqual([]);
expect(makeDataSeries([], defaultConfig)).toEqual([]);
});
it('handles multi-series query results', () => {
const expectedData = { ...expectedDataSeries[0], name: 'default name: data name' };
expect(monitorHelper.makeDataSeries([...data(), ...data()], defaultConfig)).toEqual([
expect(makeDataSeries([...data(), ...data()], defaultConfig)).toEqual([
expectedData,
expectedData,
]);
@ -39,10 +63,7 @@ describe('monitor helper', () => {
name: '{{cmd}}',
};
const [result] = monitorHelper.makeDataSeries(
[{ metric: { cmd: 'brpop' }, values: series }],
config,
);
const [result] = makeDataSeries([{ metric: { cmd: 'brpop' }, values: series }], config);
expect(result.name).toEqual('brpop');
});
@ -53,7 +74,7 @@ describe('monitor helper', () => {
name: '',
};
const [result] = monitorHelper.makeDataSeries(
const [result] = makeDataSeries(
[
{
metric: {
@ -79,7 +100,7 @@ describe('monitor helper', () => {
name: 'backend: {{ backend }}',
};
const [result] = monitorHelper.makeDataSeries(
const [result] = makeDataSeries(
[{ metric: { backend: 'HA Server' }, values: series }],
config,
);
@ -90,10 +111,7 @@ describe('monitor helper', () => {
it('supports repeated template variables', () => {
const config = { ...defaultConfig, name: '{{cmd}}, {{cmd}}' };
const [result] = monitorHelper.makeDataSeries(
[{ metric: { cmd: 'brpop' }, values: series }],
config,
);
const [result] = makeDataSeries([{ metric: { cmd: 'brpop' }, values: series }], config);
expect(result.name).toEqual('brpop, brpop');
});
@ -101,7 +119,7 @@ describe('monitor helper', () => {
it('supports hyphenated template variables', () => {
const config = { ...defaultConfig, name: 'expired - {{ test-attribute }}' };
const [result] = monitorHelper.makeDataSeries(
const [result] = makeDataSeries(
[{ metric: { 'test-attribute': 'test-attribute-value' }, values: series }],
config,
);
@ -115,7 +133,7 @@ describe('monitor helper', () => {
name: '{{job}}: {{cmd}}',
};
const [result] = monitorHelper.makeDataSeries(
const [result] = makeDataSeries(
[{ metric: { cmd: 'brpop', job: 'redis' }, values: series }],
config,
);
@ -129,7 +147,7 @@ describe('monitor helper', () => {
name: '{{cmd}}',
};
const [firstSeries, secondSeries] = monitorHelper.makeDataSeries(
const [firstSeries, secondSeries] = makeDataSeries(
[
{ metric: { cmd: 'brpop' }, values: series },
{ metric: { cmd: 'zrangebyscore' }, values: series },

View File

@ -1,10 +1,10 @@
import { GlButton, GlFormSelect, GlLabel, GlTable } from '@gitlab/ui';
import { GlButton, GlNewDropdown, GlFormSelect, GlLabel, GlTable } from '@gitlab/ui';
import { getByRole } from '@testing-library/dom';
import { mount, shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import JiraImportForm from '~/jira_import/components/jira_import_form.vue';
import { issuesPath, jiraProjects, userMappings } from '../mock_data';
import { issuesPath, jiraProjects, userMappings as defaultUserMappings } from '../mock_data';
describe('JiraImportForm', () => {
let axiosMock;
@ -16,11 +16,21 @@ describe('JiraImportForm', () => {
const getSelectDropdown = () => wrapper.find(GlFormSelect);
const getContinueButton = () => wrapper.find(GlButton);
const getCancelButton = () => wrapper.findAll(GlButton).at(1);
const getTable = () => wrapper.find(GlTable);
const getUserDropdown = () => getTable().find(GlNewDropdown);
const getHeader = name => getByRole(wrapper.element, 'columnheader', { name });
const mountComponent = ({ isSubmitting = false, mountFunction = shallowMount } = {}) =>
const mountComponent = ({
isSubmitting = false,
userMappings = defaultUserMappings,
mountFunction = shallowMount,
} = {}) =>
mountFunction(JiraImportForm, {
propsData: {
importLabel,
@ -121,13 +131,53 @@ describe('JiraImportForm', () => {
it('shows all user mappings', () => {
wrapper = mountComponent({ mountFunction: mount });
expect(wrapper.find(GlTable).findAll('tbody tr').length).toBe(userMappings.length);
expect(getTable().findAll('tbody tr')).toHaveLength(2);
});
it('shows correct information in each cell', () => {
wrapper = mountComponent({ mountFunction: mount });
expect(wrapper.find(GlTable).element).toMatchSnapshot();
expect(getTable().element).toMatchSnapshot();
});
describe('when there is no Jira->GitLab user mapping', () => {
it('shows the logged in user in the dropdown', () => {
wrapper = mountComponent({
mountFunction: mount,
userMappings: [
{
jiraAccountId: 'aei23f98f-q23fj98qfj',
jiraDisplayName: 'Jane Doe',
jiraEmail: 'janedoe@example.com',
gitlabId: undefined,
gitlabUsername: undefined,
},
],
});
expect(getUserDropdown().text()).toContain(currentUsername);
});
});
describe('when there is a Jira->GitLab user mapping', () => {
it('shows the mapped user in the dropdown', () => {
const gitlabUsername = 'mai';
wrapper = mountComponent({
mountFunction: mount,
userMappings: [
{
jiraAccountId: 'aei23f98f-q23fj98qfj',
jiraDisplayName: 'Jane Doe',
jiraEmail: 'janedoe@example.com',
gitlabId: 14,
gitlabUsername,
},
],
});
expect(getUserDropdown().text()).toContain(gitlabUsername);
});
});
});
});
@ -137,13 +187,13 @@ describe('JiraImportForm', () => {
it('is shown', () => {
wrapper = mountComponent();
expect(wrapper.find(GlButton).text()).toBe('Continue');
expect(getContinueButton().text()).toBe('Continue');
});
it('is in loading state when the form is submitting', async () => {
wrapper = mountComponent({ isSubmitting: true });
expect(wrapper.find(GlButton).props('loading')).toBe(true);
expect(getContinueButton().props('loading')).toBe(true);
});
});

View File

@ -443,7 +443,7 @@ describe('Dashboard Panel', () => {
describe('csvText', () => {
it('converts metrics data from json to csv', () => {
const header = `timestamp,${graphData.y_label}`;
const header = `timestamp,"${graphData.y_label} > ${graphData.metrics[0].label}"`;
const data = graphData.metrics[0].result[0].values;
const firstRow = `${data[0][0]},${data[0][1]}`;
const secondRow = `${data[1][0]},${data[1][1]}`;

View File

@ -0,0 +1,126 @@
import { timeSeriesGraphData } from './graph_data';
import { graphDataToCsv } from '~/monitoring/csv_export';
describe('monitoring export_csv', () => {
describe('graphDataToCsv', () => {
const expectCsvToMatchLines = (csv, lines) => expect(`${lines.join('\r\n')}\r\n`).toEqual(csv);
it('should return a csv with 0 metrics', () => {
const data = timeSeriesGraphData({}, { metricCount: 0 });
expect(graphDataToCsv(data)).toEqual('');
});
it('should return a csv with 1 metric with no data', () => {
const data = timeSeriesGraphData({}, { metricCount: 1 });
// When state is NO_DATA, result is null
data.metrics[0].result = null;
expect(graphDataToCsv(data)).toEqual('');
});
it('should return a csv with 1 metric', () => {
const data = timeSeriesGraphData({}, { metricCount: 1 });
expectCsvToMatchLines(graphDataToCsv(data), [
`timestamp,"Y Axis > Metric 1"`,
'2015-07-01T20:10:50.000Z,1',
'2015-07-01T20:12:50.000Z,2',
'2015-07-01T20:14:50.000Z,3',
]);
});
it('should return a csv with multiple metrics and one with no data', () => {
const data = timeSeriesGraphData({}, { metricCount: 2 });
// When state is NO_DATA, result is null
data.metrics[0].result = null;
expectCsvToMatchLines(graphDataToCsv(data), [
`timestamp,"Y Axis > Metric 2"`,
'2015-07-01T20:10:50.000Z,1',
'2015-07-01T20:12:50.000Z,2',
'2015-07-01T20:14:50.000Z,3',
]);
});
it('should return a csv when not all metrics have the same timestamps', () => {
const data = timeSeriesGraphData({}, { metricCount: 3 });
// Add an "odd" timestamp that is not in the dataset
Object.assign(data.metrics[2].result[0], {
value: ['2016-01-01T00:00:00.000Z', 9],
values: [['2016-01-01T00:00:00.000Z', 9]],
});
expectCsvToMatchLines(graphDataToCsv(data), [
`timestamp,"Y Axis > Metric 1","Y Axis > Metric 2","Y Axis > Metric 3"`,
'2015-07-01T20:10:50.000Z,1,1,',
'2015-07-01T20:12:50.000Z,2,2,',
'2015-07-01T20:14:50.000Z,3,3,',
'2016-01-01T00:00:00.000Z,,,9',
]);
});
it('should escape double quotes in metric labels with two double quotes ("")', () => {
const data = timeSeriesGraphData({}, { metricCount: 1 });
data.metrics[0].label = 'My "quoted" metric';
expectCsvToMatchLines(graphDataToCsv(data), [
`timestamp,"Y Axis > My ""quoted"" metric"`,
'2015-07-01T20:10:50.000Z,1',
'2015-07-01T20:12:50.000Z,2',
'2015-07-01T20:14:50.000Z,3',
]);
});
it('should return a csv with multiple metrics', () => {
const data = timeSeriesGraphData({}, { metricCount: 3 });
expectCsvToMatchLines(graphDataToCsv(data), [
`timestamp,"Y Axis > Metric 1","Y Axis > Metric 2","Y Axis > Metric 3"`,
'2015-07-01T20:10:50.000Z,1,1,1',
'2015-07-01T20:12:50.000Z,2,2,2',
'2015-07-01T20:14:50.000Z,3,3,3',
]);
});
it('should return a csv with 1 metric and multiple series with labels', () => {
const data = timeSeriesGraphData({}, { isMultiSeries: true });
expectCsvToMatchLines(graphDataToCsv(data), [
`timestamp,"Y Axis > Metric 1","Y Axis > Metric 1"`,
'2015-07-01T20:10:50.000Z,1,4',
'2015-07-01T20:12:50.000Z,2,5',
'2015-07-01T20:14:50.000Z,3,6',
]);
});
it('should return a csv with 1 metric and multiple series', () => {
const data = timeSeriesGraphData({}, { isMultiSeries: true, withLabels: false });
expectCsvToMatchLines(graphDataToCsv(data), [
`timestamp,"Y Axis > __name__: up, job: prometheus, instance: localhost:9090","Y Axis > __name__: up, job: node, instance: localhost:9091"`,
'2015-07-01T20:10:50.000Z,1,4',
'2015-07-01T20:12:50.000Z,2,5',
'2015-07-01T20:14:50.000Z,3,6',
]);
});
it('should return a csv with multiple metrics and multiple series', () => {
const data = timeSeriesGraphData(
{},
{ metricCount: 3, isMultiSeries: true, withLabels: false },
);
expectCsvToMatchLines(graphDataToCsv(data), [
`timestamp,"Y Axis > __name__: up, job: prometheus, instance: localhost:9090","Y Axis > __name__: up, job: node, instance: localhost:9091","Y Axis > __name__: up, job: prometheus, instance: localhost:9090","Y Axis > __name__: up, job: node, instance: localhost:9091","Y Axis > __name__: up, job: prometheus, instance: localhost:9090","Y Axis > __name__: up, job: node, instance: localhost:9091"`,
'2015-07-01T20:10:50.000Z,1,4,1,4,1,4',
'2015-07-01T20:12:50.000Z,2,5,2,5,2,5',
'2015-07-01T20:14:50.000Z,3,6,3,6,3,6',
]);
});
});
});

View File

@ -83,7 +83,7 @@ const matrixMultiResult = ({ values1 = ['1', '2', '3'], values2 = ['4', '5', '6'
* @param {Object} dataOptions.isMultiSeries
*/
export const timeSeriesGraphData = (panelOptions = {}, dataOptions = {}) => {
const { metricCount = 1, isMultiSeries = false } = dataOptions;
const { metricCount = 1, isMultiSeries = false, withLabels = true } = dataOptions;
return mapPanelToViewModel({
title: 'Time Series Panel',
@ -91,7 +91,7 @@ export const timeSeriesGraphData = (panelOptions = {}, dataOptions = {}) => {
x_label: 'X Axis',
y_label: 'Y Axis',
metrics: Array.from(Array(metricCount), (_, i) => ({
label: `Metric ${i + 1}`,
label: withLabels ? `Metric ${i + 1}` : undefined,
state: metricStates.OK,
result: isMultiSeries ? matrixMultiResult() : matrixSingleResult(),
})),

View File

@ -17,9 +17,11 @@ describe('Actions TestReports Store', () => {
const summary = { total_count: 1 };
const fullReportEndpoint = `${TEST_HOST}/test_reports.json`;
const suiteEndpoint = `${TEST_HOST}/tests/:suite_name.json`;
const summaryEndpoint = `${TEST_HOST}/test_reports/summary.json`;
const defaultState = {
fullReportEndpoint,
suiteEndpoint,
summaryEndpoint,
testReports: {},
selectedSuite: null,
@ -100,6 +102,65 @@ describe('Actions TestReports Store', () => {
});
});
describe('fetch test suite', () => {
beforeEach(() => {
const buildIds = [1];
testReports.test_suites[0].build_ids = buildIds;
const endpoint = suiteEndpoint.replace(':suite_name', testReports.test_suites[0].name);
mock
.onGet(endpoint, { params: { build_ids: buildIds } })
.replyOnce(200, testReports.test_suites[0], {});
});
it('sets test suite and shows tests', done => {
const suite = testReports.test_suites[0];
const index = 0;
testAction(
actions.fetchTestSuite,
index,
{ ...state, testReports },
[{ type: types.SET_SUITE, payload: { suite, index } }],
[{ type: 'toggleLoading' }, { type: 'toggleLoading' }],
done,
);
});
it('should create flash on API error', done => {
const index = 0;
testAction(
actions.fetchTestSuite,
index,
{ ...state, testReports, suiteEndpoint: null },
[],
[{ type: 'toggleLoading' }, { type: 'toggleLoading' }],
() => {
expect(createFlash).toHaveBeenCalled();
done();
},
);
});
describe('when we already have the suite data', () => {
it('should not fetch suite', done => {
const index = 0;
testReports.test_suites[0].hasFullSuite = true;
testAction(actions.fetchTestSuite, index, { ...state, testReports }, [], [], done);
});
});
describe('when we already have the full report data', () => {
it('should not fetch suite', done => {
const index = 0;
testReports.hasFullReport = true;
testAction(actions.fetchTestSuite, index, { ...state, testReports }, [], [], done);
});
});
});
describe('fetch full report', () => {
beforeEach(() => {
mock.onGet(fullReportEndpoint).replyOnce(200, testReports, {});

View File

@ -29,6 +29,21 @@ describe('Mutations TestReports Store', () => {
});
});
describe('set suite', () => {
it('should set the suite at the given index', () => {
mockState.testReports = testReports;
const suite = { name: 'test_suite' };
const index = 0;
const expectedState = { ...mockState };
expectedState.testReports.test_suites[index] = { suite, hasFullSuite: true };
mutations[types.SET_SUITE](mockState, { suite, index });
expect(mockState.testReports.test_suites[index]).toEqual(
expectedState.testReports.test_suites[index],
);
});
});
describe('set selected suite index', () => {
it('should set selectedSuiteIndex', () => {
const selectedSuiteIndex = 0;

View File

@ -22,7 +22,7 @@ describe('Test reports app', () => {
const testSummaryTable = () => wrapper.find(TestSummaryTable);
const actionSpies = {
fetchFullReport: jest.fn(),
fetchTestSuite: jest.fn(),
fetchSummary: jest.fn(),
setSelectedSuiteIndex: jest.fn(),
removeSelectedSuiteIndex: jest.fn(),
@ -91,28 +91,14 @@ describe('Test reports app', () => {
});
describe('when a suite is clicked', () => {
describe('when the full test report has already been received', () => {
beforeEach(() => {
createComponent({ hasFullReport: true });
testSummaryTable().vm.$emit('row-click', 0);
});
it('should only call setSelectedSuiteIndex', () => {
expect(actionSpies.setSelectedSuiteIndex).toHaveBeenCalled();
expect(actionSpies.fetchFullReport).not.toHaveBeenCalled();
});
beforeEach(() => {
createComponent({ hasFullReport: true });
testSummaryTable().vm.$emit('row-click', 0);
});
describe('when the full test report has not been received', () => {
beforeEach(() => {
createComponent({ hasFullReport: false });
testSummaryTable().vm.$emit('row-click', 0);
});
it('should call setSelectedSuiteIndex and fetchFullReport', () => {
expect(actionSpies.setSelectedSuiteIndex).toHaveBeenCalled();
expect(actionSpies.fetchFullReport).toHaveBeenCalled();
});
it('should call setSelectedSuiteIndex and fetchTestSuite', () => {
expect(actionSpies.setSelectedSuiteIndex).toHaveBeenCalled();
expect(actionSpies.fetchTestSuite).toHaveBeenCalled();
});
});

View File

@ -24,7 +24,7 @@ afterEach(() =>
}),
);
initializeTestTimeout(process.env.CI ? 5000 : 500);
initializeTestTimeout(process.env.CI ? 6000 : 500);
Vue.config.devtools = false;
Vue.config.productionTip = false;

View File

@ -1,5 +1,5 @@
/* eslint-disable
jasmine/no-global-setup, jasmine/no-unsafe-spy, no-underscore-dangle, no-console
jasmine/no-global-setup, no-underscore-dangle, no-console
*/
import $ from 'jquery';
@ -81,17 +81,6 @@ window.addEventListener('unhandledrejection', event => {
console.error(event.reason.stack || event.reason);
});
// Add global function to spy on a module's dependencies via rewire
window.spyOnDependency = (module, name) => {
const dependency = module.__GetDependency__(name);
const spy = jasmine.createSpy(name, dependency);
module.__Rewire__(name, spy);
return spy;
};
// Reset any rewired modules after each test (see babel-plugin-rewire)
afterEach(__rewire_reset_all__); // eslint-disable-line
// HACK: Chrome 59 disconnects if there are too many synchronous tests in a row
// because it appears to lock up the thread that communicates to Karma's socket
// This async beforeEach gets called on every spec and releases the JS thread long

View File

@ -73,6 +73,45 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do
it { is_expected.to be_falsey }
end
context 'when config does not contain script' do
let(:name) { :build }
let(:config) do
{ before_script: "cd ${PROJ_DIR} " }
end
it { is_expected.to be_truthy }
end
context 'when using the default job without script' do
let(:name) { :default }
let(:config) do
{ before_script: "cd ${PROJ_DIR} " }
end
it { is_expected.to be_falsey }
end
context 'when using the default job with script' do
let(:name) { :default }
let(:config) do
{
before_script: "cd ${PROJ_DIR} ",
script: "ls"
}
end
it { is_expected.to be_truthy }
end
context 'there are no shared keys between jobs and bridges' do
subject(:shared_values) do
described_class::ALLOWED_KEYS & Gitlab::Ci::Config::Entry::Bridge::ALLOWED_KEYS
end
it { is_expected.to be_empty }
end
end
describe 'validations' do

View File

@ -2484,6 +2484,14 @@ module Gitlab
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs config should contain at least one visible job")
end
it "returns errors if the job script is not defined" do
config = YAML.dump({ rspec: { before_script: "test" } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec script can't be blank")
end
it "returns errors if there are no visible jobs defined" do
config = YAML.dump({ before_script: ["bundle update"], '.hidden'.to_sym => { script: 'ls' } })
expect do

View File

@ -10,7 +10,7 @@ RSpec.describe Gitlab::IncomingEmail do
end
it 'returns true' do
expect(described_class.enabled?).to be_truthy
expect(described_class.enabled?).to be(true)
end
end
@ -20,7 +20,7 @@ RSpec.describe Gitlab::IncomingEmail do
end
it "returns false" do
expect(described_class.enabled?).to be_falsey
expect(described_class.enabled?).to be(false)
end
end
end
@ -32,7 +32,7 @@ RSpec.describe Gitlab::IncomingEmail do
end
it 'confirms that wildcard is supported' do
expect(described_class.supports_wildcard?).to be_truthy
expect(described_class.supports_wildcard?).to be(true)
end
end
@ -42,7 +42,7 @@ RSpec.describe Gitlab::IncomingEmail do
end
it 'returns that wildcard is not supported' do
expect(described_class.supports_wildcard?).to be_falsey
expect(described_class.supports_wildcard?).to be(false)
end
end
@ -52,7 +52,7 @@ RSpec.describe Gitlab::IncomingEmail do
end
it 'returns that wildcard is not supported' do
expect(described_class.supports_wildcard?).to be_falsey
expect(described_class.supports_wildcard?).to be(false)
end
end
end

View File

@ -2157,11 +2157,6 @@ babel-plugin-lodash@^3.3.4:
lodash "^4.17.10"
require-package-name "^2.0.1"
babel-plugin-rewire@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/babel-plugin-rewire/-/babel-plugin-rewire-1.2.0.tgz#822562d72ed2c84e47c0f95ee232c920853e9d89"
integrity sha512-JBZxczHw3tScS+djy6JPLMjblchGhLI89ep15H3SyjujIzlxo5nr6Yjo7AXotdeVczeBmWs0tF8PgJWDdgzAkQ==
babel-preset-jest@^24.6.0:
version "24.6.0"
resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-24.6.0.tgz#66f06136eefce87797539c0d63f1769cc3915984"