Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-11-17 09:09:23 +00:00
parent d49d44c810
commit 6fa3630aad
39 changed files with 471 additions and 89 deletions

View File

@ -15,9 +15,6 @@ tasks:
cd /workspace/gitlab-development-kit cd /workspace/gitlab-development-kit
[[ ! -L /workspace/gitlab-development-kit/gitlab ]] && ln -fs /workspace/gitlab /workspace/gitlab-development-kit/gitlab [[ ! -L /workspace/gitlab-development-kit/gitlab ]] && ln -fs /workspace/gitlab /workspace/gitlab-development-kit/gitlab
mv /workspace/gitlab-development-kit/secrets.yml /workspace/gitlab-development-kit/gitlab/config mv /workspace/gitlab-development-kit/secrets.yml /workspace/gitlab-development-kit/gitlab/config
# make webpack static, prevents that GitLab tries to connect to localhost webpack from browser outside the workspace
echo "webpack:" >> gdk.yml
echo " static: true" >> gdk.yml
# reconfigure GDK # reconfigure GDK
echo "$(date) Reconfiguring GDK" | tee -a /workspace/startup.log echo "$(date) Reconfiguring GDK" | tee -a /workspace/startup.log
gdk reconfigure gdk reconfigure
@ -43,6 +40,7 @@ tasks:
fi fi
# start GDK # start GDK
echo "$(date) Starting GDK" | tee -a /workspace/startup.log echo "$(date) Starting GDK" | tee -a /workspace/startup.log
export DEV_SERVER_PUBLIC_ADDR=$(gp url 3808)
export RAILS_HOSTS=$(gp url 3000 | sed -e 's+^http[s]*://++') export RAILS_HOSTS=$(gp url 3000 | sed -e 's+^http[s]*://++')
gdk start gdk start
# Run DB migrations # Run DB migrations

View File

