Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-05-27 12:08:19 +00:00
parent 7892ed2e23
commit 90c9981395
58 changed files with 819 additions and 424 deletions

View file

@ -66,6 +66,7 @@ export default {
ref="contentViewer"
:content="content"
:type="activeViewer.fileType"
data-qa-selector="file_content"
/>
</template>
</div>

View file

@ -1,13 +1,10 @@
import { __ } from '~/locale';
class RecentSearchesServiceError {
class RecentSearchesServiceError extends Error {
constructor(message) {
super(message || __('Recent Searches Service is unavailable'));
this.name = 'RecentSearchesServiceError';
this.message = message || __('Recent Searches Service is unavailable');
}
}
// Can't use `extends` for builtin prototypes and get true inheritance yet
RecentSearchesServiceError.prototype = Error.prototype;
export default RecentSearchesServiceError;

View file

@ -6,6 +6,7 @@ import { addDelimiter } from './lib/utils/text_utility';
import flash from './flash';
import CreateMergeRequestDropdown from './create_merge_request_dropdown';
import IssuablesHelper from './helpers/issuables_helper';
import { joinPaths } from '~/lib/utils/url_utility';
import { __ } from './locale';
export default class Issue {
@ -14,6 +15,16 @@ export default class Issue {
if ($('.js-close-blocked-issue-warning').length) this.initIssueWarningBtnEventListener();
if ($('.js-alert-moved-from-service-desk-warning').length) {
const trimmedPathname = window.location.pathname.slice(1);
this.alertMovedFromServiceDeskDismissedKey = joinPaths(
trimmedPathname,
'alert-issue-moved-from-service-desk-dismissed',
);
this.initIssueMovedFromServiceDeskDismissHandler();
}
Issue.$btnNewBranch = $('#new-branch');
Issue.createMrDropdownWrap = document.querySelector('.create-mr-dropdown-wrap');
@ -167,6 +178,21 @@ export default class Issue {
});
}
initIssueMovedFromServiceDeskDismissHandler() {
const alertMovedFromServiceDeskWarning = $('.js-alert-moved-from-service-desk-warning');
if (!localStorage.getItem(this.alertMovedFromServiceDeskDismissedKey)) {
alertMovedFromServiceDeskWarning.show();
}
alertMovedFromServiceDeskWarning.on('click', '.js-close', e => {
e.preventDefault();
e.stopImmediatePropagation();
alertMovedFromServiceDeskWarning.remove();
localStorage.setItem(this.alertMovedFromServiceDeskDismissedKey, true);
});
}
static submitNoteForm(form) {
const noteText = form.find('textarea.js-note-text').val();
if (noteText && noteText.trim().length > 0) {

View file

@ -1,68 +0,0 @@
<script>
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import { mapState } from 'vuex';
import { approximateDuration, calculateRemainingMilliseconds } from '~/lib/utils/datetime_utility';
import {
EXPIRATION_POLICY_ALERT_TITLE,
EXPIRATION_POLICY_ALERT_PRIMARY_BUTTON,
EXPIRATION_POLICY_ALERT_FULL_MESSAGE,
EXPIRATION_POLICY_ALERT_SHORT_MESSAGE,
} from '../constants';
export default {
components: {
GlAlert,
GlSprintf,
GlLink,
},
computed: {
...mapState(['config', 'images', 'isLoading']),
isEmpty() {
return !this.images || this.images.length === 0;
},
showAlert() {
return this.config.expirationPolicy?.enabled;
},
timeTillRun() {
const difference = calculateRemainingMilliseconds(this.config.expirationPolicy?.next_run_at);
return approximateDuration(difference / 1000);
},
alertConfiguration() {
if (this.isEmpty || this.isLoading) {
return {
title: null,
primaryButton: null,
message: EXPIRATION_POLICY_ALERT_SHORT_MESSAGE,
};
}
return {
title: EXPIRATION_POLICY_ALERT_TITLE,
primaryButton: EXPIRATION_POLICY_ALERT_PRIMARY_BUTTON,
message: EXPIRATION_POLICY_ALERT_FULL_MESSAGE,
};
},
},
};
</script>
<template>
<gl-alert
v-if="showAlert"
:dismissible="false"
:primary-button-text="alertConfiguration.primaryButton"
:primary-button-link="config.settingsPath"
:title="alertConfiguration.title"
>
<gl-sprintf :message="alertConfiguration.message">
<template #days>
<strong>{{ timeTillRun }}</strong>
</template>
<template #link="{content}">
<gl-link :href="config.expirationPolicyHelpPagePath" target="_blank">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</gl-alert>
</template>

View file

@ -0,0 +1,138 @@
<script>
import { GlSprintf, GlLink, GlIcon } from '@gitlab/ui';
import { n__ } from '~/locale';
import { approximateDuration, calculateRemainingMilliseconds } from '~/lib/utils/datetime_utility';
import {
CONTAINER_REGISTRY_TITLE,
LIST_INTRO_TEXT,
EXPIRATION_POLICY_WILL_RUN_IN,
EXPIRATION_POLICY_DISABLED_TEXT,
EXPIRATION_POLICY_DISABLED_MESSAGE,
} from '../constants';
export default {
components: {
GlIcon,
GlSprintf,
GlLink,
},
props: {
expirationPolicy: {
type: Object,
default: () => ({}),
required: false,
},
imagesCount: {
type: Number,
default: 0,
required: false,
},
helpPagePath: {
type: String,
default: '',
required: false,
},
expirationPolicyHelpPagePath: {
type: String,
default: '',
required: false,
},
hideExpirationPolicyData: {
type: Boolean,
required: false,
default: false,
},
},
loader: {
repeat: 10,
width: 1000,
height: 40,
},
i18n: {
CONTAINER_REGISTRY_TITLE,
LIST_INTRO_TEXT,
EXPIRATION_POLICY_DISABLED_MESSAGE,
},
computed: {
imagesCountText() {
return n__(
'ContainerRegistry|%{count} Image repository',
'ContainerRegistry|%{count} Image repositories',
this.imagesCount,
);
},
timeTillRun() {
const difference = calculateRemainingMilliseconds(this.expirationPolicy?.next_run_at);
return approximateDuration(difference / 1000);
},
expirationPolicyEnabled() {
return this.expirationPolicy?.enabled;
},
expirationPolicyText() {
return this.expirationPolicyEnabled
? EXPIRATION_POLICY_WILL_RUN_IN
: EXPIRATION_POLICY_DISABLED_TEXT;
},
showExpirationPolicyTip() {
return (
!this.expirationPolicyEnabled && this.imagesCount > 0 && !this.hideExpirationPolicyData
);
},
},
};
</script>
<template>
<div>
<div
class="gl-display-flex gl-justify-content-space-between gl-align-items-center"
data-testid="header"
>
<h4 data-testid="title">{{ $options.i18n.CONTAINER_REGISTRY_TITLE }}</h4>
<div class="gl-display-none d-sm-block" data-testid="commands-slot">
<slot name="commands"></slot>
</div>
</div>
<div
v-if="imagesCount"
class="gl-display-flex gl-align-items-center gl-mt-1 gl-mb-3 gl-text-gray-700"
data-testid="subheader"
>
<span class="gl-mr-3" data-testid="images-count">
<gl-icon class="gl-mr-1" name="container-image" />
<gl-sprintf :message="imagesCountText">
<template #count>
{{ imagesCount }}
</template>
</gl-sprintf>
</span>
<span v-if="!hideExpirationPolicyData" data-testid="expiration-policy">
<gl-icon class="gl-mr-1" name="expire" />
<gl-sprintf :message="expirationPolicyText">
<template #time>
{{ timeTillRun }}
</template>
</gl-sprintf>
</span>
</div>
<div data-testid="info-area">
<p>
<span data-testid="default-intro">
<gl-sprintf :message="$options.i18n.LIST_INTRO_TEXT">
<template #docLink="{content}">
<gl-link :href="helpPagePath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</span>
<span v-if="showExpirationPolicyTip" data-testid="expiration-disabled-message">
<gl-sprintf :message="$options.i18n.EXPIRATION_POLICY_DISABLED_MESSAGE">
<template #docLink="{content}">
<gl-link :href="expirationPolicyHelpPagePath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</span>
</p>
</div>
</div>
</template>

View file

@ -5,10 +5,10 @@ import { s__ } from '~/locale';
export const CONTAINER_REGISTRY_TITLE = s__('ContainerRegistry|Container Registry');
export const CONNECTION_ERROR_TITLE = s__('ContainerRegistry|Docker connection error');
export const CONNECTION_ERROR_MESSAGE = s__(
`ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}`,
`ContainerRegistry|We are having trouble connecting to the Registry, which could be due to an issue with your project name or path. %{docLinkStart}More information%{docLinkEnd}`,
);
export const LIST_INTRO_TEXT = s__(
`ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}`,
`ContainerRegistry|With the GitLab Container Registry, every project can have its own space to store images. %{docLinkStart}More information%{docLinkEnd}`,
);
export const LIST_DELETE_BUTTON_DISABLED = s__(
@ -103,20 +103,21 @@ export const ADMIN_GARBAGE_COLLECTION_TIP = s__(
// Expiration policies
export const EXPIRATION_POLICY_ALERT_TITLE = s__(
'ContainerRegistry|Retention policy has been Enabled',
export const EXPIRATION_POLICY_WILL_RUN_IN = s__(
'ContainerRegistry|Expiration policy will run in %{time}',
);
export const EXPIRATION_POLICY_ALERT_PRIMARY_BUTTON = s__('ContainerRegistry|Edit Settings');
export const EXPIRATION_POLICY_ALERT_FULL_MESSAGE = s__(
'ContainerRegistry|The retention and expiration policy for this Container Registry has been enabled and will run in %{days}. For more information visit the %{linkStart}documentation%{linkEnd}',
export const EXPIRATION_POLICY_DISABLED_TEXT = s__(
'ContainerRegistry|Expiration policy is disabled',
);
export const EXPIRATION_POLICY_ALERT_SHORT_MESSAGE = s__(
'ContainerRegistry|The retention and expiration policy for this Container Registry has been enabled. For more information visit the %{linkStart}documentation%{linkEnd}',
export const EXPIRATION_POLICY_DISABLED_MESSAGE = s__(
'ContainerRegistry|Expiration policies help manage the storage space used by the Container Registry, but the expiration policies for this registry are disabled. Contact your administrator to enable. %{docLinkStart}More information%{docLinkEnd}',
);
// Quick Start
export const QUICK_START = s__('ContainerRegistry|Quick Start');
export const QUICK_START = s__('ContainerRegistry|CLI Commands');
export const LOGIN_COMMAND_LABEL = s__('ContainerRegistry|Login');
export const COPY_LOGIN_TITLE = s__('ContainerRegistry|Copy login command');
export const BUILD_COMMAND_LABEL = s__('ContainerRegistry|Build an image');

View file

@ -14,17 +14,15 @@ import Tracking from '~/tracking';
import ProjectEmptyState from '../components/project_empty_state.vue';
import GroupEmptyState from '../components/group_empty_state.vue';
import ProjectPolicyAlert from '../components/project_policy_alert.vue';
import QuickstartDropdown from '../components/quickstart_dropdown.vue';
import RegistryHeader from '../components/registry_header.vue';
import ImageList from '../components/image_list.vue';
import CliCommands from '../components/cli_commands.vue';
import {
DELETE_IMAGE_SUCCESS_MESSAGE,
DELETE_IMAGE_ERROR_MESSAGE,
CONTAINER_REGISTRY_TITLE,
CONNECTION_ERROR_TITLE,
CONNECTION_ERROR_MESSAGE,
LIST_INTRO_TEXT,
REMOVE_REPOSITORY_MODAL_TEXT,
REMOVE_REPOSITORY_LABEL,
SEARCH_PLACEHOLDER_TEXT,
@ -39,8 +37,6 @@ export default {
GlEmptyState,
ProjectEmptyState,
GroupEmptyState,
ProjectPolicyAlert,
QuickstartDropdown,
ImageList,
GlModal,
GlSprintf,
@ -48,6 +44,8 @@ export default {
GlAlert,
GlSkeletonLoader,
GlSearchBoxByClick,
RegistryHeader,
CliCommands,
},
directives: {
GlTooltip: GlTooltipDirective,
@ -59,10 +57,8 @@ export default {
height: 40,
},
i18n: {
CONTAINER_REGISTRY_TITLE,
CONNECTION_ERROR_TITLE,
CONNECTION_ERROR_MESSAGE,
LIST_INTRO_TEXT,
REMOVE_REPOSITORY_MODAL_TEXT,
REMOVE_REPOSITORY_LABEL,
SEARCH_PLACEHOLDER_TEXT,
@ -85,7 +81,7 @@ export default {
label: 'registry_repository_delete',
};
},
showQuickStartDropdown() {
showCommands() {
return Boolean(!this.isLoading && !this.config?.isGroupPage && this.images?.length);
},
showDeleteAlert() {
@ -149,8 +145,6 @@ export default {
</gl-sprintf>
</gl-alert>
<project-policy-alert v-if="!config.isGroupPage" class="mt-2" />
<gl-empty-state
v-if="config.characterError"
:title="$options.i18n.CONNECTION_ERROR_TITLE"
@ -170,21 +164,17 @@ export default {
</gl-empty-state>
<template v-else>
<div>
<div class="d-flex justify-content-between align-items-center">
<h4>{{ $options.i18n.CONTAINER_REGISTRY_TITLE }}</h4>
<quickstart-dropdown v-if="showQuickStartDropdown" class="d-none d-sm-block" />
</div>
<p>
<gl-sprintf :message="$options.i18n.LIST_INTRO_TEXT">
<template #docLink="{content}">
<gl-link :href="config.helpPagePath" target="_blank">
{{ content }}
</gl-link>
<registry-header
:images-count="pagination.total"
:expiration-policy="config.expirationPolicy"
:help-page-path="config.helpPagePath"
:expiration-policy-help-page-path="config.expirationPolicyHelpPagePath"
:hide-expiration-policy-data="config.isGroupPage"
>
<template #commands>
<cli-commands v-if="showCommands" />
</template>
</gl-sprintf>
</p>
</div>
</registry-header>
<div v-if="isLoading" class="mt-2">
<gl-skeleton-loader
@ -201,7 +191,7 @@ export default {
</div>
<template v-else>
<template v-if="!isEmpty">
<div class="gl-display-flex gl-p-1" data-testid="listHeader">
<div class="gl-display-flex gl-p-1 gl-mt-3" data-testid="listHeader">
<div class="gl-flex-fill-1">
<h5>{{ $options.i18n.IMAGE_REPOSITORY_LIST_LABEL }}</h5>
</div>

View file

@ -1,5 +1,9 @@
import { __ } from '~/locale';
import { generateToolbarItem } from './toolbar_service';
import { generateToolbarItem } from './editor_service';
export const CUSTOM_EVENTS = {
openAddImageModal: 'gl_openAddImageModal',
};
/* eslint-disable @gitlab/require-i18n-strings */
const TOOLBAR_ITEM_CONFIGS = [

View file

@ -12,7 +12,6 @@ const buildWrapper = propsData => {
return instance.$el;
};
// eslint-disable-next-line import/prefer-default-export
export const generateToolbarItem = config => {
const { icon, classes, event, command, tooltip, isDivider } = config;
@ -30,3 +29,8 @@ export const generateToolbarItem = config => {
},
};
};
export const addCustomEventListener = (editorInstance, event, handler) => {
editorInstance.eventManager.addEventType(event);
editorInstance.eventManager.listen(event, handler);
};

View file

@ -2,7 +2,15 @@
import 'codemirror/lib/codemirror.css';
import '@toast-ui/editor/dist/toastui-editor.css';
import { EDITOR_OPTIONS, EDITOR_TYPES, EDITOR_HEIGHT, EDITOR_PREVIEW_STYLE } from './constants';
import {
EDITOR_OPTIONS,
EDITOR_TYPES,
EDITOR_HEIGHT,
EDITOR_PREVIEW_STYLE,
CUSTOM_EVENTS,
} from './constants';
import { addCustomEventListener } from './editor_service';
export default {
components: {
@ -49,6 +57,16 @@ export default {
getMarkdown() {
return this.$refs.editor.invoke('getMarkdown');
},
onLoad(editorInstance) {
addCustomEventListener(
editorInstance,
CUSTOM_EVENTS.openAddImageModal,
this.onOpenAddImageModal,
);
},
onOpenAddImageModal() {
// TODO - add image modal (next MR)
},
},
};
</script>
@ -61,5 +79,6 @@ export default {
:initial-edit-type="initialEditType"
:height="height"
@change="onContentChanged"
@load="onLoad"
/>
</template>

View file

@ -60,7 +60,7 @@ module Mutations
snippet = service_response.payload[:snippet]
{
snippet: snippet.valid? ? snippet : nil,
snippet: service_response.success? ? snippet : nil,
errors: errors_on_object(snippet)
}
end

View file

@ -11,6 +11,7 @@
- can_create_issue = show_new_issue_link?(@project)
= render_if_exists "projects/issues/alert_blocked", issue: @issue, current_user: current_user
= render_if_exists "projects/issues/alert_moved_from_service_desk", issue: @issue
.detail-page-header
.detail-page-header-body

View file

@ -5,7 +5,6 @@
.row.registry-placeholder.prepend-bottom-10
.col-12
#js-container-registry{ data: { endpoint: project_container_registry_index_path(@project),
settings_path: project_settings_ci_cd_path(@project),
expiration_policy: @project.container_expiration_policy.to_json,
"help_page_path" => help_page_path('user/packages/container_registry/index'),
"two_factor_auth_help_link" => help_page_path('user/profile/account/two_factor_authentication'),

View file

@ -2,7 +2,7 @@
const BABEL_ENV = process.env.BABEL_ENV || process.env.NODE_ENV || null;
const presets = [
let presets = [
[
'@babel/preset-env',
{
@ -49,6 +49,17 @@ if (isJest) {
https://gitlab.com/gitlab-org/gitlab-foss/issues/58390
*/
plugins.push('babel-plugin-dynamic-import-node');
presets = [
[
'@babel/preset-env',
{
targets: {
node: 'current',
},
},
],
];
}
module.exports = { presets, plugins, sourceType: 'unambiguous' };

View file

@ -0,0 +1,5 @@
---
title: Improve Container Registry UI header
merge_request: 32424
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Fix bug in snippet create mutation with non ActiveRecord errors
merge_request: 33085
author:
type: fixed

View file

@ -0,0 +1,5 @@
---
title: Remove unused WAF indexes from CI variables
merge_request: 30021
author:
type: other

View file

@ -3,7 +3,7 @@
require 'yaml'
SEE_DOC = "See [the documentation](https://docs.gitlab.com/ee/development/changelog.html)."
SEE_DOC = "See the [changelog documentation](https://docs.gitlab.com/ee/development/changelog.html)."
CREATE_CHANGELOG_MESSAGE = <<~MSG
If you want to create a changelog entry for GitLab FOSS, run the following:
@ -20,14 +20,29 @@ bin/changelog --ee -m %<mr_iid>s "%<mr_title>s"
If this merge request [doesn't need a CHANGELOG entry](https://docs.gitlab.com/ee/development/changelog.html#what-warrants-a-changelog-entry), feel free to ignore this message.
MSG
SUGGEST_MR_COMMENT = <<~SUGGEST_COMMENT
```suggestion
merge_request: %<mr_iid>s
```
#{SEE_DOC}
SUGGEST_COMMENT
def check_changelog_yaml(path)
yaml = YAML.safe_load(File.read(path))
raw_file = File.read(path)
yaml = YAML.safe_load(raw_file)
fail "`title` should be set, in #{gitlab.html_link(path)}! #{SEE_DOC}" if yaml["title"].nil?
fail "`type` should be set, in #{gitlab.html_link(path)}! #{SEE_DOC}" if yaml["type"].nil?
if yaml["merge_request"].nil? && !helper.security_mr?
mr_line = raw_file.lines.find_index("merge_request:\n")
if mr_line
markdown(format(SUGGEST_MR_COMMENT, mr_iid: gitlab.mr_json["iid"]), file: path, line: mr_line.succ)
else
message "Consider setting `merge_request` to #{gitlab.mr_json["iid"]} in #{gitlab.html_link(path)}. #{SEE_DOC}"
end
elsif yaml["merge_request"] != gitlab.mr_json["iid"] && !helper.security_mr?
fail "Merge request ID was not set to #{gitlab.mr_json["iid"]}! #{SEE_DOC}"
end

View file

@ -0,0 +1,18 @@
# frozen_string_literal: true
class RemoveIndexOnPipelineIdFromCiPipelineVariables < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
INDEX_NAME = 'index_ci_pipeline_variables_on_pipeline_id'
disable_ddl_transaction!
def up
remove_concurrent_index_by_name :ci_pipeline_variables, INDEX_NAME
end
def down
add_concurrent_index :ci_pipeline_variables, :pipeline_id, name: INDEX_NAME, where: "key = 'AUTO_DEVOPS_MODSECURITY_SEC_RULE_ENGINE'"
end
end

View file

@ -0,0 +1,18 @@
# frozen_string_literal: true
class RemoveIndexOnPipelineIdFromCiVariables < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
INDEX_NAME = 'index_ci_variables_on_project_id'
disable_ddl_transaction!
def up
remove_concurrent_index_by_name :ci_variables, INDEX_NAME
end
def down
add_concurrent_index :ci_variables, :project_id, name: INDEX_NAME, where: "key = 'AUTO_DEVOPS_MODSECURITY_SEC_RULE_ENGINE'"
end
end

View file

@ -9338,8 +9338,6 @@ CREATE INDEX index_ci_pipeline_schedules_on_owner_id ON public.ci_pipeline_sched
CREATE INDEX index_ci_pipeline_schedules_on_project_id ON public.ci_pipeline_schedules USING btree (project_id);
CREATE INDEX index_ci_pipeline_variables_on_pipeline_id ON public.ci_pipeline_variables USING btree (pipeline_id) WHERE ((key)::text = 'AUTO_DEVOPS_MODSECURITY_SEC_RULE_ENGINE'::text);
CREATE UNIQUE INDEX index_ci_pipeline_variables_on_pipeline_id_and_key ON public.ci_pipeline_variables USING btree (pipeline_id, key);
CREATE INDEX index_ci_pipelines_config_on_pipeline_id ON public.ci_pipelines_config USING btree (pipeline_id);
@ -9436,8 +9434,6 @@ CREATE INDEX index_ci_triggers_on_owner_id ON public.ci_triggers USING btree (ow
CREATE INDEX index_ci_triggers_on_project_id ON public.ci_triggers USING btree (project_id);
CREATE INDEX index_ci_variables_on_project_id ON public.ci_variables USING btree (project_id) WHERE ((key)::text = 'AUTO_DEVOPS_MODSECURITY_SEC_RULE_ENGINE'::text);
CREATE UNIQUE INDEX index_ci_variables_on_project_id_and_key_and_environment_scope ON public.ci_variables USING btree (project_id, key, environment_scope);
CREATE UNIQUE INDEX index_cluster_groups_on_cluster_id_and_group_id ON public.cluster_groups USING btree (cluster_id, group_id);
@ -13885,6 +13881,8 @@ COPY "schema_migrations" (version) FROM STDIN;
20200420172752
20200420172927
20200420201933
20200421054930
20200421054948
20200421092907
20200421111005
20200421233150

View file

@ -104,7 +104,7 @@ generate a short-lived JWT that is pull-only-capable to access the
```ruby
gitlab_rails['geo_registry_replication_enabled'] = true
gitlab_rails['geo_registry_replication_primary_api_url'] = 'http://primary.example.com:4567/' # Primary registry address, it will be used by the secondary node to directly communicate to primary registry
gitlab_rails['geo_registry_replication_primary_api_url'] = 'https://primary.example.com:5050/' # Primary registry address, it will be used by the secondary node to directly communicate to primary registry
```
1. Reconfigure the **secondary** node for the change to take effect:

View file

@ -235,7 +235,7 @@ Kubernetes won't be shown.
Reports that go over the 20 MB limit won't be loaded. Affected reports:
- [Merge Request security reports](../user/project/merge_requests/index.md#security-reports-ultimate)
- [Merge Request security reports](../user/project/merge_requests/testing_and_reports_in_merge_requests.md#security-reports-ultimate)
- [CI/CD parameter `artifacts:expose_as`](../ci/yaml/README.md#artifactsexpose_as)
- [JUnit test reports](../ci/junit_test_reports.md)

View file

@ -28,7 +28,7 @@ best place to integrate your own product and its results into GitLab.
implications for app security, corporate policy, or compliance. When complete,
the job reports back on its status and creates a
[job artifact](../../user/project/pipelines/job_artifacts.md) as a result.
- The [Merge Request Security Widget](../../user/project/merge_requests/index.md#security-reports-ultimate)
- The [Merge Request Security Widget](../../user/project/merge_requests/testing_and_reports_in_merge_requests.md#security-reports-ultimate)
displays the results of the pipeline's security checks and the developer can
review them. The developer can review both a summary and a detailed version
of the results.
@ -79,7 +79,7 @@ and complete an intgration with the Secure stage.
- If you need a new kind of scan or report, [create an issue](https://gitlab.com/gitlab-org/gitlab/-/issues/new#)
and add the label `devops::secure`.
- Once the job is completed, the data can be seen:
- In the [Merge Request Security Report](../../user/project/merge_requests/index.md#security-reports-ultimate) ([MR Security Report data flow](https://gitlab.com/snippets/1910005#merge-request-view)).
- In the [Merge Request Security Report](../../user/project/merge_requests/testing_and_reports_in_merge_requests.md#security-reports-ultimate) ([MR Security Report data flow](https://gitlab.com/snippets/1910005#merge-request-view)).
- While [browsing a Job Artifact](../../user/project/pipelines/job_artifacts.md).
- In the [Security Dashboard](../../user/application_security/security_dashboard/index.md) ([Dashboard data flow](https://gitlab.com/snippets/1910005#project-and-group-dashboards)).
1. Optional: Provide a way to interact with results as Vulnerabilities:

View file

@ -171,11 +171,10 @@ Please see the table below for some examples:
| Latest stable version | Your version | Recommended upgrade path | Note |
| --------------------- | ------------ | ------------------------ | ---- |
| 9.4.5 | 8.13.4 | `8.13.4` -> `8.17.7` -> `9.4.5` | `8.17.7` is the last version in version `8` |
| 10.1.4 | 8.13.4 | `8.13.4 -> 8.17.7 -> 9.5.10 -> 10.1.4` | `8.17.7` is the last version in version `8`, `9.5.10` is the last version in version `9` |
| 11.3.4 | 8.13.4 | `8.13.4` -> `8.17.7` -> `9.5.10` -> `10.8.7` -> `11.3.4` | `8.17.7` is the last version in version `8`, `9.5.10` is the last version in version `9`, `10.8.7` is the last version in version `10` |
| 12.5.10 | 11.3.4 | `11.3.4` -> `11.11.8` -> `12.0.12` -> `12.5.10` | `11.11.8` is the last version in version `11`. `12.0.x` [is a required step](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23211#note_272842444). |
| 12.8.5 | 9.2.6 | `9.2.6` -> `9.5.10` -> `10.8.7` -> `11.11.8` -> `12.0.12` -> `12.8.5` | Four intermediate versions are required: the final 9.5, 10.8, 11.11 releases, plus 12.0. |
| 13.2.0 | 11.5.0 | `11.5.0` -> `11.11.8` -> `12.0.12` -> `12.10.6` -> `13.0.0` -> `13.2.0` | Five intermediate versions are required: the final 11.11, 12.0, 12.10 releases, plus 13.0. |
NOTE: **Note:**
Instructions for installing a specific version of GitLab or downloading the package locally for installation can be found at [GitLab Repositories](https://packages.gitlab.com/gitlab).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View file

@ -26,7 +26,7 @@ have its own space to store its Docker images.
You can read more about Docker Registry at <https://docs.docker.com/registry/introduction/>.
![Container Registry repositories](img/container_registry_repositories_v13_0.png)
![Container Registry repositories](img/container_registry_repositories_v13_1.png)
## Enable the Container Registry for your project
@ -62,7 +62,7 @@ for both projects and groups.
Navigate to your project's **{package}** **Packages & Registries > Container Registry**.
![Container Registry project repositories](img/container_registry_repositories_with_quickstart_v13_0.png)
![Container Registry project repositories](img/container_registry_repositories_with_quickstart_v13_1.png)
This view will:
@ -77,7 +77,7 @@ This view will:
Navigate to your groups's **{package}** **Packages & Registries > Container Registry**.
![Container Registry group repositories](img/container_registry_group_repositories_v13_0.png)
![Container Registry group repositories](img/container_registry_group_repositories_v13_1.png)
This view will:

View file

@ -86,35 +86,7 @@ See the features at your disposal to [review and manage merge requests](reviewin
## Testing and reports in merge requests
GitLab has the ability to test the changes included in a merge request, and can display
or link to useful information directly in the merge request page:
| Feature | Description |
|--------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [Accessibility Testing](accessibility_testing.md) | Automatically report A11y violations for changed pages in merge requests |
| [Browser Performance Testing](browser_performance_testing.md) **(PREMIUM)** | Quickly determine the performance impact of pending code changes. |
| [Code Quality](code_quality.md) **(STARTER)** | Analyze your source code quality using the [Code Climate](https://codeclimate.com/) analyzer and show the Code Climate report right in the merge request widget area. |
| [Display arbitrary job artifacts](../../../ci/yaml/README.md#artifactsexpose_as) | Configure CI pipelines with the `artifacts:expose_as` parameter to directly link to selected [artifacts](../../../ci/pipelines/job_artifacts.md) in merge requests. |
| [GitLab CI/CD](../../../ci/README.md) | Build, test, and deploy your code in a per-branch basis with built-in CI/CD. |
| [JUnit test reports](../../../ci/junit_test_reports.md) | Configure your CI jobs to use JUnit test reports, and let GitLab display a report on the merge request so that its easier and faster to identify the failure without having to check the entire job log. |
| [License Compliance](../../compliance/license_compliance/index.md) **(ULTIMATE)** | Manage the licenses of your dependencies. |
| [Metrics Reports](../../../ci/metrics_reports.md) **(PREMIUM)** | Display the Metrics Report on the merge request so that it's fast and easy to identify changes to important metrics. |
| [Multi-Project pipelines](../../../ci/multi_project_pipelines.md) **(PREMIUM)** | When you set up GitLab CI/CD across multiple projects, you can visualize the entire pipeline, including all cross-project interdependencies. |
| [Pipelines for merge requests](../../../ci/merge_request_pipelines/index.md) | Customize a specific pipeline structure for merge requests in order to speed the cycle up by running only important jobs. |
| [Pipeline Graphs](../../../ci/pipelines/index.md#visualize-pipelines) | View the status of pipelines within the merge request, including the deployment process. |
| [Test Coverage visualization](test_coverage_visualization.md) | See test coverage results for merge requests, within the file diff. |
### Security Reports **(ULTIMATE)**
In addition to the reports listed above, GitLab can do many types of [Security reports](../../application_security/index.md),
generated by scanning and reporting any vulnerabilities found in your project:
| Feature | Description |
|-----------------------------------------------------------------------------------------|------------------------------------------------------------------|
| [Container Scanning](../../application_security/container_scanning/index.md) | Analyze your Docker images for known vulnerabilities. |
| [Dynamic Application Security Testing (DAST)](../../application_security/dast/index.md) | Analyze your running web applications for known vulnerabilities. |
| [Dependency Scanning](../../application_security/dependency_scanning/index.md) | Analyze your dependencies for known vulnerabilities. |
| [Static Application Security Testing (SAST)](../../application_security/sast/index.md) | Analyze your source code for known vulnerabilities. |
Learn about the options for [testing and reports](testing_and_reports_in_merge_requests.md) on the changes in a merge request.
## Authorization for merge requests

View file

@ -0,0 +1,36 @@
---
type: index
description: "Test your code and display reports in merge requests"
---
# Tests and reports in merge requests
GitLab has the ability to test the changes included in a feature branch and display reports
or link to useful information directly from merge requests:
| Feature | Description |
|--------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [Accessibility Testing](accessibility_testing.md) | Automatically report A11y violations for changed pages in merge requests. |
| [Browser Performance Testing](browser_performance_testing.md) **(PREMIUM)** | Quickly determine the performance impact of pending code changes. |
| [Code Quality](code_quality.md) **(STARTER)** | Analyze your source code quality using the [Code Climate](https://codeclimate.com/) analyzer and show the Code Climate report right in the merge request widget area. |
| [Display arbitrary job artifacts](../../../ci/yaml/README.md#artifactsexpose_as) | Configure CI pipelines with the `artifacts:expose_as` parameter to directly link to selected [artifacts](../../../ci/pipelines/job_artifacts.md) in merge requests. |
| [GitLab CI/CD](../../../ci/README.md) | Build, test, and deploy your code in a per-branch basis with built-in CI/CD. |
| [JUnit test reports](../../../ci/junit_test_reports.md) | Configure your CI jobs to use JUnit test reports, and let GitLab display a report on the merge request so that its easier and faster to identify the failure without having to check the entire job log. |
| [License Compliance](../../compliance/license_compliance/index.md) **(ULTIMATE)** | Manage the licenses of your dependencies. |
| [Metrics Reports](../../../ci/metrics_reports.md) **(PREMIUM)** | Display the Metrics Report on the merge request so that it's fast and easy to identify changes to important metrics. |
| [Multi-Project pipelines](../../../ci/multi_project_pipelines.md) **(PREMIUM)** | When you set up GitLab CI/CD across multiple projects, you can visualize the entire pipeline, including all cross-project interdependencies. |
| [Pipelines for merge requests](../../../ci/merge_request_pipelines/index.md) | Customize a specific pipeline structure for merge requests in order to speed the cycle up by running only important jobs. |
| [Pipeline Graphs](../../../ci/pipelines/index.md#visualize-pipelines) | View the status of pipelines within the merge request, including the deployment process. |
| [Test Coverage visualization](test_coverage_visualization.md) | See test coverage results for merge requests, within the file diff. |
## Security Reports **(ULTIMATE)**
In addition to the reports listed above, GitLab can do many types of [Security reports](../../application_security/index.md),
generated by scanning and reporting any vulnerabilities found in your project:
| Feature | Description |
|-----------------------------------------------------------------------------------------|------------------------------------------------------------------|
| [Container Scanning](../../application_security/container_scanning/index.md) | Analyze your Docker images for known vulnerabilities. |
| [Dynamic Application Security Testing (DAST)](../../application_security/dast/index.md) | Analyze your running web applications for known vulnerabilities. |
| [Dependency Scanning](../../application_security/dependency_scanning/index.md) | Analyze your dependencies for known vulnerabilities. |
| [Static Application Security Testing (SAST)](../../application_security/sast/index.md) | Analyze your source code for known vulnerabilities. |

View file

@ -5839,6 +5839,11 @@ msgstr ""
msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
msgstr ""
msgid "ContainerRegistry|%{count} Image repository"
msgid_plural "ContainerRegistry|%{count} Image repositories"
msgstr[0] ""
msgstr[1] ""
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
@ -5851,6 +5856,9 @@ msgstr ""
msgid "ContainerRegistry|Build an image"
msgstr ""
msgid "ContainerRegistry|CLI Commands"
msgstr ""
msgid "ContainerRegistry|Compressed Size"
msgstr ""
@ -5875,15 +5883,21 @@ msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
msgid "ContainerRegistry|Edit Settings"
msgstr ""
msgid "ContainerRegistry|Expiration interval:"
msgstr ""
msgid "ContainerRegistry|Expiration policies help manage the storage space used by the Container Registry, but the expiration policies for this registry are disabled. Contact your administrator to enable. %{docLinkStart}More information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|Expiration policy is disabled"
msgstr ""
msgid "ContainerRegistry|Expiration policy successfully saved."
msgstr ""
msgid "ContainerRegistry|Expiration policy will run in %{time}"
msgstr ""
msgid "ContainerRegistry|Expiration policy:"
msgstr ""
@ -5923,9 +5937,6 @@ msgstr ""
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-master%{codeEnd} or %{codeStart}release-.*%{codeEnd} are supported"
msgstr ""
@ -5946,9 +5957,6 @@ msgid_plural "ContainerRegistry|Remove tags"
msgstr[0] ""
msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
@ -6000,12 +6008,6 @@ msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
msgstr ""
msgid "ContainerRegistry|The retention and expiration policy for this Container Registry has been enabled and will run in %{days}. For more information visit the %{linkStart}documentation%{linkEnd}"
msgstr ""
msgid "ContainerRegistry|The retention and expiration policy for this Container Registry has been enabled. For more information visit the %{linkStart}documentation%{linkEnd}"
msgstr ""
msgid "ContainerRegistry|The value of this input should be less than 255 characters"
msgstr ""
@ -6027,7 +6029,7 @@ msgstr ""
msgid "ContainerRegistry|To widen your search, change or remove the filters above."
msgstr ""
msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgid "ContainerRegistry|We are having trouble connecting to the Registry, which could be due to an issue with your project name or path. %{docLinkStart}More information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@ -6036,7 +6038,7 @@ msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. Push at least one Docker image in one of this group's projects in order to show up here. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
msgid "ContainerRegistry|With the GitLab Container Registry, every project can have its own space to store images. %{docLinkStart}More information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|You are about to remove %{item} tags. Are you sure?"
@ -22388,6 +22390,9 @@ msgstr ""
msgid "This project does not belong to a group and can therefore not make use of group Runners."
msgstr ""
msgid "This project does not have %{service_desk_link_start}Service Desk%{service_desk_link_end} enabled, so the user who created the issue will no longer receive email notifications about new activity."
msgstr ""
msgid "This project does not have a wiki homepage yet"
msgstr ""

View file

@ -89,6 +89,7 @@ module QA
autoload :ProjectSnippet, 'qa/resource/project_snippet'
autoload :UserGPG, 'qa/resource/user_gpg'
autoload :Visibility, 'qa/resource/visibility'
autoload :ProjectSnippet, 'qa/resource/project_snippet'
module KubernetesCluster
autoload :Base, 'qa/resource/kubernetes_cluster/base'

View file

@ -35,6 +35,10 @@ module QA
element :file_content
end
view 'app/assets/javascripts/blob/components/blob_content.vue' do
element :file_content
end
view 'app/assets/javascripts/snippets/components/snippet_header.vue' do
element :snippet_action_button
element :delete_snippet_button
@ -57,6 +61,10 @@ module QA
has_element? :snippet_description_field, text: snippet_description
end
def has_no_snippet_description?
has_no_element?(:snippet_description_field)
end
def has_visibility_type?(visibility_type)
within_element(:snippet_box) do
has_text?(visibility_type)

View file

@ -2,8 +2,8 @@
module QA
context 'Create', :smoke do
describe 'Snippet creation' do
it 'User creates a snippet' do
describe 'Personal snippet creation' do
it 'User creates a personal snippet' do
Flow::Login.sign_in
Page::Main::Menu.perform(&:go_to_snippets)

View file

@ -0,0 +1,30 @@
# frozen_string_literal: true
module QA
context 'Create' do # to be converted to a smoke test once proved to be stable
describe 'Project snippet creation' do
it 'User creates a project snippet' do
Flow::Login.sign_in
Resource::ProjectSnippet.fabricate_via_browser_ui! do |snippet|
snippet.title = 'Project snippet'
snippet.description = ' '
snippet.visibility = 'Internal'
snippet.file_name = 'markdown_file.md'
snippet.file_content = "### Snippet heading\n\n[Gitlab link](https://gitlab.com/)"
end
Page::Dashboard::Snippet::Show.perform do |snippet|
expect(snippet).to have_snippet_title('Project snippet')
expect(snippet).to have_no_snippet_description
expect(snippet).to have_visibility_type(/internal/i)
expect(snippet).to have_file_name('markdown_file.md')
expect(snippet).to have_file_content('Snippet heading')
expect(snippet).to have_file_content('Gitlab link')
expect(snippet).not_to have_file_content('###')
expect(snippet).not_to have_file_content('https://gitlab.com/')
end
end
end
end
end

View file

@ -95,6 +95,45 @@ describe 'issue move to another project' do
expect(page).to have_no_selector('#move_to_project_id')
end
end
context 'service desk issue moved to a project with service desk disabled', :js do
let(:project_title) { 'service desk disabled project' }
let(:warning_selector) { '.js-alert-moved-from-service-desk-warning' }
let(:namespace) { create(:namespace) }
let(:regular_project) { create(:project, title: project_title, service_desk_enabled: false) }
let(:service_desk_project) { build(:project, :private, namespace: namespace, service_desk_enabled: true) }
let(:service_desk_issue) { create(:issue, project: service_desk_project, author: ::User.support_bot) }
before do
allow(::Gitlab).to receive(:com?).and_return(true)
allow(::Gitlab::IncomingEmail).to receive(:enabled?).and_return(true)
allow(::Gitlab::IncomingEmail).to receive(:supports_wildcard?).and_return(true)
regular_project.add_reporter(user)
service_desk_project.add_reporter(user)
visit issue_path(service_desk_issue)
find('.js-move-issue').click
wait_for_requests
find('.js-move-issue-dropdown-item', text: project_title).click
find('.js-move-issue-confirmation-button').click
end
it 'shows an alert after being moved' do
expect(page).to have_content('This project does not have Service Desk enabled')
end
it 'does not show an alert after being dismissed' do
find("#{warning_selector} .js-close").click
expect(page).to have_no_selector(warning_selector)
page.refresh
expect(page).to have_no_selector(warning_selector)
end
end
end
def issue_path(issue)

View file

@ -30,10 +30,10 @@ describe 'Container Registry', :js do
expect(page).to have_content _('There are no container images stored for this project')
end
it 'list page has quickstart' do
it 'list page has cli commands' do
visit_container_registry
expect(page).to have_content _('Quick Start')
expect(page).to have_content _('CLI Commands')
end
end

View file

@ -4,9 +4,8 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { Blob as MockBlob } from './mock_data';
import { numberToHumanSize } from '~/lib/utils/number_utils';
const mockHumanReadableSize = 'a lot';
jest.mock('~/lib/utils/number_utils', () => ({
numberToHumanSize: jest.fn(() => mockHumanReadableSize),
numberToHumanSize: jest.fn(() => 'a lot'),
}));
describe('Blob Header Filepath', () => {
@ -57,7 +56,7 @@ describe('Blob Header Filepath', () => {
it('renders filesize in a human-friendly format', () => {
createComponent();
expect(numberToHumanSize).toHaveBeenCalled();
expect(wrapper.vm.blobSize).toBe(mockHumanReadableSize);
expect(wrapper.vm.blobSize).toBe('a lot');
});
it('renders a slot and prepends its contents to the existing one', () => {

View file

@ -4,6 +4,7 @@ import './element_scroll_to';
import './form_element';
import './get_client_rects';
import './inner_text';
import './mutation_observer';
import './window_scroll_to';
import './scroll_by';
import './size_properties';

View file

@ -0,0 +1,7 @@
/* eslint-disable class-methods-use-this */
class MutationObserverStub {
disconnect() {}
observe() {}
}
global.MutationObserver = MutationObserverStub;

View file

@ -69,26 +69,21 @@ describe('IDE commit form', () => {
});
});
it('collapses if lastCommitMsg is set to empty and current view is not commit view', () => {
it('collapses if lastCommitMsg is set to empty and current view is not commit view', async () => {
store.state.lastCommitMsg = 'abc';
store.state.currentActivityView = leftSidebarViews.edit.name;
await vm.$nextTick();
return vm
.$nextTick()
.then(() => {
// if commit message is set, form is uncollapsed
expect(vm.isCompact).toBe(false);
store.state.lastCommitMsg = '';
await vm.$nextTick();
return vm.$nextTick();
})
.then(() => {
// collapsed when set to empty
expect(vm.isCompact).toBe(true);
});
});
});
describe('full', () => {
beforeEach(() => {

View file

@ -4,6 +4,7 @@ import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
import { TEST_HOST } from 'helpers/test_constants';
import MetricEmbed from '~/monitoring/components/embeds/metric_embed.vue';
import { groups, initialState, metricsData, metricsWithData } from './mock_data';
import { setHTMLFixture } from 'helpers/fixtures';
const localVue = createLocalVue();
localVue.use(Vuex);
@ -25,6 +26,8 @@ describe('MetricEmbed', () => {
}
beforeEach(() => {
setHTMLFixture('<div class="layout-page"></div>');
actions = {
setInitialState: jest.fn(),
setShowErrorBanner: jest.fn(),

View file

@ -7,6 +7,7 @@ import linkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines
import graphJSON from './mock_data';
import linkedPipelineJSON from './linked_pipelines_mock_data';
import PipelinesMediator from '~/pipelines/pipeline_details_mediator';
import { setHTMLFixture } from 'helpers/fixtures';
describe('graph component', () => {
const store = new PipelineStore();
@ -15,6 +16,10 @@ describe('graph component', () => {
let wrapper;
beforeEach(() => {
setHTMLFixture('<div class="layout-page"></div>');
});
afterEach(() => {
wrapper.destroy();
wrapper = null;

View file

@ -19,7 +19,7 @@ exports[`Registry Project Empty state to match the default snapshot 1`] = `
</p>
<h5>
Quick Start
CLI Commands
</h5>
<p

View file

@ -3,7 +3,7 @@ import { mount, createLocalVue } from '@vue/test-utils';
import { GlDropdown, GlFormGroup, GlFormInputGroup } from '@gitlab/ui';
import Tracking from '~/tracking';
import * as getters from '~/registry/explorer/stores/getters';
import QuickstartDropdown from '~/registry/explorer/components/quickstart_dropdown.vue';
import QuickstartDropdown from '~/registry/explorer/components/cli_commands.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import {
@ -19,7 +19,7 @@ import {
const localVue = createLocalVue();
localVue.use(Vuex);
describe('quickstart_dropdown', () => {
describe('cli_commands', () => {
let wrapper;
let store;

View file

@ -1,132 +0,0 @@
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlSprintf, GlAlert, GlLink } from '@gitlab/ui';
import * as dateTimeUtils from '~/lib/utils/datetime_utility';
import component from '~/registry/explorer/components/project_policy_alert.vue';
import {
EXPIRATION_POLICY_ALERT_TITLE,
EXPIRATION_POLICY_ALERT_PRIMARY_BUTTON,
} from '~/registry/explorer/constants';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Project Policy Alert', () => {
let wrapper;
let store;
const defaultState = {
config: {
expirationPolicy: {
enabled: true,
},
settingsPath: 'foo',
expirationPolicyHelpPagePath: 'bar',
},
images: [],
isLoading: false,
};
const findAlert = () => wrapper.find(GlAlert);
const findLink = () => wrapper.find(GlLink);
const createComponent = (state = defaultState) => {
store = new Vuex.Store({
state,
});
wrapper = shallowMount(component, {
localVue,
store,
stubs: {
GlSprintf,
},
});
};
const documentationExpectation = () => {
it('contain a documentation link', () => {
createComponent();
expect(findLink().attributes('href')).toBe(defaultState.config.expirationPolicyHelpPagePath);
expect(findLink().text()).toBe('documentation');
});
};
beforeEach(() => {
jest.spyOn(dateTimeUtils, 'approximateDuration').mockReturnValue('1 day');
});
afterEach(() => {
wrapper.destroy();
});
describe('is hidden', () => {
it('when expiration policy does not exist', () => {
createComponent({ config: {} });
expect(findAlert().exists()).toBe(false);
});
it('when expiration policy exist but is disabled', () => {
createComponent({
...defaultState,
config: {
expirationPolicy: {
enabled: false,
},
},
});
expect(findAlert().exists()).toBe(false);
});
});
describe('is visible', () => {
it('when expiration policy exists and is enabled', () => {
createComponent();
expect(findAlert().exists()).toBe(true);
});
});
describe('full info alert', () => {
beforeEach(() => {
createComponent({ ...defaultState, images: [1] });
});
it('has a primary button', () => {
const alert = findAlert();
expect(alert.props('primaryButtonText')).toBe(EXPIRATION_POLICY_ALERT_PRIMARY_BUTTON);
expect(alert.props('primaryButtonLink')).toBe(defaultState.config.settingsPath);
});
it('has a title', () => {
const alert = findAlert();
expect(alert.props('title')).toBe(EXPIRATION_POLICY_ALERT_TITLE);
});
it('has the full message', () => {
expect(findAlert().html()).toContain('<strong>1 day</strong>');
});
documentationExpectation();
});
describe('compact info alert', () => {
beforeEach(() => {
createComponent({ ...defaultState, images: [] });
});
it('does not have a button', () => {
const alert = findAlert();
expect(alert.props('primaryButtonText')).toBe(null);
});
it('does not have a title', () => {
const alert = findAlert();
expect(alert.props('title')).toBe(null);
});
it('has the short message', () => {
expect(findAlert().html()).not.toContain('<strong>1 day</strong>');
});
documentationExpectation();
});
});

View file

@ -0,0 +1,221 @@
import { shallowMount } from '@vue/test-utils';
import { GlSprintf, GlLink } from '@gitlab/ui';
import Component from '~/registry/explorer/components/registry_header.vue';
import {
CONTAINER_REGISTRY_TITLE,
LIST_INTRO_TEXT,
EXPIRATION_POLICY_DISABLED_MESSAGE,
EXPIRATION_POLICY_DISABLED_TEXT,
EXPIRATION_POLICY_WILL_RUN_IN,
} from '~/registry/explorer/constants';
jest.mock('~/lib/utils/datetime_utility', () => ({
approximateDuration: jest.fn(),
calculateRemainingMilliseconds: jest.fn(),
}));
describe('registry_header', () => {
let wrapper;
const findHeader = () => wrapper.find('[data-testid="header"]');
const findTitle = () => wrapper.find('[data-testid="title"]');
const findCommandsSlot = () => wrapper.find('[data-testid="commands-slot"]');
const findInfoArea = () => wrapper.find('[data-testid="info-area"]');
const findIntroText = () => wrapper.find('[data-testid="default-intro"]');
const findSubHeader = () => wrapper.find('[data-testid="subheader"]');
const findImagesCountSubHeader = () => wrapper.find('[data-testid="images-count"]');
const findExpirationPolicySubHeader = () => wrapper.find('[data-testid="expiration-policy"]');
const findDisabledExpirationPolicyMessage = () =>
wrapper.find('[data-testid="expiration-disabled-message"]');
const mountComponent = (propsData, slots) => {
wrapper = shallowMount(Component, {
stubs: {
GlSprintf,
},
propsData,
slots,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('header', () => {
it('exists', () => {
mountComponent();
expect(findHeader().exists()).toBe(true);
});
it('contains the title of the page', () => {
mountComponent();
const title = findTitle();
expect(title.exists()).toBe(true);
expect(title.text()).toBe(CONTAINER_REGISTRY_TITLE);
});
it('has a commands slot', () => {
mountComponent(null, { commands: 'baz' });
expect(findCommandsSlot().text()).toBe('baz');
});
});
describe('subheader', () => {
describe('when there are no images', () => {
it('is hidden ', () => {
mountComponent();
expect(findSubHeader().exists()).toBe(false);
});
});
describe('when there are images', () => {
it('is visible', () => {
mountComponent({ imagesCount: 1 });
expect(findSubHeader().exists()).toBe(true);
});
describe('sub header parts', () => {
describe('images count', () => {
it('exists', () => {
mountComponent({ imagesCount: 1 });
expect(findImagesCountSubHeader().exists()).toBe(true);
});
it('when there is one image', () => {
mountComponent({ imagesCount: 1 });
expect(findImagesCountSubHeader().text()).toMatchInterpolatedText('1 Image repository');
});
it('when there is more than one image', () => {
mountComponent({ imagesCount: 3 });
expect(findImagesCountSubHeader().text()).toMatchInterpolatedText(
'3 Image repositories',
);
});
});
describe('expiration policy', () => {
it('when is disabled', () => {
mountComponent({
expirationPolicy: { enabled: false },
expirationPolicyHelpPagePath: 'foo',
imagesCount: 1,
});
const text = findExpirationPolicySubHeader();
expect(text.exists()).toBe(true);
expect(text.text()).toMatchInterpolatedText(EXPIRATION_POLICY_DISABLED_TEXT);
});
it('when is enabled', () => {
mountComponent({
expirationPolicy: { enabled: true },
expirationPolicyHelpPagePath: 'foo',
imagesCount: 1,
});
const text = findExpirationPolicySubHeader();
expect(text.exists()).toBe(true);
expect(text.text()).toMatchInterpolatedText(EXPIRATION_POLICY_WILL_RUN_IN);
});
it('when the expiration policy is completely disabled', () => {
mountComponent({
expirationPolicy: { enabled: true },
expirationPolicyHelpPagePath: 'foo',
imagesCount: 1,
hideExpirationPolicyData: true,
});
const text = findExpirationPolicySubHeader();
expect(text.exists()).toBe(false);
});
});
});
});
});
describe('info area', () => {
it('exists', () => {
mountComponent();
expect(findInfoArea().exists()).toBe(true);
});
describe('default message', () => {
beforeEach(() => {
mountComponent({ helpPagePath: 'bar' });
});
it('exists', () => {
expect(findIntroText().exists()).toBe(true);
});
it('has the correct copy', () => {
expect(findIntroText().text()).toMatchInterpolatedText(LIST_INTRO_TEXT);
});
it('has the correct link', () => {
expect(
findIntroText()
.find(GlLink)
.attributes('href'),
).toBe('bar');
});
});
describe('expiration policy info message', () => {
describe('when there are no images', () => {
it('is hidden', () => {
mountComponent();
expect(findDisabledExpirationPolicyMessage().exists()).toBe(false);
});
});
describe('when there are images', () => {
describe('when expiration policy is disabled', () => {
beforeEach(() => {
mountComponent({
expirationPolicy: { enabled: false },
expirationPolicyHelpPagePath: 'foo',
imagesCount: 1,
});
});
it('message exist', () => {
expect(findDisabledExpirationPolicyMessage().exists()).toBe(true);
});
it('has the correct copy', () => {
expect(findDisabledExpirationPolicyMessage().text()).toMatchInterpolatedText(
EXPIRATION_POLICY_DISABLED_MESSAGE,
);
});
it('has the correct link', () => {
expect(
findDisabledExpirationPolicyMessage()
.find(GlLink)
.attributes('href'),
).toBe('foo');
});
});
describe('when expiration policy is enabled', () => {
it('message does not exist', () => {
mountComponent({
expirationPolicy: { enabled: true },
imagesCount: 1,
});
expect(findDisabledExpirationPolicyMessage().exists()).toBe(false);
});
});
describe('when the expiration policy is completely disabled', () => {
it('message does not exist', () => {
mountComponent({
expirationPolicy: { enabled: true },
imagesCount: 1,
hideExpirationPolicyData: true,
});
expect(findDisabledExpirationPolicyMessage().exists()).toBe(false);
});
});
});
});
});
});

View file

@ -3,10 +3,10 @@ import { GlSkeletonLoader, GlSprintf, GlAlert, GlSearchBoxByClick } from '@gitla
import Tracking from '~/tracking';
import waitForPromises from 'helpers/wait_for_promises';
import component from '~/registry/explorer/pages/list.vue';
import QuickstartDropdown from '~/registry/explorer/components/quickstart_dropdown.vue';
import CliCommands from '~/registry/explorer/components/cli_commands.vue';
import GroupEmptyState from '~/registry/explorer/components/group_empty_state.vue';
import ProjectEmptyState from '~/registry/explorer/components/project_empty_state.vue';
import ProjectPolicyAlert from '~/registry/explorer/components/project_policy_alert.vue';
import RegistryHeader from '~/registry/explorer/components/registry_header.vue';
import ImageList from '~/registry/explorer/components/image_list.vue';
import { createStore } from '~/registry/explorer/stores/';
import {
@ -32,14 +32,14 @@ describe('List Page', () => {
const findDeleteModal = () => wrapper.find(GlModal);
const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader);
const findImagesList = () => wrapper.find({ ref: 'imagesList' });
const findEmptyState = () => wrapper.find(GlEmptyState);
const findQuickStartDropdown = () => wrapper.find(QuickstartDropdown);
const findCliCommands = () => wrapper.find(CliCommands);
const findProjectEmptyState = () => wrapper.find(ProjectEmptyState);
const findGroupEmptyState = () => wrapper.find(GroupEmptyState);
const findProjectPolicyAlert = () => wrapper.find(ProjectPolicyAlert);
const findRegistryHeader = () => wrapper.find(RegistryHeader);
const findDeleteAlert = () => wrapper.find(GlAlert);
const findImageList = () => wrapper.find(ImageList);
const findListHeader = () => wrapper.find('[data-testid="listHeader"]');
@ -53,6 +53,7 @@ describe('List Page', () => {
GlModal,
GlEmptyState,
GlSprintf,
RegistryHeader,
},
mocks: {
$toast,
@ -76,21 +77,6 @@ describe('List Page', () => {
wrapper.destroy();
});
describe('Expiration policy notification', () => {
beforeEach(() => {
mountComponent();
});
it('shows up on project page', () => {
expect(findProjectPolicyAlert().exists()).toBe(true);
});
it('does show up on group page', () => {
store.commit(SET_INITIAL_STATE, { isGroupPage: true });
return wrapper.vm.$nextTick().then(() => {
expect(findProjectPolicyAlert().exists()).toBe(false);
});
});
});
describe('API calls', () => {
it.each`
imageList | name | called
@ -109,6 +95,11 @@ describe('List Page', () => {
);
});
it('contains registry header', () => {
mountComponent();
expect(findRegistryHeader().exists()).toBe(true);
});
describe('connection error', () => {
const config = {
characterError: true,
@ -139,7 +130,7 @@ describe('List Page', () => {
it('should not show the loading or default state', () => {
expect(findSkeletonLoader().exists()).toBe(false);
expect(findImagesList().exists()).toBe(false);
expect(findImageList().exists()).toBe(false);
});
});
@ -156,11 +147,11 @@ describe('List Page', () => {
});
it('imagesList is not visible', () => {
expect(findImagesList().exists()).toBe(false);
expect(findImageList().exists()).toBe(false);
});
it('quick start is not visible', () => {
expect(findQuickStartDropdown().exists()).toBe(false);
it('cli commands is not visible', () => {
expect(findCliCommands().exists()).toBe(false);
});
});
@ -171,8 +162,8 @@ describe('List Page', () => {
return waitForPromises();
});
it('quick start is not visible', () => {
expect(findQuickStartDropdown().exists()).toBe(false);
it('cli commands is not visible', () => {
expect(findCliCommands().exists()).toBe(false);
});
it('project empty state is visible', () => {
@ -193,8 +184,8 @@ describe('List Page', () => {
expect(findGroupEmptyState().exists()).toBe(true);
});
it('quick start is not visible', () => {
expect(findQuickStartDropdown().exists()).toBe(false);
it('cli commands is not visible', () => {
expect(findCliCommands().exists()).toBe(false);
});
it('list header is not visible', () => {
@ -210,7 +201,7 @@ describe('List Page', () => {
});
it('quick start is visible', () => {
expect(findQuickStartDropdown().exists()).toBe(true);
expect(findCliCommands().exists()).toBe(true);
});
it('list component is visible', () => {

View file

@ -43,15 +43,6 @@ Object.assign(global, {
preloadFixtures() {},
});
Object.assign(global, {
MutationObserver() {
return {
disconnect() {},
observe() {},
};
},
});
// custom-jquery-matchers was written for an old Jest version, we need to make it compatible
Object.entries(jqueryMatchers).forEach(([matcherName, matcherFactory]) => {
// Don't override existing Jest matcher
@ -69,12 +60,6 @@ expect.extend(customMatchers);
// Tech debt issue TBD
testUtilsConfig.logModifiedComponents = false;
// Basic stub for MutationObserver
global.MutationObserver = () => ({
disconnect: () => {},
observe: () => {},
});
Object.assign(global, {
requestIdleCallback(cb) {
const start = Date.now();

View file

@ -0,0 +1,47 @@
import {
generateToolbarItem,
addCustomEventListener,
} from '~/vue_shared/components/rich_content_editor/editor_service';
describe('Editor Service', () => {
describe('generateToolbarItem', () => {
const config = {
icon: 'bold',
command: 'some-command',
tooltip: 'Some Tooltip',
event: 'some-event',
};
const generatedItem = generateToolbarItem(config);
it('generates the correct command', () => {
expect(generatedItem.options.command).toBe(config.command);
});
it('generates the correct tooltip', () => {
expect(generatedItem.options.tooltip).toBe(config.tooltip);
});
it('generates the correct event', () => {
expect(generatedItem.options.event).toBe(config.event);
});
it('generates a divider when isDivider is set to true', () => {
const isDivider = true;
expect(generateToolbarItem({ isDivider })).toBe('divider');
});
});
describe('addCustomEventListener', () => {
const mockInstance = { eventManager: { addEventType: jest.fn(), listen: jest.fn() } };
const event = 'someCustomEvent';
const handler = jest.fn();
it('registers an event type on the instance and adds an event handler', () => {
addCustomEventListener(mockInstance, event, handler);
expect(mockInstance.eventManager.addEventType).toHaveBeenCalledWith(event);
expect(mockInstance.eventManager.listen).toHaveBeenCalledWith(event, handler);
});
});
});

View file

@ -5,8 +5,16 @@ import {
EDITOR_TYPES,
EDITOR_HEIGHT,
EDITOR_PREVIEW_STYLE,
CUSTOM_EVENTS,
} from '~/vue_shared/components/rich_content_editor/constants';
import { addCustomEventListener } from '~/vue_shared/components/rich_content_editor/editor_service';
jest.mock('~/vue_shared/components/rich_content_editor/editor_service', () => ({
...jest.requireActual('~/vue_shared/components/rich_content_editor/editor_service'),
addCustomEventListener: jest.fn(),
}));
describe('Rich Content Editor', () => {
let wrapper;
@ -56,4 +64,17 @@ describe('Rich Content Editor', () => {
expect(wrapper.emitted().input[0][0]).toBe(changedMarkdown);
});
});
describe('when editor is loaded', () => {
it('adds the CUSTOM_EVENTS.openAddImageModal custom event listener', () => {
const mockInstance = { eventManager: { addEventType: jest.fn(), listen: jest.fn() } };
findEditor().vm.$emit('load', mockInstance);
expect(addCustomEventListener).toHaveBeenCalledWith(
mockInstance,
CUSTOM_EVENTS.openAddImageModal,
wrapper.vm.onOpenAddImageModal,
);
});
});
});

View file

@ -1,29 +0,0 @@
import { generateToolbarItem } from '~/vue_shared/components/rich_content_editor/toolbar_service';
describe('Toolbar Service', () => {
const config = {
icon: 'bold',
command: 'some-command',
tooltip: 'Some Tooltip',
event: 'some-event',
};
const generatedItem = generateToolbarItem(config);
it('generates the correct command', () => {
expect(generatedItem.options.command).toBe(config.command);
});
it('generates the correct tooltip', () => {
expect(generatedItem.options.tooltip).toBe(config.tooltip);
});
it('generates the correct event', () => {
expect(generatedItem.options.event).toBe(config.event);
});
it('generates a divider when isDivider is set to true', () => {
const isDivider = true;
expect(generateToolbarItem({ isDivider })).toBe('divider');
});
});

View file

@ -109,31 +109,21 @@ describe 'Creating a Snippet' do
context 'when the project path is invalid' do
let(:project_path) { 'foobar' }
it 'returns an an error' do
subject
errors = json_response['errors']
expect(errors.first['message']).to eq(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
end
it_behaves_like 'a mutation that returns top-level errors',
errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
end
context 'when the feature is disabled' do
it 'returns an an error' do
before do
project.project_feature.update_attribute(:snippets_access_level, ProjectFeature::DISABLED)
subject
errors = json_response['errors']
expect(errors.first['message']).to eq(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
end
end
end
context 'when there are ActiveRecord validation errors' do
let(:title) { '' }
it_behaves_like 'a mutation that returns errors in the response', errors: ["Title can't be blank"]
it_behaves_like 'a mutation that returns top-level errors',
errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
end
end
shared_examples 'does not create snippet' do
it 'does not create the Snippet' do
expect do
subject
@ -147,7 +137,21 @@ describe 'Creating a Snippet' do
end
end
context 'when there uploaded files' do
context 'when there are ActiveRecord validation errors' do
let(:title) { '' }
it_behaves_like 'a mutation that returns errors in the response', errors: ["Title can't be blank"]
it_behaves_like 'does not create snippet'
end
context 'when there non ActiveRecord errors' do
let(:file_name) { 'invalid://file/path' }
it_behaves_like 'a mutation that returns errors in the response', errors: ['Repository Error creating the snippet - Invalid file name']
it_behaves_like 'does not create snippet'
end
context 'when there are uploaded files' do
shared_examples 'expected files argument' do |file_value, expected_value|
let(:uploaded_files) { file_value }