Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-11-23 06:12:53 +00:00
parent 1f60908cc0
commit 22622fab4a
10 changed files with 615 additions and 143 deletions

View file

@ -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.
<!-- Base labels. -->

View file

@ -1,13 +1,28 @@
<script>
import { GlTable, GlButton } from '@gitlab/ui';
import { GlTable, GlButton, GlPagination, GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
import { __ } from '~/locale';
import Api, { DEFAULT_PER_PAGE } from '~/api';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
import createFlash from '~/flash';
export default {
name: 'DeployKeysTable',
i18n: {
pageTitle: __('Public deploy keys'),
newDeployKeyButtonText: __('New deploy key'),
emptyStateTitle: __('No public deploy keys'),
emptyStateDescription: __(
'Deploy keys grant read/write access to all repositories in your instance',
),
remove: __('Remove deploy key'),
edit: __('Edit deploy key'),
pagination: {
next: __('Next'),
prev: __('Prev'),
},
apiErrorMessage: __('An error occurred fetching the public deploy keys. Please try again.'),
},
fields: [
{
@ -29,13 +44,83 @@ export default {
{
key: 'actions',
label: __('Actions'),
tdClass: 'gl-lg-w-1px gl-white-space-nowrap',
thClass: 'gl-lg-w-1px gl-white-space-nowrap',
},
],
DEFAULT_PER_PAGE,
components: {
GlTable,
GlButton,
GlPagination,
TimeAgoTooltip,
GlLoadingIcon,
GlEmptyState,
},
inject: ['editPath', 'deletePath', 'createPath', 'emptyStateSvgPath'],
data() {
return {
page: 1,
totalItems: 0,
loading: false,
items: [],
};
},
computed: {
shouldShowTable() {
return this.totalItems !== 0 || this.loading;
},
},
watch: {
page(newPage) {
this.fetchDeployKeys(newPage);
},
},
mounted() {
this.fetchDeployKeys();
},
methods: {
editHref(id) {
return this.editPath.replace(':id', id);
},
projectHref(project) {
return `/${cleanLeadingSeparator(project.path_with_namespace)}`;
},
async fetchDeployKeys(page) {
this.loading = true;
try {
const { headers, data: items } = await Api.deployKeys({
page,
public: true,
});
if (this.totalItems === 0) {
this.totalItems = parseInt(headers?.['x-total'], 10) || 0;
}
this.items = items.map(
({ id, title, fingerprint, projects_with_write_access, created_at }) => ({
id,
title,
fingerprint,
projects: projects_with_write_access,
created: created_at,
}),
);
} catch (error) {
createFlash({
message: this.$options.i18n.apiErrorMessage,
captureError: true,
error,
});
this.totalItems = 0;
this.items = [];
}
this.loading = false;
},
},
};
</script>
@ -45,10 +130,71 @@ export default {
<h4 class="gl-m-0">
{{ $options.i18n.pageTitle }}
</h4>
<gl-button variant="confirm" :href="createPath">{{
<gl-button variant="confirm" :href="createPath" data-testid="new-deploy-key-button">{{
$options.i18n.newDeployKeyButtonText
}}</gl-button>
</div>
<gl-table :fields="$options.fields" data-testid="deploy-keys-list" />
<template v-if="shouldShowTable">
<gl-table
:busy="loading"
:items="items"
:fields="$options.fields"
stacked="lg"
data-testid="deploy-keys-list"
>
<template #table-busy>
<gl-loading-icon size="lg" class="gl-my-5" />
</template>
<template #cell(projects)="{ item: { projects } }">
<a
v-for="project in projects"
:key="project.id"
:href="projectHref(project)"
class="gl-display-block"
>{{ project.name_with_namespace }}</a
>
</template>
<template #cell(fingerprint)="{ item: { fingerprint } }">
<code>{{ fingerprint }}</code>
</template>
<template #cell(created)="{ item: { created } }">
<time-ago-tooltip :time="created" />
</template>
<template #head(actions)="{ label }">
<span class="gl-sr-only">{{ label }}</span>
</template>
<template #cell(actions)="{ item: { id } }">
<gl-button
icon="pencil"
:aria-label="$options.i18n.edit"
:href="editHref(id)"
class="gl-mr-2"
/>
<gl-button variant="danger" icon="remove" :aria-label="$options.i18n.remove" />
</template>
</gl-table>
<gl-pagination
v-if="!loading"
v-model="page"
:per-page="$options.DEFAULT_PER_PAGE"
:total-items="totalItems"
:next-text="$options.i18n.pagination.next"
:prev-text="$options.i18n.pagination.prev"
align="center"
/>
</template>
<gl-empty-state
v-else
:svg-path="emptyStateSvgPath"
:title="$options.i18n.emptyStateTitle"
:description="$options.i18n.emptyStateDescription"
:primary-button-text="$options.i18n.newDeployKeyButtonText"
:primary-button-link="createPath"
/>
</div>
</template>

View file

@ -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);

View file

@ -388,7 +388,7 @@ export default {
<div
v-if="showMultiLineComment"
data-testid="multiline-comment"
class="gl-mb-3 gl-text-gray-500 gl-border-gray-200 gl-border-b-solid gl-border-b-1 gl-pb-3"
class="gl-mb-5 gl-text-gray-500 gl-border-gray-100 gl-border-b-solid gl-border-b-1 gl-pb-4"
>
<gl-sprintf :message="__('Comment on lines %{startLine} to %{endLine}')">
<template #startLine>

View file

@ -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

View file

@ -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`

View file

@ -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 ""

View file

@ -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,
});
});
});
});

View file

@ -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;

View file

@ -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