diff --git a/.gitlab/merge_request_templates/Quarantine End to End Test.md b/.gitlab/merge_request_templates/Quarantine End to End Test.md
index 4caebb7f1bb..a8d2378eee0 100644
--- a/.gitlab/merge_request_templates/Quarantine End to End Test.md
+++ b/.gitlab/merge_request_templates/Quarantine End to End Test.md
@@ -19,14 +19,14 @@ the noise (due to constantly failing tests, flaky tests, and so on) so that new
- [ ] [Code review guidelines](https://docs.gitlab.com/ee/development/code_review.html)
- [ ] [Style guides](https://docs.gitlab.com/ee/development/contributing/style_guides.html)
- [ ] Quarantine test check-list
- - [ ] Follow the [Quarantining Tests guide](https://about.gitlab.com/handbook/engineering/quality/guidelines/debugging-qa-test-failures/#quarantining-tests).
- - [ ] Confirm the test has a [`quarantine:` tag with the specified quarantine type](https://about.gitlab.com/handbook/engineering/quality/guidelines/debugging-qa-test-failures/#quarantined-test-types).
+ - [ ] Follow the [Quarantining Tests guide](https://about.gitlab.com/handbook/engineering/quality/quality-engineering/debugging-qa-test-failures/#quarantining-tests).
+ - [ ] Confirm the test has a [`quarantine:` tag with the specified quarantine type](https://about.gitlab.com/handbook/engineering/quality/quality-engineering/debugging-qa-test-failures/#quarantined-test-types).
- [ ] Note if the test should be [quarantined for a specific environment](https://docs.gitlab.com/ee/development/testing_guide/end_to_end/execution_context_selection.html#quarantine-a-test-for-a-specific-environment).
- [ ] (Optionally) In case of an emergency (e.g. blocked deployments), consider adding labels to pick into auto-deploy (~"Pick into auto-deploy" ~"priority::1" ~"severity::1").
- [ ] Dequarantine test check-list
- - [ ] Follow the [Dequarantining Tests guide](https://about.gitlab.com/handbook/engineering/quality/guidelines/debugging-qa-test-failures/#dequarantining-tests).
+ - [ ] Follow the [Dequarantining Tests guide](https://about.gitlab.com/handbook/engineering/quality/quality-engineering/debugging-qa-test-failures/#dequarantining-tests).
- [ ] Confirm the test consistently passes on the target GitLab environment(s).
- - [ ] (Optionally) [Trigger a manual GitLab-QA pipeline](https://about.gitlab.com/handbook/engineering/quality/guidelines/tips-and-tricks/#running-gitlab-qa-pipeline-against-a-specific-gitlab-release) against a specific GitLab environment using the `RELEASE` variable from the `package-and-qa` job of the current merge request.
+ - [ ] (Optionally) [Trigger a manual GitLab-QA pipeline](https://about.gitlab.com/handbook/engineering/quality/quality-engineering/tips-and-tricks/#running-gitlab-qa-pipeline-against-a-specific-gitlab-release) against a specific GitLab environment using the `RELEASE` variable from the `package-and-qa` job of the current merge request.
- [ ] To ensure a faster turnaround, ask in the `#quality` Slack channel for someone to review and merge the merge request, rather than assigning it directly.
diff --git a/app/assets/javascripts/admin/deploy_keys/components/table.vue b/app/assets/javascripts/admin/deploy_keys/components/table.vue
index 97a5a2f2f32..e971a1d1550 100644
--- a/app/assets/javascripts/admin/deploy_keys/components/table.vue
+++ b/app/assets/javascripts/admin/deploy_keys/components/table.vue
@@ -1,13 +1,28 @@
@@ -45,10 +130,71 @@ export default {
{{
+ {{
$options.i18n.newDeployKeyButtonText
}}
-
+
+
+
+
+
+
+
+ {{ project.name_with_namespace }}
+
+
+
+ {{ fingerprint }}
+
+
+
+
+
+
+
+ {{ label }}
+
+
+
+
+
+
+
+
+
+
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index adf3e122a64..8c996b448aa 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -91,6 +91,7 @@ const Api = {
projectNotificationSettingsPath: '/api/:version/projects/:id/notification_settings',
groupNotificationSettingsPath: '/api/:version/groups/:id/notification_settings',
notificationSettingsPath: '/api/:version/notification_settings',
+ deployKeysPath: '/api/:version/deploy_keys',
group(groupId, callback = () => {}) {
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
@@ -950,6 +951,12 @@ const Api = {
return axios.delete(url);
},
+ deployKeys(params = {}) {
+ const url = Api.buildUrl(this.deployKeysPath);
+
+ return axios.get(url, { params: { per_page: DEFAULT_PER_PAGE, ...params } });
+ },
+
async updateNotificationSettings(projectId, groupId, data = {}) {
let url = Api.buildUrl(this.notificationSettingsPath);
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index e35d8d94289..e74d4deeaec 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -388,7 +388,7 @@ export default {
diff --git a/doc/ci/pipelines/job_artifacts.md b/doc/ci/pipelines/job_artifacts.md
index 7ecee5508ef..783f8fc6b8d 100644
--- a/doc/ci/pipelines/job_artifacts.md
+++ b/doc/ci/pipelines/job_artifacts.md
@@ -50,6 +50,109 @@ the pipeline that finishes later creates the job artifact.
For more examples, view the [keyword reference for the `.gitlab-ci.yml` file](../yaml/index.md#artifacts).
+### Use CI/CD variables to define the artifacts name
+
+You can use [CI/CD variables](../variables/index.md) to dynamically define the
+artifacts file's name.
+
+For example, to create an archive with a name of the current job:
+
+```yaml
+job:
+ artifacts:
+ name: "$CI_JOB_NAME"
+ paths:
+ - binaries/
+```
+
+To create an archive with a name of the current branch or tag including only
+the binaries directory:
+
+```yaml
+job:
+ artifacts:
+ name: "$CI_COMMIT_REF_NAME"
+ paths:
+ - binaries/
+```
+
+If your branch-name contains forward slashes
+(for example `feature/my-feature`) it's advised to use `$CI_COMMIT_REF_SLUG`
+instead of `$CI_COMMIT_REF_NAME` for proper naming of the artifact.
+
+To create an archive with a name of the current job and the current branch or
+tag including only the binaries directory:
+
+```yaml
+job:
+ artifacts:
+ name: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME"
+ paths:
+ - binaries/
+```
+
+To create an archive with a name of the current [stage](../yaml/index.md#stages) and branch name:
+
+```yaml
+job:
+ artifacts:
+ name: "$CI_JOB_STAGE-$CI_COMMIT_REF_NAME"
+ paths:
+ - binaries/
+```
+
+If you use **Windows Batch** to run your shell scripts you must replace
+`$` with `%`:
+
+```yaml
+job:
+ artifacts:
+ name: "%CI_JOB_STAGE%-%CI_COMMIT_REF_NAME%"
+ paths:
+ - binaries/
+```
+
+If you use **Windows PowerShell** to run your shell scripts you must replace
+`$` with `$env:`:
+
+```yaml
+job:
+ artifacts:
+ name: "$env:CI_JOB_STAGE-$env:CI_COMMIT_REF_NAME"
+ paths:
+ - binaries/
+```
+
+### Exclude files from job artifacts
+
+Use [`artifacts:exclude`](../yaml/index.md#artifactsexclude) to prevent files from
+being added to an artifacts archive.
+
+For example, to store all files in `binaries/`, but not `*.o` files located in
+subdirectories of `binaries/`.
+
+```yaml
+artifacts:
+ paths:
+ - binaries/
+ exclude:
+ - binaries/**/*.o
+```
+
+Unlike [`artifacts:paths`](../yaml/index.md#artifactspaths), `exclude` paths are not recursive.
+To exclude all of the contents of a directory, match them explicitly rather
+than matching the directory itself.
+
+For example, to store all files in `binaries/` but nothing located in the `temp/` subdirectory:
+
+```yaml
+artifacts:
+ paths:
+ - binaries/
+ exclude:
+ - binaries/temp/**/*
+```
+
## Download job artifacts
You can download job artifacts or view the job archive:
@@ -103,6 +206,35 @@ To delete a job:
1. On the top right of the job's log, select **Erase job log** (**{remove}**).
1. On the confirmation dialog, select **OK**.
+## Expose job artifacts in the merge request UI
+
+Use the [`artifacts:expose_as`](../yaml/index.md#artifactsexpose_as) keyword to expose
+[job artifacts](../pipelines/job_artifacts.md) in the [merge request](../../user/project/merge_requests/index.md) UI.
+
+For example, to match a single file:
+
+```yaml
+test:
+ script: ["echo 'test' > file.txt"]
+ artifacts:
+ expose_as: 'artifact 1'
+ paths: ['file.txt']
+```
+
+With this configuration, GitLab adds a link **artifact 1** to the relevant merge request
+that points to `file1.txt`. To access the link, select **View exposed artifact**
+below the pipeline graph in the merge request overview.
+
+An example that matches an entire directory:
+
+```yaml
+test:
+ script: ["mkdir test && echo 'test' > test/file.txt"]
+ artifacts:
+ expose_as: 'artifact 1'
+ paths: ['test/']
+```
+
## Retrieve job artifacts for other projects
To retrieve a job artifact from a different project, you might need to use a
diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md
index 31300276e7b..8e8942eaff7 100644
--- a/doc/ci/yaml/index.md
+++ b/doc/ci/yaml/index.md
@@ -628,12 +628,13 @@ test_job_2:
### `artifacts`
-Use `artifacts` to specify a list of files and directories that are
+Use `artifacts` to specify which files to save as [job artifacts](../pipelines/job_artifacts.md).
+Job artifacts are a list of files and directories that are
attached to the job when it [succeeds, fails, or always](#artifactswhen).
The artifacts are sent to GitLab after the job finishes. They are
-available for download in the GitLab UI if the size is not
-larger than the [maximum artifact size](../../user/gitlab_com/index.md#gitlab-cicd).
+available for download in the GitLab UI if the size is smaller than the
+the [maximum artifact size](../../user/gitlab_com/index.md#gitlab-cicd).
By default, jobs in later stages automatically download all the artifacts created
by jobs in earlier stages. You can control artifact download behavior in jobs with
@@ -652,16 +653,18 @@ artifacts are restored after [caches](#cache).
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15122) in GitLab 13.1
> - Requires GitLab Runner 13.1
-`exclude` makes it possible to prevent files from being added to an artifacts
-archive.
+Use `artifacts:exclude` to prevent files from being added to an artifacts archive.
-Similar to [`artifacts:paths`](#artifactspaths), `exclude` paths are relative
-to the project directory. You can use Wildcards that use
-[glob](https://en.wikipedia.org/wiki/Glob_(programming)) or
-[`doublestar.PathMatch`](https://pkg.go.dev/github.com/bmatcuk/doublestar@v1.2.2?tab=doc#PathMatch) patterns.
+**Keyword type**: Job keyword. You can use it only as part of a job or in the
+[`default:` section](#default).
-For example, to store all files in `binaries/`, but not `*.o` files located in
-subdirectories of `binaries/`:
+**Possible inputs**:
+
+- An array of file paths, relative to the project directory.
+- You can use Wildcards that use [glob](https://en.wikipedia.org/wiki/Glob_(programming)) or
+ [`doublestar.PathMatch`](https://pkg.go.dev/github.com/bmatcuk/doublestar@v1.2.2?tab=doc#PathMatch) patterns.
+
+**Example of `artifacts:exclude`**:
```yaml
artifacts:
@@ -671,20 +674,18 @@ artifacts:
- binaries/**/*.o
```
-Unlike [`artifacts:paths`](#artifactspaths), `exclude` paths are not recursive. To exclude all of the contents of a directory, you can match them explicitly rather than matching the directory itself.
+This example stores all files in `binaries/`, but not `*.o` files located in
+subdirectories of `binaries/`.
-For example, to store all files in `binaries/` but nothing located in the `temp/` subdirectory:
+**Additional details**:
-```yaml
-artifacts:
- paths:
- - binaries/
- exclude:
- - binaries/temp/**/*
-```
+- `artifacts:exclude` paths are not searched recursively.
+- Files matched by [`artifacts:untracked`](#artifactsuntracked) can be excluded using
+ `artifacts:exclude` too.
-Files matched by [`artifacts:untracked`](#artifactsuntracked) can be excluded using
-`artifacts:exclude` too.
+**Related topics**:
+
+- [Exclude files from job artifacts](../pipelines/job_artifacts.md#exclude-files-from-job-artifacts)
#### `artifacts:expire_in`
@@ -704,8 +705,14 @@ they expire and are deleted. The `expire_in` setting does not affect:
pipeline artifacts. See [When pipeline artifacts are deleted](../pipelines/pipeline_artifacts.md#when-pipeline-artifacts-are-deleted)
for more information.
-The value of `expire_in` is an elapsed time in seconds, unless a unit is provided. Valid values
-include:
+After their expiry, artifacts are deleted hourly by default (using a cron job), and are not
+accessible anymore.
+
+**Keyword type**: Job keyword. You can use it only as part of a job or in the
+[`default:` section](#default).
+
+**Possible inputs**: The expiry time. If no unit is provided, the time is in seconds.
+Valid values include:
- `'42'`
- `42 seconds`
@@ -717,7 +724,7 @@ include:
- `3 weeks and 2 days`
- `never`
-To expire artifacts one week after being uploaded:
+**Example of `artifacts:expire_in`**:
```yaml
job:
@@ -725,28 +732,31 @@ job:
expire_in: 1 week
```
-The expiration time period begins when the artifact is uploaded and stored on GitLab. If the expiry
-time is not defined, it defaults to the
-[instance wide setting](../../user/admin_area/settings/continuous_integration.md#default-artifacts-expiration)
-(30 days by default).
+**Additional details**:
-To override the expiration date and protect artifacts from being automatically deleted:
-
-- Select **Keep** on the job page.
-- [In GitLab 13.3 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/22761), set the value of
- `expire_in` to `never`.
-
-After their expiry, artifacts are deleted hourly by default (using a cron job), and are not
-accessible anymore.
+- The expiration time period begins when the artifact is uploaded and stored on GitLab.
+ If the expiry time is not defined, it defaults to the [instance wide setting](../../user/admin_area/settings/continuous_integration.md#default-artifacts-expiration).
+- To override the expiration date and protect artifacts from being automatically deleted:
+ - Select **Keep** on the job page.
+ - [In GitLab 13.3 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/22761), set the value of
+ `expire_in` to `never`.
#### `artifacts:expose_as`
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15018) in GitLab 12.5.
-Use the `expose_as` keyword to expose [job artifacts](../pipelines/job_artifacts.md)
-in the [merge request](../../user/project/merge_requests/index.md) UI.
+Use the `artifacts:expose_as` keyword to
+[expose job artifacts in the merge request UI](../pipelines/job_artifacts.md#expose-job-artifacts-in-the-merge-request-ui).
-For example, to match a single file:
+**Keyword type**: Job keyword. You can use it only as part of a job or in the
+[`default:` section](#default).
+
+**Possible inputs**:
+
+- The name to display in the merge request UI for the artifacts download link.
+ Must be combined with [`artifacts:paths`](#artifactspaths).
+
+**Example of `artifacts:expose_as`**:
```yaml
test:
@@ -756,108 +766,50 @@ test:
paths: ['file.txt']
```
-With this configuration, GitLab adds a link **artifact 1** to the relevant merge request
-that points to `file1.txt`. To access the link, select **View exposed artifact**
-below the pipeline graph in the merge request overview.
+**Additional details**:
-An example that matches an entire directory:
-
-```yaml
-test:
- script: ["mkdir test && echo 'test' > test/file.txt"]
- artifacts:
- expose_as: 'artifact 1'
- paths: ['test/']
-```
-
-Note the following:
-
-- Artifacts do not display in the merge request UI when using variables to define the `artifacts:paths`.
+- If `artifacts:paths` uses [CI/CD variables](../variables/index.md), the artifacts do not display in the UI.
- A maximum of 10 job artifacts per merge request can be exposed.
- Glob patterns are unsupported.
-- If a directory is specified, the link is to the job [artifacts browser](../pipelines/job_artifacts.md#download-job-artifacts) if there is more than
- one file in the directory.
-- For exposed single file artifacts with `.html`, `.htm`, `.txt`, `.json`, `.xml`,
- and `.log` extensions, if [GitLab Pages](../../administration/pages/index.md) is:
- - Enabled, GitLab automatically renders the artifact.
- - Not enabled, the file is displayed in the artifacts browser.
+- If a directory is specified and there is more than one file in the directory,
+ the link is to the job [artifacts browser](../pipelines/job_artifacts.md#download-job-artifacts).
+- If [GitLab Pages](../../administration/pages/index.md) is enabled, GitLab automatically
+ renders the artifacts when the artifacts is a single file with one of these extensions:
+ - `.html` or `.htm`
+ - `.txt`
+ - `.json`
+ - `.xml`
+ - `.log`
#### `artifacts:name`
-Use the `name` directive to define the name of the created artifacts
-archive. You can specify a unique name for every archive. The `artifacts:name`
-variable can make use of any of the [predefined variables](../variables/index.md).
-The default name is `artifacts`, which becomes `artifacts.zip` when you download it.
+Use the `artifacts:name` keyword to define the name of the created artifacts
+archive. You can specify a unique name for every archive.
+
+If not defined, the default name is `artifacts`, which becomes `artifacts.zip` when downloaded.
+
+**Keyword type**: Job keyword. You can use it only as part of a job or in the
+[`default:` section](#default).
+
+**Possible inputs**:
+
+- The name of the artifacts archive. Can use [CI/CD variables](../variables/index.md).
+
+**Example of `artifacts:name`**:
To create an archive with a name of the current job:
```yaml
job:
artifacts:
- name: "$CI_JOB_NAME"
+ name: "job1-artifacts-file"
paths:
- binaries/
```
-To create an archive with a name of the current branch or tag including only
-the binaries directory:
+**Related topics**:
-```yaml
-job:
- artifacts:
- name: "$CI_COMMIT_REF_NAME"
- paths:
- - binaries/
-```
-
-If your branch-name contains forward slashes
-(for example `feature/my-feature`) it's advised to use `$CI_COMMIT_REF_SLUG`
-instead of `$CI_COMMIT_REF_NAME` for proper naming of the artifact.
-
-To create an archive with a name of the current job and the current branch or
-tag including only the binaries directory:
-
-```yaml
-job:
- artifacts:
- name: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME"
- paths:
- - binaries/
-```
-
-To create an archive with a name of the current [stage](#stages) and branch name:
-
-```yaml
-job:
- artifacts:
- name: "$CI_JOB_STAGE-$CI_COMMIT_REF_NAME"
- paths:
- - binaries/
-```
-
----
-
-If you use **Windows Batch** to run your shell scripts you must replace
-`$` with `%`:
-
-```yaml
-job:
- artifacts:
- name: "%CI_JOB_STAGE%-%CI_COMMIT_REF_NAME%"
- paths:
- - binaries/
-```
-
-If you use **Windows PowerShell** to run your shell scripts you must replace
-`$` with `$env:`:
-
-```yaml
-job:
- artifacts:
- name: "$env:CI_JOB_STAGE-$env:CI_COMMIT_REF_NAME"
- paths:
- - binaries/
-```
+- [Use CI/CD variables to define the artifacts name.](../pipelines/job_artifacts.md#use-cicd-variables-to-define-the-artifacts-name)
#### `artifacts:paths`
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 5ed681c5195..765e066463c 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3576,6 +3576,9 @@ msgstr ""
msgid "An error occurred fetching the project authors."
msgstr ""
+msgid "An error occurred fetching the public deploy keys. Please try again."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -23536,6 +23539,9 @@ msgstr ""
msgid "No projects found"
msgstr ""
+msgid "No public deploy keys"
+msgstr ""
+
msgid "No public groups"
msgstr ""
diff --git a/spec/frontend/admin/deploy_keys/components/table_spec.js b/spec/frontend/admin/deploy_keys/components/table_spec.js
index 3b3be488043..98b834e3235 100644
--- a/spec/frontend/admin/deploy_keys/components/table_spec.js
+++ b/spec/frontend/admin/deploy_keys/components/table_spec.js
@@ -1,8 +1,17 @@
import { merge } from 'lodash';
-import { GlTable, GlButton } from '@gitlab/ui';
+import { GlLoadingIcon, GlEmptyState, GlPagination } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import responseBody from 'test_fixtures/api/deploy_keys/index.json';
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import DeployKeysTable from '~/admin/deploy_keys/components/table.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import Api, { DEFAULT_PER_PAGE } from '~/api';
+import createFlash from '~/flash';
+
+jest.mock('~/api');
+jest.mock('~/flash');
describe('DeployKeysTable', () => {
let wrapper;
@@ -14,12 +23,53 @@ describe('DeployKeysTable', () => {
emptyStateSvgPath: '/assets/illustrations/empty-state/empty-deploy-keys.svg',
};
+ const deployKey = responseBody[0];
+ const deployKey2 = responseBody[1];
+
const createComponent = (provide = {}) => {
wrapper = mountExtended(DeployKeysTable, {
provide: merge({}, defaultProvide, provide),
});
};
+ const findEditButton = (index) =>
+ wrapper.findAllByLabelText(DeployKeysTable.i18n.edit, { selector: 'a' }).at(index);
+ const findRemoveButton = (index) =>
+ wrapper.findAllByLabelText(DeployKeysTable.i18n.remove, { selector: 'button' }).at(index);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findTimeAgoTooltip = (index) => wrapper.findAllComponents(TimeAgoTooltip).at(index);
+ const findPagination = () => wrapper.findComponent(GlPagination);
+
+ const expectDeployKeyIsRendered = (expectedDeployKey, expectedRowIndex) => {
+ const editButton = findEditButton(expectedRowIndex);
+ const timeAgoTooltip = findTimeAgoTooltip(expectedRowIndex);
+
+ expect(wrapper.findByText(expectedDeployKey.title).exists()).toBe(true);
+ expect(wrapper.findByText(expectedDeployKey.fingerprint, { selector: 'code' }).exists()).toBe(
+ true,
+ );
+ expect(timeAgoTooltip.exists()).toBe(true);
+ expect(timeAgoTooltip.props('time')).toBe(expectedDeployKey.created_at);
+ expect(editButton.exists()).toBe(true);
+ expect(editButton.attributes('href')).toBe(`/admin/deploy_keys/${expectedDeployKey.id}/edit`);
+ expect(findRemoveButton(expectedRowIndex).exists()).toBe(true);
+ };
+
+ const itRendersTheEmptyState = () => {
+ it('renders empty state', () => {
+ const emptyState = wrapper.findComponent(GlEmptyState);
+
+ expect(emptyState.exists()).toBe(true);
+ expect(emptyState.props()).toMatchObject({
+ svgPath: defaultProvide.emptyStateSvgPath,
+ title: DeployKeysTable.i18n.emptyStateTitle,
+ description: DeployKeysTable.i18n.emptyStateDescription,
+ primaryButtonText: DeployKeysTable.i18n.newDeployKeyButtonText,
+ primaryButtonLink: defaultProvide.createPath,
+ });
+ });
+ };
+
afterEach(() => {
wrapper.destroy();
});
@@ -30,18 +80,128 @@ describe('DeployKeysTable', () => {
expect(wrapper.findByText(DeployKeysTable.i18n.pageTitle).exists()).toBe(true);
});
- it('renders table', () => {
- createComponent();
-
- expect(wrapper.findComponent(GlTable).exists()).toBe(true);
- });
-
it('renders `New deploy key` button', () => {
createComponent();
- const newDeployKeyButton = wrapper.findComponent(GlButton);
+ const newDeployKeyButton = wrapper.findByTestId('new-deploy-key-button');
- expect(newDeployKeyButton.text()).toBe(DeployKeysTable.i18n.newDeployKeyButtonText);
+ expect(newDeployKeyButton.exists()).toBe(true);
expect(newDeployKeyButton.attributes('href')).toBe(defaultProvide.createPath);
});
+
+ describe('when `/deploy_keys` API request is pending', () => {
+ beforeEach(() => {
+ Api.deployKeys.mockImplementation(() => new Promise(() => {}));
+ });
+
+ it('shows loading icon', async () => {
+ createComponent();
+
+ await nextTick();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+ });
+
+ describe('when `/deploy_keys` API request is successful', () => {
+ describe('when there are deploy keys', () => {
+ beforeEach(() => {
+ Api.deployKeys.mockResolvedValue({
+ data: responseBody,
+ headers: { 'x-total': `${responseBody.length}` },
+ });
+
+ createComponent();
+ });
+
+ it('renders deploy keys in table', () => {
+ expectDeployKeyIsRendered(deployKey, 0);
+ expectDeployKeyIsRendered(deployKey2, 1);
+ });
+ });
+
+ describe('pagination', () => {
+ beforeEach(() => {
+ Api.deployKeys.mockResolvedValueOnce({
+ data: [deployKey],
+ headers: { 'x-total': '2' },
+ });
+
+ createComponent();
+ });
+
+ it('renders pagination', () => {
+ const pagination = findPagination();
+ expect(pagination.exists()).toBe(true);
+ expect(pagination.props()).toMatchObject({
+ value: 1,
+ perPage: DEFAULT_PER_PAGE,
+ totalItems: responseBody.length,
+ nextText: DeployKeysTable.i18n.pagination.next,
+ prevText: DeployKeysTable.i18n.pagination.prev,
+ align: 'center',
+ });
+ });
+
+ describe('when pagination is changed', () => {
+ it('calls API with `page` parameter', async () => {
+ const pagination = findPagination();
+ expectDeployKeyIsRendered(deployKey, 0);
+
+ Api.deployKeys.mockResolvedValue({
+ data: [deployKey2],
+ headers: { 'x-total': '2' },
+ });
+
+ pagination.vm.$emit('input', 2);
+
+ await nextTick();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(pagination.exists()).toBe(false);
+
+ await waitForPromises();
+
+ expect(Api.deployKeys).toHaveBeenCalledWith({
+ page: 2,
+ public: true,
+ });
+ expectDeployKeyIsRendered(deployKey2, 0);
+ });
+ });
+ });
+
+ describe('when there are no deploy keys', () => {
+ beforeEach(() => {
+ Api.deployKeys.mockResolvedValue({
+ data: [],
+ headers: { 'x-total': '0' },
+ });
+
+ createComponent();
+ });
+
+ itRendersTheEmptyState();
+ });
+ });
+
+ describe('when `deploy_keys` API request is unsuccessful', () => {
+ const error = new Error('Network Error');
+
+ beforeEach(() => {
+ Api.deployKeys.mockRejectedValue(error);
+
+ createComponent();
+ });
+
+ itRendersTheEmptyState();
+
+ it('displays flash', () => {
+ expect(createFlash).toHaveBeenCalledWith({
+ message: DeployKeysTable.i18n.apiErrorMessage,
+ captureError: true,
+ error,
+ });
+ });
+ });
});
diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js
index c3e5a2973d7..75faf6d66fa 100644
--- a/spec/frontend/api_spec.js
+++ b/spec/frontend/api_spec.js
@@ -1,5 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
-import Api from '~/api';
+import Api, { DEFAULT_PER_PAGE } from '~/api';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
@@ -1574,6 +1574,51 @@ describe('Api', () => {
});
});
+ describe('deployKeys', () => {
+ it('fetches deploy keys', async () => {
+ const deployKeys = [
+ {
+ id: 7,
+ title: 'My title 1',
+ created_at: '2021-10-29T16:59:55.229Z',
+ expires_at: null,
+ key:
+ 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDLvQzRX960N7dxPdge9o5a96+M4GEGQ7rxT2D3wAQDtQFjQV5ZcKb5wfeLtYLe3kRVI4lCO10PXeQppb1XBaYmVO31IaRkcgmMEPVyfp76Dp4CJZz6aMEbbcqfaHkDre0Fa8kzTXnBJVh2NeDbBfGMjFM5NRQLhKykodNsepO6dQ== dummy@gitlab.com',
+ fingerprint: '81:93:63:b9:1e:24:a2:aa:e0:87:d3:3f:42:81:f2:c2',
+ projects_with_write_access: [
+ {
+ id: 11,
+ description: null,
+ name: 'project1',
+ name_with_namespace: 'John Doe3 / project1',
+ path: 'project1',
+ path_with_namespace: 'namespace1/project1',
+ created_at: '2021-10-29T16:59:54.668Z',
+ },
+ {
+ id: 12,
+ description: null,
+ name: 'project2',
+ name_with_namespace: 'John Doe4 / project2',
+ path: 'project2',
+ path_with_namespace: 'namespace2/project2',
+ created_at: '2021-10-29T16:59:55.116Z',
+ },
+ ],
+ },
+ ];
+
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/deploy_keys`;
+ mock.onGet(expectedUrl).reply(httpStatus.OK, deployKeys);
+
+ const params = { page: 2, public: true };
+ const { data } = await Api.deployKeys(params);
+
+ expect(data).toEqual(deployKeys);
+ expect(mock.history.get[0].params).toEqual({ ...params, per_page: DEFAULT_PER_PAGE });
+ });
+ });
+
describe('Feature Flag User List', () => {
let expectedUrl;
let projectId;
diff --git a/spec/frontend/fixtures/api_deploy_keys.rb b/spec/frontend/fixtures/api_deploy_keys.rb
new file mode 100644
index 00000000000..7027b8c975b
--- /dev/null
+++ b/spec/frontend/fixtures/api_deploy_keys.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::DeployKeys, '(JavaScript fixtures)', type: :request do
+ include ApiHelpers
+ include JavaScriptFixturesHelpers
+
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:project2) { create(:project) }
+ let_it_be(:deploy_key) { create(:deploy_key, public: true) }
+ let_it_be(:deploy_key2) { create(:deploy_key, public: true) }
+ let_it_be(:deploy_keys_project) { create(:deploy_keys_project, :write_access, project: project, deploy_key: deploy_key) }
+ let_it_be(:deploy_keys_project2) { create(:deploy_keys_project, :write_access, project: project2, deploy_key: deploy_key) }
+ let_it_be(:deploy_keys_project3) { create(:deploy_keys_project, :write_access, project: project, deploy_key: deploy_key2) }
+ let_it_be(:deploy_keys_project4) { create(:deploy_keys_project, :write_access, project: project2, deploy_key: deploy_key2) }
+
+ it 'api/deploy_keys/index.json' do
+ get api("/deploy_keys", admin)
+
+ expect(response).to be_successful
+ end
+end