@ -1,11 +1,13 @@
<script> <script>
import { GlButton, GlDropdown, GlDropdownItem, GlIcon, GlLink, GlModal } from '@gitlab/ui'; import { GlButton, GlDropdown, GlDropdownItem, GlIcon, GlLink, GlModal } from '@gitlab/ui';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import createFlash from '~/flash'; import createFlash, { FLASH_TYPES } from '~/flash';
import { IssuableType } from '~/issuable_show/constants'; import { IssuableType } from '~/issuable_show/constants';
import { IssuableStatus, IssueStateEvent } from '~/issue_show/constants'; import { IssuableStatus, IssueStateEvent } from '~/issue_show/constants';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { visitUrl } from '~/lib/utils/url_utility';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import promoteToEpicMutation from '../queries/promote_to_epic.mutation.graphql';
import updateIssueMutation from '../queries/update_issue.mutation.graphql'; import updateIssueMutation from '../queries/update_issue.mutation.graphql';
export default { export default {
@ -24,10 +26,21 @@ export default {
text: __('Yes, close issue'), text: __('Yes, close issue'),
attributes: [{ variant: 'warning' }], attributes: [{ variant: 'warning' }],
}, },
i18n: {
promoteErrorMessage: __(
'Something went wrong while promoting the issue to an epic. Please try again.',
),
promoteSuccessMessage: __(
'The issue was successfully promoted to an epic. Redirecting to epic...',
),
},
inject: { inject: {
canCreateIssue: { canCreateIssue: {
default: false, default: false,
}, },
canPromoteToEpic: {
default: false,
},
canReopenIssue: { canReopenIssue: {
default: false, default: false,
}, },
@ -135,6 +148,37 @@ export default {
this.isUpdatingState = false; this.isUpdatingState = false;
}); });
}, },
promoteToEpic() {
this.isUpdatingState = true;
this.$apollo
.mutate({
mutation: promoteToEpicMutation,
variables: {
input: {
iid: this.iid,
projectPath: this.projectPath,
},
},
})
.then(({ data }) => {
if (data.promoteToEpic.errors.length) {
createFlash({ message: data.promoteToEpic.errors.join('; ') });
return;
}
createFlash({
message: this.$options.i18n.promoteSuccessMessage,
type: FLASH_TYPES.SUCCESS,
});
visitUrl(data.promoteToEpic.epic.webPath);
})
.catch(() => createFlash({ message: this.$options.i18n.promoteErrorMessage }))
.finally(() => {
this.isUpdatingState = false;
});
},
}, },
}; };
</script> </script>
@ -152,6 +196,9 @@ export default {
<gl-dropdown-item v-if="canCreateIssue" :href="newIssuePath"> <gl-dropdown-item v-if="canCreateIssue" :href="newIssuePath">
{{ newIssueTypeText }} {{ newIssueTypeText }}
</gl-dropdown-item> </gl-dropdown-item>
<gl-dropdown-item v-if="canPromoteToEpic" :disabled="isUpdatingState" @click="promoteToEpic">
{{ __('Promote to epic') }}
</gl-dropdown-item>
<gl-dropdown-item v-if="!isIssueAuthor" :href="reportAbusePath"> <gl-dropdown-item v-if="!isIssueAuthor" :href="reportAbusePath">
{{ __('Report abuse') }} {{ __('Report abuse') }}
</gl-dropdown-item> </gl-dropdown-item>
@ -190,6 +237,14 @@ export default {
<gl-dropdown-item v-if="canCreateIssue" :href="newIssuePath"> <gl-dropdown-item v-if="canCreateIssue" :href="newIssuePath">
{{ newIssueTypeText }} {{ newIssueTypeText }}
</gl-dropdown-item> </gl-dropdown-item>
<gl-dropdown-item
v-if="canPromoteToEpic"
:disabled="isUpdatingState"
data-testid="promote-button"
@click="promoteToEpic"
>
{{ __('Promote to epic') }}
</gl-dropdown-item>
<gl-dropdown-item v-if="!isIssueAuthor" :href="reportAbusePath"> <gl-dropdown-item v-if="!isIssueAuthor" :href="reportAbusePath">
{{ __('Report abuse') }} {{ __('Report abuse') }}
</gl-dropdown-item> </gl-dropdown-item>

View File

@ -45,6 +45,7 @@ export function initIssueHeaderActions(store) {
store, store,
provide: { provide: {
canCreateIssue: parseBoolean(el.dataset.canCreateIssue), canCreateIssue: parseBoolean(el.dataset.canCreateIssue),
canPromoteToEpic: parseBoolean(el.dataset.canPromoteToEpic),
canReopenIssue: parseBoolean(el.dataset.canReopenIssue), canReopenIssue: parseBoolean(el.dataset.canReopenIssue),
canReportSpam: parseBoolean(el.dataset.canReportSpam), canReportSpam: parseBoolean(el.dataset.canReportSpam),
canUpdateIssue: parseBoolean(el.dataset.canUpdateIssue), canUpdateIssue: parseBoolean(el.dataset.canUpdateIssue),

View File

@ -0,0 +1,8 @@
mutation promoteToEpic($input: PromoteToEpicInput!) {
promoteToEpic(input: $input) {
epic {
webPath
}
errors
}
}

View File

@ -2,8 +2,6 @@ import UsersSelect from '~/users_select';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import initBoards from '~/boards'; import initBoards from '~/boards';
document.addEventListener('DOMContentLoaded', () => { new UsersSelect(); // eslint-disable-line no-new
new UsersSelect(); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new initBoards();
initBoards();
});

View File

@ -1,3 +1,3 @@
import initForm from '../../../../shared/milestones/form'; import initForm from '../../../../shared/milestones/form';
document.addEventListener('DOMContentLoaded', () => initForm(false)); initForm(false);

View File

@ -1,3 +1,3 @@
import initForm from '../../../../shared/milestones/form'; import initForm from '../../../../shared/milestones/form';
document.addEventListener('DOMContentLoaded', () => initForm(false)); initForm(false);

View File

@ -4,13 +4,11 @@ import Group from '~/group';
import GroupPathValidator from './group_path_validator'; import GroupPathValidator from './group_path_validator';
import initFilePickers from '~/file_pickers'; import initFilePickers from '~/file_pickers';
document.addEventListener('DOMContentLoaded', () => { const parentId = $('#group_parent_id');
const parentId = $('#group_parent_id'); if (!parentId.val()) {
if (!parentId.val()) { new GroupPathValidator(); // eslint-disable-line no-new
new GroupPathValidator(); // eslint-disable-line no-new }
} BindInOut.initAll();
BindInOut.initAll(); initFilePickers();
initFilePickers();
return new Group(); new Group(); // eslint-disable-line no-new
});

View File

@ -1,7 +1,5 @@
import initPackageList from '~/packages/list/packages_list_app_bundle'; import initPackageList from '~/packages/list/packages_list_app_bundle';
document.addEventListener('DOMContentLoaded', () => { if (document.getElementById('js-vue-packages-list')) {
if (document.getElementById('js-vue-packages-list')) { initPackageList();
initPackageList(); }
}
});

View File

@ -141,10 +141,9 @@ export default {
addMultipleToDiscussionWarning() { addMultipleToDiscussionWarning() {
return sprintf( return sprintf(
__( __(
'%{icon}You are about to add %{usersTag} people to the discussion. They will all receive a notification.', 'You are about to add %{usersTag} people to the discussion. They will all receive a notification.',
), ),
{ {
icon: '<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>',
usersTag: `<strong><span class="js-referenced-users-count">${this.referencedUsers.length}</span></strong>`, usersTag: `<strong><span class="js-referenced-users-count">${this.referencedUsers.length}</span></strong>`,
}, },
false, false,
@ -293,6 +292,7 @@ export default {
<template v-if="previewMarkdown && !markdownPreviewLoading"> <template v-if="previewMarkdown && !markdownPreviewLoading">
<div v-if="referencedCommands" class="referenced-commands" v-html="referencedCommands"></div> <div v-if="referencedCommands" class="referenced-commands" v-html="referencedCommands"></div>
<div v-if="shouldShowReferencedUsers" class="referenced-users"> <div v-if="shouldShowReferencedUsers" class="referenced-users">
<gl-icon name="warning-solid" />
<span v-html="addMultipleToDiscussionWarning"></span> <span v-html="addMultipleToDiscussionWarning"></span>
</div> </div>
</template> </template>

View File

@ -201,6 +201,15 @@ $line-removed-dark: $red-200;
// Misc component overrides that should live elsewhere // Misc component overrides that should live elsewhere
.gl-label { .gl-label {
filter: brightness(0.9) contrast(1.1); filter: brightness(0.9) contrast(1.1);
// This applies to the gl-label markups
// rendered and cached in the backend (labels_helper.rb)
&.gl-label-scoped {
.gl-label-text-scoped,
.gl-label-close {
color: $gray-900;
}
}
} }
// white-ish text for light labels // white-ish text for light labels
@ -210,6 +219,15 @@ $line-removed-dark: $red-200;
color: $gray-900; color: $gray-900;
} }
// This applies to "gl-labels" from "gitlab-ui"
.gl-label.gl-label-scoped.gl-label-text-dark,
.gl-label.gl-label-scoped.gl-label-text-light {
.gl-label-text-scoped,
.gl-label-close {
color: $gray-900;
}
}
// duplicated class as the original .atwho-view style is added later // duplicated class as the original .atwho-view style is added later
.atwho-view.atwho-view { .atwho-view.atwho-view {
background-color: $white; background-color: $white;

View File

@ -14,7 +14,7 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action do before_action do
push_frontend_feature_flag(:dag_pipeline_tab, project, default_enabled: true) push_frontend_feature_flag(:dag_pipeline_tab, project, default_enabled: true)
push_frontend_feature_flag(:pipelines_security_report_summary, project) push_frontend_feature_flag(:pipelines_security_report_summary, project)
push_frontend_feature_flag(:new_pipeline_form, project) push_frontend_feature_flag(:new_pipeline_form, project, default_enabled: true)
push_frontend_feature_flag(:graphql_pipeline_header, project, type: :development, default_enabled: false) push_frontend_feature_flag(:graphql_pipeline_header, project, type: :development, default_enabled: false)
push_frontend_feature_flag(:graphql_pipeline_details, project, type: :development, default_enabled: false) push_frontend_feature_flag(:graphql_pipeline_details, project, type: :development, default_enabled: false)
push_frontend_feature_flag(:new_pipeline_form_prefilled_vars, project, type: :development) push_frontend_feature_flag(:new_pipeline_form_prefilled_vars, project, type: :development)

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
module Types
class ContainerRepositoryCleanupStatusEnum < BaseEnum
graphql_name 'ContainerRepositoryCleanupStatus'
description 'Status of the tags cleanup of a container repository'
value 'UNSCHEDULED', value: 'cleanup_unscheduled', description: 'The tags cleanup is not scheduled. This is the default state.'
value 'SCHEDULED', value: 'cleanup_scheduled', description: 'The tags cleanup is scheduled and is going to be executed shortly.'
value 'UNFINISHED', value: 'cleanup_unfinished', description: 'The tags cleanup has been partially executed. There are still remaining tags to delete.'
value 'ONGOING', value: 'cleanup_ongoing', description: 'The tags cleanup is ongoing.'
end
end

View File

@ -15,6 +15,7 @@ module Types
field :created_at, Types::TimeType, null: false, description: 'Timestamp when the container repository was created.' field :created_at, Types::TimeType, null: false, description: 'Timestamp when the container repository was created.'
field :updated_at, Types::TimeType, null: false, description: 'Timestamp when the container repository was updated.' field :updated_at, Types::TimeType, null: false, description: 'Timestamp when the container repository was updated.'
field :expiration_policy_started_at, Types::TimeType, null: true, description: 'Timestamp when the cleanup done by the expiration policy was started on the container repository.' field :expiration_policy_started_at, Types::TimeType, null: true, description: 'Timestamp when the cleanup done by the expiration policy was started on the container repository.'
field :expiration_policy_cleanup_status, Types::ContainerRepositoryCleanupStatusEnum, null: true, description: 'The tags cleanup status for the container repository.'
field :status, Types::ContainerRepositoryStatusEnum, null: true, description: 'Status of the container repository.' field :status, Types::ContainerRepositoryStatusEnum, null: true, description: 'Status of the container repository.'
field :tags_count, GraphQL::INT_TYPE, null: false, description: 'Number of tags associated with this image.' field :tags_count, GraphQL::INT_TYPE, null: false, description: 'Number of tags associated with this image.'
field :can_delete, GraphQL::BOOLEAN_TYPE, null: false, description: 'Can the current user delete the container repository.' field :can_delete, GraphQL::BOOLEAN_TYPE, null: false, description: 'Can the current user delete the container repository.'

View File

@ -22,7 +22,7 @@
.dropdown.inline.js-ca-dropdown .dropdown.inline.js-ca-dropdown
%button.dropdown-menu-toggle{ "data-toggle" => "dropdown", :type => "button" } %button.dropdown-menu-toggle{ "data-toggle" => "dropdown", :type => "button" }
%span.dropdown-label {{ n__('Last %d day', 'Last %d days', 30) }} %span.dropdown-label {{ n__('Last %d day', 'Last %d days', 30) }}
%i.fa.fa-chevron-down = sprite_icon("chevron-down", css_class: "dropdown-menu-toggle-icon gl-top-3")
%ul.dropdown-menu.dropdown-menu-right %ul.dropdown-menu.dropdown-menu-right
%li %li
%a{ "href" => "#", "data-value" => "7" } %a{ "href" => "#", "data-value" => "7" }

View File

@ -6,7 +6,7 @@
= s_('Pipeline|Run Pipeline') = s_('Pipeline|Run Pipeline')
%hr %hr
- if Feature.enabled?(:new_pipeline_form, @project) - if Feature.enabled?(:new_pipeline_form, @project, default_enabled: true)
#js-new-pipeline{ data: { project_id: @project.id, #js-new-pipeline{ data: { project_id: @project.id,
pipelines_path: project_pipelines_path(@project), pipelines_path: project_pipelines_path(@project),
config_variables_path: config_variables_namespace_project_pipelines_path(@project.namespace, @project), config_variables_path: config_variables_namespace_project_pipelines_path(@project.namespace, @project),

View File

@ -0,0 +1,5 @@
---
title: Add cleanup status field to graphQL ContainerRepositoryType
merge_request: 47544
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Replace fa-chevron-down in project level VSA
merge_request: 47885
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Replace fa-exclamation-triangle in markdown field MERGE_REQUEST_ID
merge_request: 47786
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Default enable new_pipeline_form
merge_request: 46915
author:
type: added

View File

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/229632
milestone: '13.2' milestone: '13.2'
type: development type: development
group: group::continuous integration group: group::continuous integration
default_enabled: false default_enabled: true

View File

@ -18,6 +18,7 @@ const IS_DEV_SERVER = process.env.WEBPACK_DEV_SERVER === 'true';
const IS_EE = require('./helpers/is_ee_env'); const IS_EE = require('./helpers/is_ee_env');
const DEV_SERVER_HOST = process.env.DEV_SERVER_HOST || 'localhost'; const DEV_SERVER_HOST = process.env.DEV_SERVER_HOST || 'localhost';
const DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808; const DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808;
const DEV_SERVER_PUBLIC_ADDR = process.env.DEV_SERVER_PUBLIC_ADDR;
const DEV_SERVER_HTTPS = process.env.DEV_SERVER_HTTPS && process.env.DEV_SERVER_HTTPS !== 'false'; const DEV_SERVER_HTTPS = process.env.DEV_SERVER_HTTPS && process.env.DEV_SERVER_HTTPS !== 'false';
const DEV_SERVER_LIVERELOAD = IS_DEV_SERVER && process.env.DEV_SERVER_LIVERELOAD !== 'false'; const DEV_SERVER_LIVERELOAD = IS_DEV_SERVER && process.env.DEV_SERVER_LIVERELOAD !== 'false';
const WEBPACK_REPORT = process.env.WEBPACK_REPORT && process.env.WEBPACK_REPORT !== 'false'; const WEBPACK_REPORT = process.env.WEBPACK_REPORT && process.env.WEBPACK_REPORT !== 'false';
@ -554,6 +555,7 @@ module.exports = {
devServer: { devServer: {
host: DEV_SERVER_HOST, host: DEV_SERVER_HOST,
port: DEV_SERVER_PORT, port: DEV_SERVER_PORT,
public: DEV_SERVER_PUBLIC_ADDR,
https: DEV_SERVER_HTTPS, https: DEV_SERVER_HTTPS,
headers: { headers: {
'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Origin': '*',

View File

@ -3313,6 +3313,11 @@ type ContainerRepository {
""" """
createdAt: Time! createdAt: Time!
"""
The tags cleanup status for the container repository.
"""
expirationPolicyCleanupStatus: ContainerRepositoryCleanupStatus
""" """
Timestamp when the cleanup done by the expiration policy was started on the container repository. Timestamp when the cleanup done by the expiration policy was started on the container repository.
""" """
@ -3354,6 +3359,31 @@ type ContainerRepository {
updatedAt: Time! updatedAt: Time!
} }
"""
Status of the tags cleanup of a container repository
"""
enum ContainerRepositoryCleanupStatus {
"""
The tags cleanup is ongoing.
"""
ONGOING
"""
The tags cleanup is scheduled and is going to be executed shortly.
"""
SCHEDULED
"""
The tags cleanup has been partially executed. There are still remaining tags to delete.
"""
UNFINISHED
"""
The tags cleanup is not scheduled. This is the default state.
"""
UNSCHEDULED
}
""" """
The connection type for ContainerRepository. The connection type for ContainerRepository.
""" """
@ -3388,6 +3418,11 @@ type ContainerRepositoryDetails {
""" """
createdAt: Time! createdAt: Time!
"""
The tags cleanup status for the container repository.
"""
expirationPolicyCleanupStatus: ContainerRepositoryCleanupStatus
""" """
Timestamp when the cleanup done by the expiration policy was started on the container repository. Timestamp when the cleanup done by the expiration policy was started on the container repository.
""" """

View File

@ -8947,6 +8947,20 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "expirationPolicyCleanupStatus",
"description": "The tags cleanup status for the container repository.",
"args": [
],
"type": {
"kind": "ENUM",
"name": "ContainerRepositoryCleanupStatus",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "expirationPolicyStartedAt", "name": "expirationPolicyStartedAt",
"description": "Timestamp when the cleanup done by the expiration policy was started on the container repository.", "description": "Timestamp when the cleanup done by the expiration policy was started on the container repository.",
@ -9091,6 +9105,41 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "ENUM",
"name": "ContainerRepositoryCleanupStatus",
"description": "Status of the tags cleanup of a container repository",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "UNSCHEDULED",
"description": "The tags cleanup is not scheduled. This is the default state.",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "SCHEDULED",
"description": "The tags cleanup is scheduled and is going to be executed shortly.",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "UNFINISHED",
"description": "The tags cleanup has been partially executed. There are still remaining tags to delete.",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "ONGOING",
"description": "The tags cleanup is ongoing.",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "ContainerRepositoryConnection", "name": "ContainerRepositoryConnection",
@ -9199,6 +9248,20 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "expirationPolicyCleanupStatus",
"description": "The tags cleanup status for the container repository.",
"args": [
],
"type": {
"kind": "ENUM",
"name": "ContainerRepositoryCleanupStatus",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "expirationPolicyStartedAt", "name": "expirationPolicyStartedAt",
"description": "Timestamp when the cleanup done by the expiration policy was started on the container repository.", "description": "Timestamp when the cleanup done by the expiration policy was started on the container repository.",

View File

@ -535,6 +535,7 @@ A container repository.
| ----- | ---- | ----------- | | ----- | ---- | ----------- |
| `canDelete` | Boolean! | Can the current user delete the container repository. | | `canDelete` | Boolean! | Can the current user delete the container repository. |
| `createdAt` | Time! | Timestamp when the container repository was created. | | `createdAt` | Time! | Timestamp when the container repository was created. |
| `expirationPolicyCleanupStatus` | ContainerRepositoryCleanupStatus | The tags cleanup status for the container repository. |
| `expirationPolicyStartedAt` | Time | Timestamp when the cleanup done by the expiration policy was started on the container repository. | | `expirationPolicyStartedAt` | Time | Timestamp when the cleanup done by the expiration policy was started on the container repository. |
| `id` | ID! | ID of the container repository. | | `id` | ID! | ID of the container repository. |
| `location` | String! | URL of the container repository. | | `location` | String! | URL of the container repository. |
@ -552,6 +553,7 @@ Details of a container repository.
| ----- | ---- | ----------- | | ----- | ---- | ----------- |
| `canDelete` | Boolean! | Can the current user delete the container repository. | | `canDelete` | Boolean! | Can the current user delete the container repository. |
| `createdAt` | Time! | Timestamp when the container repository was created. | | `createdAt` | Time! | Timestamp when the container repository was created. |
| `expirationPolicyCleanupStatus` | ContainerRepositoryCleanupStatus | The tags cleanup status for the container repository. |
| `expirationPolicyStartedAt` | Time | Timestamp when the cleanup done by the expiration policy was started on the container repository. | | `expirationPolicyStartedAt` | Time | Timestamp when the cleanup done by the expiration policy was started on the container repository. |
| `id` | ID! | ID of the container repository. | | `id` | ID! | ID of the container repository. |
| `location` | String! | URL of the container repository. | | `location` | String! | URL of the container repository. |
@ -3812,6 +3814,17 @@ Mode of a commit action.
| `SEVEN_DAYS` | 7 days until tags are automatically removed | | `SEVEN_DAYS` | 7 days until tags are automatically removed |
| `THIRTY_DAYS` | 30 days until tags are automatically removed | | `THIRTY_DAYS` | 30 days until tags are automatically removed |
### ContainerRepositoryCleanupStatus
Status of the tags cleanup of a container repository.
| Value | Description |
| ----- | ----------- |
| `ONGOING` | The tags cleanup is ongoing. |
| `SCHEDULED` | The tags cleanup is scheduled and is going to be executed shortly. |
| `UNFINISHED` | The tags cleanup has been partially executed. There are still remaining tags to delete. |
| `UNSCHEDULED` | The tags cleanup is not scheduled. This is the default state. |
### ContainerRepositoryStatus ### ContainerRepositoryStatus
Status of a container repository. Status of a container repository.

View File

@ -397,7 +397,7 @@ field :foo, GraphQL::STRING_TYPE,
'if `my_feature_flag` feature flag is disabled' 'if `my_feature_flag` feature flag is disabled'
def foo def foo
object.foo unless Feature.enabled?(:my_feature_flag, object) object.foo if Feature.enabled?(:my_feature_flag, object)
end end
``` ```

View File

@ -63,6 +63,8 @@ job finishes but the DAST job fails, the security dashboard doesn't show SAST re
the analyzer outputs an the analyzer outputs an
[exit code](../../../development/integrations/secure.md#exit-code). [exit code](../../../development/integrations/secure.md#exit-code).
You can filter the vulnerabilities list by selecting from the **Severity** and **Scanner** dropdowns.
## Project Security Dashboard ## Project Security Dashboard
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/235558) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.6. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/235558) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.6.
@ -105,6 +107,11 @@ You can filter the vulnerabilities by one or more of the following:
| Severity | Critical, High, Medium, Low, Info, Unknown | | Severity | Critical, High, Medium, Low, Info, Unknown |
| Scanner | [Available Scanners](../index.md#security-scanning-tools) | | Scanner | [Available Scanners](../index.md#security-scanning-tools) |
You can filter the vulnerabilities list by selecting from the **Status**, **Severity**, and
**Scanner** dropdowns. In the **Scanner** dropdown, select individual scanners or scanner groups to
toggle those scanners. The **Scanner** dropdown includes both GitLab scanners, and in GitLab 13.6
and later, custom scanners.
You can also dismiss vulnerabilities in the table: You can also dismiss vulnerabilities in the table:
1. Select the checkbox for each vulnerability you want to dismiss. 1. Select the checkbox for each vulnerability you want to dismiss.
@ -260,6 +267,11 @@ You can filter which vulnerabilities the vulnerability report displays by:
| Scanner | [Available Scanners](../index.md#security-scanning-tools) | | Scanner | [Available Scanners](../index.md#security-scanning-tools) |
| Project | Projects configured in the Security Center settings | | Project | Projects configured in the Security Center settings |
You can filter the vulnerabilities list by selecting from the **Status**, **Severity**, and
**Scanner**, and **Project** dropdowns. In the **Scanner** dropdown, select individual scanners or
scanner groups to toggle those scanners. The **Scanner** dropdown includes both GitLab scanners, and
in GitLab 13.6 and later, custom scanners.
Clicking any vulnerability in the table takes you to its Clicking any vulnerability in the table takes you to its
[Vulnerability Details](../vulnerabilities) page to see more information on that vulnerability. [Vulnerability Details](../vulnerabilities) page to see more information on that vulnerability.
To create an issue associated with the vulnerability, click the **Create Issue** button. To create an issue associated with the vulnerability, click the **Create Issue** button.

View File

@ -95,12 +95,13 @@ While you can view and manage the full details of an issue on the [issue page](#
you can also work with multiple issues at a time using the [Issues List](#issues-list), you can also work with multiple issues at a time using the [Issues List](#issues-list),
[Issue Boards](#issue-boards), Issue references, and [Epics](#epics)**(PREMIUM)**. [Issue Boards](#issue-boards), Issue references, and [Epics](#epics)**(PREMIUM)**.
Key actions for Issues include: Key actions for issues include:
- [Creating issues](managing_issues.md#create-a-new-issue) - [Creating issues](managing_issues.md#create-a-new-issue)
- [Moving issues](managing_issues.md#moving-issues) - [Moving issues](managing_issues.md#moving-issues)
- [Closing issues](managing_issues.md#closing-issues) - [Closing issues](managing_issues.md#closing-issues)
- [Deleting issues](managing_issues.md#deleting-issues) - [Deleting issues](managing_issues.md#deleting-issues)
- [Promoting issues](managing_issues.md#promote-an-issue-to-an-epic) **(PREMIUM)**
### Issue page ### Issue page

View File

@ -7,9 +7,15 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Managing issues # Managing issues
[GitLab Issues](index.md) are the fundamental medium for collaborating on ideas and [GitLab Issues](index.md) are the fundamental medium for collaborating on ideas and
planning work in GitLab. [Creating](#create-a-new-issue), [moving](#moving-issues), planning work in GitLab.
[closing](#closing-issues), and [deleting](#deleting-issues) are key actions that
you can do with issues. Key actions for issues include:
- [Creating issues](#create-a-new-issue)
- [Moving issues](#moving-issues)
- [Closing issues](#closing-issues)
- [Deleting issues](#deleting-issues)
- [Promoting issues](#promote-an-issue-to-an-epic) **(PREMIUM)**
## Create a new issue ## Create a new issue
@ -280,6 +286,23 @@ editing it and clicking on the delete button.
![delete issue - button](img/delete_issue.png) ![delete issue - button](img/delete_issue.png)
## Promote an issue to an epic **(PREMIUM)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3777) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.6.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/37081) to [GitLab Premium](https://about.gitlab.com/pricing/) in 12.8.
> - Promoting issues to epics via the UI [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/233974) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.6.
You can promote an issue to an epic in the immediate parent group.
To promote an issue to an epic:
1. In an issue, select the vertical ellipsis (**{ellipsis_v}**) button.
1. Select **Promote to epic**.
Alternatively, you can use the `/promote` [quick action](../quick_actions.md#quick-actions-for-issues-merge-requests-and-epics).
Read more about promoting an issue to an epic on the [Manage epics page](../../group/epics/manage_epics.md#promote-an-issue-to-an-epic).
## Add an issue to an iteration **(STARTER)** ## Add an issue to an iteration **(STARTER)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216158) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.2. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216158) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.2.

View File

@ -528,9 +528,6 @@ msgstr ""
msgid "%{host} sign-in from new location" msgid "%{host} sign-in from new location"
msgstr "" msgstr ""
msgid "%{icon}You are about to add %{usersTag} people to the discussion. They will all receive a notification."
msgstr ""
msgid "%{integrations_link_start}Integrations%{link_end} enable you to make third-party applications part of your GitLab workflow. If the available integrations don't meet your needs, consider using a %{webhooks_link_start}webhook%{link_end}." msgid "%{integrations_link_start}Integrations%{link_end} enable you to make third-party applications part of your GitLab workflow. If the available integrations don't meet your needs, consider using a %{webhooks_link_start}webhook%{link_end}."
msgstr "" msgstr ""
@ -7707,6 +7704,9 @@ msgstr ""
msgid "Could not restore the group" msgid "Could not restore the group"
msgstr "" msgstr ""
msgid "Could not retrieve custom scanners for scanner filter. Please try again later."
msgstr ""
msgid "Could not revoke impersonation token %{token_name}." msgid "Could not revoke impersonation token %{token_name}."
msgstr "" msgstr ""
@ -21810,6 +21810,9 @@ msgstr ""
msgid "Promote issue to an epic" msgid "Promote issue to an epic"
msgstr "" msgstr ""
msgid "Promote to epic"
msgstr ""
msgid "Promote to group label" msgid "Promote to group label"
msgstr "" msgstr ""
@ -25341,6 +25344,9 @@ msgstr ""
msgid "Something went wrong while performing the action." msgid "Something went wrong while performing the action."
msgstr "" msgstr ""
msgid "Something went wrong while promoting the issue to an epic. Please try again."
msgstr ""
msgid "Something went wrong while reopening a requirement." msgid "Something went wrong while reopening a requirement."
msgstr "" msgstr ""
@ -26956,6 +26962,9 @@ msgstr ""
msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage." msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage."
msgstr "" msgstr ""
msgid "The issue was successfully promoted to an epic. Redirecting to epic..."
msgstr ""
msgid "The license for Deploy Board is required to use this feature." msgid "The license for Deploy Board is required to use this feature."
msgstr "" msgstr ""
@ -30764,6 +30773,9 @@ msgstr ""
msgid "You already have pending todo for this alert" msgid "You already have pending todo for this alert"
msgstr "" msgstr ""
msgid "You are about to add %{usersTag} people to the discussion. They will all receive a notification."
msgstr ""
msgid "You are about to delete %{domain} from your instance. This domain will no longer be available to any Knative application." msgid "You are about to delete %{domain} from your instance. This domain will no longer be available to any Knative application."
msgstr "" msgstr ""

View File

@ -0,0 +1,12 @@
{
"type": "array",
"items": {
"type": "object",
"required": ["node"],
"properties": {
"node": {
"$ref": "./container_repository.json"
}
}
}
}

View File

@ -1,6 +1,6 @@
{ {
"type": "object", "type": "object",
"required": ["id", "name", "path", "location", "createdAt", "updatedAt", "tagsCount", "canDelete"], "required": ["id", "name", "path", "location", "createdAt", "updatedAt", "tagsCount", "canDelete", "expirationPolicyCleanupStatus"],
"properties": { "properties": {
"id": { "id": {
"type": "string" "type": "string"
@ -31,6 +31,10 @@
}, },
"canDelete": { "canDelete": {
"type": "boolean" "type": "boolean"
},
"expirationPolicyCleanupStatus": {
"type": "string",
"enum": ["UNSCHEDULED", "SCHEDULED", "UNFINISHED", "ONGOING"]
} }
} }
} }

View File

@ -1,37 +1,8 @@
{ {
"type": "object", "type": "object",
"required": ["id", "name", "path", "location", "createdAt", "updatedAt", "tagsCount", "canDelete", "tags"], "required": ["tags"],
"allOf": [{ "$ref": "./container_repository.json" }],
"properties": { "properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
},
"location": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"updatedAt": {
"type": "string"
},
"expirationPolicyStartedAt": {
"type": ["string", "null"]
},
"status": {
"type": ["string", "null"]
},
"tagsCount": {
"type": "integer"
},
"canDelete": {
"type": "boolean"
},
"tags": { "tags": {
"type": "object", "type": "object",
"required": ["nodes"], "required": ["nodes"],

View File

@ -1,14 +1,21 @@
import { GlButton, GlDropdown, GlDropdownItem, GlLink, GlModal } from '@gitlab/ui'; import { GlButton, GlDropdown, GlDropdownItem, GlLink, GlModal } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils'; import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
import createFlash, { FLASH_TYPES } from '~/flash';
import { IssuableType } from '~/issuable_show/constants'; import { IssuableType } from '~/issuable_show/constants';
import HeaderActions from '~/issue_show/components/header_actions.vue'; import HeaderActions from '~/issue_show/components/header_actions.vue';
import { IssuableStatus, IssueStateEvent } from '~/issue_show/constants'; import { IssuableStatus, IssueStateEvent } from '~/issue_show/constants';
import promoteToEpicMutation from '~/issue_show/queries/promote_to_epic.mutation.graphql';
import * as urlUtility from '~/lib/utils/url_utility';
import createStore from '~/notes/stores'; import createStore from '~/notes/stores';
jest.mock('~/flash');
describe('HeaderActions component', () => { describe('HeaderActions component', () => {
let dispatchEventSpy; let dispatchEventSpy;
let mutateMock;
let wrapper; let wrapper;
let visitUrlSpy;
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
@ -16,6 +23,7 @@ describe('HeaderActions component', () => {
const defaultProps = { const defaultProps = {
canCreateIssue: true, canCreateIssue: true,
canPromoteToEpic: true,
canReopenIssue: true, canReopenIssue: true,
canReportSpam: true, canReportSpam: true,
canUpdateIssue: true, canUpdateIssue: true,
@ -29,7 +37,27 @@ describe('HeaderActions component', () => {
submitAsSpamPath: 'gitlab-org/gitlab-test/-/issues/32/submit_as_spam', submitAsSpamPath: 'gitlab-org/gitlab-test/-/issues/32/submit_as_spam',
}; };
const mutate = jest.fn().mockResolvedValue({ data: { updateIssue: { errors: [] } } }); const updateIssueMutationResponse = { data: { updateIssue: { errors: [] } } };
const promoteToEpicMutationResponse = {
data: {
promoteToEpic: {
errors: [],
epic: {
webPath: '/groups/gitlab-org/-/epics/1',
},
},
},
};
const promoteToEpicMutationErrorResponse = {
data: {
promoteToEpic: {
errors: ['The issue has already been promoted to an epic.'],
epic: {},
},
},
};
const findToggleIssueStateButton = () => wrapper.find(GlButton); const findToggleIssueStateButton = () => wrapper.find(GlButton);
@ -50,7 +78,10 @@ describe('HeaderActions component', () => {
props = {}, props = {},
issueState = IssuableStatus.Open, issueState = IssuableStatus.Open,
blockedByIssues = [], blockedByIssues = [],
mutateResponse = {},
} = {}) => { } = {}) => {
mutateMock = jest.fn().mockResolvedValue(mutateResponse);
store.getters.getNoteableData.state = issueState; store.getters.getNoteableData.state = issueState;
store.getters.getNoteableData.blocked_by_issues = blockedByIssues; store.getters.getNoteableData.blocked_by_issues = blockedByIssues;
@ -63,7 +94,7 @@ describe('HeaderActions component', () => {
}, },
mocks: { mocks: {
$apollo: { $apollo: {
mutate, mutate: mutateMock,
}, },
}, },
}); });
@ -73,6 +104,9 @@ describe('HeaderActions component', () => {
if (dispatchEventSpy) { if (dispatchEventSpy) {
dispatchEventSpy.mockRestore(); dispatchEventSpy.mockRestore();
} }
if (visitUrlSpy) {
visitUrlSpy.mockRestore();
}
wrapper.destroy(); wrapper.destroy();
}); });
@ -90,7 +124,11 @@ describe('HeaderActions component', () => {
beforeEach(() => { beforeEach(() => {
dispatchEventSpy = jest.spyOn(document, 'dispatchEvent'); dispatchEventSpy = jest.spyOn(document, 'dispatchEvent');
wrapper = mountComponent({ props: { issueType }, issueState }); wrapper = mountComponent({
props: { issueType },
issueState,
mutateResponse: updateIssueMutationResponse,
});
}); });
it(`has text "${buttonText}"`, () => { it(`has text "${buttonText}"`, () => {
@ -100,11 +138,11 @@ describe('HeaderActions component', () => {
it('calls apollo mutation', () => { it('calls apollo mutation', () => {
findToggleIssueStateButton().vm.$emit('click'); findToggleIssueStateButton().vm.$emit('click');
expect(mutate).toHaveBeenCalledWith( expect(mutateMock).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
variables: { variables: {
input: { input: {
iid: defaultProps.iid.toString(), iid: defaultProps.iid,
projectPath: defaultProps.projectPath, projectPath: defaultProps.projectPath,
stateEvent: newIssueState, stateEvent: newIssueState,
}, },
@ -129,15 +167,17 @@ describe('HeaderActions component', () => {
${'desktop dropdown'} | ${false} | ${findDesktopDropdownItems} ${'desktop dropdown'} | ${false} | ${findDesktopDropdownItems}
`('$description', ({ isCloseIssueItemVisible, findDropdownItems }) => { `('$description', ({ isCloseIssueItemVisible, findDropdownItems }) => {
describe.each` describe.each`
description | itemText | isItemVisible | canUpdateIssue | canCreateIssue | isIssueAuthor | canReportSpam description | itemText | isItemVisible | canUpdateIssue | canCreateIssue | isIssueAuthor | canReportSpam | canPromoteToEpic
${`when user can update ${issueType}`} | ${`Close ${issueType}`} | ${isCloseIssueItemVisible} | ${true} | ${true} | ${true} | ${true} ${`when user can update ${issueType}`} | ${`Close ${issueType}`} | ${isCloseIssueItemVisible} | ${true} | ${true} | ${true} | ${true} | ${true}
${`when user cannot update ${issueType}`} | ${`Close ${issueType}`} | ${false} | ${false} | ${true} | ${true} | ${true} ${`when user cannot update ${issueType}`} | ${`Close ${issueType}`} | ${false} | ${false} | ${true} | ${true} | ${true} | ${true}
${`when user can create ${issueType}`} | ${`New ${issueType}`} | ${true} | ${true} | ${true} | ${true} | ${true} ${`when user can create ${issueType}`} | ${`New ${issueType}`} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${`when user cannot create ${issueType}`} | ${`New ${issueType}`} | ${false} | ${true} | ${false} | ${true} | ${true} ${`when user cannot create ${issueType}`} | ${`New ${issueType}`} | ${false} | ${true} | ${false} | ${true} | ${true} | ${true}
${'when user can report abuse'} | ${'Report abuse'} | ${true} | ${true} | ${true} | ${false} | ${true} ${'when user can promote to epic'} | ${'Promote to epic'} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${'when user cannot report abuse'} | ${'Report abuse'} | ${false} | ${true} | ${true} | ${true} | ${true} ${'when user cannot promote to epic'} | ${'Promote to epic'} | ${false} | ${true} | ${true} | ${true} | ${true} | ${false}
${'when user can submit as spam'} | ${'Submit as spam'} | ${true} | ${true} | ${true} | ${true} | ${true} ${'when user can report abuse'} | ${'Report abuse'} | ${true} | ${true} | ${true} | ${false} | ${true} | ${true}
${'when user cannot submit as spam'} | ${'Submit as spam'} | ${false} | ${true} | ${true} | ${true} | ${false} ${'when user cannot report abuse'} | ${'Report abuse'} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true}
${'when user can submit as spam'} | ${'Submit as spam'} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${'when user cannot submit as spam'} | ${'Submit as spam'} | ${false} | ${true} | ${true} | ${true} | ${false} | ${true}
`( `(
'$description', '$description',
({ ({
@ -147,6 +187,7 @@ describe('HeaderActions component', () => {
canCreateIssue, canCreateIssue,
isIssueAuthor, isIssueAuthor,
canReportSpam, canReportSpam,
canPromoteToEpic,
}) => { }) => {
beforeEach(() => { beforeEach(() => {
wrapper = mountComponent({ wrapper = mountComponent({
@ -156,6 +197,7 @@ describe('HeaderActions component', () => {
isIssueAuthor, isIssueAuthor,
issueType, issueType,
canReportSpam, canReportSpam,
canPromoteToEpic,
}, },
}); });
}); });
@ -172,6 +214,65 @@ describe('HeaderActions component', () => {
}); });
}); });
describe('when "Promote to epic" button is clicked', () => {
describe('when response is successful', () => {
beforeEach(() => {
visitUrlSpy = jest.spyOn(urlUtility, 'visitUrl').mockReturnValue({});
wrapper = mountComponent({
mutateResponse: promoteToEpicMutationResponse,
});
wrapper.find('[data-testid="promote-button"]').vm.$emit('click');
});
it('invokes GraphQL mutation when clicked', () => {
expect(mutateMock).toHaveBeenCalledWith(
expect.objectContaining({
mutation: promoteToEpicMutation,
variables: {
input: {
iid: defaultProps.iid,
projectPath: defaultProps.projectPath,
},
},
}),
);
});
it('shows a success message and tells the user they are being redirected', () => {
expect(createFlash).toHaveBeenCalledWith({
message: 'The issue was successfully promoted to an epic. Redirecting to epic...',
type: FLASH_TYPES.SUCCESS,
});
});
it('redirects to newly created epic path', () => {
expect(visitUrlSpy).toHaveBeenCalledWith(
promoteToEpicMutationResponse.data.promoteToEpic.epic.webPath,
);
});
});
describe('when response contains errors', () => {
beforeEach(() => {
visitUrlSpy = jest.spyOn(urlUtility, 'visitUrl').mockReturnValue({});
wrapper = mountComponent({
mutateResponse: promoteToEpicMutationErrorResponse,
});
wrapper.find('[data-testid="promote-button"]').vm.$emit('click');
});
it('shows an error message', () => {
expect(createFlash).toHaveBeenCalledWith({
message: promoteToEpicMutationErrorResponse.data.promoteToEpic.errors.join('; '),
});
});
});
});
describe('modal', () => { describe('modal', () => {
const blockedByIssues = [ const blockedByIssues = [
{ iid: 13, web_url: 'gitlab-org/gitlab-test/-/issues/13' }, { iid: 13, web_url: 'gitlab-org/gitlab-test/-/issues/13' },
@ -197,7 +298,7 @@ describe('HeaderActions component', () => {
it('calls apollo mutation when primary button is clicked', () => { it('calls apollo mutation when primary button is clicked', () => {
findModal().vm.$emit('primary'); findModal().vm.$emit('primary');
expect(mutate).toHaveBeenCalledWith( expect(mutateMock).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
variables: { variables: {
input: { input: {

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['ContainerRepositoryCleanupStatus'] do
it 'exposes all statuses' do
expected_keys = ContainerRepository.expiration_policy_cleanup_statuses
.keys
.map { |k| k.gsub('cleanup_', '') }
.map(&:upcase)
expect(described_class.values.keys).to contain_exactly(*expected_keys)
end
end

View File

@ -3,7 +3,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe GitlabSchema.types['ContainerRepositoryDetails'] do RSpec.describe GitlabSchema.types['ContainerRepositoryDetails'] do
fields = %i[id name path location created_at updated_at expiration_policy_started_at status tags_count can_delete tags] fields = %i[id name path location created_at updated_at expiration_policy_started_at status tags_count can_delete expiration_policy_cleanup_status tags]
it { expect(described_class.graphql_name).to eq('ContainerRepositoryDetails') } it { expect(described_class.graphql_name).to eq('ContainerRepositoryDetails') }

View File

@ -3,7 +3,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe GitlabSchema.types['ContainerRepository'] do RSpec.describe GitlabSchema.types['ContainerRepository'] do
fields = %i[id name path location created_at updated_at expiration_policy_started_at status tags_count can_delete] fields = %i[id name path location created_at updated_at expiration_policy_started_at status tags_count can_delete expiration_policy_cleanup_status]
it { expect(described_class.graphql_name).to eq('ContainerRepository') } it { expect(described_class.graphql_name).to eq('ContainerRepository') }
@ -20,4 +20,12 @@ RSpec.describe GitlabSchema.types['ContainerRepository'] do
is_expected.to have_graphql_type(Types::ContainerRepositoryStatusEnum) is_expected.to have_graphql_type(Types::ContainerRepositoryStatusEnum)
end end
end end
describe 'expiration_policy_cleanup_status field' do
subject { described_class.fields['expirationPolicyCleanupStatus'] }
it 'returns cleanup status enum' do
is_expected.to have_graphql_type(Types::ContainerRepositoryCleanupStatusEnum)
end
end
end end

View File

@ -34,7 +34,7 @@ RSpec.describe 'container repository details' do
subject subject
end end
it 'matches the expected schema' do it 'matches the JSON schema' do
expect(container_repository_details_response).to match_schema('graphql/container_repository_details') expect(container_repository_details_response).to match_schema('graphql/container_repository_details')
end end
end end

View File

@ -47,6 +47,10 @@ RSpec.describe 'getting container repositories in a project' do
before do before do
subject subject
end end
it 'matches the JSON schema' do
expect(container_repositories_response).to match_schema('graphql/container_repositories')
end
end end
context 'with different permissions' do context 'with different permissions' do