Commit 05a19b52 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'djadmin-dast-selector-ui' into 'master'

DAST Profile Selector UI changes

See merge request gitlab-org/gitlab!51957
parents 57c07954 fa47fcd2
......@@ -106,6 +106,9 @@ export default {
dastSiteValidationDocsPath: {
default: '',
},
profilesLibraryPath: {
default: '',
},
},
props: {
helpPagePath: {
......@@ -165,6 +168,11 @@ export default {
? s__('OnDemandScans|Edit on-demand DAST scan')
: s__('OnDemandScans|New on-demand DAST scan');
},
manageProfilesLabel() {
return this.glFeatures.dastSavedScans
? s__('OnDemandScans|Manage DAST scans')
: s__('OnDemandScans|Manage profiles');
},
selectedScannerProfile() {
return this.selectedScannerProfileId
? this.scannerProfiles.find(({ id }) => id === this.selectedScannerProfileId)
......@@ -304,7 +312,12 @@ export default {
<template>
<gl-form novalidate @submit.prevent="onSubmit()">
<header class="gl-mb-6">
<h2>{{ title }}</h2>
<div class="gl-mt-6 gl-display-flex">
<h2 class="gl-flex-grow-1 gl-my-0">{{ title }}</h2>
<gl-button :href="profilesLibraryPath" data-testid="manage-profiles-link">
{{ manageProfilesLabel }}
</gl-button>
</div>
<p>
<gl-sprintf
:message="
......
<script>
import { GlButton, GlCard, GlFormGroup, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import {
GlButton,
GlCard,
GlFormGroup,
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
GlTooltipDirective,
} from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
export default {
name: 'OnDemandScansProfileSelector',
......@@ -9,6 +18,10 @@ export default {
GlFormGroup,
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
libraryPath: {
......@@ -30,10 +43,24 @@ export default {
default: null,
},
},
data() {
return { searchTerm: '' };
},
computed: {
selectedProfile() {
return this.value ? this.profiles.find(({ id }) => this.value === id) : null;
},
filteredProfiles() {
if (this.searchTerm) {
return fuzzaldrinPlus.filter(this.profiles, this.searchTerm, {
key: ['profileName'],
});
}
return this.profiles;
},
filteredProfilesEmpty() {
return this.filteredProfiles.length === 0;
},
},
};
</script>
......@@ -47,24 +74,13 @@ export default {
<slot name="title"></slot>
</h3>
</div>
<div class="col-5 gl-text-right">
<gl-button
:href="profiles.length ? libraryPath : null"
:disabled="!profiles.length"
variant="success"
category="secondary"
size="small"
data-testid="manage-profiles-link"
>
{{ s__('OnDemandScans|Manage profiles') }}
</gl-button>
</div>
</div>
</template>
<gl-form-group v-if="profiles.length">
<template #label>
<slot name="label"></slot>
</template>
<gl-dropdown
:text="
selectedProfile
......@@ -74,8 +90,11 @@ export default {
class="mw-460"
data-testid="profiles-dropdown"
>
<template #header>
<gl-search-box-by-type v-model.trim="searchTerm" />
</template>
<gl-dropdown-item
v-for="profile in profiles"
v-for="profile in filteredProfiles"
:key="profile.id"
:is-checked="value === profile.id"
is-check-item
......@@ -83,12 +102,33 @@ export default {
>
{{ profile.profileName }}
</gl-dropdown-item>
<div v-show="filteredProfilesEmpty" class="gl-p-3 gl-text-center">
{{ __('No matching results...') }}
</div>
<template #footer>
<gl-dropdown-item :href="newProfilePath" data-testid="create-profile-option">
<slot name="new-profile"></slot>
</gl-dropdown-item>
<gl-dropdown-item :href="libraryPath" data-testid="manage-profiles-option">
<slot name="manage-profile"></slot>
</gl-dropdown-item>
</template>
</gl-dropdown>
<div
v-if="value && $scopedSlots.summary"
data-testid="selected-profile-summary"
class="gl-mt-6 gl-pt-6 gl-border-t-solid gl-border-gray-100 gl-border-t-1"
>
<gl-button
v-if="selectedProfile"
v-gl-tooltip
category="primary"
icon="pencil"
:title="s__('DastProfiles|Edit profile')"
:href="selectedProfile.editPath"
class="gl-absolute gl-right-7"
/>
<slot name="summary"></slot>
</div>
</gl-form-group>
......
......@@ -56,7 +56,8 @@ export default {
'OnDemandScans|No profile yet. In order to create a new scan, you need to have at least one completed scanner profile.',
)
}}</template>
<template #new-profile>{{ s__('OnDemandScans|Create a new scanner profile') }}</template>
<template #new-profile>{{ s__('OnDemandScans|Create new scanner profile') }}</template>
<template #manage-profile>{{ s__('OnDemandScans|Manage scanner profiles') }}</template>
<template #summary>
<slot name="summary"></slot>
</template>
......
......@@ -59,7 +59,8 @@ export default {
'OnDemandScans|No profile yet. In order to create a new scan, you need to have at least one completed site profile.',
)
}}</template>
<template #new-profile>{{ s__('OnDemandScans|Create a new site profile') }}</template>
<template #new-profile>{{ s__('OnDemandScans|Create new site profile') }}</template>
<template #manage-profile>{{ s__('OnDemandScans|Manage site profiles') }}</template>
<template #summary>
<slot name="summary"></slot>
</template>
......
......@@ -13,6 +13,7 @@ export default () => {
dastSiteValidationDocsPath,
projectPath,
defaultBranch,
profilesLibraryPath,
scannerProfilesLibraryPath,
siteProfilesLibraryPath,
newSiteProfilePath,
......@@ -25,6 +26,7 @@ export default () => {
el,
apolloProvider,
provide: {
profilesLibraryPath,
scannerProfilesLibraryPath,
siteProfilesLibraryPath,
newScannerProfilePath,
......
......@@ -8,6 +8,7 @@ module Projects::OnDemandScansHelper
'empty-state-svg-path' => image_path('illustrations/empty-state/ondemand-scan-empty.svg'),
'default-branch' => project.default_branch,
'project-path' => project.path_with_namespace,
'profiles-library-path' => project_security_configuration_dast_profiles_path(project),
'scanner-profiles-library-path' => project_security_configuration_dast_profiles_path(project, anchor: 'scanner-profiles'),
'site-profiles-library-path' => project_security_configuration_dast_profiles_path(project, anchor: 'site-profiles'),
'new-scanner-profile-path' => new_project_security_configuration_dast_profiles_dast_scanner_profile_path(project),
......
---
title: Improve UX for DAST Profiles selector
merge_request: 51957
author:
type: changed
......@@ -21,28 +21,6 @@ exports[`OnDemandScansScannerProfileSelector renders properly with profiles 1`]
Scanner profile
</h3>
</div>
<div
class="col-5 gl-text-right"
>
<a
class="btn btn-success btn-sm gl-button btn-success-secondary"
data-testid="manage-profiles-link"
href="/test/scanner/profiles/library/path"
>
<!---->
<!---->
<span
class="gl-button-text"
>
Manage profiles
</span>
</a>
</div>
</div>
</div>
......@@ -104,11 +82,45 @@ exports[`OnDemandScansScannerProfileSelector renders properly with profiles 1`]
<div
class="gl-new-dropdown-inner"
>
<!---->
<div
class="gl-new-dropdown-header gl-border-b-0!"
>
<!---->
<div
class="gl-search-box-by-type"
>
<svg
aria-hidden="true"
class="gl-search-box-by-type-search-icon gl-icon s16"
data-testid="search-icon"
>
<use
href="#search"
/>
</svg>
<input
aria-label="Search"
class="gl-form-input gl-search-box-by-type-input form-control"
placeholder="Search"
type="text"
/>
<div
class="gl-search-box-by-type-right-icons"
>
<!---->
<!---->
</div>
</div>
</div>
<div
class="gl-new-dropdown-contents"
>
<li
class="gl-new-dropdown-item"
role="presentation"
......@@ -189,9 +201,86 @@ exports[`OnDemandScansScannerProfileSelector renders properly with profiles 1`]
<!---->
</button>
</li>
<div
class="gl-p-3 gl-text-center"
style="display: none;"
>
No matching results...
</div>
</div>
<!---->
<div
class="gl-new-dropdown-footer"
>
<li
class="gl-new-dropdown-item"
role="presentation"
>
<a
class="dropdown-item"
data-testid="create-profile-option"
href="/test/new/scanner/profile/path"
role="menuitem"
target="_self"
>
<!---->
<!---->
<!---->
<div
class="gl-new-dropdown-item-text-wrapper"
>
<p
class="gl-new-dropdown-item-text-primary"
>
Create new scanner profile
</p>
<!---->
</div>
<!---->
</a>
</li>
<li
class="gl-new-dropdown-item"
role="presentation"
>
<a
class="dropdown-item"
data-testid="manage-profiles-option"
href="/test/scanner/profiles/library/path"
role="menuitem"
target="_self"
>
<!---->
<!---->
<!---->
<div
class="gl-new-dropdown-item-text-wrapper"
>
<p
class="gl-new-dropdown-item-text-primary"
>
Manage scanner profiles
</p>
<!---->
</div>
<!---->
</a>
</li>
</div>
</div>
</ul>
......@@ -201,6 +290,26 @@ exports[`OnDemandScansScannerProfileSelector renders properly with profiles 1`]
class="gl-mt-6 gl-pt-6 gl-border-t-solid gl-border-gray-100 gl-border-t-1"
data-testid="selected-profile-summary"
>
<a
class="btn gl-absolute gl-right-7 btn-default btn-md gl-button btn-icon"
href="/scanner_profile/edit/1"
title="Edit profile"
>
<!---->
<svg
aria-hidden="true"
class="gl-button-icon gl-icon s16"
data-testid="pencil-icon"
>
<use
href="#pencil"
/>
</svg>
<!---->
</a>
<div>
Scanner profile #1's summary
</div>
......@@ -236,29 +345,6 @@ exports[`OnDemandScansScannerProfileSelector renders properly without profiles 1
Scanner profile
</h3>
</div>
<div
class="col-5 gl-text-right"
>
<button
class="btn btn-success btn-sm disabled gl-button btn-success-secondary"
data-testid="manage-profiles-link"
disabled="disabled"
type="button"
>
<!---->
<!---->
<span
class="gl-button-text"
>
Manage profiles
</span>
</button>
</div>
</div>
</div>
......@@ -284,7 +370,7 @@ exports[`OnDemandScansScannerProfileSelector renders properly without profiles 1
<span
class="gl-button-text"
>
Create a new scanner profile
Create new scanner profile
</span>
</a>
</div>
......
......@@ -21,28 +21,6 @@ exports[`OnDemandScansSiteProfileSelector renders properly with profiles 1`] = `
Site profile
</h3>
</div>
<div
class="col-5 gl-text-right"
>
<a
class="btn btn-success btn-sm gl-button btn-success-secondary"
data-testid="manage-profiles-link"
href="/test/site/profiles/library/path"
>
<!---->
<!---->
<span
class="gl-button-text"
>
Manage profiles
</span>
</a>
</div>
</div>
</div>
......@@ -104,11 +82,45 @@ exports[`OnDemandScansSiteProfileSelector renders properly with profiles 1`] = `
<div
class="gl-new-dropdown-inner"
>
<!---->
<div
class="gl-new-dropdown-header gl-border-b-0!"
>
<!---->
<div
class="gl-search-box-by-type"
>
<svg
aria-hidden="true"
class="gl-search-box-by-type-search-icon gl-icon s16"
data-testid="search-icon"
>
<use
href="#search"
/>
</svg>
<input
aria-label="Search"
class="gl-form-input gl-search-box-by-type-input form-control"
placeholder="Search"
type="text"
/>
<div
class="gl-search-box-by-type-right-icons"
>
<!---->
<!---->
</div>
</div>
</div>
<div
class="gl-new-dropdown-contents"
>
<li
class="gl-new-dropdown-item"
role="presentation"
......@@ -189,9 +201,86 @@ exports[`OnDemandScansSiteProfileSelector renders properly with profiles 1`] = `
<!---->
</button>
</li>
<div
class="gl-p-3 gl-text-center"
style="display: none;"
>
No matching results...
</div>
</div>
<!---->
<div
class="gl-new-dropdown-footer"
>
<li
class="gl-new-dropdown-item"
role="presentation"
>
<a
class="dropdown-item"
data-testid="create-profile-option"
href="/test/new/site/profile/path"
role="menuitem"
target="_self"
>
<!---->
<!---->
<!---->
<div
class="gl-new-dropdown-item-text-wrapper"
>
<p
class="gl-new-dropdown-item-text-primary"
>
Create new site profile
</p>
<!---->
</div>
<!---->
</a>
</li>
<li
class="gl-new-dropdown-item"
role="presentation"
>
<a
class="dropdown-item"
data-testid="manage-profiles-option"
href="/test/site/profiles/library/path"
role="menuitem"
target="_self"
>
<!---->
<!---->
<!---->
<div
class="gl-new-dropdown-item-text-wrapper"
>
<p
class="gl-new-dropdown-item-text-primary"
>
Manage site profiles
</p>
<!---->
</div>
<!---->
</a>
</li>
</div>
</div>
</ul>
......@@ -201,6 +290,26 @@ exports[`OnDemandScansSiteProfileSelector renders properly with profiles 1`] = `
class="gl-mt-6 gl-pt-6 gl-border-t-solid gl-border-gray-100 gl-border-t-1"
data-testid="selected-profile-summary"
>
<a
class="btn gl-absolute gl-right-7 btn-default btn-md gl-button btn-icon"
href="/site_profiles/edit/1"
title="Edit profile"
>
<!---->
<svg
aria-hidden="true"
class="gl-button-icon gl-icon s16"
data-testid="pencil-icon"
>
<use
href="#pencil"
/>
</svg>
<!---->
</a>
<div>
Site profile #1's summary
</div>
......@@ -236,29 +345,6 @@ exports[`OnDemandScansSiteProfileSelector renders properly without profiles 1`]
Site profile
</h3>
</div>
<div
class="col-5 gl-text-right"
>
<button
class="btn btn-success btn-sm disabled gl-button btn-success-secondary"
data-testid="manage-profiles-link"
disabled="disabled"
type="button"
>
<!---->
<!---->
<span
class="gl-button-text"
>
Manage profiles
</span>
</button>
</div>
</div>
</div>
......@@ -284,7 +370,7 @@ exports[`OnDemandScansSiteProfileSelector renders properly without profiles 1`]
<span
class="gl-button-text"
>
Create a new site profile
Create new site profile
</span>
</a>
</div>
......
......@@ -13,8 +13,20 @@ describe('OnDemandScansProfileSelector', () => {
profiles: [],
};
const defaultDropdownItems = [
{
text: 'Create new profile',
isChecked: false,
},
{
text: 'Manage scanner profiles',
isChecked: false,
},
];
const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`);
const findProfilesLibraryPathLink = () => findByTestId('manage-profiles-link');
const findCreateProfileOption = () => findByTestId('create-profile-option');
const findManageProfilesOption = () => findByTestId('manage-profiles-option');
const findProfilesDropdown = () => findByTestId('profiles-dropdown');
const findCreateNewProfileLink = () => findByTestId('create-profile-link');
const findSelectedProfileSummary = () => findByTestId('selected-profile-summary');
......@@ -25,7 +37,6 @@ describe('OnDemandScansProfileSelector', () => {
text: x.text(),
isChecked: x.props('isChecked'),
}));
const selectFirstProfile = () => {
return findProfilesDropdown().find(GlDropdownItem).vm.$emit('click');
};
......@@ -41,7 +52,8 @@ describe('OnDemandScansProfileSelector', () => {
label: 'Use existing scanner profile',
summary: `<div>Profile's summary</div>`,
'no-profiles': 'No profile yet',
'new-profile': 'Create a new profile',
'new-profile': 'Create new profile',
'manage-profile': 'Manage scanner profiles',
},
},
options,
......@@ -64,8 +76,8 @@ describe('OnDemandScansProfileSelector', () => {
createFullComponent();
});
it('disables the link to profiles library', () => {
expect(findProfilesLibraryPathLink().props('disabled')).toBe(true);
it('do not show profile selector', () => {
expect(findProfilesDropdown().exists()).toBe(false);
});
it('shows a help text and a link to create a new profile', () => {
......@@ -74,7 +86,7 @@ describe('OnDemandScansProfileSelector', () => {
expect(wrapper.text()).toContain('No profile yet');
expect(link.exists()).toBe(true);
expect(link.attributes('href')).toBe('/path/to/new/profile/form');
expect(link.text()).toBe('Create a new profile');
expect(link.text()).toBe('Create new profile');
});
});
......@@ -85,11 +97,6 @@ describe('OnDemandScansProfileSelector', () => {
});
});
it('enables link to profiles management', () => {
expect(findProfilesLibraryPathLink().props('disabled')).toBe(false);
expect(findProfilesLibraryPathLink().attributes('href')).toBe('/path/to/profiles/library');
});
it('shows a dropdown containing the profiles', () => {
const dropdown = findProfilesDropdown();
......@@ -105,12 +112,21 @@ describe('OnDemandScansProfileSelector', () => {
});
it('shows dropdown items for each profile', () => {
expect(parseDropdownItems()).toEqual(
scannerProfiles.map((x) => ({
expect(parseDropdownItems()).toEqual([
...scannerProfiles.map((x) => ({
text: x.profileName,
isChecked: false,
})),
);
...defaultDropdownItems,
]);
});
it('show options for profiles management', () => {
expect(findCreateProfileOption().exists()).toBe(true);
expect(findCreateProfileOption().attributes('href')).toBe('/path/to/new/profile/form');
expect(findManageProfilesOption().exists()).toBe(true);
expect(findManageProfilesOption().attributes('href')).toBe('/path/to/profiles/library');
});
it('does not show summary', () => {
......@@ -139,12 +155,13 @@ describe('OnDemandScansProfileSelector', () => {
});
it('displays item as checked', () => {
expect(parseDropdownItems()).toEqual(
scannerProfiles.map((x, i) => ({
expect(parseDropdownItems()).toEqual([
...scannerProfiles.map((x, i) => ({
text: x.profileName,
isChecked: i === 0,
})),
);
...defaultDropdownItems,
]);
});
});
});
......@@ -13,6 +13,7 @@ RSpec.describe Projects::OnDemandScansHelper do
'empty-state-svg-path' => match_asset_path('/assets/illustrations/empty-state/ondemand-scan-empty.svg'),
'default-branch' => project.default_branch,
'project-path' => project.path_with_namespace,
'profiles-library-path' => project_security_configuration_dast_profiles_path(project),
'scanner-profiles-library-path' => project_security_configuration_dast_profiles_path(project, anchor: 'scanner-profiles'),
'site-profiles-library-path' => project_security_configuration_dast_profiles_path(project, anchor: 'site-profiles'),
'new-scanner-profile-path' => new_project_security_configuration_dast_profiles_dast_scanner_profile_path(project),
......
......@@ -19447,6 +19447,9 @@ msgstr ""
msgid "No matching results for \"%{query}\""
msgstr ""
msgid "No matching results..."
msgstr ""
msgid "No members found"
msgstr ""
......@@ -19996,10 +19999,10 @@ msgstr ""
msgid "OnDemandScans|Could not run the scan. Please try again."
msgstr ""
msgid "OnDemandScans|Create a new scanner profile"
msgid "OnDemandScans|Create new scanner profile"
msgstr ""
msgid "OnDemandScans|Create a new site profile"
msgid "OnDemandScans|Create new site profile"
msgstr ""
msgid "OnDemandScans|Description (optional)"
......@@ -20011,9 +20014,18 @@ msgstr ""
msgid "OnDemandScans|For example: Tests the login page for SQL injections"
msgstr ""
msgid "OnDemandScans|Manage DAST scans"
msgstr ""
msgid "OnDemandScans|Manage profiles"
msgstr ""
msgid "OnDemandScans|Manage scanner profiles"
msgstr ""
msgid "OnDemandScans|Manage site profiles"
msgstr ""
msgid "OnDemandScans|My daily scan"
msgstr ""
......
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