Skip to content
Snippets Groups Projects
Commit 948fbf46 authored by 🤖 GitLab Bot 🤖's avatar 🤖 GitLab Bot 🤖
Browse files

Automatic merge of gitlab-org/gitlab master

parents c9361f9f 61803fb1
No related branches found
No related tags found
No related merge requests found
Showing
with 395 additions and 193 deletions
......@@ -18,23 +18,6 @@ Lint/EmptyFile:
- 'db/seeds.rb'
- 'ee/db/geo/seeds.rb'
# Offense count: 13
Lint/MixedRegexpCaptureTypes:
Exclude:
- 'app/models/alert_management/alert.rb'
- 'app/models/integrations/ewm.rb'
- 'app/uploaders/file_uploader.rb'
- 'ee/lib/gitlab/code_owners/reference_extractor.rb'
- 'lib/gitlab/ci/pipeline/expression/lexeme/string.rb'
- 'lib/gitlab/dependency_linker/gemfile_linker.rb'
- 'lib/gitlab/diff/suggestions_parser.rb'
- 'lib/gitlab/github_import/representation/note.rb'
- 'lib/gitlab/metrics/system.rb'
- 'lib/gitlab/request_profiler/profile.rb'
- 'lib/gitlab/slash_commands/issue_move.rb'
- 'lib/gitlab/slash_commands/issue_new.rb'
- 'lib/gitlab/slash_commands/run.rb'
# Offense count: 200
# Cop supports --auto-correct.
Lint/RedundantCopDisableDirective:
......
---
Lint/MixedRegexpCaptureTypes:
Exclude:
- 'app/models/alert_management/alert.rb'
- 'app/models/integrations/ewm.rb'
- 'app/uploaders/file_uploader.rb'
- 'ee/lib/gitlab/code_owners/reference_extractor.rb'
- 'lib/gitlab/ci/pipeline/expression/lexeme/string.rb'
- 'lib/gitlab/dependency_linker/gemfile_linker.rb'
- 'lib/gitlab/diff/suggestions_parser.rb'
- 'lib/gitlab/github_import/representation/note.rb'
- 'lib/gitlab/metrics/system.rb'
- 'lib/gitlab/request_profiler/profile.rb'
- 'lib/gitlab/slash_commands/issue_move.rb'
- 'lib/gitlab/slash_commands/issue_new.rb'
- 'lib/gitlab/slash_commands/run.rb'
......@@ -3,12 +3,12 @@ import { GlTab, GlTabs, GlSprintf, GlLink, GlAlert } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
import SectionLayout from '~/vue_shared/security_configuration/components/section_layout.vue';
import AutoDevOpsAlert from './auto_dev_ops_alert.vue';
import AutoDevOpsEnabledAlert from './auto_dev_ops_enabled_alert.vue';
import { AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY } from './constants';
import FeatureCard from './feature_card.vue';
import TrainingProviderList from './training_provider_list.vue';
import SectionLayout from './section_layout.vue';
import UpgradeBanner from './upgrade_banner.vue';
export const i18n = {
......@@ -173,7 +173,7 @@ export default {
@dismiss="dismissAutoDevopsEnabledAlert"
/>
<section-layout :heading="$options.i18n.securityTesting">
<section-layout class="gl-border-b-0" :heading="$options.i18n.securityTesting">
<template #description>
<p>
<span data-testid="latest-pipeline-info-security">
......
<script>
import SectionLoader from './section_loader.vue';
export default {
name: 'SectionLayout',
components: {
SectionLoader,
},
props: {
heading: {
type: String,
required: true,
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
},
};
</script>
<template>
<div class="row gl-line-height-20 gl-pt-6">
<div class="col-lg-4">
<div class="row gl-m-0 gl-border-b gl-line-height-20 gl-py-6">
<div class="col-lg-4 gl-pl-0 gl-pr-9">
<h2 class="gl-font-size-h2 gl-mt-0">{{ heading }}</h2>
<slot name="description"></slot>
</div>
<div class="col-lg-8">
<slot name="features"></slot>
<div class="col-lg-8 gl-pr-0 gl-pl-0">
<section-loader v-if="isLoading" />
<slot v-else name="features"></slot>
</div>
</div>
</template>
<script>
import { GlCard, GlSkeletonLoader } from '@gitlab/ui';
export default {
name: 'SectionLoader',
components: {
GlCard,
GlSkeletonLoader,
},
};
</script>
<template>
<div>
<gl-skeleton-loader :width="1248" :height="180">
<rect x="0" y="0" width="100" height="15" rx="4" />
<rect x="0" y="24" width="460" height="32" rx="4" />
<rect x="0" y="71" width="100" height="15" rx="4" />
<rect x="0" y="95" width="460" height="72" rx="4" />
</gl-skeleton-loader>
<gl-card v-for="i in 2" :key="i" class="gl-mb-5">
<template #header>
<gl-skeleton-loader :width="1248" :height="15">
<rect x="0" y="0" width="300" height="15" rx="4" />
</gl-skeleton-loader>
</template>
<gl-skeleton-loader :width="1248" :height="15">
<rect x="0" y="0" width="600" height="15" rx="4" />
</gl-skeleton-loader>
<gl-skeleton-loader :width="1248" :height="15">
<rect x="0" y="0" width="300" height="15" rx="4" />
</gl-skeleton-loader>
</gl-card>
</div>
</template>
......@@ -136,9 +136,49 @@ These shortcuts are available when browsing the files in a project (go to
These shortcuts are available when editing a file with the [Web IDE](project/web_ide/index.md):
| macOS shortcut | Windows shortcut | Description |
|---------------------------------------|---------------------|-------------|
| <kbd>Command</kbd> + <kbd>p</kbd> | <kbd>Control</kbd> + <kbd>p</kbd> | Search for, and then open another file for editing. |
| macOS shortcut | Windows shortcut | Description |
|---------------------------------|---------------------|-------------|
| <kbd>Option</kbd> + <kbd>Command</kbd> + <kbd></kbd> | | Add cursor above |
| <kbd>Option</kbd> + <kbd>Command</kbd> + <kbd></kbd> | | Add cursor below |
| <kbd>Shift</kbd> + <kbd>Option</kbd> + <kbd>I</kbd> | | Add cursors to line ends |
| <kbd>Command</kbd> + <kbd>K</kbd>, <kbd>Command</kbd> + <kbd>C</kbd> | <kbd>Control</kbd> + <kbd>K</kbd>, <kbd>Control</kbd> + <kbd>C</kbd> _or_ <kbd>Control</kbd> + <kbd>/</kbd> | Add line comment |
| <kbd>Command</kbd> + <kbd>D</kbd> | | Add selection to next find match |
| <kbd>Command</kbd> + <kbd>F2</kbd> | | Change all occurrences |
| <kbd>F1</kbd> | | Command palette |
| <kbd>Shift</kbd> + <kbd>Option</kbd> + <kbd></kbd> | | Copy line down |
| <kbd>Shift</kbd> + <kbd>Option</kbd> + <kbd></kbd> | | Copy line up |
| <kbd>Command</kbd> + <kbd>U</kbd> | | Cursor undo |
| <kbd>Command</kbd> + <kbd>Backspace<kbd> | | Delete all left |
| <kbd>Control</kbd> + <kbd>K</kbd> | | Delete all right |
| <kbd>Shift</kbd> + <kbd>Command</kbd> <kbd>K</kbd> | | Delete line |
| <kbd>Control</kbd> + <kbd>Shift</kbd> + <kbd>Command</kbd> + <kbd></kbd> | | Expand selection |
| <kbd>Command</kbd> + <kbd>P</kbd> | | File finder |
| <kbd>Command</kbd> + <kbd>F</kbd> | | Find |
| <kbd>Enter</kbd> | | Find next |
| <kbd>Command</kbd> + <kbd>F3</kbd> | | Find next selection |
| <kbd>Shift</kbd> + <kbd>Enter</kbd> + <kbd>F3</kbd> | | Find previous |
| <kbd>Shift</kbd> + <kbd>Command</kbd> + <kbd>F3</kbd> | | Find previous selection |
| <kbd>Command</kbd> + <kbd>E</kbd> | | Find with selection |
| <kbd>Option</kbd> + <kbd>Command</kbd> + <kbd>&#91;</kbd> | | Fold |
| <kbd>Command</kbd> + <kbd>K</kbd> , <kbd>Command</kbd> + <kbd>O</kbd> | | Fold all |
| <kbd>Command</kbd> + <kbd>K</kbd> , <kbd>Command</kbd> + <kbd>/</kbd> | | Fold all block comments |
| <kbd>Command</kbd> + <kbd>K</kbd> , <kbd>Command</kbd> + <kbd>8</kbd> | | Fold all regions |
| <kbd>Command</kbd> + <kbd>K</kbd> , <kbd>Command</kbd> + <kbd>-</kbd> | | Fold all regions except selected |
| <kbd>Command</kbd> + <kbd>K</kbd> , <kbd>Command</kbd> + <kbd>1</kbd> | | Fold level 1 |
| <kbd>Command</kbd> + <kbd>K</kbd> , <kbd>Command</kbd> + <kbd>2</kbd> | | Fold level 2 |
| <kbd>Command</kbd> + <kbd>K</kbd> , <kbd>Command</kbd> + <kbd>3</kbd> | | Fold level 3 |
| <kbd>Command</kbd> + <kbd>K</kbd> , <kbd>Command</kbd> + <kbd>4</kbd> | | Fold level 4 |
| <kbd>Command</kbd> + <kbd>K</kbd> , <kbd>Command</kbd> + <kbd>5</kbd> | | Fold level 5 |
| <kbd>Command</kbd> + <kbd>K</kbd> , <kbd>Command</kbd> + <kbd>6</kbd> | | Fold level 6 |
| <kbd>Command</kbd> + <kbd>K</kbd> , <kbd>Command</kbd> + <kbd>7</kbd> | | Fold level 7 |
| <kbd>Command</kbd> + <kbd>K</kbd> + <kbd>Command</kbd> + <kbd>&#91;</kbd> | | Fold recursively |
| <kbd>Shift</kbd> + <kbd>Command</kbd> + <kbd>\ </kbd> | | Go to bracket |
| <kbd>Control</kbd> + <kbd>G</kbd> | | Go to line or column |
| <kbd>Option</kbd> + <kbd>F8</kbd> | | Go to next problem (error, warning, info) |
| <kbd>F8</kbd> | | Go to next problem in files (error, warning, info) |
| <kbd>Shift</kbd> + <kbd>Option</kbd> + <kbd>F8</kbd> | | Go to previous problem (error, warning, info) |
| <kbd>Shift</kbd> + <kbd>F8</kbd> | | Go to previous problem in files (error, warning, info) |
| <kbd>Command</kbd> + <kbd>p</kbd> | <kbd>Control</kbd> + <kbd>p</kbd> | Search for, and then open another file for editing. |
| <kbd>Command</kbd> + <kbd>Enter</kbd> | <kbd>Control</kbd> + <kbd>Enter</kbd> | Commit (when editing the commit message). |
### Repository graph
......
......@@ -8,6 +8,9 @@ export const HELP_PAGE_PATH = helpPagePath('user/application_security/dast/index
export const LEARN_MORE_TEXT = s__(
'OnDemandScans|%{learnMoreLinkStart}Learn more about on-demand scans%{learnMoreLinkEnd}.',
);
export const DAST_CONFIGURATION_HELP_PATH = helpPagePath('user/application_security/dast/index', {
anchor: 'on-demand-scans',
});
export const PIPELINE_TABS_KEYS = ['all', 'running', 'finished', 'scheduled', 'saved'];
export const PIPELINES_PER_PAGE = 20;
......
......@@ -2,13 +2,12 @@
import {
GlAlert,
GlButton,
GlCard,
GlForm,
GlIcon,
GlFormGroup,
GlFormInput,
GlFormTextarea,
GlLink,
GlSkeletonLoader,
GlSprintf,
GlSafeHtmlDirective,
GlTooltipDirective,
......@@ -26,7 +25,8 @@ import RefSelector from '~/ref/components/ref_selector.vue';
import { REF_TYPE_BRANCHES } from '~/ref/constants';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import validation from '~/vue_shared/directives/validation';
import { HELP_PAGE_PATH } from 'ee/on_demand_scans/constants';
import { HELP_PAGE_PATH, DAST_CONFIGURATION_HELP_PATH } from 'ee/on_demand_scans/constants';
import SectionLayout from '~/vue_shared/security_configuration/components/section_layout.vue';
import dastProfileCreateMutation from '../graphql/dast_profile_create.mutation.graphql';
import dastProfileUpdateMutation from '../graphql/dast_profile_update.mutation.graphql';
import {
......@@ -69,6 +69,46 @@ export default {
saveAndRunScanBtnId: 'scan-submit-button',
saveScanBtnId: 'scan-save-button',
helpPagePath: HELP_PAGE_PATH,
dastConfigurationHelpPath: DAST_CONFIGURATION_HELP_PATH,
i18n: {
newOnDemandScanHeader: s__('OnDemandScans|New on-demand scan'),
newOnDemandScanHeaderDescription: s__(
'OnDemandScans|On-demand scans run outside the DevOps cycle and find vulnerabilities in your projects. %{learnMoreLinkStart}Learn more%{learnMoreLinkEnd}',
),
editOnDemandScanHeader: s__('OnDemandScans|Edit on-demand scan'),
branchSelectorHelpText: s__(
'OnDemandScans|Scan results will be associated with the selected branch.',
),
dastConfigurationHeader: s__('OnDemandScans|DAST configuration'),
dastConfigurationDescription: s__(
"OnDemandScans|DAST scans for vulnerabilities in your project's running application, website, or API. For details of all configuration options, see the %{linkStart}GitLab DAST documentation%{linkEnd}.",
),
scanConfigurationNameLabel: s__('OnDemandScans|Scan name'),
scanConfigurationNamePlaceholder: s__('OnDemandScans|My daily scan'),
scanConfigurationDescriptionLabel: s__('OnDemandScans|Description (optional)'),
scanConfigurationDescriptionPlaceholder: s__(
`OnDemandScans|For example: Tests the login page for SQL injections`,
),
scanConfigurationHeader: s__('OnDemandScans|Scan configuration'),
scanConfigurationDescription: s__(
'OnDemandScans|Define the fundamental configuration options for your on-demand scan.',
),
scanConfigurationDefaultBranchLabel: s__(
'OnDemandScans|You must create a repository within your project to run an on-demand scan.',
),
scanTypeHeader: s__('OnDemandScans|Scan type'),
scanTypeText: s__('OnDemandScans|Dynamic Application Security Testing (DAST)'),
scanTypeTooltip: s__(
'OnDemandScans|Analyze a deployed version of your web application for known vulnerabilities by examining it from the outside in. DAST works by simulating external attacks on your application while it is running.',
),
scanScheduleHeader: s__('OnDemandScans|Scan schedule'),
scanScheduleDescription: s__(
'OnDemandScans|Add a schedule to run this scan at a specified date and time or on a recurring basis. Scheduled scans are automatically saved to scan library.',
),
saveAndRunScanButton: s__('OnDemandScans|Save and run scan'),
saveScanButton: s__('OnDemandScans|Save scan'),
cancelButton: s__('OnDemandScans|Cancel'),
},
components: {
RefSelector,
ProfileConflictAlert,
......@@ -77,15 +117,15 @@ export default {
ScanSchedule,
GlAlert,
GlButton,
GlCard,
GlForm,
GlIcon,
GlFormGroup,
GlFormInput,
GlFormTextarea,
GlLink,
GlSkeletonLoader,
GlSprintf,
LocalStorageSync,
SectionLayout,
},
directives: {
SafeHtml: GlSafeHtmlDirective,
......@@ -153,8 +193,8 @@ export default {
},
title() {
return this.isEdit
? s__('OnDemandScans|Edit on-demand DAST scan')
: s__('OnDemandScans|New on-demand DAST scan');
? this.$options.i18n.editOnDemandScanHeader
: this.$options.i18n.newOnDemandScanHeader;
},
selectedScannerProfile() {
return this.selectedScannerProfileId
......@@ -329,21 +369,12 @@ export default {
:value="formFieldValues"
@input="updateFromStorage"
/>
<header class="gl-mb-6">
<header class="gl-border-b gl-pb-6">
<div class="gl-mt-6 gl-display-flex">
<h2 class="gl-flex-grow-1 gl-my-0">{{ title }}</h2>
<gl-button :href="onDemandScansPath" data-testid="manage-profiles-link">
{{ s__('OnDemandScans|Manage DAST scans') }}
</gl-button>
<h1 class="gl-font-size-h1 gl-flex-grow-1 gl-my-0">{{ title }}</h1>
</div>
<p>
<gl-sprintf
:message="
s__(
'OnDemandScans|On-demand scans run outside the DevOps cycle and find vulnerabilities in your projects. %{learnMoreLinkStart}Learn more%{learnMoreLinkEnd}',
)
"
>
<gl-sprintf :message="$options.i18n.newOnDemandScanHeaderDescription">
<template #learnMoreLink="{ content }">
<gl-link :href="$options.helpPagePath" data-testid="help-page-link">
{{ content }}
......@@ -366,104 +397,128 @@ export default {
<li v-for="error in errors" :key="error" v-safe-html="error"></li>
</ul>
</gl-alert>
<section-layout
v-if="!failedToLoadProfiles"
:heading="$options.i18n.scanConfigurationHeader"
:is-loading="isLoadingProfiles"
>
<template #description>
<p>{{ $options.i18n.scanConfigurationDescription }}</p>
</template>
<template #features>
<gl-form-group
class="gl-mb-6"
:label="$options.i18n.scanConfigurationNameLabel"
:invalid-feedback="form.fields.name.feedback"
>
<gl-form-input
v-model="form.fields.name.value"
v-validation:[form.showValidation]
data-testid="dast-scan-name-input"
type="text"
:placeholder="$options.i18n.scanConfigurationNamePlaceholder"
:state="form.fields.name.state"
name="name"
required
/>
</gl-form-group>
<template v-if="isLoadingProfiles">
<gl-skeleton-loader :width="1248" :height="180">
<rect x="0" y="0" width="100" height="15" rx="4" />
<rect x="0" y="24" width="460" height="32" rx="4" />
<rect x="0" y="71" width="100" height="15" rx="4" />
<rect x="0" y="95" width="460" height="72" rx="4" />
</gl-skeleton-loader>
<gl-card v-for="i in 2" :key="i" class="gl-mb-5">
<template #header>
<gl-skeleton-loader :width="1248" :height="15">
<rect x="0" y="0" width="300" height="15" rx="4" />
</gl-skeleton-loader>
</template>
<gl-skeleton-loader :width="1248" :height="15">
<rect x="0" y="0" width="600" height="15" rx="4" />
</gl-skeleton-loader>
<gl-skeleton-loader :width="1248" :height="15">
<rect x="0" y="0" width="300" height="15" rx="4" />
</gl-skeleton-loader>
</gl-card>
</template>
<template v-else-if="!failedToLoadProfiles">
<gl-form-group
:label="s__('OnDemandScans|Scan name')"
:invalid-feedback="form.fields.name.feedback"
>
<gl-form-input
v-model="form.fields.name.value"
v-validation:[form.showValidation]
class="mw-460"
data-testid="dast-scan-name-input"
type="text"
:placeholder="s__('OnDemandScans|My daily scan')"
:state="form.fields.name.state"
name="name"
required
/>
</gl-form-group>
<gl-form-group :label="s__('OnDemandScans|Description (optional)')">
<gl-form-textarea
v-model="form.fields.description.value"
class="mw-460"
data-testid="dast-scan-description-input"
:placeholder="s__(`OnDemandScans|For example: Tests the login page for SQL injections`)"
name="description"
:state="form.fields.description.state"
/>
</gl-form-group>
<gl-form-group class="gl-mb-6" :label="$options.i18n.scanConfigurationDescriptionLabel">
<gl-form-textarea
v-model="form.fields.description.value"
data-testid="dast-scan-description-input"
:placeholder="$options.i18n.scanConfigurationDescriptionPlaceholder"
name="description"
:state="form.fields.description.state"
/>
</gl-form-group>
<gl-form-group :label="__('Branch')">
<ref-selector
v-model="selectedBranch"
data-testid="dast-scan-branch-input"
no-flip
:enabled-ref-types="$options.enabledRefTypes"
:project-id="projectPath"
:translations="{
dropdownHeader: __('Select a branch'),
searchPlaceholder: __('Search'),
noRefSelected: __('No available branches'),
noResults: __('No available branches'),
}"
<gl-form-group class="gl-mb-6" :label="$options.i18n.scanTypeHeader">
<span>{{ $options.i18n.scanTypeText }}</span>
<gl-icon
v-gl-tooltip="$options.i18n.scanTypeTooltip"
name="question-o"
class="gl-ml-2 gl-link gl-cursor-pointer"
/>
</gl-form-group>
<gl-form-group class="gl-mb-3" :label="__('Branch')">
<small class="form-text text-gl-muted gl-mt-0 gl-mb-5">
{{ $options.i18n.branchSelectorHelpText }}
</small>
<ref-selector
v-model="selectedBranch"
data-testid="dast-scan-branch-input"
no-flip
:enabled-ref-types="$options.enabledRefTypes"
:project-id="projectPath"
:translations="{
dropdownHeader: __('Select a branch'),
searchPlaceholder: __('Search'),
noRefSelected: __('No available branches'),
noResults: __('No available branches'),
}"
/>
<div v-if="!defaultBranch" class="gl-text-red-500 gl-mt-3">
{{ $options.i18n.scanConfigurationDefaultBranchLabel }}
</div>
</gl-form-group>
</template>
</section-layout>
<section-layout
v-if="!failedToLoadProfiles"
:heading="$options.i18n.dastConfigurationHeader"
:is-loading="isLoadingProfiles"
>
<template #description>
<gl-sprintf :message="$options.i18n.dastConfigurationDescription">
<template #link="{ content }">
<gl-link :href="$options.dastConfigurationHelpPath">{{ content }}</gl-link>
</template>
</gl-sprintf>
</template>
<template #features>
<scanner-profile-selector
v-model="selectedScannerProfileId"
class="gl-mb-6"
:profiles="scannerProfiles"
:selected-profile="selectedScannerProfile"
:has-conflict="hasProfilesConflict"
:dast-scan-id="dastScanId"
/>
<div v-if="!defaultBranch" class="gl-text-red-500 gl-mt-3">
{{
s__(
'OnDemandScans|You must create a repository within your project to run an on-demand scan.',
)
}}
</div>
</gl-form-group>
<scanner-profile-selector
v-model="selectedScannerProfileId"
class="gl-mb-5"
:profiles="scannerProfiles"
:selected-profile="selectedScannerProfile"
:has-conflict="hasProfilesConflict"
:dast-scan-id="dastScanId"
/>
<site-profile-selector
v-model="selectedSiteProfileId"
class="gl-mb-5"
:profiles="siteProfiles"
:selected-profile="selectedSiteProfile"
:has-conflict="hasProfilesConflict"
:dast-scan-id="dastScanId"
/>
<site-profile-selector
v-model="selectedSiteProfileId"
class="gl-mb-3"
:profiles="siteProfiles"
:selected-profile="selectedSiteProfile"
:has-conflict="hasProfilesConflict"
:dast-scan-id="dastScanId"
/>
</template>
</section-layout>
<scan-schedule v-model="profileSchedule" class="gl-mb-5" />
<section-layout
v-if="!failedToLoadProfiles"
:heading="$options.i18n.scanScheduleHeader"
:is-loading="isLoadingProfiles"
>
<template #description>
<p>{{ $options.i18n.scanScheduleDescription }}</p>
</template>
<template #features>
<scan-schedule v-model="profileSchedule" />
<profile-conflict-alert
v-if="hasProfilesConflict"
data-testid="on-demand-scans-profiles-conflict-alert"
/>
<profile-conflict-alert
v-if="hasProfilesConflict"
data-testid="on-demand-scans-profiles-conflict-alert"
/>
</template>
</section-layout>
<div class="gl-mt-6 gl-pt-6">
<div v-if="!failedToLoadProfiles">
<div class="gl-pt-6">
<gl-button
type="submit"
variant="confirm"
......@@ -472,7 +527,7 @@ export default {
:disabled="isSubmitButtonDisabled"
:loading="loading === $options.saveAndRunScanBtnId"
>
{{ s__('OnDemandScans|Save and run scan') }}
{{ $options.i18n.saveAndRunScanButton }}
</gl-button>
<gl-button
variant="confirm"
......@@ -482,16 +537,16 @@ export default {
:loading="loading === $options.saveScanBtnId"
@click="onSubmit({ runAfter: false, button: $options.saveScanBtnId })"
>
{{ s__('OnDemandScans|Save scan') }}
{{ $options.i18n.saveScanButton }}
</gl-button>
<gl-button
data-testid="on-demand-scan-cancel-button"
:disabled="Boolean(loading)"
@click="onCancelClicked"
>
{{ __('Cancel') }}
{{ $options.i18n.cancelButton }}
</gl-button>
</div>
</template>
</div>
</gl-form>
</template>
<script>
import { GlCard, GlDatepicker, GlFormCheckbox, GlFormGroup } from '@gitlab/ui';
import { GlDatepicker, GlFormGroup, GlToggle } from '@gitlab/ui';
import { s__ } from '~/locale';
import DropdownInput from 'ee/security_configuration/components/dropdown_input.vue';
import {
dateAndTimeToISOString,
......@@ -12,10 +13,16 @@ import { toGraphQLCadence, fromGraphQLCadence } from '../utils';
export default {
name: 'ScanSchedule',
i18n: {
scanScheduleToggleText: s__('OnDemandScans|Enable scan schedule'),
scanStartTimeLabel: s__('OnDemandScans|Start time'),
scanScheduleRepeatLabel: s__('OnDemandScans|at'),
scanScheduleRepeatDefaultLabel: s__('OnDemandScans|Repeats'),
scanScheduleTimezoneLabel: s__('OnDemandScans|Timezone'),
},
components: {
GlCard,
GlDatepicker,
GlFormCheckbox,
GlToggle,
GlFormGroup,
DropdownInput,
TimezoneDropdown,
......@@ -91,30 +98,23 @@ export default {
</script>
<template>
<gl-card class="gl-bg-gray-10">
<div class="row">
<div class="col-12 col-md-6">
<gl-form-checkbox v-model="form.isScheduledScan" class="gl-mb-3" @input="handleInput">
<span class="gl-font-weight-bold">{{ s__('OnDemandScans|Schedule scan') }}</span>
</gl-form-checkbox>
<gl-form-group
class="gl-pl-6"
data-testid="profile-schedule-form-group"
:disabled="!form.isScheduledScan"
>
<div class="row">
<div class="col-12 col-md-6">
<gl-toggle
v-model="form.isScheduledScan"
class="gl-mb-3"
:label="$options.i18n.scanScheduleToggleText"
@change="handleInput"
/>
<transition name="fade">
<gl-form-group v-if="form.isScheduledScan" data-testid="profile-schedule-form-group">
<div class="gl-font-weight-bold gl-mb-3">
{{ s__('OnDemandScans|Start time') }}
{{ $options.i18n.scanStartTimeLabel }}
</div>
<timezone-dropdown
v-model="timezone"
:timezone-data="timezones"
:disabled="!form.isScheduledScan"
@input="handleInput"
/>
<div class="gl-display-flex gl-align-items-center">
<div class="gl-display-flex gl-align-items-center gl-mb-5">
<gl-datepicker v-model="form.startDate" @input="handleInput" />
<span class="gl-px-3">
{{ __('at') }}
{{ $options.i18n.scanScheduleRepeatLabel }}
</span>
<input
v-model="form.startTime"
......@@ -123,19 +123,30 @@ export default {
@input="handleInput"
/>
</div>
<div class="gl-font-weight-bold gl-mb-3">
{{ $options.i18n.scanScheduleTimezoneLabel }}
</div>
<timezone-dropdown
v-model="timezone"
:timezone-data="timezones"
:disabled="!form.isScheduledScan"
@input="handleInput"
/>
<dropdown-input
v-model="form.cadence"
:label="__('Repeats')"
:default-text="__('Repeats')"
:label="$options.i18n.scanScheduleRepeatDefaultLabel"
:default-text="$options.i18n.scanScheduleRepeatDefaultLabel"
:options="$options.SCAN_CADENCE_OPTIONS"
:disabled="!form.isScheduledScan"
field="repeat-input"
class="gl-mt-5"
data-testid="schedule-cadence-input"
@input="handleInput"
/>
</gl-form-group>
</div>
</transition>
</div>
</gl-card>
</div>
</template>
......@@ -24,8 +24,8 @@ export default {
computed: {
summary() {
if (
this.collapsedData[0].new_licenses.length > 0 &&
this.collapsedData[0].removed_licenses.length > 0
this.collapsedData[0].new_licenses?.length > 0 &&
this.collapsedData[0].removed_licenses?.length > 0
) {
const newLicenses = n__(
'%d new license',
......@@ -40,13 +40,13 @@ export default {
);
return sprintf(__(`License Compliance detected ${newLicenses} and ${removedLicenses}`));
} else if (this.collapsedData[0].new_licenses.length > 0) {
} else if (this.collapsedData[0].new_licenses?.length > 0) {
return n__(
'LicenseCompliance|License Compliance detected %d new license',
'LicenseCompliance|License Compliance detected %d new licenses',
this.collapsedData[0].new_licenses.length,
);
} else if (this.collapsedData[0].removed_licenses.length > 0) {
} else if (this.collapsedData[0].removed_licenses?.length > 0) {
return n__(
'LicenseCompliance|License Compliance detected %d removed license',
'LicenseCompliance|License Compliance detected %d removed licenses',
......@@ -57,7 +57,7 @@ export default {
return s__('LicenseCompliance|License Compliance detected no new licenses');
},
statusIcon() {
if (this.collapsedData[0].new_licenses.length === 0) {
if (this.collapsedData[0].new_licenses?.length === 0) {
return EXTENSION_ICONS.success;
}
return EXTENSION_ICONS.warning;
......
......@@ -91,6 +91,9 @@ export default {
!this.shouldShowExtension
);
},
shouldShowLicenseComplianceExtension() {
return window.gon?.features?.refactorLicenseComplianceExtension;
},
hasLoadPerformanceMetrics() {
return (
this.mr.loadPerformanceMetrics?.degraded?.length > 0 ||
......@@ -220,7 +223,7 @@ export default {
},
methods: {
registerLicenseCompliance() {
if (this.shouldShowExtension) {
if (this.shouldShowLicenseComplianceExtension) {
registerExtension(licenseComplianceExtension);
}
},
......@@ -475,7 +478,7 @@ export default {
</mr-widget-enable-feature-prompt>
<mr-widget-licenses
v-if="shouldRenderLicenseReport && !shouldShowExtension"
v-if="shouldRenderLicenseReport && !shouldShowLicenseComplianceExtension"
:api-url="mr.licenseScanning.managed_licenses_path"
:approvals-api-path="mr.apiApprovalsPath"
:licenses-api-path="licensesApiPath"
......
......@@ -19,6 +19,7 @@ module MergeRequestsController
push_frontend_feature_flag(:refactor_mr_widgets_extensions, @project, default_enabled: :yaml)
push_frontend_feature_flag(:refactor_mr_widget_test_summary, @project, default_enabled: :yaml)
push_frontend_feature_flag(:refactor_mr_widgets_extensions_user, current_user, default_enabled: :yaml)
push_frontend_feature_flag(:refactor_license_compliance_extension, @project, default_enabled: :yaml)
push_frontend_feature_flag(:status_checks_add_status_field, default_enabled: :yaml)
push_frontend_feature_flag(:lc_remove_legacy_approval_status, @project, default_enabled: :yaml)
end
......
- add_to_breadcrumbs s_('OnDemandScans|On-demand Scans'), project_on_demand_scans_path(@project, anchor: 'saved')
- breadcrumb_title s_('OnDemandScans|Edit on-demand DAST scan')
- page_title s_('OnDemandScans|Edit on-demand DAST scan')
- @content_class = "limit-container-width" unless fluid_layout
#js-on-demand-scans-form{ data: on_demand_scans_form_data(@project).merge({ dast_scan: @dast_profile.to_json }) }
- add_to_breadcrumbs s_('OnDemandScans|On-demand Scans'), project_on_demand_scans_path(@project, anchor: 'saved')
- breadcrumb_title s_('OnDemandScans|New on-demand DAST scan')
- page_title s_('OnDemandScans|New on-demand DAST scan')
- @content_class = "limit-container-width" unless fluid_layout
#js-on-demand-scans-form{ data: on_demand_scans_form_data(@project) }
---
name: refactor_license_compliance_extension
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84128
rollout_issue_url:
milestone: '15.0'
type: development
group: group::composition analysis
default_enabled: false
......@@ -25,6 +25,7 @@
stub_licensed_features(external_status_checks: true)
stub_feature_flags(refactor_mr_widgets_extensions: false)
stub_feature_flags(refactor_mr_widgets_extensions_user: false)
stub_feature_flags(refactor_license_compliance_extension: false)
end
context 'user is authorized' do
......
......@@ -26,8 +26,9 @@
end
it 'shows new scan page', :aggregate_failures, :js do
expect(page).to have_content 'New on-demand DAST scan'
expect(page).to have_link 'Manage DAST scans'
expect(page).to have_content 'New on-demand scan'
expect(page).to have_content 'Scan configuration'
expect(page).to have_content 'DAST configuration'
expect(page).to have_button 'Save and run scan'
expect(page).to have_button 'Save scan'
......
......@@ -9,6 +9,8 @@ import OnDemandScansForm from 'ee/on_demand_scans_form/components/on_demand_scan
import ScannerProfileSelector from 'ee/on_demand_scans_form/components/profile_selector/scanner_profile_selector.vue';
import SiteProfileSelector from 'ee/on_demand_scans_form/components/profile_selector/site_profile_selector.vue';
import ScanSchedule from 'ee/on_demand_scans_form/components/scan_schedule.vue';
import SectionLayout from '~/vue_shared/security_configuration/components/section_layout.vue';
import SectionLoader from '~/vue_shared/security_configuration/components/section_loader.vue';
import dastProfileCreateMutation from 'ee/on_demand_scans_form/graphql/dast_profile_create.mutation.graphql';
import dastProfileUpdateMutation from 'ee/on_demand_scans_form/graphql/dast_profile_update.mutation.graphql';
import dastScannerProfilesQuery from 'ee/security_configuration/dast_profiles/graphql/dast_scanner_profiles.query.graphql';
......@@ -177,6 +179,8 @@ describe('OnDemandScansForm', () => {
RefSelector: RefSelectorStub,
LocalStorageSync,
ScanSchedule: true,
SectionLayout,
SectionLoader,
},
},
{ ...options, localVue, apolloProvider },
......@@ -214,7 +218,7 @@ describe('OnDemandScansForm', () => {
it('renders properly', () => {
createComponent();
expect(wrapper.text()).toContain('New on-demand DAST scan');
expect(wrapper.text()).toContain('New on-demand scan');
expect(wrapper.findComponent(ScanSchedule).exists()).toBe(true);
});
......@@ -289,7 +293,7 @@ describe('OnDemandScansForm', () => {
});
it('sets the title properly', () => {
expect(wrapper.text()).toContain('Edit on-demand DAST scan');
expect(wrapper.text()).toContain('Edit on-demand scan');
});
it('populates the fields with passed values', () => {
......
import { GlDatepicker, GlFormCheckbox, GlFormGroup } from '@gitlab/ui';
import { GlDatepicker, GlFormGroup, GlToggle } from '@gitlab/ui';
import { merge } from 'lodash';
import { nextTick } from 'vue';
import mockTimezones from 'test_fixtures/timezones/full.json';
......@@ -15,7 +15,7 @@ describe('ScanSchedule', () => {
let wrapper;
// Finders
const findCheckbox = () => wrapper.findComponent(GlFormCheckbox);
const findToggle = () => wrapper.findComponent(GlToggle);
const findProfileScheduleFormGroup = () => wrapper.findByTestId('profile-schedule-form-group');
const findTimezoneDropdown = () => wrapper.findComponent(TimezoneDropdown);
const findDatepicker = () => wrapper.findComponent(GlDatepicker);
......@@ -42,8 +42,8 @@ describe('ScanSchedule', () => {
GlFormGroup: stubComponent(GlFormGroup, {
props: ['disabled'],
}),
GlFormCheckbox: stubComponent(GlFormCheckbox, {
props: ['checked'],
GlToggle: stubComponent(GlToggle, {
props: ['value'],
}),
TimezoneDropdown: stubComponent(TimezoneDropdown, {
props: ['disabled', 'timezoneData', 'value'],
......@@ -64,14 +64,17 @@ describe('ScanSchedule', () => {
createComponent();
});
it('by default, checkbox is unchecked and fields are disabled', () => {
expect(findCheckbox().props('checked')).toBe(false);
expect(findProfileScheduleFormGroup().props('disabled')).toBe(true);
expect(findTimezoneDropdown().props('disabled')).toBe(true);
expect(findCadenceInput().props('disabled')).toBe(true);
it('by default, checkbox is unchecked and fields are hidden', () => {
expect(findToggle().props('value')).toBe(false);
expect(findProfileScheduleFormGroup().exists()).toBe(false);
expect(findTimezoneDropdown().exists()).toBe(false);
expect(findCadenceInput().exists()).toBe(false);
});
it('initializes timezone dropdown properly', () => {
it('initializes timezone dropdown properly', async () => {
findToggle().vm.$emit('change', true);
await nextTick();
const timezoneDropdown = findTimezoneDropdown();
expect(timezoneDropdown.props('timezoneData')).toEqual(mockTimezones);
......@@ -82,14 +85,14 @@ describe('ScanSchedule', () => {
describe('once schedule is activated', () => {
beforeEach(() => {
createComponent();
findCheckbox().vm.$emit('input', true);
findToggle().vm.$emit('change', true);
});
it('enables fields', () => {
expect(findTimezoneDropdown().attributes('disabled')).toBeUndefined();
expect(findProfileScheduleFormGroup().props('disabled')).toBe(false);
expect(findTimezoneDropdown().props('disabled')).toBe(false);
expect(findCadenceInput().props('disabled')).toBe(false);
it('shows fields', () => {
expect(findTimezoneDropdown().exists()).toBe(true);
expect(findProfileScheduleFormGroup().exists()).toBe(true);
expect(findTimezoneDropdown().exists()).toBe(true);
expect(findCadenceInput().exists()).toBe(true);
});
it('emits input payload', () => {
......@@ -142,7 +145,7 @@ describe('ScanSchedule', () => {
});
it('deactives schedule when checkbox is unchecked', async () => {
findCheckbox().vm.$emit('input', false);
findToggle().vm.$emit('change', false);
await nextTick();
expect(wrapper.emitted().input).toHaveLength(2);
......@@ -175,7 +178,7 @@ describe('ScanSchedule', () => {
},
});
expect(findCheckbox().props('checked')).toBe(true);
expect(findToggle().props('value')).toBe(true);
expect(findDatepicker().props('value')).toEqual(new Date(schedule.startsAt));
expect(findTimeInput().element.value).toBe('08:45');
expect(findCadenceInput().props('value')).toBe(SCAN_CADENCE_OPTIONS[3].value);
......
......@@ -43,6 +43,8 @@ class ErrorTracking::ClientKeys < ::API::Base
delete '/client_keys/:key_id' do
key = user_project.error_tracking_client_keys.find(params[:key_id])
key.destroy!
present key, with: Entities::ErrorTracking::ClientKey
end
end
end
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment