Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-07-30 00:10:03 +00:00
parent e4df6a7c53
commit 420215876f
22 changed files with 187 additions and 134 deletions

View file

@ -21,6 +21,7 @@ export default {
name: this.environment.name,
externalUrl: this.environment.external_url,
},
loading: false,
};
},
methods: {
@ -28,6 +29,7 @@ export default {
this.formEnvironment = environment;
},
onSubmit() {
this.loading = true;
axios
.put(this.updateEnvironmentPath, {
id: this.environment.id,
@ -38,6 +40,7 @@ export default {
.catch((error) => {
const message = error.response.data.message[0];
createFlash({ message });
this.loading = false;
});
},
},
@ -48,6 +51,7 @@ export default {
:cancel-path="projectEnvironmentsPath"
:environment="formEnvironment"
:title="__('Edit environment')"
:loading="loading"
@change="onChange"
@submit="onSubmit"
/>

View file

@ -26,6 +26,11 @@ export default {
required: true,
type: String,
},
loading: {
required: false,
type: Boolean,
default: false,
},
},
i18n: {
header: __('Environments'),
@ -42,21 +47,26 @@ export default {
helpPagePath: helpPagePath('ci/environments/index.md'),
data() {
return {
errors: {
visited: {
name: null,
url: null,
},
};
},
computed: {
valid() {
return {
name: this.visited.name && this.environment.name !== '',
url: this.visited.url && isAbsolute(this.environment.externalUrl),
};
},
},
methods: {
onChange(env) {
this.$emit('change', env);
},
validateUrl() {
this.errors.url = isAbsolute(this.environment.externalUrl);
},
validateName() {
this.errors.name = this.environment.name !== '';
visit(field) {
this.visited[field] = true;
},
},
};
@ -89,40 +99,45 @@ export default {
<gl-form-group
:label="$options.i18n.nameLabel"
label-for="environment_name"
:state="errors.name"
:state="valid.name"
:invalid-feedback="$options.i18n.nameFeedback"
>
<gl-form-input
id="environment_name"
:value="environment.name"
:state="errors.name"
:state="valid.name"
name="environment[name]"
required
@input="onChange({ ...environment, name: $event })"
@blur="validateName"
@blur="visit('name')"
/>
</gl-form-group>
<gl-form-group
:label="$options.i18n.urlLabel"
:state="errors.url"
:state="valid.url"
:invalid-feedback="$options.i18n.urlFeedback"
label-for="environment_external_url"
>
<gl-form-input
id="environment_external_url"
:value="environment.externalUrl"
:state="errors.url"
:state="valid.url"
name="environment[external_url]"
type="url"
@input="onChange({ ...environment, externalUrl: $event })"
@blur="validateUrl"
@blur="visit('url')"
/>
</gl-form-group>
<div class="form-actions">
<gl-button type="submit" variant="confirm" name="commit" class="js-no-auto-disable">{{
$options.i18n.save
}}</gl-button>
<gl-button
:loading="loading"
type="submit"
variant="confirm"
name="commit"
class="js-no-auto-disable"
>{{ $options.i18n.save }}</gl-button
>
<gl-button :href="cancelPath">{{ $options.i18n.cancel }}</gl-button>
</div>
</gl-form>

View file

@ -15,6 +15,7 @@ export default {
name: '',
externalUrl: '',
},
loading: false,
};
},
methods: {
@ -22,6 +23,7 @@ export default {
this.environment = env;
},
onSubmit() {
this.loading = true;
axios
.post(this.projectEnvironmentsPath, {
name: this.environment.name,
@ -31,6 +33,7 @@ export default {
.catch((error) => {
const message = error.response.data.message[0];
createFlash({ message });
this.loading = false;
});
},
},
@ -41,6 +44,7 @@ export default {
:cancel-path="projectEnvironmentsPath"
:environment="environment"
:title="__('New environment')"
:loading="loading"
@change="onChange($event)"
@submit="onSubmit"
/>

View file

@ -83,7 +83,11 @@ export default {
<div class="gl-display-flex gl-align-items-baseline">
<h3 class="gl-font-lg gl-m-0 gl-mr-3">{{ feature.name }}</h3>
<div :class="statusClasses" data-testid="feature-status">
<div
:class="statusClasses"
data-testid="feature-status"
:data-qa-selector="`${feature.type}_status`"
>
<template v-if="hasStatus">
<template v-if="enabled">
<gl-icon name="check-circle-filled" />
@ -112,6 +116,7 @@ export default {
:href="feature.configurationPath"
variant="confirm"
:category="configurationButton.category"
:data-qa-selector="`${feature.type}_enable_button`"
class="gl-mt-5"
>
{{ configurationButton.text }}

View file

@ -1,21 +0,0 @@
.row.gl-mt-3.gl-mb-3
.col-lg-3
%h4.gl-mt-0
= _("Environments")
%p
- link_to_read_more = link_to(_("More information"), help_page_path("ci/environments/index.md"))
= _("Environments allow you to track deployments of your application %{link_to_read_more}.").html_safe % { link_to_read_more: link_to_read_more }
= form_for [@project, @environment], html: { class: 'col-lg-9' } do |f|
= form_errors(@environment)
.form-group
= f.label :name, _('Name'), class: 'label-bold'
= f.text_field :name, required: true, class: 'form-control'
.form-group
= f.label :external_url, _('External URL'), class: 'label-bold'
= f.url_field :external_url, class: 'form-control'
.form-actions
= f.submit _('Save'), class: 'gl-button btn btn-confirm'
= link_to _('Cancel'), project_environments_path(@project), class: 'gl-button btn btn-cancel'

View file

@ -334,8 +334,8 @@ end
The iteration uses the `id` column of the `projects` table. The batching does not affect the
subquery. This means for each iteration, the subquery is executed by the database. This adds a
constant "load" on the query which often ends up in statement timeouts. We have an unknown number
of confidential issues, the execution time and the accessed database rows depend on the data
distribution in the `issues` table.
of [confidential issues](../user/project/issues/confidential_issues.md), the execution time
and the accessed database rows depend on the data distribution in the `issues` table.
NOTE:
Using subqueries works only when the subquery returns a small number of rows.

View file

@ -86,7 +86,8 @@ is calculated properly.
### Confidential issues
Confidential issues can be accessed only by project members who are at least
[Confidential issues](../user/project/issues/confidential_issues.md) can be accessed
only by project members who are at least
reporters (they can't be accessed by guests). Additionally they can be accessed
by their authors and assignees.

View file

@ -152,7 +152,8 @@ a resource that some subsequent readers should not be able to see.
For example, you might create an issue, and refer to a confidential issue `#1234`,
which you have access to. This is rendered in the cached HTML as a link to
that confidential issue, with data attributes containing its ID, the ID of the
that [confidential issue](../user/project/issues/confidential_issues.md),
with data attributes containing its ID, the ID of the
project and other confidential data. A later reader, who has access to your issue
might not have permission to read issue `#1234`, and so we need to redact
these sensitive pieces of data. This is what `ReferenceParser` classes do.

View file

@ -124,7 +124,7 @@ To publish an incident:
1. Create an issue in the project you enabled the GitLab Status Page settings in.
1. A [project or group owner](../../user/permissions.md) must use the
`/publish` [quick action](../../user/project/quick_actions.md) to publish the
issue to the GitLab Status Page. Confidential issues can't be published.
issue to the GitLab Status Page. [Confidential issues](../../user/project/issues/confidential_issues.md) can't be published.
A background worker publishes the issue onto the Status Page using the credentials
you provided during setup. As part of publication, GitLab:
@ -168,5 +168,6 @@ To change the incident status from `open` to `closed`, close the incident issue
within GitLab. Closing the issue triggers a background worker to update the
GitLab Status Page website.
If you make a published issue confidential, GitLab unpublishes it from your
GitLab Status Page website.
If you
[make a published issue confidential](../../user/project/issues/confidential_issues.md#making-an-issue-confidential),
GitLab unpublishes it from your GitLab Status Page website.

View file

@ -28,7 +28,7 @@ If the following conditions are met, a **Request CVE ID** button appears in your
- The project is hosted in GitLab.com.
- The project is public.
- You are a maintainer of the project.
- The issue is confidential.
- The issue is [confidential](../project/issues/confidential_issues.md).
## Submitting a CVE ID Request
@ -37,7 +37,7 @@ the [GitLab CVE project](https://gitlab.com/gitlab-org/cves).
![CVE ID request button](img/cve_id_request_button.png)
Creating the confidential issue starts the CVE request process.
Creating the [confidential issue](../project/issues/confidential_issues.md) starts the CVE request process.
![New CVE ID request issue](img/new_cve_request_issue.png)

View file

@ -138,6 +138,8 @@ who have at least the Reporter role.
![Confidential comments](img/confidential_comments_v13_9.png)
You can also make an [entire issue confidential](../project/issues/confidential_issues.md).
## Show only comments
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/26723) in GitLab 11.5.

View file

@ -193,7 +193,10 @@ or newest items to be shown first.
If you're working on items that contain private information, you can make an epic confidential.
NOTE:
A confidential epic can only contain confidential issues and confidential child epics.
A confidential epic can only contain [confidential issues](../../project/issues/confidential_issues.md)
and confidential child epics. However, merge requests are public, if created in a public project.
Read [Merge requests for confidential issues](../../project/merge_requests/confidential.md)
to learn how to create a confidential merge request.
To make an epic confidential:

View file

@ -69,7 +69,7 @@ You can also filter epics in the Roadmap view by the epics':
- Author
- Label
- Milestone
- Confidentiality
- [Confidentiality](../epics/manage_epics.md#make-an-epic-confidential)
- Epic
- Your Reaction

View file

@ -46,7 +46,7 @@ The following table lists project permissions available for each role:
| Action | Guest | Reporter | Developer |Maintainer| Owner |
|---------------------------------------------------|---------|------------|-------------|----------|--------|
| Assign issues | ✓ (*16*)| ✓ | ✓ | ✓ | ✓ |
| Create confidential issue | ✓ | ✓ | ✓ | ✓ | ✓ |
| Create [confidential issue](project/issues/confidential_issues.md) | ✓ | ✓ | ✓ | ✓ | ✓ |
| Create new issue | ✓ | ✓ | ✓ | ✓ | ✓ |
| Download and browse job artifacts | ✓ (*3*) | ✓ | ✓ | ✓ | ✓ |
| Download project | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
@ -97,7 +97,7 @@ The following table lists project permissions available for each role:
| [Set issue estimate and record time spent](project/time_tracking.md) | | ✓ | ✓ | ✓ | ✓ |
| View CI/CD analytics | | ✓ | ✓ | ✓ | ✓ |
| View Code Review analytics **(PREMIUM)** | | ✓ | ✓ | ✓ | ✓ |
| View confidential issues | (*2*) | ✓ | ✓ | ✓ | ✓ |
| View [confidential issues](project/issues/confidential_issues.md) | (*2*) | ✓ | ✓ | ✓ | ✓ |
| View Error Tracking list | | ✓ | ✓ | ✓ | ✓ |
| View License list **(ULTIMATE)** | | ✓ | ✓ | ✓ | ✓ |
| View metrics dashboard annotations | | ✓ | ✓ | ✓ | ✓ |
@ -198,7 +198,7 @@ The following table lists project permissions available for each role:
| Remove protected branches (*4*) | | | | | |
1. Guest users are able to perform this action on public and internal projects, but not private projects. This doesn't apply to [external users](#external-users) where explicit access must be given even if the project is internal.
1. Guest users can only view the confidential issues they created themselves.
1. Guest users can only view the [confidential issues](project/issues/confidential_issues.md) they created themselves.
1. If **Public pipelines** is enabled in **Project Settings > CI/CD**.
1. Not allowed for Guest, Reporter, Developer, Maintainer, or Owner. See [protected branches](project/protected_branches.md).
1. If the [branch is protected](project/protected_branches.md), this depends on the access Developers and Maintainers are given.
@ -256,7 +256,7 @@ Read through the documentation on [permissions for File Locking](project/file_lo
### Confidential Issues permissions
Confidential issues can be accessed by users with reporter and higher permission levels,
[Confidential issues](project/issues/confidential_issues.md) can be accessed by users with reporter and higher permission levels,
as well as by guest users that create a confidential issue. To learn more,
read through the documentation on [permissions and access to confidential issues](project/issues/confidential_issues.md#permissions-and-access-to-confidential-issues).

View file

@ -45,8 +45,8 @@ system note in the issue's comments.
## Indications of a confidential issue
There are a few things that visually separate a confidential issue from a
regular one. In the issues index page view, you can see the eye-slash icon
next to the issues that are marked as confidential.
regular one. In the issues index page view, you can see the eye-slash (**(eye-slash)**) icon
next to the issues that are marked as confidential:
![Confidential issues index page](img/confidential_issues_index_page.png)
@ -91,3 +91,6 @@ sees in the project's search results respectively.
## Related links
- [Merge requests for confidential issues](../merge_requests/confidential.md)
- [Make an epic confidential](../../group/epics/manage_epics.md#make-an-epic-confidential)
- [Mark a comment as confidential](../../discussions/index.md#mark-a-comment-as-confidential)
- [Security practices for confidential merge requests](https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#security-releases-critical-non-critical-as-a-developer) at GitLab

View file

@ -59,7 +59,7 @@ When you're creating a new issue, these are the fields you can fill in:
- Title
- Description
- Checkbox to make the issue confidential
- Checkbox to make the issue [confidential](confidential_issues.md)
- Assignee
- Weight
- [Epic](../../group/epics/index.md)

View file

@ -70,4 +70,6 @@ to the public upstream project.
## Related links
- [Confidential issues](../issues/confidential_issues.md)
- [Make an epic confidential](../../group/epics/manage_epics.md#make-an-epic-confidential)
- [Mark a comment as confidential](../../discussions/index.md#mark-a-comment-as-confidential)
- [Security practices for confidential merge requests](https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#security-releases-critical-non-critical-as-a-developer) at GitLab

View file

@ -140,7 +140,7 @@ when merged.
If the issue is [confidential](../issues/confidential_issues.md),
you may want to use a different workflow for
[merge requests for confidential issues](../issues/confidential_issues.md#merge-requests-for-confidential-issues)
[merge requests for confidential issues](confidential.md)
to prevent confidential information from being exposed.
### Deleting the source branch

View file

@ -12470,9 +12470,6 @@ msgstr ""
msgid "Environments Dashboard"
msgstr ""
msgid "Environments allow you to track deployments of your application %{link_to_read_more}."
msgstr ""
msgid "Environments allow you to track deployments of your application. %{linkStart}More information%{linkEnd}."
msgstr ""

View file

@ -1,3 +1,4 @@
import { GlLoadingIcon } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
@ -43,7 +44,9 @@ describe('~/environments/components/edit.vue', () => {
wrapper.destroy();
});
const fillForm = async (expected, response) => {
const showsLoading = () => wrapper.find(GlLoadingIcon).exists();
const submitForm = async (expected, response) => {
mock
.onPut(DEFAULT_OPTS.provide.updateEnvironmentPath, {
name: expected.name,
@ -72,10 +75,20 @@ describe('~/environments/components/edit.vue', () => {
expect(input().element.value).toBe(value);
});
it('shows loader after form is submitted', async () => {
const expected = { name: 'test', url: 'https://google.ca' };
expect(showsLoading()).toBe(false);
await submitForm(expected, [200, { path: '/test' }]);
expect(showsLoading()).toBe(true);
});
it('submits the updated environment on submit', async () => {
const expected = { name: 'test', url: 'https://google.ca' };
await fillForm(expected, [200, { path: '/test' }]);
await submitForm(expected, [200, { path: '/test' }]);
expect(visitUrl).toHaveBeenCalledWith('/test');
});
@ -83,8 +96,9 @@ describe('~/environments/components/edit.vue', () => {
it('shows errors on error', async () => {
const expected = { name: 'test', url: 'https://google.ca' };
await fillForm(expected, [400, { message: ['name taken'] }]);
await submitForm(expected, [400, { message: ['name taken'] }]);
expect(createFlash).toHaveBeenCalledWith({ message: 'name taken' });
expect(showsLoading()).toBe(false);
});
});

View file

@ -1,97 +1,105 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import EnvironmentForm from '~/environments/components/environment_form.vue';
jest.mock('~/lib/utils/csrf');
const DEFAULT_OPTS = {
propsData: {
environment: { name: '', externalUrl: '' },
title: 'environment',
cancelPath: '/cancel',
},
const DEFAULT_PROPS = {
environment: { name: '', externalUrl: '' },
title: 'environment',
cancelPath: '/cancel',
};
describe('~/environments/components/form.vue', () => {
let wrapper;
const createWrapper = (opts = {}) =>
const createWrapper = (propsData = {}) =>
mountExtended(EnvironmentForm, {
...DEFAULT_OPTS,
...opts,
propsData: {
...DEFAULT_PROPS,
...propsData,
},
});
beforeEach(() => {
wrapper = createWrapper();
});
afterEach(() => {
wrapper.destroy();
});
it('links to documentation regarding environments', () => {
const link = wrapper.findByRole('link', { name: 'More information' });
expect(link.attributes('href')).toBe('/help/ci/environments/index.md');
});
it('links the cancel button to the cancel path', () => {
const cancel = wrapper.findByRole('link', { name: 'Cancel' });
expect(cancel.attributes('href')).toBe(DEFAULT_OPTS.propsData.cancelPath);
});
describe('name input', () => {
let name;
describe('default', () => {
beforeEach(() => {
name = wrapper.findByLabelText('Name');
wrapper = createWrapper();
});
it('should emit changes to the name', async () => {
await name.setValue('test');
await name.trigger('blur');
expect(wrapper.emitted('change')).toEqual([[{ name: 'test', externalUrl: '' }]]);
it('links to documentation regarding environments', () => {
const link = wrapper.findByRole('link', { name: 'More information' });
expect(link.attributes('href')).toBe('/help/ci/environments/index.md');
});
it('should validate that the name is required', async () => {
await name.setValue('');
await name.trigger('blur');
it('links the cancel button to the cancel path', () => {
const cancel = wrapper.findByRole('link', { name: 'Cancel' });
expect(wrapper.findByText('This field is required').exists()).toBe(true);
expect(name.attributes('aria-invalid')).toBe('true');
expect(cancel.attributes('href')).toBe(DEFAULT_PROPS.cancelPath);
});
describe('name input', () => {
let name;
beforeEach(() => {
name = wrapper.findByLabelText('Name');
});
it('should emit changes to the name', async () => {
await name.setValue('test');
await name.trigger('blur');
expect(wrapper.emitted('change')).toEqual([[{ name: 'test', externalUrl: '' }]]);
});
it('should validate that the name is required', async () => {
await name.setValue('');
await name.trigger('blur');
expect(wrapper.findByText('This field is required').exists()).toBe(true);
expect(name.attributes('aria-invalid')).toBe('true');
});
});
describe('url input', () => {
let url;
beforeEach(() => {
url = wrapper.findByLabelText('External URL');
});
it('should emit changes to the url', async () => {
await url.setValue('https://example.com');
await url.trigger('blur');
expect(wrapper.emitted('change')).toEqual([
[{ name: '', externalUrl: 'https://example.com' }],
]);
});
it('should validate that the url is required', async () => {
await url.setValue('example.com');
await url.trigger('blur');
expect(wrapper.findByText('The URL should start with http:// or https://').exists()).toBe(
true,
);
expect(url.attributes('aria-invalid')).toBe('true');
});
});
it('submits when the form does', async () => {
await wrapper.findByRole('form', { title: 'environment' }).trigger('submit');
expect(wrapper.emitted('submit')).toEqual([[]]);
});
});
describe('url input', () => {
let url;
beforeEach(() => {
url = wrapper.findByLabelText('External URL');
});
it('should emit changes to the url', async () => {
await url.setValue('https://example.com');
await url.trigger('blur');
expect(wrapper.emitted('change')).toEqual([
[{ name: '', externalUrl: 'https://example.com' }],
]);
});
it('should validate that the url is required', async () => {
await url.setValue('example.com');
await url.trigger('blur');
expect(wrapper.findByText('The URL should start with http:// or https://').exists()).toBe(
true,
);
expect(url.attributes('aria-invalid')).toBe('true');
});
});
it('submits when the form does', async () => {
await wrapper.findByRole('form', { title: 'environment' }).trigger('submit');
expect(wrapper.emitted('submit')).toEqual([[]]);
it('shows a loading icon while loading', () => {
wrapper = createWrapper({ loading: true });
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
});

View file

@ -1,3 +1,4 @@
import { GlLoadingIcon } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
@ -39,7 +40,9 @@ describe('~/environments/components/new.vue', () => {
wrapper.destroy();
});
const fillForm = async (expected, response) => {
const showsLoading = () => wrapper.find(GlLoadingIcon).exists();
const submitForm = async (expected, response) => {
mock
.onPost(DEFAULT_OPTS.provide.projectEnvironmentsPath, {
name: expected.name,
@ -68,10 +71,20 @@ describe('~/environments/components/new.vue', () => {
expect(input().element.value).toBe(value);
});
it('shows loader after form is submitted', async () => {
const expected = { name: 'test', url: 'https://google.ca' };
expect(showsLoading()).toBe(false);
await submitForm(expected, [200, { path: '/test' }]);
expect(showsLoading()).toBe(true);
});
it('submits the new environment on submit', async () => {
const expected = { name: 'test', url: 'https://google.ca' };
await fillForm(expected, [200, { path: '/test' }]);
await submitForm(expected, [200, { path: '/test' }]);
expect(visitUrl).toHaveBeenCalledWith('/test');
});
@ -79,8 +92,9 @@ describe('~/environments/components/new.vue', () => {
it('shows errors on error', async () => {
const expected = { name: 'test', url: 'https://google.ca' };
await fillForm(expected, [400, { message: ['name taken'] }]);
await submitForm(expected, [400, { message: ['name taken'] }]);
expect(createFlash).toHaveBeenCalledWith({ message: 'name taken' });
expect(showsLoading()).toBe(false);
});
});