+
@@ -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 }}
diff --git a/app/views/projects/environments/_form.html.haml b/app/views/projects/environments/_form.html.haml
deleted file mode 100644
index a295c8f6fb0..00000000000
--- a/app/views/projects/environments/_form.html.haml
+++ /dev/null
@@ -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'
diff --git a/doc/development/iterating_tables_in_batches.md b/doc/development/iterating_tables_in_batches.md
index 3f2b467fec9..fba6f2f616d 100644
--- a/doc/development/iterating_tables_in_batches.md
+++ b/doc/development/iterating_tables_in_batches.md
@@ -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.
diff --git a/doc/development/permissions.md b/doc/development/permissions.md
index 177fedcf454..b7079e9fb8e 100644
--- a/doc/development/permissions.md
+++ b/doc/development/permissions.md
@@ -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.
diff --git a/doc/development/reference_processing.md b/doc/development/reference_processing.md
index 2fd0ce51b39..ad6552e88fe 100644
--- a/doc/development/reference_processing.md
+++ b/doc/development/reference_processing.md
@@ -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.
diff --git a/doc/operations/incident_management/status_page.md b/doc/operations/incident_management/status_page.md
index d63d42e07c1..d14e4120511 100644
--- a/doc/operations/incident_management/status_page.md
+++ b/doc/operations/incident_management/status_page.md
@@ -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.
diff --git a/doc/user/application_security/cve_id_request.md b/doc/user/application_security/cve_id_request.md
index aaf701c91dc..009f8d828f7 100644
--- a/doc/user/application_security/cve_id_request.md
+++ b/doc/user/application_security/cve_id_request.md
@@ -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)
diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md
index 825f9be6ba6..a1d8863710c 100644
--- a/doc/user/discussions/index.md
+++ b/doc/user/discussions/index.md
@@ -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.
diff --git a/doc/user/group/epics/manage_epics.md b/doc/user/group/epics/manage_epics.md
index b063ba9a75c..1b36d6f03df 100644
--- a/doc/user/group/epics/manage_epics.md
+++ b/doc/user/group/epics/manage_epics.md
@@ -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:
diff --git a/doc/user/group/roadmap/index.md b/doc/user/group/roadmap/index.md
index 88d43715c58..811297c6eda 100644
--- a/doc/user/group/roadmap/index.md
+++ b/doc/user/group/roadmap/index.md
@@ -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
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index d3b33f79f27..16509565525 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -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).
diff --git a/doc/user/project/issues/confidential_issues.md b/doc/user/project/issues/confidential_issues.md
index e6705933ae9..136e8ee2ebb 100644
--- a/doc/user/project/issues/confidential_issues.md
+++ b/doc/user/project/issues/confidential_issues.md
@@ -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
diff --git a/doc/user/project/issues/managing_issues.md b/doc/user/project/issues/managing_issues.md
index c570bc9612a..a2185c83f4d 100644
--- a/doc/user/project/issues/managing_issues.md
+++ b/doc/user/project/issues/managing_issues.md
@@ -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)
diff --git a/doc/user/project/merge_requests/confidential.md b/doc/user/project/merge_requests/confidential.md
index b50c5ee0ea9..6df84dd1dd1 100644
--- a/doc/user/project/merge_requests/confidential.md
+++ b/doc/user/project/merge_requests/confidential.md
@@ -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
diff --git a/doc/user/project/merge_requests/getting_started.md b/doc/user/project/merge_requests/getting_started.md
index ce39f39f0a1..1c8ee587838 100644
--- a/doc/user/project/merge_requests/getting_started.md
+++ b/doc/user/project/merge_requests/getting_started.md
@@ -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
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 2a4f61d307f..6a0796bb5f0 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/spec/frontend/environments/edit_environment_spec.js b/spec/frontend/environments/edit_environment_spec.js
index de497b18dd6..3e7f5dd5ff4 100644
--- a/spec/frontend/environments/edit_environment_spec.js
+++ b/spec/frontend/environments/edit_environment_spec.js
@@ -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);
});
});
diff --git a/spec/frontend/environments/environment_form_spec.js b/spec/frontend/environments/environment_form_spec.js
index 2d7cdc8a0bd..ed8fda71dab 100644
--- a/spec/frontend/environments/environment_form_spec.js
+++ b/spec/frontend/environments/environment_form_spec.js
@@ -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);
});
});
diff --git a/spec/frontend/environments/new_environment_spec.js b/spec/frontend/environments/new_environment_spec.js
index b92c4a688b7..f6d970e02d8 100644
--- a/spec/frontend/environments/new_environment_spec.js
+++ b/spec/frontend/environments/new_environment_spec.js
@@ -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);
});
});