Commit 288c393c authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 8a64580c b148b97b
...@@ -4,7 +4,7 @@ import VueDraggable from 'vuedraggable'; ...@@ -4,7 +4,7 @@ import VueDraggable from 'vuedraggable';
import permissionsQuery from 'shared_queries/design_management/design_permissions.query.graphql'; import permissionsQuery from 'shared_queries/design_management/design_permissions.query.graphql';
import getDesignListQuery from 'shared_queries/design_management/get_design_list.query.graphql'; import getDesignListQuery from 'shared_queries/design_management/get_design_list.query.graphql';
import createFlash, { FLASH_TYPES } from '~/flash'; import createFlash, { FLASH_TYPES } from '~/flash';
import { getFilename } from '~/lib/utils/file_upload'; import { getFilename, validateImageName } from '~/lib/utils/file_upload';
import { __, s__, sprintf } from '~/locale'; import { __, s__, sprintf } from '~/locale';
import DesignDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue'; import DesignDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
import DeleteButton from '../components/delete_button.vue'; import DeleteButton from '../components/delete_button.vue';
...@@ -284,12 +284,16 @@ export default { ...@@ -284,12 +284,16 @@ export default {
return; return;
} }
event.preventDefault(); event.preventDefault();
let filename = getFilename(event); const fileList = [...files];
if (!filename || filename === 'image.png') { fileList.forEach((file) => {
filename = `design_${Date.now()}.png`; let filename = getFilename(file);
} filename = validateImageName(file);
const newFile = new File([files[0]], filename); if (!filename || filename === 'image.png') {
this.onUploadDesign([newFile]); filename = `design_${Date.now()}.png`;
}
const newFile = new File([file], filename);
this.onUploadDesign([newFile]);
});
} }
}, },
toggleOnPasteListener() { toggleOnPasteListener() {
......
...@@ -43,7 +43,6 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) { ...@@ -43,7 +43,6 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
let pasteText; let pasteText;
let addFileToForm; let addFileToForm;
let updateAttachingMessage; let updateAttachingMessage;
let isImage;
let uploadFile; let uploadFile;
formTextarea.wrap('<div class="div-dropzone"></div>'); formTextarea.wrap('<div class="div-dropzone"></div>');
...@@ -173,7 +172,7 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) { ...@@ -173,7 +172,7 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
return dropzoneInstance.addFile(file); return dropzoneInstance.addFile(file);
}); });
}); });
// eslint-disable-next-line consistent-return
handlePaste = (event) => { handlePaste = (event) => {
const pasteEvent = event.originalEvent; const pasteEvent = event.originalEvent;
const { clipboardData } = pasteEvent; const { clipboardData } = pasteEvent;
...@@ -186,32 +185,22 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) { ...@@ -186,32 +185,22 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
const text = converter.convertToTableMarkdown(); const text = converter.convertToTableMarkdown();
pasteText(text); pasteText(text);
} else { } else {
const image = isImage(pasteEvent); const fileList = [...clipboardData.files];
fileList.forEach((file) => {
if (image) { if (file.type.indexOf('image') !== -1) {
event.preventDefault(); event.preventDefault();
const MAX_FILE_NAME_LENGTH = 246; const MAX_FILE_NAME_LENGTH = 246;
const filename = getFilename(pasteEvent) || 'image.png';
const truncateFilename = truncate(filename, MAX_FILE_NAME_LENGTH); const filename = getFilename(file) || 'image.png';
const text = `{{${truncateFilename}}}`; const truncateFilename = truncate(filename, MAX_FILE_NAME_LENGTH);
pasteText(text); const text = `{{${truncateFilename}}}`;
pasteText(text);
return uploadFile(image.getAsFile(), truncateFilename);
} uploadFile(file, truncateFilename);
} }
} });
};
isImage = (data) => {
let i = 0;
while (i < data.clipboardData.items.length) {
const item = data.clipboardData.items[i];
if (item.type.indexOf('image') !== -1) {
return item;
} }
i += 1;
} }
return false;
}; };
pasteText = (text, shouldPad) => { pasteText = (text, shouldPad) => {
......
...@@ -5,6 +5,7 @@ import { ...@@ -5,6 +5,7 @@ import {
VALIDATE_INTEGRATION_FORM_EVENT, VALIDATE_INTEGRATION_FORM_EVENT,
GET_JIRA_ISSUE_TYPES_EVENT, GET_JIRA_ISSUE_TYPES_EVENT,
} from '~/integrations/constants'; } from '~/integrations/constants';
import { s__, __ } from '~/locale';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import JiraUpgradeCta from './jira_upgrade_cta.vue'; import JiraUpgradeCta from './jira_upgrade_cta.vue';
...@@ -94,33 +95,38 @@ export default { ...@@ -94,33 +95,38 @@ export default {
eventHub.$emit(GET_JIRA_ISSUE_TYPES_EVENT); eventHub.$emit(GET_JIRA_ISSUE_TYPES_EVENT);
}, },
}, },
i18n: {
sectionTitle: s__('JiraService|View Jira issues in GitLab'),
sectionDescription: s__(
'JiraService|Work on Jira issues without leaving GitLab. Adds a Jira menu to access your list of Jira issues and view any issue as read-only.',
),
enableCheckboxLabel: s__('JiraService|Enable Jira issues'),
enableCheckboxHelp: s__(
'JiraService|Warning: All GitLab users that have access to this GitLab project are able to view all issues from the Jira project specified below.',
),
projectKeyLabel: s__('JiraService|Jira project key'),
projectKeyPlaceholder: s__('JiraService|For example, AB'),
requiredFieldFeedback: __('This field is required.'),
issueTrackerConflictWarning: s__(
'JiraService|Displaying Jira issues while leaving the GitLab issue functionality enabled might be confusing. Consider %{linkStart}disabling GitLab issues%{linkEnd} if they won’t otherwise be used.',
),
},
}; };
</script> </script>
<template> <template>
<div> <div>
<gl-form-group <gl-form-group :label="$options.i18n.sectionTitle" label-for="jira-issue-settings">
:label="s__('JiraService|View Jira issues in GitLab')"
label-for="jira-issue-settings"
>
<div id="jira-issue-settings"> <div id="jira-issue-settings">
<p> <p>
{{ {{ $options.i18n.sectionDescription }}
s__(
'JiraService|Work on Jira issues without leaving GitLab. Adds a Jira menu to access your list of Jira issues and view any issue as read-only.',
)
}}
</p> </p>
<template v-if="showJiraIssuesIntegration"> <template v-if="showJiraIssuesIntegration">
<input name="service[issues_enabled]" type="hidden" :value="enableJiraIssues || false" /> <input name="service[issues_enabled]" type="hidden" :value="enableJiraIssues || false" />
<gl-form-checkbox v-model="enableJiraIssues" :disabled="isInheriting"> <gl-form-checkbox v-model="enableJiraIssues" :disabled="isInheriting">
{{ s__('JiraService|Enable Jira issues') }} {{ $options.i18n.enableCheckboxLabel }}
<template #help> <template #help>
{{ {{ $options.i18n.enableCheckboxHelp }}
s__(
'JiraService|Warning: All GitLab users that have access to this GitLab project are able to view all issues from the Jira project specified below.',
)
}}
</template> </template>
</gl-form-checkbox> </gl-form-checkbox>
<template v-if="enableJiraIssues"> <template v-if="enableJiraIssues">
...@@ -152,30 +158,25 @@ export default { ...@@ -152,30 +158,25 @@ export default {
</gl-form-group> </gl-form-group>
<template v-if="showJiraIssuesIntegration"> <template v-if="showJiraIssuesIntegration">
<gl-form-group <gl-form-group
:label="s__('JiraService|Jira project key')" :label="$options.i18n.projectKeyLabel"
label-for="service_project_key" label-for="service_project_key"
:invalid-feedback="__('This field is required.')" :invalid-feedback="$options.i18n.requiredFieldFeedback"
:state="validProjectKey" :state="validProjectKey"
data-testid="project-key-form-group"
> >
<gl-form-input <gl-form-input
id="service_project_key" id="service_project_key"
v-model="projectKey" v-model="projectKey"
name="service[project_key]" name="service[project_key]"
:placeholder="s__('JiraService|For example, AB')" :placeholder="$options.i18n.projectKeyPlaceholder"
:required="enableJiraIssues" :required="enableJiraIssues"
:state="validProjectKey" :state="validProjectKey"
:disabled="!enableJiraIssues" :disabled="!enableJiraIssues"
:readonly="isInheriting" :readonly="isInheriting"
/> />
</gl-form-group> </gl-form-group>
<p v-if="gitlabIssuesEnabled"> <p v-if="gitlabIssuesEnabled" data-testid="conflict-warning-text">
<gl-sprintf <gl-sprintf :message="$options.i18n.issueTrackerConflictWarning">
:message="
s__(
'JiraService|Displaying Jira issues while leaving the GitLab issue functionality enabled might be confusing. Consider %{linkStart}disabling GitLab issues%{linkEnd} if they won’t otherwise be used.',
)
"
>
<template #link="{ content }"> <template #link="{ content }">
<gl-link :href="editProjectPath" target="_blank">{{ content }}</gl-link> <gl-link :href="editProjectPath" target="_blank">{{ content }}</gl-link>
</template> </template>
......
...@@ -15,13 +15,17 @@ export default (buttonSelector, fileSelector) => { ...@@ -15,13 +15,17 @@ export default (buttonSelector, fileSelector) => {
}); });
}; };
export const getFilename = ({ clipboardData }) => { export const getFilename = (file) => {
let value; let fileName;
if (window.clipboardData && window.clipboardData.getData) { if (file) {
value = window.clipboardData.getData('Text'); fileName = file.name;
} else if (clipboardData && clipboardData.getData) {
value = clipboardData.getData('text/plain');
} }
value = value.split('\r');
return value[0]; return fileName;
};
export const validateImageName = (file) => {
const fileName = file.name ? file.name : 'image.png';
const legalImageRegex = /^[\w.\-+]+\.(png|jpg|jpeg|gif|bmp|tiff|ico|webp)$/;
return legalImageRegex.test(fileName) ? fileName : 'image.png';
}; };
...@@ -9,7 +9,7 @@ class Admin::HooksController < Admin::ApplicationController ...@@ -9,7 +9,7 @@ class Admin::HooksController < Admin::ApplicationController
urgency :low, [:test] urgency :low, [:test]
def index def index
@hooks = SystemHook.all @hooks = SystemHook.all.load
@hook = SystemHook.new @hook = SystemHook.new
end end
......
...@@ -16,7 +16,7 @@ class Projects::HooksController < Projects::ApplicationController ...@@ -16,7 +16,7 @@ class Projects::HooksController < Projects::ApplicationController
urgency :low, [:test] urgency :low, [:test]
def index def index
@hooks = @project.hooks @hooks = @project.hooks.load
@hook = ProjectHook.new @hook = ProjectHook.new
end end
......
...@@ -104,8 +104,7 @@ module Projects ...@@ -104,8 +104,7 @@ module Projects
CreatePipelineWorker.perform_async(project.id, current_user.id, project.default_branch, :web, ignore_skip_ci: true, save_on_errors: false) CreatePipelineWorker.perform_async(project.id, current_user.id, project.default_branch, :web, ignore_skip_ci: true, save_on_errors: false)
# rubocop:enable CodeReuse/Worker # rubocop:enable CodeReuse/Worker
pipelines_link_start = '<a href="%{url}">'.html_safe % { url: project_pipelines_path(@project) } flash[:toast] = _("A new Auto DevOps pipeline has been created, go to the Pipelines page for details")
flash[:toast] = _("A new Auto DevOps pipeline has been created, go to %{pipelines_link_start}Pipelines page%{pipelines_link_end} for details") % { pipelines_link_start: pipelines_link_start, pipelines_link_end: "</a>".html_safe }
end end
def define_variables def define_variables
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
.card-header .card-header
%h5 %h5
= hook_class.underscore.humanize.titleize.pluralize = hook_class.underscore.humanize.titleize.pluralize
(#{hooks.load.size}) (#{hooks.size})
- if hooks.any? - if hooks.any?
%ul.content-list %ul.content-list
......
...@@ -350,7 +350,7 @@ GITLAB_PASSWORD=<GDK root password> bundle exec bin/qa Test::Instance::All http: ...@@ -350,7 +350,7 @@ GITLAB_PASSWORD=<GDK root password> bundle exec bin/qa Test::Instance::All http:
Where `<test_file>` is: Where `<test_file>` is:
- `qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb` when running the Login example. - `qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb` when running the Login example.
- `qa/specs/features/browser_ui/2_plan/issues/create_issue_spec.rb` when running the Issue example. - `qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb` when running the Issue example.
## End-to-end test merge request template ## End-to-end test merge request template
......
...@@ -66,7 +66,12 @@ export default { ...@@ -66,7 +66,12 @@ export default {
PolicyEditorLayout, PolicyEditorLayout,
DimDisableContainer, DimDisableContainer,
}, },
inject: ['networkDocumentationPath', 'noEnvironmentSvgPath', 'projectId', 'policiesPath'], inject: [
'networkDocumentationPath',
'policyEditorEmptyStateSvgPath',
'projectId',
'policiesPath',
],
props: { props: {
existingPolicy: { existingPolicy: {
type: Object, type: Object,
...@@ -303,7 +308,7 @@ export default { ...@@ -303,7 +308,7 @@ export default {
:description="$options.i18n.noEnvironmentDescription" :description="$options.i18n.noEnvironmentDescription"
:primary-button-link="networkDocumentationPath" :primary-button-link="networkDocumentationPath"
:primary-button-text="$options.i18n.noEnvironmentButton" :primary-button-text="$options.i18n.noEnvironmentButton"
:svg-path="noEnvironmentSvgPath" :svg-path="policyEditorEmptyStateSvgPath"
title="" title=""
/> />
</template> </template>
<script> <script>
import { GlEmptyState } from '@gitlab/ui';
import { joinPaths, visitUrl } from '~/lib/utils/url_utility'; import { joinPaths, visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale'; import { __, s__ } from '~/locale';
import { EDITOR_MODES, EDITOR_MODE_YAML } from '../constants'; import { EDITOR_MODES, EDITOR_MODE_YAML } from '../constants';
import PolicyEditorLayout from '../policy_editor_layout.vue'; import PolicyEditorLayout from '../policy_editor_layout.vue';
import { import {
...@@ -18,11 +19,22 @@ export default { ...@@ -18,11 +19,22 @@ export default {
EDITOR_MODES: [EDITOR_MODES[1]], EDITOR_MODES: [EDITOR_MODES[1]],
i18n: { i18n: {
createMergeRequest: __('Create merge request'), createMergeRequest: __('Create merge request'),
notOwnerButtonText: __('Learn more'),
notOwnerDescription: s__(
'SecurityOrchestration|Scan execution policies can only be created by project owners.',
),
}, },
components: { components: {
GlEmptyState,
PolicyEditorLayout, PolicyEditorLayout,
}, },
inject: ['disableScanExecutionUpdate', 'projectId', 'projectPath'], inject: [
'disableScanExecutionUpdate',
'policyEditorEmptyStateSvgPath',
'projectId',
'projectPath',
'scanExecutionDocumentationPath',
],
props: { props: {
assignedPolicyProject: { assignedPolicyProject: {
type: Object, type: Object,
...@@ -110,9 +122,9 @@ export default { ...@@ -110,9 +122,9 @@ export default {
<template> <template>
<policy-editor-layout <policy-editor-layout
v-if="!disableScanExecutionUpdate"
:custom-save-button-text="$options.i18n.createMergeRequest" :custom-save-button-text="$options.i18n.createMergeRequest"
:default-editor-mode="$options.DEFAULT_EDITOR_MODE" :default-editor-mode="$options.DEFAULT_EDITOR_MODE"
:disable-update="disableScanExecutionUpdate"
:editor-modes="$options.EDITOR_MODES" :editor-modes="$options.EDITOR_MODES"
:is-editing="isEditing" :is-editing="isEditing"
:is-removing-policy="isRemovingPolicy" :is-removing-policy="isRemovingPolicy"
...@@ -123,4 +135,12 @@ export default { ...@@ -123,4 +135,12 @@ export default {
@save-policy="handleModifyPolicy()" @save-policy="handleModifyPolicy()"
@update-yaml="updateYaml" @update-yaml="updateYaml"
/> />
<gl-empty-state
v-else
:description="$options.i18n.notOwnerDescription"
:primary-button-link="scanExecutionDocumentationPath"
:primary-button-text="$options.i18n.notOwnerButtonText"
:svg-path="policyEditorEmptyStateSvgPath"
title=""
/>
</template> </template>
...@@ -21,15 +21,16 @@ export default () => { ...@@ -21,15 +21,16 @@ export default () => {
environmentsEndpoint, environmentsEndpoint,
configureAgentHelpPath, configureAgentHelpPath,
createAgentHelpPath, createAgentHelpPath,
networkDocumentationPath,
networkPoliciesEndpoint, networkPoliciesEndpoint,
noEnvironmentSvgPath, networkDocumentationPath,
policiesPath, policiesPath,
policy, policy,
policyEditorEmptyStateSvgPath,
policyType, policyType,
projectPath, projectPath,
projectId, projectId,
environmentId, environmentId,
scanExecutionDocumentationPath,
} = el.dataset; } = el.dataset;
// We require the project to have at least one available environment. // We require the project to have at least one available environment.
...@@ -66,12 +67,13 @@ export default () => { ...@@ -66,12 +67,13 @@ export default () => {
configureAgentHelpPath, configureAgentHelpPath,
createAgentHelpPath, createAgentHelpPath,
disableScanExecutionUpdate: parseBoolean(disableScanExecutionUpdate), disableScanExecutionUpdate: parseBoolean(disableScanExecutionUpdate),
policyType,
networkDocumentationPath, networkDocumentationPath,
noEnvironmentSvgPath, policyEditorEmptyStateSvgPath,
policyType,
projectId, projectId,
projectPath, projectPath,
policiesPath, policiesPath,
scanExecutionDocumentationPath,
}, },
store, store,
render(createElement) { render(createElement) {
......
...@@ -18,7 +18,7 @@ class Groups::HooksController < Groups::ApplicationController ...@@ -18,7 +18,7 @@ class Groups::HooksController < Groups::ApplicationController
urgency :low, [:test] urgency :low, [:test]
def index def index
@hooks = @group.hooks @hooks = @group.hooks.load
@hook = GroupHook.new @hook = GroupHook.new
end end
......
...@@ -29,13 +29,14 @@ module Projects::Security::PoliciesHelper ...@@ -29,13 +29,14 @@ module Projects::Security::PoliciesHelper
create_agent_help_path: help_page_url('user/clusters/agent/index.md', anchor: 'create-an-agent-record-in-gitlab'), create_agent_help_path: help_page_url('user/clusters/agent/index.md', anchor: 'create-an-agent-record-in-gitlab'),
environments_endpoint: project_environments_path(project), environments_endpoint: project_environments_path(project),
environment_id: environment&.id, environment_id: environment&.id,
network_documentation_path: help_page_path('user/application_security/policies/index'), network_documentation_path: help_page_path('user/application_security/policies/index', anchor: 'container-network-policy'),
no_environment_svg_path: image_path('illustrations/monitoring/unable_to_connect.svg'),
policy: policy&.to_json, policy: policy&.to_json,
policy_editor_empty_state_svg_path: image_path('illustrations/monitoring/unable_to_connect.svg'),
policy_type: policy_type, policy_type: policy_type,
project_path: project.full_path, project_path: project.full_path,
project_id: project.id, project_id: project.id,
policies_path: project_security_policies_path(project) policies_path: project_security_policies_path(project),
scan_execution_documentation_path: help_page_path('user/application_security/policies/index', anchor: 'scan-execution-policy-editor')
} }
end end
end end
...@@ -52,7 +52,7 @@ describe('NetworkPolicyEditor component', () => { ...@@ -52,7 +52,7 @@ describe('NetworkPolicyEditor component', () => {
}, },
provide: { provide: {
networkDocumentationPath: 'path/to/docs', networkDocumentationPath: 'path/to/docs',
noEnvironmentSvgPath: 'path/to/svg', policyEditorEmptyStateSvgPath: 'path/to/svg',
policiesPath: '/threat-monitoring', policiesPath: '/threat-monitoring',
projectId: '21', projectId: '21',
...provide, ...provide,
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlEmptyState } from '@gitlab/ui';
import PolicyEditorLayout from 'ee/threat_monitoring/components/policy_editor/policy_editor_layout.vue'; import PolicyEditorLayout from 'ee/threat_monitoring/components/policy_editor/policy_editor_layout.vue';
import { import {
DEFAULT_SCAN_EXECUTION_POLICY, DEFAULT_SCAN_EXECUTION_POLICY,
...@@ -40,8 +41,10 @@ jest.mock('ee/threat_monitoring/components/policy_editor/scan_execution_policy/l ...@@ -40,8 +41,10 @@ jest.mock('ee/threat_monitoring/components/policy_editor/scan_execution_policy/l
describe('ScanExecutionPolicyEditor', () => { describe('ScanExecutionPolicyEditor', () => {
let wrapper; let wrapper;
const defaultProjectPath = 'path/to/project'; const defaultProjectPath = 'path/to/project';
const policyEditorEmptyStateSvgPath = 'path/to/svg';
const scanExecutionDocumentationPath = 'path/to/docs';
const factory = ({ propsData = {} } = {}) => { const factory = ({ propsData = {}, provide = {} } = {}) => {
wrapper = shallowMount(ScanExecutionPolicyEditor, { wrapper = shallowMount(ScanExecutionPolicyEditor, {
propsData: { propsData: {
assignedPolicyProject: DEFAULT_ASSIGNED_POLICY_PROJECT, assignedPolicyProject: DEFAULT_ASSIGNED_POLICY_PROJECT,
...@@ -49,8 +52,11 @@ describe('ScanExecutionPolicyEditor', () => { ...@@ -49,8 +52,11 @@ describe('ScanExecutionPolicyEditor', () => {
}, },
provide: { provide: {
disableScanExecutionUpdate: false, disableScanExecutionUpdate: false,
policyEditorEmptyStateSvgPath,
projectId: 1, projectId: 1,
projectPath: defaultProjectPath, projectPath: defaultProjectPath,
scanExecutionDocumentationPath,
...provide,
}, },
}); });
}; };
...@@ -59,45 +65,59 @@ describe('ScanExecutionPolicyEditor', () => { ...@@ -59,45 +65,59 @@ describe('ScanExecutionPolicyEditor', () => {
return factory({ propsData: { existingPolicy: mockDastScanExecutionObject, isEditing: true } }); return factory({ propsData: { existingPolicy: mockDastScanExecutionObject, isEditing: true } });
}; };
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findPolicyEditorLayout = () => wrapper.findComponent(PolicyEditorLayout); const findPolicyEditorLayout = () => wrapper.findComponent(PolicyEditorLayout);
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
it('updates the policy yaml when "update-yaml" is emitted', async () => { describe('default', () => {
factory(); it('updates the policy yaml when "update-yaml" is emitted', async () => {
await wrapper.vm.$nextTick(); factory();
const newManifest = 'new yaml!'; await wrapper.vm.$nextTick();
expect(findPolicyEditorLayout().attributes('yamleditorvalue')).toBe( const newManifest = 'new yaml!';
DEFAULT_SCAN_EXECUTION_POLICY, expect(findPolicyEditorLayout().attributes('yamleditorvalue')).toBe(
DEFAULT_SCAN_EXECUTION_POLICY,
);
await findPolicyEditorLayout().vm.$emit('update-yaml', newManifest);
expect(findPolicyEditorLayout().attributes('yamleditorvalue')).toBe(newManifest);
});
it.each`
status | action | event | factoryFn | yamlEditorValue
${'to save a new policy'} | ${SECURITY_POLICY_ACTIONS.APPEND} | ${'save-policy'} | ${factory} | ${DEFAULT_SCAN_EXECUTION_POLICY}
${'to update an existing policy'} | ${SECURITY_POLICY_ACTIONS.REPLACE} | ${'save-policy'} | ${factoryWithExistingPolicy} | ${mockDastScanExecutionManifest}
${'to delete an existing policy'} | ${SECURITY_POLICY_ACTIONS.REMOVE} | ${'remove-policy'} | ${factoryWithExistingPolicy} | ${mockDastScanExecutionManifest}
`(
'navigates to the new merge request when "modifyPolicy" is emitted $status',
async ({ action, event, factoryFn, yamlEditorValue }) => {
factoryFn();
await wrapper.vm.$nextTick();
findPolicyEditorLayout().vm.$emit(event);
await wrapper.vm.$nextTick();
expect(modifyPolicy).toHaveBeenCalledTimes(1);
expect(modifyPolicy).toHaveBeenCalledWith({
action,
assignedPolicyProject: DEFAULT_ASSIGNED_POLICY_PROJECT,
projectPath: defaultProjectPath,
yamlEditorValue,
});
await wrapper.vm.$nextTick();
expect(visitUrl).toHaveBeenCalled();
expect(visitUrl).toHaveBeenCalledWith('/tests/-/merge_requests/2');
},
); );
await findPolicyEditorLayout().vm.$emit('update-yaml', newManifest);
expect(findPolicyEditorLayout().attributes('yamleditorvalue')).toBe(newManifest);
}); });
it.each` describe('when a user is not an owner of the project', () => {
status | action | event | factoryFn | yamlEditorValue it('displays the empty state with the appropriate properties', async () => {
${'to save a new policy'} | ${SECURITY_POLICY_ACTIONS.APPEND} | ${'save-policy'} | ${factory} | ${DEFAULT_SCAN_EXECUTION_POLICY} factory({ provide: { disableScanExecutionUpdate: true } });
${'to update an existing policy'} | ${SECURITY_POLICY_ACTIONS.REPLACE} | ${'save-policy'} | ${factoryWithExistingPolicy} | ${mockDastScanExecutionManifest}
${'to delete an existing policy'} | ${SECURITY_POLICY_ACTIONS.REMOVE} | ${'remove-policy'} | ${factoryWithExistingPolicy} | ${mockDastScanExecutionManifest}
`(
'navigates to the new merge request when "modifyPolicy" is emitted $status',
async ({ action, event, factoryFn, yamlEditorValue }) => {
factoryFn();
await wrapper.vm.$nextTick();
findPolicyEditorLayout().vm.$emit(event);
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(modifyPolicy).toHaveBeenCalledTimes(1); expect(findEmptyState().props()).toMatchObject({
expect(modifyPolicy).toHaveBeenCalledWith({ primaryButtonLink: scanExecutionDocumentationPath,
action, svgPath: policyEditorEmptyStateSvgPath,
assignedPolicyProject: DEFAULT_ASSIGNED_POLICY_PROJECT,
projectPath: defaultProjectPath,
yamlEditorValue,
}); });
await wrapper.vm.$nextTick(); });
expect(visitUrl).toHaveBeenCalled(); });
expect(visitUrl).toHaveBeenCalledWith('/tests/-/merge_requests/2');
},
);
}); });
...@@ -46,13 +46,14 @@ RSpec.describe Projects::Security::PoliciesHelper do ...@@ -46,13 +46,14 @@ RSpec.describe Projects::Security::PoliciesHelper do
create_agent_help_path: kind_of(String), create_agent_help_path: kind_of(String),
environments_endpoint: kind_of(String), environments_endpoint: kind_of(String),
network_documentation_path: kind_of(String), network_documentation_path: kind_of(String),
no_environment_svg_path: kind_of(String), policy_editor_empty_state_svg_path: kind_of(String),
project_path: project.full_path, project_path: project.full_path,
project_id: project.id, project_id: project.id,
policies_path: kind_of(String), policies_path: kind_of(String),
environment_id: environment&.id, environment_id: environment&.id,
policy: policy&.to_json, policy: policy&.to_json,
policy_type: policy_type policy_type: policy_type,
scan_execution_documentation_path: kind_of(String)
} }
end end
......
...@@ -1471,7 +1471,7 @@ msgstr "" ...@@ -1471,7 +1471,7 @@ msgstr ""
msgid "A merge request hasn't yet been merged" msgid "A merge request hasn't yet been merged"
msgstr "" msgstr ""
msgid "A new Auto DevOps pipeline has been created, go to %{pipelines_link_start}Pipelines page%{pipelines_link_end} for details" msgid "A new Auto DevOps pipeline has been created, go to the Pipelines page for details"
msgstr "" msgstr ""
msgid "A new Release %{tag} for %{name} was published. Visit the %{release_link_start}Releases page%{release_link_end} to read more about it." msgid "A new Release %{tag} for %{name} was published. Visit the %{release_link_start}Releases page%{release_link_end} to read more about it."
...@@ -30439,6 +30439,9 @@ msgstr "" ...@@ -30439,6 +30439,9 @@ msgstr ""
msgid "SecurityOrchestration|Scan execution" msgid "SecurityOrchestration|Scan execution"
msgstr "" msgstr ""
msgid "SecurityOrchestration|Scan execution policies can only be created by project owners."
msgstr ""
msgid "SecurityOrchestration|Scan to be performed every %{cadence} on the %{branches}" msgid "SecurityOrchestration|Scan to be performed every %{cadence} on the %{branches}"
msgstr "" msgstr ""
......
...@@ -669,6 +669,20 @@ describe('Design management index page', () => { ...@@ -669,6 +669,20 @@ describe('Design management index page', () => {
expect(variables.files).toEqual(event.clipboardData.files.map((f) => new File([f], ''))); expect(variables.files).toEqual(event.clipboardData.files.map((f) => new File([f], '')));
}); });
it('display original file name', () => {
event.clipboardData.files = [new File([new Blob()], 'test.png', { type: 'image/png' })];
document.dispatchEvent(event);
const [{ mutation, variables }] = mockMutate.mock.calls[0];
expect(mutation).toBe(uploadDesignMutation);
expect(variables).toStrictEqual({
files: expect.any(Array),
iid: '1',
projectPath: 'project-path',
});
expect(variables.files[0].name).toEqual('test.png');
});
it('renames a design if it has an image.png filename', () => { it('renames a design if it has an image.png filename', () => {
event.clipboardData.getData = () => 'image.png'; event.clipboardData.getData = () => 'image.png';
document.dispatchEvent(event); document.dispatchEvent(event);
......
...@@ -71,6 +71,7 @@ describe('dropzone_input', () => { ...@@ -71,6 +71,7 @@ describe('dropzone_input', () => {
triggerPasteEvent({ triggerPasteEvent({
types: ['text/plain', 'text/html', 'text/rtf', 'Files'], types: ['text/plain', 'text/html', 'text/rtf', 'Files'],
getData: () => longFileName, getData: () => longFileName,
files: [new File([new Blob()], longFileName, { type: 'image/png' })],
items: [ items: [
{ {
kind: 'file', kind: 'file',
...@@ -84,6 +85,24 @@ describe('dropzone_input', () => { ...@@ -84,6 +85,24 @@ describe('dropzone_input', () => {
await waitForPromises(); await waitForPromises();
expect(axiosMock.history.post[0].data.get('file').name).toHaveLength(246); expect(axiosMock.history.post[0].data.get('file').name).toHaveLength(246);
}); });
it('display original file name in comment box', async () => {
const axiosMock = new MockAdapter(axios);
triggerPasteEvent({
types: ['Files'],
files: [new File([new Blob()], 'test.png', { type: 'image/png' })],
items: [
{
kind: 'file',
type: 'image/png',
getAsFile: () => new Blob(),
},
],
});
axiosMock.onPost().reply(httpStatusCodes.OK, { link: { markdown: 'foo' } });
await waitForPromises();
expect(axiosMock.history.post[0].data.get('file').name).toEqual('test.png');
});
}); });
describe('shows error message', () => { describe('shows error message', () => {
......
import { GlFormCheckbox, GlFormInput } from '@gitlab/ui'; import { GlFormCheckbox, GlFormInput } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { GET_JIRA_ISSUE_TYPES_EVENT } from '~/integrations/constants'; import {
GET_JIRA_ISSUE_TYPES_EVENT,
VALIDATE_INTEGRATION_FORM_EVENT,
} from '~/integrations/constants';
import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue'; import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue';
import eventHub from '~/integrations/edit/event_hub'; import eventHub from '~/integrations/edit/event_hub';
import { createStore } from '~/integrations/edit/store'; import { createStore } from '~/integrations/edit/store';
...@@ -17,12 +20,17 @@ describe('JiraIssuesFields', () => { ...@@ -17,12 +20,17 @@ describe('JiraIssuesFields', () => {
upgradePlanPath: 'https://gitlab.com', upgradePlanPath: 'https://gitlab.com',
}; };
const createComponent = ({ isInheriting = false, props, ...options } = {}) => { const createComponent = ({
isInheriting = false,
mountFn = mountExtended,
props,
...options
} = {}) => {
store = createStore({ store = createStore({
defaultState: isInheriting ? {} : undefined, defaultState: isInheriting ? {} : undefined,
}); });
wrapper = mountExtended(JiraIssuesFields, { wrapper = mountFn(JiraIssuesFields, {
propsData: { ...defaultProps, ...props }, propsData: { ...defaultProps, ...props },
store, store,
stubs: ['jira-issue-creation-vulnerabilities'], stubs: ['jira-issue-creation-vulnerabilities'],
...@@ -38,12 +46,19 @@ describe('JiraIssuesFields', () => { ...@@ -38,12 +46,19 @@ describe('JiraIssuesFields', () => {
const findEnableCheckboxDisabled = () => const findEnableCheckboxDisabled = () =>
findEnableCheckbox().find('[type=checkbox]').attributes('disabled'); findEnableCheckbox().find('[type=checkbox]').attributes('disabled');
const findProjectKey = () => wrapper.findComponent(GlFormInput); const findProjectKey = () => wrapper.findComponent(GlFormInput);
const findProjectKeyFormGroup = () => wrapper.findByTestId('project-key-form-group');
const findPremiumUpgradeCTA = () => wrapper.findByTestId('premium-upgrade-cta'); const findPremiumUpgradeCTA = () => wrapper.findByTestId('premium-upgrade-cta');
const findUltimateUpgradeCTA = () => wrapper.findByTestId('ultimate-upgrade-cta'); const findUltimateUpgradeCTA = () => wrapper.findByTestId('ultimate-upgrade-cta');
const findJiraForVulnerabilities = () => wrapper.findByTestId('jira-for-vulnerabilities'); const findJiraForVulnerabilities = () => wrapper.findByTestId('jira-for-vulnerabilities');
const findConflictWarning = () => wrapper.findByTestId('conflict-warning-text');
const setEnableCheckbox = async (isEnabled = true) => const setEnableCheckbox = async (isEnabled = true) =>
findEnableCheckbox().vm.$emit('input', isEnabled); findEnableCheckbox().vm.$emit('input', isEnabled);
const assertProjectKeyState = (expectedStateValue) => {
expect(findProjectKey().attributes('state')).toBe(expectedStateValue);
expect(findProjectKeyFormGroup().attributes('state')).toBe(expectedStateValue);
};
describe('template', () => { describe('template', () => {
describe.each` describe.each`
showJiraIssuesIntegration | showJiraVulnerabilitiesIntegration showJiraIssuesIntegration | showJiraVulnerabilitiesIntegration
...@@ -151,19 +166,18 @@ describe('JiraIssuesFields', () => { ...@@ -151,19 +166,18 @@ describe('JiraIssuesFields', () => {
}); });
describe('GitLab issues warning', () => { describe('GitLab issues warning', () => {
const expectedText = 'Consider disabling GitLab issues'; it.each`
gitlabIssuesEnabled | scenario
it('contains warning when GitLab issues is enabled', () => { ${true} | ${'displays conflict warning'}
createComponent(); ${false} | ${'does not display conflict warning'}
`(
expect(wrapper.text()).toContain(expectedText); '$scenario when `gitlabIssuesEnabled` is `$gitlabIssuesEnabled`',
}); ({ gitlabIssuesEnabled }) => {
createComponent({ props: { gitlabIssuesEnabled } });
it('does not contain warning when GitLab issues is disabled', () => {
createComponent({ props: { gitlabIssuesEnabled: false } }); expect(findConflictWarning().exists()).toBe(gitlabIssuesEnabled);
},
expect(wrapper.text()).not.toContain(expectedText); );
});
}); });
describe('Vulnerabilities creation', () => { describe('Vulnerabilities creation', () => {
...@@ -211,5 +225,44 @@ describe('JiraIssuesFields', () => { ...@@ -211,5 +225,44 @@ describe('JiraIssuesFields', () => {
expect(eventHubEmitSpy).toHaveBeenCalledWith(GET_JIRA_ISSUE_TYPES_EVENT); expect(eventHubEmitSpy).toHaveBeenCalledWith(GET_JIRA_ISSUE_TYPES_EVENT);
}); });
}); });
describe('Project key input field', () => {
beforeEach(() => {
createComponent({
props: {
initialProjectKey: '',
initialEnableJiraIssues: true,
},
mountFn: shallowMountExtended,
});
});
it('sets Project Key `state` attribute to `true` by default', () => {
assertProjectKeyState('true');
});
describe('when event hub recieves `VALIDATE_INTEGRATION_FORM_EVENT` event', () => {
describe('with no project key', () => {
it('sets Project Key `state` attribute to `undefined`', async () => {
eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
await wrapper.vm.$nextTick();
assertProjectKeyState(undefined);
});
});
describe('when project key is set', () => {
it('sets Project Key `state` attribute to `true`', async () => {
eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
// set the project key
await findProjectKey().vm.$emit('input', 'AB');
await wrapper.vm.$nextTick();
assertProjectKeyState('true');
});
});
});
});
}); });
}); });
import fileUpload, { getFilename } from '~/lib/utils/file_upload'; import fileUpload, { getFilename, validateImageName } from '~/lib/utils/file_upload';
describe('File upload', () => { describe('File upload', () => {
beforeEach(() => { beforeEach(() => {
...@@ -64,13 +64,23 @@ describe('File upload', () => { ...@@ -64,13 +64,23 @@ describe('File upload', () => {
}); });
describe('getFilename', () => { describe('getFilename', () => {
it('returns first value correctly', () => { it('returns file name', () => {
const event = { const file = new File([], 'test.jpg');
clipboardData: {
getData: () => 'test.png\rtest.txt', expect(getFilename(file)).toBe('test.jpg');
}, });
}; });
expect(getFilename(event)).toBe('test.png'); describe('file name validator', () => {
it('validate file name', () => {
const file = new File([], 'test.jpg');
expect(validateImageName(file)).toBe('test.jpg');
});
it('illegal file name should be rename to image.png', () => {
const file = new File([], 'test<.png');
expect(validateImageName(file)).toBe('image.png');
}); });
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment