Commit 9e5596cc authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 515ec7e2 b9e97cb9
......@@ -5,10 +5,29 @@ import GpgBadges from '~/gpg_badges';
import initBlob from '~/pages/projects/init_blob';
import initWebIdeLink from '~/pages/projects/shared/web_ide_link';
import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
import BlobContentViewer from '~/repository/components/blob_content_viewer.vue';
import '~/sourcegraph/load';
new BlobViewer(); // eslint-disable-line no-new
initBlob();
const viewBlobEl = document.querySelector('#js-view-blob-app');
if (viewBlobEl) {
const { blobPath } = viewBlobEl.dataset;
// eslint-disable-next-line no-new
new Vue({
el: viewBlobEl,
render(createElement) {
return createElement(BlobContentViewer, {
props: {
path: blobPath,
},
});
},
});
} else {
new BlobViewer(); // eslint-disable-line no-new
initBlob();
}
const CommitPipelineStatusEl = document.querySelector('.js-commit-pipeline-status');
const statusLink = document.querySelector('.commit-actions .ci-status-link');
......
......@@ -11,6 +11,11 @@
#blob-content-holder.blob-content-holder
- if @code_navigation_path
#js-code-navigation{ data: { code_navigation_path: @code_navigation_path, blob_path: blob.path, definition_path_prefix: project_blob_path(@project, @ref) } }
%article.file-holder
= render 'projects/blob/header', blob: blob
= render 'projects/blob/content', blob: blob
- if Feature.enabled?(:refactor_blob_viewer, @project, default_enabled: :yaml)
#js-view-blob-app{ data: { blob_path: blob.path } }
.gl-spinner-container
= loading_icon(size: 'md')
- else
%article.file-holder
= render 'projects/blob/header', blob: blob
= render 'projects/blob/content', blob: blob
---
title: Fix N+1 in REST projects and service desk
merge_request: 58747
author:
type: performance
......@@ -3,10 +3,13 @@ import { GlLoadingIcon } from '@gitlab/ui';
import GroupSecurityVulnerabilities from 'ee/security_dashboard/components/first_class_group_security_dashboard_vulnerabilities.vue';
import Filters from 'ee/security_dashboard/components/first_class_vulnerability_filters.vue';
import SecurityDashboardLayout from 'ee/security_dashboard/components/security_dashboard_layout.vue';
import { PROJECT_LOADING_ERROR_MESSAGE } from 'ee/security_dashboard/helpers';
import createFlash from '~/flash';
import { vulnerabilitiesSeverityCountScopes } from '../constants';
import groupProjectsQuery from '../graphql/queries/group_projects.query.graphql';
import CsvExportButton from './csv_export_button.vue';
import DashboardNotConfigured from './empty_states/group_dashboard_not_configured.vue';
import SurveyRequestBanner from './survey_request_banner.vue';
import VulnerabilitiesCountList from './vulnerability_count_list.vue';
export default {
......@@ -18,6 +21,7 @@ export default {
DashboardNotConfigured,
GlLoadingIcon,
VulnerabilitiesCountList,
SurveyRequestBanner,
},
inject: ['groupFullPath'],
apollo: {
......@@ -29,11 +33,8 @@ export default {
update(data) {
return data.group.projects.nodes;
},
result() {
this.projectsWereFetched = true;
},
error() {
this.projectsWereFetched = false;
createFlash({ message: PROJECT_LOADING_ERROR_MESSAGE });
},
},
},
......@@ -41,12 +42,14 @@ export default {
return {
filters: {},
projects: [],
projectsWereFetched: false,
};
},
computed: {
isLoadingProjects() {
return this.$apollo.queries.projects.loading;
},
hasNoProjects() {
return this.projects.length === 0 && this.projectsWereFetched;
return this.projects.length === 0;
},
},
methods: {
......@@ -59,27 +62,32 @@ export default {
</script>
<template>
<div>
<gl-loading-icon v-if="!projectsWereFetched" size="lg" class="gl-mt-6" />
<dashboard-not-configured v-else-if="hasNoProjects" />
<security-dashboard-layout v-else>
<template #header>
<header class="gl-my-6 gl-display-flex gl-align-items-center">
<h2 class="gl-flex-grow-1 gl-my-0">
{{ s__('SecurityReports|Vulnerability Report') }}
</h2>
<csv-export-button />
</header>
<vulnerabilities-count-list
:scope="$options.vulnerabilitiesSeverityCountScopes.group"
:full-path="groupFullPath"
:filters="filters"
/>
</template>
<template #sticky>
<filters :projects="projects" @filterChange="handleFilterChange" />
</template>
<group-security-vulnerabilities :filters="filters" />
</security-dashboard-layout>
<gl-loading-icon v-if="isLoadingProjects" size="lg" class="gl-mt-6" />
<div v-else-if="hasNoProjects">
<survey-request-banner class="gl-mt-5" />
<dashboard-not-configured />
</div>
<security-dashboard-layout v-else>
<template #header>
<survey-request-banner class="gl-mt-5" />
<header class="gl-my-6 gl-display-flex gl-align-items-center">
<h2 class="gl-flex-grow-1 gl-my-0">
{{ s__('SecurityReports|Vulnerability Report') }}
</h2>
<csv-export-button />
</header>
<vulnerabilities-count-list
:scope="$options.vulnerabilitiesSeverityCountScopes.group"
:full-path="groupFullPath"
:filters="filters"
/>
</template>
<template #sticky>
<filters :projects="projects" @filterChange="handleFilterChange" />
</template>
<group-security-vulnerabilities :filters="filters" />
</security-dashboard-layout>
</template>
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import Filters from 'ee/security_dashboard/components/first_class_vulnerability_filters.vue';
import SecurityDashboardLayout from 'ee/security_dashboard/components/security_dashboard_layout.vue';
import instanceProjectsQuery from 'ee/security_dashboard/graphql/queries/instance_projects.query.graphql';
......@@ -8,6 +9,7 @@ import { PROJECT_LOADING_ERROR_MESSAGE } from '../helpers';
import CsvExportButton from './csv_export_button.vue';
import DashboardNotConfigured from './empty_states/instance_dashboard_not_configured.vue';
import InstanceSecurityVulnerabilities from './first_class_instance_security_dashboard_vulnerabilities.vue';
import SurveyRequestBanner from './survey_request_banner.vue';
import VulnerabilitiesCountList from './vulnerability_count_list.vue';
export default {
......@@ -18,6 +20,8 @@ export default {
Filters,
DashboardNotConfigured,
VulnerabilitiesCountList,
SurveyRequestBanner,
GlLoadingIcon,
},
apollo: {
projects: {
......@@ -40,14 +44,8 @@ export default {
isLoadingProjects() {
return this.$apollo.queries.projects.loading;
},
hasProjectsData() {
return !this.isLoadingProjects && this.projects.length > 0;
},
shouldShowDashboard() {
return this.hasProjectsData;
},
shouldShowEmptyState() {
return !this.isLoadingProjects && this.projects.length === 0;
hasNoProjects() {
return this.projects.length === 0;
},
},
methods: {
......@@ -60,30 +58,33 @@ export default {
</script>
<template>
<security-dashboard-layout>
<dashboard-not-configured v-if="shouldShowEmptyState" />
<gl-loading-icon v-if="isLoadingProjects" size="lg" class="gl-mt-6" />
<div v-else-if="hasNoProjects">
<survey-request-banner class="gl-mt-5" />
<dashboard-not-configured />
</div>
<security-dashboard-layout v-else>
<template #header>
<div v-if="shouldShowDashboard">
<header class="gl-my-6 gl-display-flex gl-align-items-center" data-testid="header">
<h2 class="gl-flex-grow-1 gl-my-0">
{{ s__('SecurityReports|Vulnerability Report') }}
</h2>
<csv-export-button />
</header>
<vulnerabilities-count-list
:scope="$options.vulnerabilitiesSeverityCountScopes.instance"
:filters="filters"
/>
</div>
<survey-request-banner class="gl-mt-5" />
<header class="gl-my-6 gl-display-flex gl-align-items-center" data-testid="header">
<h2 class="gl-flex-grow-1 gl-my-0">
{{ s__('SecurityReports|Vulnerability Report') }}
</h2>
<csv-export-button />
</header>
<vulnerabilities-count-list
:scope="$options.vulnerabilitiesSeverityCountScopes.instance"
:filters="filters"
/>
</template>
<template #sticky>
<filters v-if="shouldShowDashboard" :projects="projects" @filterChange="handleFilterChange" />
<filters :projects="projects" @filterChange="handleFilterChange" />
</template>
<instance-security-vulnerabilities
v-if="shouldShowDashboard"
:projects="projects"
:filters="filters"
/>
<instance-security-vulnerabilities :projects="projects" :filters="filters" />
</security-dashboard-layout>
</template>
......@@ -9,6 +9,7 @@ import Filters from './first_class_vulnerability_filters.vue';
import ProjectPipelineStatus from './project_pipeline_status.vue';
import ProjectVulnerabilitiesApp from './project_vulnerabilities.vue';
import SecurityDashboardLayout from './security_dashboard_layout.vue';
import SurveyRequestBanner from './survey_request_banner.vue';
import VulnerabilitiesCountList from './vulnerability_count_list.vue';
export default {
......@@ -21,6 +22,7 @@ export default {
VulnerabilitiesCountList,
CsvExportButton,
Filters,
SurveyRequestBanner,
},
mixins: [glFeatureFlagsMixin()],
inject: ['dashboardDocumentation', 'autoFixDocumentation', 'projectFullPath'],
......@@ -58,6 +60,8 @@ export default {
<template>
<div>
<survey-request-banner class="gl-mt-5" />
<template v-if="pipeline.id">
<auto-fix-user-callout
v-if="shouldShowAutoFixUserCallout"
......@@ -67,7 +71,9 @@ export default {
<security-dashboard-layout>
<template #header>
<div class="gl-mt-6 gl-display-flex">
<h4 class="gl-flex-grow-1 gl-my-0">{{ __('Vulnerability Report') }}</h4>
<h4 class="gl-flex-grow-1 gl-my-0">
{{ s__('SecurityReports|Vulnerability Report') }}
</h4>
<csv-export-button />
</div>
<project-pipeline-status :pipeline="pipeline" />
......
<script>
import { s__ } from '~/locale';
import SurveyRequestBanner from './survey_request_banner.vue';
export default {
components: { SurveyRequestBanner },
i18n: {
title: s__('SecurityReports|Security Dashboard'),
},
......@@ -12,6 +14,8 @@ export default {
<div>
<slot name="loading"></slot>
<survey-request-banner v-if="!$slots.loading" class="gl-mt-5" />
<template v-if="$slots.default">
<h2 data-testid="title">{{ $options.i18n.title }}</h2>
<div class="security-charts gl-display-flex gl-flex-wrap">
......
<script>
import { GlButton, GlBanner, GlSprintf } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import showToast from '~/vue_shared/plugins/global_toast';
import { SURVEY_BANNER_LOCAL_STORAGE_KEY, SURVEY_BANNER_CURRENT_ID } from '../constants';
const SURVEY_LINK = 'https://gitlab.fra1.qualtrics.com/jfe/form/SV_7UMsVhPbjmwCp1k';
const DAYS_TO_ASK_LATER = 7;
export default {
components: { GlButton, GlBanner, GlSprintf, LocalStorageSync },
inject: ['surveyRequestSvgPath'],
data: () => ({
surveyShowDate: null,
}),
computed: {
shouldShowSurvey() {
const { surveyShowDate } = this;
const isFeatureEnabled = Boolean(gon.features?.vulnerabilityManagementSurvey);
const date = new Date(surveyShowDate);
// Survey is not enabled or user dismissed the survey by clicking the close icon.
if (!isFeatureEnabled || surveyShowDate === SURVEY_BANNER_CURRENT_ID) {
return false;
}
// Date is invalid, we should show the survey.
else if (Number.isNaN(date.getDate())) {
return true;
}
return date <= Date.now();
},
},
methods: {
hideSurvey() {
this.surveyShowDate = SURVEY_BANNER_CURRENT_ID;
},
askLater() {
const date = new Date();
date.setDate(date.getDate() + DAYS_TO_ASK_LATER);
this.surveyShowDate = date.toISOString();
showToast(this.$options.i18n.toastMessage);
},
},
i18n: {
title: s__('SecurityReports|Vulnerability Management feature survey'),
buttonText: s__('SecurityReports|Take survey'),
askAgainLater: __('Ask again later'),
description: s__(
`SecurityReports|At GitLab, we're all about iteration and feedback. That's why we are reaching out to customers like you to help guide what we work on this year for Vulnerability Management. We have a lot of exciting ideas and ask that you assist us by taking a short survey %{boldStart}no longer than 10 minutes%{boldEnd} to evaluate a few of our potential features.`,
),
toastMessage: s__(
'SecurityReports|Your feedback is important to us! We will ask again in a week.',
),
},
storageKey: SURVEY_BANNER_LOCAL_STORAGE_KEY,
surveyLink: SURVEY_LINK,
};
</script>
<template>
<local-storage-sync v-model="surveyShowDate" :storage-key="$options.storageKey">
<gl-banner
v-if="shouldShowSurvey"
:title="$options.i18n.title"
:button-text="$options.i18n.buttonText"
:svg-path="surveyRequestSvgPath"
:button-link="$options.surveyLink"
@close="hideSurvey"
>
<p>
<gl-sprintf :message="$options.i18n.description">
<template #bold="{ content }">
<span class="gl-font-weight-bold">{{ content }}</span>
</template>
</gl-sprintf>
</p>
<template #actions>
<gl-button variant="link" class="gl-ml-5" data-testid="ask-later-button" @click="askLater">
{{ $options.i18n.askAgainLater }}
</gl-button>
</template>
</gl-banner>
</local-storage-sync>
</template>
export const COLLAPSE_SECURITY_REPORTS_SUMMARY_LOCAL_STORAGE_KEY =
'hide_pipelines_security_reports_summary_details';
export const SURVEY_BANNER_LOCAL_STORAGE_KEY = 'vulnerability_management_survey_request';
// NOTE: This string needs to parse to an invalid date. Do not put any characters in between the
// word 'survey' and the number, or else it will parse to a valid date.
export const SURVEY_BANNER_CURRENT_ID = 'survey1';
export const vulnerabilitiesSeverityCountScopes = {
instance: 'instance',
group: 'group',
......
---
title: Add vulnerability management survey request banner
merge_request: 56620
author:
type: added
......@@ -6,6 +6,7 @@ import FirstClassGroupDashboard from 'ee/security_dashboard/components/first_cla
import FirstClassGroupVulnerabilities from 'ee/security_dashboard/components/first_class_group_security_dashboard_vulnerabilities.vue';
import Filters from 'ee/security_dashboard/components/first_class_vulnerability_filters.vue';
import SecurityDashboardLayout from 'ee/security_dashboard/components/security_dashboard_layout.vue';
import SurveyRequestBanner from 'ee/security_dashboard/components/survey_request_banner.vue';
import VulnerabilitiesCountList from 'ee/security_dashboard/components/vulnerability_count_list.vue';
describe('First Class Group Dashboard Component', () => {
......@@ -15,15 +16,16 @@ describe('First Class Group Dashboard Component', () => {
const emptyStateSvgPath = 'empty-state-path';
const groupFullPath = 'group-full-path';
const findDashboardLayout = () => wrapper.find(SecurityDashboardLayout);
const findGroupVulnerabilities = () => wrapper.find(FirstClassGroupVulnerabilities);
const findCsvExportButton = () => wrapper.find(CsvExportButton);
const findFilters = () => wrapper.find(Filters);
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findEmptyState = () => wrapper.find(DashboardNotConfigured);
const findVulnerabilitiesCountList = () => wrapper.find(VulnerabilitiesCountList);
const findDashboardLayout = () => wrapper.findComponent(SecurityDashboardLayout);
const findGroupVulnerabilities = () => wrapper.findComponent(FirstClassGroupVulnerabilities);
const findCsvExportButton = () => wrapper.findComponent(CsvExportButton);
const findFilters = () => wrapper.findComponent(Filters);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findEmptyState = () => wrapper.findComponent(DashboardNotConfigured);
const findVulnerabilitiesCountList = () => wrapper.findComponent(VulnerabilitiesCountList);
const findSurveyRequestBanner = () => wrapper.findComponent(SurveyRequestBanner);
const createWrapper = ({ data } = {}) => {
const createWrapper = ({ data, loading = false } = {}) => {
return shallowMount(FirstClassGroupDashboard, {
propsData: {
dashboardDocumentation,
......@@ -34,6 +36,13 @@ describe('First Class Group Dashboard Component', () => {
stubs: {
SecurityDashboardLayout,
},
mocks: {
$apollo: {
queries: {
projects: { loading },
},
},
},
});
};
......@@ -43,7 +52,7 @@ describe('First Class Group Dashboard Component', () => {
describe('when loading', () => {
beforeEach(() => {
wrapper = createWrapper();
wrapper = createWrapper({ loading: true });
});
it('loading button should be visible', () => {
......@@ -53,6 +62,10 @@ describe('First Class Group Dashboard Component', () => {
it('should not display the dashboard not configured component', () => {
expect(findEmptyState().exists()).toBe(false);
});
it('should not show the survey request banner', () => {
expect(findSurveyRequestBanner().exists()).toBe(false);
});
});
describe('when has projects', () => {
......@@ -105,6 +118,10 @@ describe('First Class Group Dashboard Component', () => {
filters: wrapper.vm.filters,
});
});
it('should show the survey request banner', () => {
expect(findSurveyRequestBanner().exists()).toBe(true);
});
});
describe('when has no projects', () => {
......@@ -125,5 +142,9 @@ describe('First Class Group Dashboard Component', () => {
it('should display the dashboard not configured component', () => {
expect(findEmptyState().exists()).toBe(true);
});
it('should show the survey request banner', () => {
expect(findSurveyRequestBanner().exists()).toBe(true);
});
});
});
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import CsvExportButton from 'ee/security_dashboard/components/csv_export_button.vue';
import DashboardNotConfigured from 'ee/security_dashboard/components/empty_states/instance_dashboard_not_configured.vue';
......@@ -5,6 +6,7 @@ import FirstClassInstanceDashboard from 'ee/security_dashboard/components/first_
import FirstClassInstanceVulnerabilities from 'ee/security_dashboard/components/first_class_instance_security_dashboard_vulnerabilities.vue';
import Filters from 'ee/security_dashboard/components/first_class_vulnerability_filters.vue';
import SecurityDashboardLayout from 'ee/security_dashboard/components/security_dashboard_layout.vue';
import SurveyRequestBanner from 'ee/security_dashboard/components/survey_request_banner.vue';
import VulnerabilitiesCountList from 'ee/security_dashboard/components/vulnerability_count_list.vue';
describe('First Class Instance Dashboard Component', () => {
......@@ -14,12 +16,14 @@ describe('First Class Instance Dashboard Component', () => {
$apollo: { queries: { projects: { loading } } },
});
const findInstanceVulnerabilities = () => wrapper.find(FirstClassInstanceVulnerabilities);
const findCsvExportButton = () => wrapper.find(CsvExportButton);
const findEmptyState = () => wrapper.find(DashboardNotConfigured);
const findFilters = () => wrapper.find(Filters);
const findVulnerabilitiesCountList = () => wrapper.find(VulnerabilitiesCountList);
const findInstanceVulnerabilities = () =>
wrapper.findComponent(FirstClassInstanceVulnerabilities);
const findCsvExportButton = () => wrapper.findComponent(CsvExportButton);
const findEmptyState = () => wrapper.findComponent(DashboardNotConfigured);
const findFilters = () => wrapper.findComponent(Filters);
const findVulnerabilitiesCountList = () => wrapper.findComponent(VulnerabilitiesCountList);
const findHeader = () => wrapper.find('[data-testid="header"]');
const findSurveyBanner = () => wrapper.findComponent(SurveyRequestBanner);
const createWrapper = ({ data = {}, stubs, mocks = defaultMocks() }) => {
return shallowMount(FirstClassInstanceDashboard, {
......@@ -58,6 +62,10 @@ describe('First Class Instance Dashboard Component', () => {
});
});
it('should show the survey banner', () => {
expect(findSurveyBanner().exists()).toBe(true);
});
it('has filters', () => {
expect(findFilters().exists()).toBe(true);
});
......@@ -93,10 +101,12 @@ describe('First Class Instance Dashboard Component', () => {
});
});
it('does not render the export button, vulnerabilities count list, or header', () => {
it('only shows the loading icon', () => {
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
expect(findCsvExportButton().exists()).toBe(false);
expect(findVulnerabilitiesCountList().exists()).toBe(false);
expect(findHeader().exists()).toBe(false);
expect(findSurveyBanner().exists()).toBe(false);
});
});
......@@ -109,8 +119,9 @@ describe('First Class Instance Dashboard Component', () => {
});
});
it('only renders the empty state', () => {
it('only renders the empty state and survey banner', () => {
expect(findEmptyState().exists()).toBe(true);
expect(findSurveyBanner().exists()).toBe(true);
expect(findCsvExportButton().exists()).toBe(false);
expect(findInstanceVulnerabilities().exists()).toBe(false);
expect(findFilters().exists()).toBe(false);
......
......@@ -10,6 +10,7 @@ import Filters from 'ee/security_dashboard/components/first_class_vulnerability_
import ProjectPipelineStatus from 'ee/security_dashboard/components/project_pipeline_status.vue';
import ProjectVulnerabilitiesApp from 'ee/security_dashboard/components/project_vulnerabilities.vue';
import SecurityDashboardLayout from 'ee/security_dashboard/components/security_dashboard_layout.vue';
import SurveyRequestBanner from 'ee/security_dashboard/components/survey_request_banner.vue';
import VulnerabilityCountList from 'ee/security_dashboard/components/vulnerability_count_list.vue';
const props = {
......@@ -38,13 +39,14 @@ const filters = { foo: 'bar' };
describe('First class Project Security Dashboard component', () => {
let wrapper;
const findFilters = () => wrapper.find(Filters);
const findProjectPipelineStatus = () => wrapper.find(ProjectPipelineStatus);
const findVulnerabilities = () => wrapper.find(ProjectVulnerabilitiesApp);
const findVulnerabilityCountList = () => wrapper.find(VulnerabilityCountList);
const findUnconfiguredState = () => wrapper.find(ReportsNotConfigured);
const findCsvExportButton = () => wrapper.find(CsvExportButton);
const findAutoFixUserCallout = () => wrapper.find(AutoFixUserCallout);
const findFilters = () => wrapper.findComponent(Filters);
const findProjectPipelineStatus = () => wrapper.findComponent(ProjectPipelineStatus);
const findVulnerabilities = () => wrapper.findComponent(ProjectVulnerabilitiesApp);
const findVulnerabilityCountList = () => wrapper.findComponent(VulnerabilityCountList);
const findUnconfiguredState = () => wrapper.findComponent(ReportsNotConfigured);
const findCsvExportButton = () => wrapper.findComponent(CsvExportButton);
const findAutoFixUserCallout = () => wrapper.findComponent(AutoFixUserCallout);
const findSurveyRequestBanner = () => wrapper.findComponent(SurveyRequestBanner);
const createComponent = (options) => {
wrapper = shallowMount(FirstClassProjectSecurityDashboard, {
......@@ -113,6 +115,10 @@ describe('First class Project Security Dashboard component', () => {
it('should display the project pipeline status', () => {
expect(findProjectPipelineStatus()).toExist();
});
it('should show the survey request banner', () => {
expect(findSurveyRequestBanner().exists()).toBe(true);
});
});
describe('auto-fix user callout', () => {
......@@ -199,5 +205,9 @@ describe('First class Project Security Dashboard component', () => {
it('displays the unconfigured state', () => {
expect(findUnconfiguredState().exists()).toBe(true);
});
it('shows the survey request banner', () => {
expect(findSurveyRequestBanner().exists()).toBe(true);
});
});
});
import { shallowMount } from '@vue/test-utils';
import SecurityChartsLayout from 'ee/security_dashboard/components/security_charts_layout.vue';
import SurveyRequestBanner from 'ee/security_dashboard/components/survey_request_banner.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
describe('Security Charts Layout component', () => {
......@@ -12,6 +13,7 @@ describe('Security Charts Layout component', () => {
const findDummyComponent = () => wrapper.findComponent(DummyComponent);
const findTitle = () => wrapper.findByTestId('title');
const findSurveyBanner = () => wrapper.findComponent(SurveyRequestBanner);
const createWrapper = (slots) => {
wrapper = extendedWrapper(shallowMount(SecurityChartsLayout, { slots }));
......@@ -21,18 +23,20 @@ describe('Security Charts Layout component', () => {
wrapper.destroy();
});
it('should render the default slot', () => {
it('should render the default slot and survey banner', () => {
createWrapper({ default: DummyComponent });
expect(findDummyComponent().exists()).toBe(true);
expect(findTitle().exists()).toBe(true);
expect(findSurveyBanner().exists()).toBe(true);
});
it('should render the empty-state slot', () => {
it('should render the empty-state slot and survey banner', () => {
createWrapper({ 'empty-state': DummyComponent });
expect(findDummyComponent().exists()).toBe(true);
expect(findTitle().exists()).toBe(false);
expect(findSurveyBanner().exists()).toBe(true);
});
it('should render the loading slot', () => {
......@@ -40,5 +44,6 @@ describe('Security Charts Layout component', () => {
expect(findDummyComponent().exists()).toBe(true);
expect(findTitle().exists()).toBe(false);
expect(findSurveyBanner().exists()).toBe(false);
});
});
import { GlBanner, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import SurveyRequestBanner from 'ee/security_dashboard/components/survey_request_banner.vue';
import {
SURVEY_BANNER_LOCAL_STORAGE_KEY,
SURVEY_BANNER_CURRENT_ID,
} from 'ee/security_dashboard/constants';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import toast from '~/vue_shared/plugins/global_toast';
jest.mock('~/vue_shared/plugins/global_toast');
describe('Survey Request Banner component', () => {
let wrapper;
const surveyRequestSvgPath = 'icon.svg';
const findGlBanner = () => wrapper.findComponent(GlBanner);
const findAskLaterButton = () => wrapper.findByTestId('ask-later-button');
const getOffsetDateString = (days) => {
const date = new Date();
date.setDate(date.getDate() + days);
return date.toISOString();
};
const createWrapper = () => {
wrapper = extendedWrapper(
shallowMount(SurveyRequestBanner, {
provide: { surveyRequestSvgPath },
stubs: { GlBanner, GlButton, LocalStorageSync },
}),
);
};
beforeEach(() => {
gon.features = {};
});
afterEach(() => {
wrapper.destroy();
localStorage.removeItem(SURVEY_BANNER_LOCAL_STORAGE_KEY);
});
describe('feature flag disabled', () => {
it('should not show banner regardless of localStorage value', () => {
[
getOffsetDateString(1),
getOffsetDateString(-1),
SURVEY_BANNER_CURRENT_ID,
'SOME OTHER ID',
].forEach((localStorageValue) => {
localStorage.setItem(SURVEY_BANNER_LOCAL_STORAGE_KEY, localStorageValue);
createWrapper();
expect(findGlBanner().exists()).toBe(false);
});
});
});
describe('feature flag enabled', () => {
beforeEach(() => {
gon.features.vulnerabilityManagementSurvey = true;
});
it('shows the banner with the correct components and props', () => {
createWrapper();
const { title, buttonText, description } = wrapper.vm.$options.i18n;
expect(findGlBanner().html()).toContain(description);
expect(findAskLaterButton().exists()).toBe(true);
expect(findGlBanner().props()).toMatchObject({
title,
buttonText,
svgPath: surveyRequestSvgPath,
});
});
it.each`
showOrHide | phrase | localStorageValue | isShown
${'hides'} | ${'a future date'} | ${getOffsetDateString(1)} | ${false}
${'shows'} | ${'a past date'} | ${getOffsetDateString(-1)} | ${true}
${'hides'} | ${'the current survey ID'} | ${SURVEY_BANNER_CURRENT_ID} | ${false}
${'shows'} | ${'a different survey ID'} | ${'SOME OTHER ID'} | ${true}
`(
'$showOrHide the banner if the localStorage value is $phrase',
async ({ localStorageValue, isShown }) => {
localStorage.setItem(SURVEY_BANNER_LOCAL_STORAGE_KEY, localStorageValue);
createWrapper();
await wrapper.vm.$nextTick();
expect(findGlBanner().exists()).toBe(isShown);
},
);
});
describe('closing the banner', () => {
beforeEach(() => {
gon.features.vulnerabilityManagementSurvey = true;
});
it('hides the banner and will set it to reshow later if the "Ask again later" button is clicked', async () => {
createWrapper();
expect(findGlBanner().exists()).toBe(true);
findAskLaterButton().vm.$emit('click');
await wrapper.vm.$nextTick();
const date = new Date(localStorage.getItem(SURVEY_BANNER_LOCAL_STORAGE_KEY));
expect(findGlBanner().exists()).toBe(false);
expect(toast).toHaveBeenCalledTimes(1);
expect(date > new Date()).toBe(true);
});
it('hides the banner and sets it to never show again if the close button is clicked', async () => {
createWrapper();
expect(findGlBanner().exists()).toBe(true);
findGlBanner().vm.$emit('close');
await wrapper.vm.$nextTick();
expect(findGlBanner().exists()).toBe(false);
expect(localStorage.getItem(SURVEY_BANNER_LOCAL_STORAGE_KEY)).toBe(SURVEY_BANNER_CURRENT_ID);
});
});
});
......@@ -132,6 +132,7 @@ module API
.preload(:project_setting)
.preload(:container_expiration_policy)
.preload(:auto_devops)
.preload(:service_desk_setting)
.preload(project_group_links: { group: :route },
fork_network: :root_project,
fork_network_member: :forked_from_project,
......
......@@ -4194,6 +4194,9 @@ msgstr ""
msgid "Ascending"
msgstr ""
msgid "Ask again later"
msgstr ""
msgid "Ask your group maintainer to set up a group runner."
msgstr ""
......@@ -27473,6 +27476,9 @@ msgstr ""
msgid "SecurityReports|Although it's rare to have no vulnerabilities, it can happen. Check your settings to make sure you've set up your dashboard correctly."
msgstr ""
msgid "SecurityReports|At GitLab, we're all about iteration and feedback. That's why we are reaching out to customers like you to help guide what we work on this year for Vulnerability Management. We have a lot of exciting ideas and ask that you assist us by taking a short survey %{boldStart}no longer than 10 minutes%{boldEnd} to evaluate a few of our potential features."
msgstr ""
msgid "SecurityReports|Change status"
msgstr ""
......@@ -27632,6 +27638,9 @@ msgstr ""
msgid "SecurityReports|Status"
msgstr ""
msgid "SecurityReports|Take survey"
msgstr ""
msgid "SecurityReports|There was an error adding the comment."
msgstr ""
......@@ -27677,6 +27686,9 @@ msgstr ""
msgid "SecurityReports|Upgrade to manage vulnerabilities"
msgstr ""
msgid "SecurityReports|Vulnerability Management feature survey"
msgstr ""
msgid "SecurityReports|Vulnerability Report"
msgstr ""
......@@ -27692,6 +27704,9 @@ msgstr ""
msgid "SecurityReports|You must sign in as an authorized user to see this report"
msgstr ""
msgid "SecurityReports|Your feedback is important to us! We will ask again in a week."
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
msgstr ""
......
......@@ -114,7 +114,7 @@ module QA
end
context 'Geo', :orchestrated, :geo do
it 'replicates a published pypi package to the Geo secondary site', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1120' do
it 'replicates a published pypi package to the Geo secondary site', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1120', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/325556', type: :investigating } do
QA::Runtime::Logger.debug('Visiting the secondary Geo site')
QA::Flow::Login.while_signed_in(address: :geo_secondary) do
......
......@@ -290,7 +290,6 @@ RSpec.describe 'Project' do
let(:project) { create(:forked_project_with_submodules) }
before do
stub_feature_flags(refactor_blob_viewer: false)
project.add_maintainer(user)
sign_in user
visit project_path(project)
......
......@@ -835,6 +835,29 @@ RSpec.describe API::Projects do
end.not_to exceed_query_limit(control.count)
end
end
context 'when service desk is enabled', :use_clean_rails_memory_store_caching do
let_it_be(:admin) { create(:admin) }
it 'avoids N+1 queries' do
allow(Gitlab::ServiceDeskEmail).to receive(:enabled?).and_return(true)
allow(Gitlab::IncomingEmail).to receive(:enabled?).and_return(true)
get api('/projects', admin)
create(:project, :public, :service_desk_enabled, namespace: admin.namespace)
control = ActiveRecord::QueryRecorder.new do
get api('/projects', admin)
end
create_list(:project, 2, :public, :service_desk_enabled, namespace: admin.namespace)
expect do
get api('/projects', admin)
end.not_to exceed_query_limit(control.count)
end
end
end
describe 'POST /projects' do
......
......@@ -277,6 +277,11 @@ RSpec.configure do |config|
# Vue issues page has feature parity with the current Haml page
stub_feature_flags(vue_issues_list: false)
# Disable `refactor_blob_viewer` as we refactor
# the blob viewer. See the follwing epic for more:
# https://gitlab.com/groups/gitlab-org/-/epics/5531
stub_feature_flags(refactor_blob_viewer: false)
allow(Gitlab::GitalyClient).to receive(:can_use_disk?).and_return(enable_rugged)
else
unstub_all_feature_flags
......
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