Add latest changes from gitlab-org/gitlab@master
|
|
@ -1,3 +1,4 @@
|
|||
import * as Sentry from '@sentry/browser';
|
||||
import { escape } from 'lodash';
|
||||
import { spriteIcon } from './lib/utils/common_utils';
|
||||
|
||||
|
|
@ -109,8 +110,65 @@ const createFlash = function createFlash(
|
|||
return flashContainer;
|
||||
};
|
||||
|
||||
/*
|
||||
* Flash banner supports different types of Flash configurations
|
||||
* along with ability to provide actionConfig which can be used to show
|
||||
* additional action or link on banner next to message
|
||||
*
|
||||
* @param {Object} options Options to control the flash message
|
||||
* @param {String} options.message Flash message text
|
||||
* @param {String} options.type Type of Flash, it can be `notice`, `success`, `warning` or `alert` (default)
|
||||
* @param {Object} options.parent Reference to parent element under which Flash needs to appear
|
||||
* @param {Object} options.actonConfig Map of config to show action on banner
|
||||
* @param {String} href URL to which action config should point to (default: '#')
|
||||
* @param {String} title Title of action
|
||||
* @param {Function} clickHandler Method to call when action is clicked on
|
||||
* @param {Boolean} options.fadeTransition Boolean to determine whether to fade the alert out
|
||||
* @param {Boolean} options.captureError Boolean to determine whether to send error to sentry
|
||||
* @param {Object} options.error Error to be captured in sentry
|
||||
*/
|
||||
const newCreateFlash = function newCreateFlash({
|
||||
message,
|
||||
type = FLASH_TYPES.ALERT,
|
||||
parent = document,
|
||||
actionConfig = null,
|
||||
fadeTransition = true,
|
||||
addBodyClass = false,
|
||||
captureError = false,
|
||||
error = null,
|
||||
}) {
|
||||
const flashContainer = parent.querySelector('.flash-container');
|
||||
|
||||
if (!flashContainer) return null;
|
||||
|
||||
flashContainer.innerHTML = createFlashEl(message, type);
|
||||
|
||||
const flashEl = flashContainer.querySelector(`.flash-${type}`);
|
||||
|
||||
if (actionConfig) {
|
||||
flashEl.insertAdjacentHTML('beforeend', createAction(actionConfig));
|
||||
|
||||
if (actionConfig.clickHandler) {
|
||||
flashEl
|
||||
.querySelector('.flash-action')
|
||||
.addEventListener('click', e => actionConfig.clickHandler(e));
|
||||
}
|
||||
}
|
||||
|
||||
removeFlashClickListener(flashEl, fadeTransition);
|
||||
|
||||
flashContainer.classList.add('gl-display-block');
|
||||
|
||||
if (addBodyClass) document.body.classList.add('flash-shown');
|
||||
|
||||
if (captureError && error) Sentry.captureException(error);
|
||||
|
||||
return flashContainer;
|
||||
};
|
||||
|
||||
export {
|
||||
createFlash as default,
|
||||
newCreateFlash,
|
||||
createFlashEl,
|
||||
createAction,
|
||||
hideFlash,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,249 @@
|
|||
<script>
|
||||
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||
import {
|
||||
GlDeprecatedButton,
|
||||
GlNewDropdown,
|
||||
GlNewDropdownDivider,
|
||||
GlNewDropdownItem,
|
||||
GlModal,
|
||||
GlIcon,
|
||||
GlModalDirective,
|
||||
GlTooltipDirective,
|
||||
} from '@gitlab/ui';
|
||||
import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
|
||||
import DuplicateDashboardModal from './duplicate_dashboard_modal.vue';
|
||||
import CreateDashboardModal from './create_dashboard_modal.vue';
|
||||
import { s__ } from '~/locale';
|
||||
import invalidUrl from '~/lib/utils/invalid_url';
|
||||
import { redirectTo } from '~/lib/utils/url_utility';
|
||||
import TrackEventDirective from '~/vue_shared/directives/track_event';
|
||||
import { getAddMetricTrackingOptions } from '../utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlDeprecatedButton,
|
||||
GlNewDropdown,
|
||||
GlNewDropdownDivider,
|
||||
GlNewDropdownItem,
|
||||
GlModal,
|
||||
GlIcon,
|
||||
DuplicateDashboardModal,
|
||||
CreateDashboardModal,
|
||||
CustomMetricsFormFields,
|
||||
},
|
||||
directives: {
|
||||
GlModal: GlModalDirective,
|
||||
GlTooltip: GlTooltipDirective,
|
||||
TrackEvent: TrackEventDirective,
|
||||
},
|
||||
props: {
|
||||
addingMetricsAvailable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
customMetricsPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: invalidUrl,
|
||||
},
|
||||
validateQueryPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: invalidUrl,
|
||||
},
|
||||
defaultBranch: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return { customMetricsFormIsValid: null };
|
||||
},
|
||||
computed: {
|
||||
...mapState('monitoringDashboard', [
|
||||
'projectPath',
|
||||
'isUpdatingStarredValue',
|
||||
'addDashboardDocumentationPath',
|
||||
]),
|
||||
...mapGetters('monitoringDashboard', ['selectedDashboard']),
|
||||
isOutOfTheBoxDashboard() {
|
||||
return this.selectedDashboard?.out_of_the_box_dashboard;
|
||||
},
|
||||
isMenuItemEnabled() {
|
||||
return {
|
||||
createDashboard: Boolean(this.projectPath),
|
||||
editDashboard: this.selectedDashboard?.can_edit,
|
||||
};
|
||||
},
|
||||
isMenuItemShown() {
|
||||
return {
|
||||
duplicateDashboard: this.isOutOfTheBoxDashboard,
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions('monitoringDashboard', ['toggleStarredValue']),
|
||||
setFormValidity(isValid) {
|
||||
this.customMetricsFormIsValid = isValid;
|
||||
},
|
||||
hideAddMetricModal() {
|
||||
this.$refs.addMetricModal.hide();
|
||||
},
|
||||
getAddMetricTrackingOptions,
|
||||
submitCustomMetricsForm() {
|
||||
this.$refs.customMetricsForm.submit();
|
||||
},
|
||||
selectDashboard(dashboard) {
|
||||
// Once the sidebar See metrics link is updated to the new URL,
|
||||
// this sort of hardcoding will not be necessary.
|
||||
// https://gitlab.com/gitlab-org/gitlab/-/issues/229277
|
||||
const baseURL = `${this.projectPath}/-/metrics`;
|
||||
const dashboardPath = encodeURIComponent(
|
||||
dashboard.out_of_the_box_dashboard ? dashboard.path : dashboard.display_name,
|
||||
);
|
||||
redirectTo(`${baseURL}/${dashboardPath}`);
|
||||
},
|
||||
},
|
||||
|
||||
modalIds: {
|
||||
addMetric: 'addMetric',
|
||||
createDashboard: 'createDashboard',
|
||||
duplicateDashboard: 'duplicateDashboard',
|
||||
},
|
||||
i18n: {
|
||||
actionsMenu: s__('Metrics|More actions'),
|
||||
duplicateDashboard: s__('Metrics|Duplicate current dashboard'),
|
||||
starDashboard: s__('Metrics|Star dashboard'),
|
||||
unstarDashboard: s__('Metrics|Unstar dashboard'),
|
||||
addMetric: s__('Metrics|Add metric'),
|
||||
editDashboardInfo: s__('Metrics|Duplicate this dashboard to edit dashboard YAML'),
|
||||
editDashboard: s__('Metrics|Edit dashboard YAML'),
|
||||
createDashboard: s__('Metrics|Create new dashboard'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-new-dropdown
|
||||
v-gl-tooltip
|
||||
data-testid="actions-menu"
|
||||
data-qa-selector="actions_menu_dropdown"
|
||||
right
|
||||
no-caret
|
||||
toggle-class="gl-px-3!"
|
||||
:title="$options.i18n.actionsMenu"
|
||||
>
|
||||
<template #button-content>
|
||||
<gl-icon class="gl-mr-0!" name="ellipsis_v" />
|
||||
</template>
|
||||
|
||||
<template v-if="addingMetricsAvailable">
|
||||
<gl-new-dropdown-item
|
||||
v-gl-modal="$options.modalIds.addMetric"
|
||||
data-qa-selector="add_metric_button"
|
||||
data-testid="add-metric-item"
|
||||
>
|
||||
{{ $options.i18n.addMetric }}
|
||||
</gl-new-dropdown-item>
|
||||
<gl-modal
|
||||
ref="addMetricModal"
|
||||
:modal-id="$options.modalIds.addMetric"
|
||||
:title="$options.i18n.addMetric"
|
||||
data-testid="add-metric-modal"
|
||||
>
|
||||
<form ref="customMetricsForm" :action="customMetricsPath" method="post">
|
||||
<custom-metrics-form-fields
|
||||
:validate-query-path="validateQueryPath"
|
||||
form-operation="post"
|
||||
@formValidation="setFormValidity"
|
||||
/>
|
||||
</form>
|
||||
<div slot="modal-footer">
|
||||
<gl-deprecated-button @click="hideAddMetricModal">
|
||||
{{ __('Cancel') }}
|
||||
</gl-deprecated-button>
|
||||
<gl-deprecated-button
|
||||
v-track-event="getAddMetricTrackingOptions()"
|
||||
data-testid="add-metric-modal-submit-button"
|
||||
:disabled="!customMetricsFormIsValid"
|
||||
variant="success"
|
||||
@click="submitCustomMetricsForm"
|
||||
>
|
||||
{{ __('Save changes') }}
|
||||
</gl-deprecated-button>
|
||||
</div>
|
||||
</gl-modal>
|
||||
</template>
|
||||
|
||||
<gl-new-dropdown-item
|
||||
v-if="isMenuItemEnabled.editDashboard"
|
||||
:href="selectedDashboard ? selectedDashboard.project_blob_path : null"
|
||||
data-qa-selector="edit_dashboard_button_enabled"
|
||||
data-testid="edit-dashboard-item-enabled"
|
||||
>
|
||||
{{ $options.i18n.editDashboard }}
|
||||
</gl-new-dropdown-item>
|
||||
|
||||
<!--
|
||||
wrapper for tooltip as button can be `disabled`
|
||||
https://bootstrap-vue.org/docs/components/tooltip#disabled-elements
|
||||
-->
|
||||
<div v-else v-gl-tooltip :title="$options.i18n.editDashboardInfo">
|
||||
<gl-new-dropdown-item
|
||||
:alt="$options.i18n.editDashboardInfo"
|
||||
:href="selectedDashboard ? selectedDashboard.project_blob_path : null"
|
||||
data-testid="edit-dashboard-item-disabled"
|
||||
disabled
|
||||
class="gl-cursor-not-allowed"
|
||||
>
|
||||
<span class="gl-text-gray-400">{{ $options.i18n.editDashboard }}</span>
|
||||
</gl-new-dropdown-item>
|
||||
</div>
|
||||
|
||||
<template v-if="isMenuItemShown.duplicateDashboard">
|
||||
<gl-new-dropdown-item
|
||||
v-gl-modal="$options.modalIds.duplicateDashboard"
|
||||
data-testid="duplicate-dashboard-item"
|
||||
>
|
||||
{{ $options.i18n.duplicateDashboard }}
|
||||
</gl-new-dropdown-item>
|
||||
|
||||
<duplicate-dashboard-modal
|
||||
:default-branch="defaultBranch"
|
||||
:modal-id="$options.modalIds.duplicateDashboard"
|
||||
data-testid="duplicate-dashboard-modal"
|
||||
@dashboardDuplicated="selectDashboard"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<gl-new-dropdown-item
|
||||
v-if="selectedDashboard"
|
||||
data-testid="star-dashboard-item"
|
||||
:disabled="isUpdatingStarredValue"
|
||||
@click="toggleStarredValue()"
|
||||
>
|
||||
{{ selectedDashboard.starred ? $options.i18n.unstarDashboard : $options.i18n.starDashboard }}
|
||||
</gl-new-dropdown-item>
|
||||
|
||||
<gl-new-dropdown-divider />
|
||||
|
||||
<gl-new-dropdown-item
|
||||
v-gl-modal="$options.modalIds.createDashboard"
|
||||
data-testid="create-dashboard-item"
|
||||
:disabled="!isMenuItemEnabled.createDashboard"
|
||||
:class="{ 'monitoring-actions-item-disabled': !isMenuItemEnabled.createDashboard }"
|
||||
>
|
||||
{{ $options.i18n.createDashboard }}
|
||||
</gl-new-dropdown-item>
|
||||
|
||||
<template v-if="isMenuItemEnabled.createDashboard">
|
||||
<create-dashboard-modal
|
||||
data-testid="create-dashboard-modal"
|
||||
:add-dashboard-documentation-path="addDashboardDocumentationPath"
|
||||
:modal-id="$options.modalIds.createDashboard"
|
||||
:project-path="projectPath"
|
||||
/>
|
||||
</template>
|
||||
</gl-new-dropdown>
|
||||
</template>
|
||||
|
|
@ -7,17 +7,11 @@ import {
|
|||
GlDeprecatedDropdownItem,
|
||||
GlDeprecatedDropdownHeader,
|
||||
GlDeprecatedDropdownDivider,
|
||||
GlNewDropdown,
|
||||
GlNewDropdownDivider,
|
||||
GlNewDropdownItem,
|
||||
GlModal,
|
||||
GlLoadingIcon,
|
||||
GlSearchBoxByType,
|
||||
GlModalDirective,
|
||||
GlTooltipDirective,
|
||||
} from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
|
||||
import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
|
||||
import invalidUrl from '~/lib/utils/invalid_url';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
|
|
@ -25,11 +19,9 @@ import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_p
|
|||
|
||||
import DashboardsDropdown from './dashboards_dropdown.vue';
|
||||
import RefreshButton from './refresh_button.vue';
|
||||
import CreateDashboardModal from './create_dashboard_modal.vue';
|
||||
import DuplicateDashboardModal from './duplicate_dashboard_modal.vue';
|
||||
import ActionsMenu from './dashboard_actions_menu.vue';
|
||||
|
||||
import TrackEventDirective from '~/vue_shared/directives/track_event';
|
||||
import { getAddMetricTrackingOptions, timeRangeToUrl } from '../utils';
|
||||
import { timeRangeToUrl } from '../utils';
|
||||
import { timeRanges } from '~/vue_shared/constants';
|
||||
import { timezones } from '../format_date';
|
||||
|
||||
|
|
@ -42,23 +34,17 @@ export default {
|
|||
GlDeprecatedDropdownItem,
|
||||
GlDeprecatedDropdownHeader,
|
||||
GlDeprecatedDropdownDivider,
|
||||
GlNewDropdown,
|
||||
GlNewDropdownDivider,
|
||||
GlNewDropdownItem,
|
||||
GlSearchBoxByType,
|
||||
GlModal,
|
||||
CustomMetricsFormFields,
|
||||
|
||||
DateTimePicker,
|
||||
DashboardsDropdown,
|
||||
RefreshButton,
|
||||
DuplicateDashboardModal,
|
||||
CreateDashboardModal,
|
||||
|
||||
ActionsMenu,
|
||||
},
|
||||
directives: {
|
||||
GlModal: GlModalDirective,
|
||||
GlTooltip: GlTooltipDirective,
|
||||
TrackEvent: TrackEventDirective,
|
||||
},
|
||||
props: {
|
||||
defaultBranch: {
|
||||
|
|
@ -94,29 +80,19 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formIsValid: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState('monitoringDashboard', [
|
||||
'emptyState',
|
||||
'environmentsLoading',
|
||||
'currentEnvironmentName',
|
||||
'isUpdatingStarredValue',
|
||||
'dashboardTimezone',
|
||||
'projectPath',
|
||||
'canAccessOperationsSettings',
|
||||
'operationsSettingsPath',
|
||||
'currentDashboard',
|
||||
'addDashboardDocumentationPath',
|
||||
'externalDashboardUrl',
|
||||
]),
|
||||
...mapGetters('monitoringDashboard', ['selectedDashboard', 'filteredEnvironments']),
|
||||
isOutOfTheBoxDashboard() {
|
||||
return this.selectedDashboard?.out_of_the_box_dashboard;
|
||||
},
|
||||
shouldShowEmptyState() {
|
||||
return Boolean(this.emptyState);
|
||||
},
|
||||
|
|
@ -130,7 +106,7 @@ export default {
|
|||
// Custom metrics only avaialble on system dashboards because
|
||||
// they are stored in the database. This can be improved. See:
|
||||
// https://gitlab.com/gitlab-org/gitlab/-/issues/28241
|
||||
this.selectedDashboard?.system_dashboard
|
||||
this.selectedDashboard?.out_of_the_box_dashboard
|
||||
);
|
||||
},
|
||||
showRearrangePanelsBtn() {
|
||||
|
|
@ -139,15 +115,12 @@ export default {
|
|||
displayUtc() {
|
||||
return this.dashboardTimezone === timezones.UTC;
|
||||
},
|
||||
shouldShowActionsMenu() {
|
||||
return Boolean(this.projectPath);
|
||||
},
|
||||
shouldShowSettingsButton() {
|
||||
return this.canAccessOperationsSettings && this.operationsSettingsPath;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions('monitoringDashboard', ['filterEnvironments', 'toggleStarredValue']),
|
||||
...mapActions('monitoringDashboard', ['filterEnvironments']),
|
||||
selectDashboard(dashboard) {
|
||||
// Once the sidebar See metrics link is updated to the new URL,
|
||||
// this sort of hardcoding will not be necessary.
|
||||
|
|
@ -171,16 +144,6 @@ export default {
|
|||
toggleRearrangingPanels() {
|
||||
this.$emit('setRearrangingPanels', !this.isRearrangingPanels);
|
||||
},
|
||||
setFormValidity(isValid) {
|
||||
this.formIsValid = isValid;
|
||||
},
|
||||
hideAddMetricModal() {
|
||||
this.$refs.addMetricModal.hide();
|
||||
},
|
||||
getAddMetricTrackingOptions,
|
||||
submitCustomMetricsForm() {
|
||||
this.$refs.customMetricsForm.submit();
|
||||
},
|
||||
getEnvironmentPath(environment) {
|
||||
// Once the sidebar See metrics link is updated to the new URL,
|
||||
// this sort of hardcoding will not be necessary.
|
||||
|
|
@ -193,16 +156,6 @@ export default {
|
|||
return mergeUrlParams({ environment }, url);
|
||||
},
|
||||
},
|
||||
modalIds: {
|
||||
addMetric: 'addMetric',
|
||||
createDashboard: 'createDashboard',
|
||||
duplicateDashboard: 'duplicateDashboard',
|
||||
},
|
||||
i18n: {
|
||||
starDashboard: s__('Metrics|Star dashboard'),
|
||||
unstarDashboard: s__('Metrics|Unstar dashboard'),
|
||||
addMetric: s__('Metrics|Add metric'),
|
||||
},
|
||||
timeRanges,
|
||||
};
|
||||
</script>
|
||||
|
|
@ -280,29 +233,6 @@ export default {
|
|||
<div class="flex-grow-1"></div>
|
||||
|
||||
<div class="d-sm-flex">
|
||||
<div v-if="selectedDashboard" class="mb-2 mr-2 d-flex">
|
||||
<!--
|
||||
wrapper for tooltip as button can be `disabled`
|
||||
https://bootstrap-vue.org/docs/components/tooltip#disabled-elements
|
||||
-->
|
||||
<div
|
||||
v-gl-tooltip
|
||||
class="flex-grow-1"
|
||||
:title="
|
||||
selectedDashboard.starred ? $options.i18n.unstarDashboard : $options.i18n.starDashboard
|
||||
"
|
||||
>
|
||||
<gl-button
|
||||
ref="toggleStarBtn"
|
||||
class="w-100"
|
||||
:disabled="isUpdatingStarredValue"
|
||||
variant="default"
|
||||
:icon="selectedDashboard.starred ? 'star' : 'star-o'"
|
||||
@click="toggleStarredValue()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showRearrangePanelsBtn" class="mb-2 mr-2 d-flex">
|
||||
<gl-button
|
||||
:pressed="isRearrangingPanels"
|
||||
|
|
@ -313,58 +243,6 @@ export default {
|
|||
{{ __('Arrange charts') }}
|
||||
</gl-button>
|
||||
</div>
|
||||
<div v-if="addingMetricsAvailable" class="mb-2 mr-2 d-flex d-sm-block">
|
||||
<gl-button
|
||||
ref="addMetricBtn"
|
||||
v-gl-modal="$options.modalIds.addMetric"
|
||||
variant="default"
|
||||
data-qa-selector="add_metric_button"
|
||||
class="flex-grow-1"
|
||||
>
|
||||
{{ $options.i18n.addMetric }}
|
||||
</gl-button>
|
||||
<gl-modal
|
||||
ref="addMetricModal"
|
||||
:modal-id="$options.modalIds.addMetric"
|
||||
:title="$options.i18n.addMetric"
|
||||
>
|
||||
<form ref="customMetricsForm" :action="customMetricsPath" method="post">
|
||||
<custom-metrics-form-fields
|
||||
:validate-query-path="validateQueryPath"
|
||||
form-operation="post"
|
||||
@formValidation="setFormValidity"
|
||||
/>
|
||||
</form>
|
||||
<div slot="modal-footer">
|
||||
<gl-button @click="hideAddMetricModal">
|
||||
{{ __('Cancel') }}
|
||||
</gl-button>
|
||||
<gl-button
|
||||
ref="submitCustomMetricsFormBtn"
|
||||
v-track-event="getAddMetricTrackingOptions()"
|
||||
:disabled="!formIsValid"
|
||||
variant="success"
|
||||
category="primary"
|
||||
@click="submitCustomMetricsForm"
|
||||
>
|
||||
{{ __('Save changes') }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</gl-modal>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="selectedDashboard && selectedDashboard.can_edit"
|
||||
class="mb-2 mr-2 d-flex d-sm-block"
|
||||
>
|
||||
<gl-button
|
||||
class="flex-grow-1 js-edit-link"
|
||||
:href="selectedDashboard.project_blob_path"
|
||||
data-qa-selector="edit_dashboard_button"
|
||||
>
|
||||
{{ __('Edit dashboard') }}
|
||||
</gl-button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="externalDashboardUrl && externalDashboardUrl.length"
|
||||
|
|
@ -382,65 +260,28 @@ export default {
|
|||
</gl-button>
|
||||
</div>
|
||||
|
||||
<!-- This separator should be displayed only if at least one of the action menu or settings button are displayed -->
|
||||
<span
|
||||
v-if="shouldShowActionsMenu || shouldShowSettingsButton"
|
||||
aria-hidden="true"
|
||||
class="gl-pl-3 border-left gl-mb-3 d-none d-sm-block"
|
||||
></span>
|
||||
|
||||
<div v-if="shouldShowActionsMenu" class="gl-mb-3 gl-mr-3 d-flex d-sm-block">
|
||||
<gl-new-dropdown
|
||||
v-gl-tooltip
|
||||
right
|
||||
class="gl-flex-grow-1"
|
||||
data-testid="actions-menu"
|
||||
data-qa-selector="actions_menu_dropdown"
|
||||
:title="s__('Metrics|Create dashboard')"
|
||||
:icon="'plus-square'"
|
||||
>
|
||||
<gl-new-dropdown-item
|
||||
v-gl-modal="$options.modalIds.createDashboard"
|
||||
data-testid="action-create-dashboard"
|
||||
>{{ s__('Metrics|Create new dashboard') }}</gl-new-dropdown-item
|
||||
>
|
||||
|
||||
<create-dashboard-modal
|
||||
data-testid="create-dashboard-modal"
|
||||
:add-dashboard-documentation-path="addDashboardDocumentationPath"
|
||||
:modal-id="$options.modalIds.createDashboard"
|
||||
:project-path="projectPath"
|
||||
/>
|
||||
|
||||
<template v-if="isOutOfTheBoxDashboard">
|
||||
<gl-new-dropdown-divider />
|
||||
|
||||
<gl-new-dropdown-item
|
||||
ref="duplicateDashboardItem"
|
||||
v-gl-modal="$options.modalIds.duplicateDashboard"
|
||||
data-testid="action-duplicate-dashboard"
|
||||
>
|
||||
{{ s__('Metrics|Duplicate current dashboard') }}
|
||||
</gl-new-dropdown-item>
|
||||
|
||||
<duplicate-dashboard-modal
|
||||
:default-branch="defaultBranch"
|
||||
:modal-id="$options.modalIds.duplicateDashboard"
|
||||
@dashboardDuplicated="selectDashboard"
|
||||
/>
|
||||
</template>
|
||||
</gl-new-dropdown>
|
||||
</div>
|
||||
|
||||
<div v-if="shouldShowSettingsButton" class="mb-2 mr-2 d-flex d-sm-block">
|
||||
<gl-button
|
||||
v-gl-tooltip
|
||||
data-testid="metrics-settings-button"
|
||||
icon="settings"
|
||||
:href="operationsSettingsPath"
|
||||
:title="s__('Metrics|Metrics Settings')"
|
||||
<div class="gl-mb-3 gl-mr-3 d-flex d-sm-block">
|
||||
<actions-menu
|
||||
:adding-metrics-available="addingMetricsAvailable"
|
||||
:custom-metrics-path="customMetricsPath"
|
||||
:validate-query-path="validateQueryPath"
|
||||
:default-branch="defaultBranch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template v-if="shouldShowSettingsButton">
|
||||
<span aria-hidden="true" class="gl-pl-3 border-left gl-mb-3 d-none d-sm-block"></span>
|
||||
|
||||
<div class="mb-2 mr-2 d-flex d-sm-block">
|
||||
<gl-button
|
||||
v-gl-tooltip
|
||||
data-testid="metrics-settings-button"
|
||||
icon="settings"
|
||||
:href="operationsSettingsPath"
|
||||
:title="s__('Metrics|Metrics Settings')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
<script>
|
||||
import { GlLink, GlSprintf } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import CodeInstruction from './code_instruction.vue';
|
||||
import { TrackingActions } from '../constants';
|
||||
import { mapGetters, mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'ComposerInstallation',
|
||||
components: {
|
||||
CodeInstruction,
|
||||
GlLink,
|
||||
GlSprintf,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['composerHelpPath']),
|
||||
...mapGetters(['composerRegistryInclude', 'composerPackageInclude']),
|
||||
},
|
||||
i18n: {
|
||||
registryInclude: s__('PackageRegistry|composer.json registry include'),
|
||||
copyRegistryInclude: s__('PackageRegistry|Copy registry include'),
|
||||
packageInclude: s__('PackageRegistry|composer.json require package include'),
|
||||
copyPackageInclude: s__('PackageRegistry|Copy require package include'),
|
||||
infoLine: s__(
|
||||
'PackageRegistry|For more information on Composer packages in GitLab, %{linkStart}see the documentation.%{linkEnd}',
|
||||
),
|
||||
},
|
||||
trackingActions: { ...TrackingActions },
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<p class="gl-mt-3 gl-font-weight-bold" data-testid="registry-include-title">
|
||||
{{ $options.i18n.registryInclude }}
|
||||
</p>
|
||||
<code-instruction
|
||||
:instruction="composerRegistryInclude"
|
||||
:copy-text="$options.i18n.copyRegistryInclude"
|
||||
:tracking-action="$options.trackingActions.COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND"
|
||||
/>
|
||||
|
||||
<p class="gl-mt-3 gl-font-weight-bold" data-testid="package-include-title">
|
||||
{{ $options.i18n.packageInclude }}
|
||||
</p>
|
||||
<code-instruction
|
||||
:instruction="composerPackageInclude"
|
||||
:copy-text="$options.i18n.copyPackageInclude"
|
||||
:tracking-action="$options.trackingActions.COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND"
|
||||
/>
|
||||
<span data-testid="help-text">
|
||||
<gl-sprintf :message="$options.i18n.infoLine">
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="composerHelpPath" target="_blank">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -4,6 +4,7 @@ import MavenInstallation from './maven_installation.vue';
|
|||
import NpmInstallation from './npm_installation.vue';
|
||||
import NugetInstallation from './nuget_installation.vue';
|
||||
import PypiInstallation from './pypi_installation.vue';
|
||||
import ComposerInstallation from './composer_installation.vue';
|
||||
import { PackageType } from '../../shared/constants';
|
||||
|
||||
export default {
|
||||
|
|
@ -14,6 +15,7 @@ export default {
|
|||
[PackageType.NPM]: NpmInstallation,
|
||||
[PackageType.NUGET]: NugetInstallation,
|
||||
[PackageType.PYPI]: PypiInstallation,
|
||||
[PackageType.COMPOSER]: ComposerInstallation,
|
||||
},
|
||||
props: {
|
||||
packageEntity: {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export const TrackingLabels = {
|
|||
NPM_INSTALLATION: 'npm_installation',
|
||||
NUGET_INSTALLATION: 'nuget_installation',
|
||||
PYPI_INSTALLATION: 'pypi_installation',
|
||||
COMPOSER_INSTALLATION: 'composer_installation',
|
||||
};
|
||||
|
||||
export const TrackingActions = {
|
||||
|
|
@ -31,6 +32,9 @@ export const TrackingActions = {
|
|||
|
||||
COPY_PIP_INSTALL_COMMAND: 'copy_pip_install_command',
|
||||
COPY_PYPI_SETUP_COMMAND: 'copy_pypi_setup_command',
|
||||
|
||||
COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND: 'copy_composer_registry_include_command',
|
||||
COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND: 'copy_composer_package_include_command',
|
||||
};
|
||||
|
||||
export const NpmManager = {
|
||||
|
|
|
|||
|
|
@ -104,3 +104,12 @@ export const pypiSetupCommand = ({ pypiSetupPath }) => `[gitlab]
|
|||
repository = ${pypiSetupPath}
|
||||
username = __token__
|
||||
password = <your personal access token>`;
|
||||
|
||||
export const composerRegistryInclude = ({ composerPath }) => {
|
||||
const base = { type: 'composer', url: composerPath };
|
||||
return JSON.stringify(base);
|
||||
};
|
||||
export const composerPackageInclude = ({ packageEntity }) => {
|
||||
const base = { package_name: packageEntity.name };
|
||||
return JSON.stringify(base);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -54,6 +54,16 @@ class Import::GiteaController < Import::GithubController
|
|||
end
|
||||
end
|
||||
|
||||
override :client_repos
|
||||
def client_repos
|
||||
@client_repos ||= filtered(client.repos)
|
||||
end
|
||||
|
||||
override :client
|
||||
def client
|
||||
@client ||= Gitlab::LegacyGithubImport::Client.new(session[access_token_key], client_options)
|
||||
end
|
||||
|
||||
override :client_options
|
||||
def client_options
|
||||
{ host: provider_url, api_version: 'v1' }
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ class Import::GithubController < Import::BaseController
|
|||
before_action :provider_auth, only: [:status, :realtime_changes, :create]
|
||||
before_action :expire_etag_cache, only: [:status, :create]
|
||||
|
||||
OAuthConfigMissingError = Class.new(StandardError)
|
||||
|
||||
rescue_from OAuthConfigMissingError, with: :missing_oauth_config
|
||||
rescue_from Octokit::Unauthorized, with: :provider_unauthorized
|
||||
rescue_from Octokit::TooManyRequests, with: :provider_rate_limit
|
||||
|
||||
|
|
@ -22,7 +25,7 @@ class Import::GithubController < Import::BaseController
|
|||
end
|
||||
|
||||
def callback
|
||||
session[access_token_key] = client.get_token(params[:code])
|
||||
session[access_token_key] = get_token(params[:code])
|
||||
redirect_to status_import_url
|
||||
end
|
||||
|
||||
|
|
@ -77,9 +80,7 @@ class Import::GithubController < Import::BaseController
|
|||
override :provider_url
|
||||
def provider_url
|
||||
strong_memoize(:provider_url) do
|
||||
provider = Gitlab::Auth::OAuth::Provider.config_for('github')
|
||||
|
||||
provider&.dig('url').presence || 'https://github.com'
|
||||
oauth_config&.dig('url').presence || 'https://github.com'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -104,11 +105,66 @@ class Import::GithubController < Import::BaseController
|
|||
end
|
||||
|
||||
def client
|
||||
@client ||= Gitlab::LegacyGithubImport::Client.new(session[access_token_key], client_options)
|
||||
@client ||= if Feature.enabled?(:remove_legacy_github_client)
|
||||
Gitlab::GithubImport::Client.new(session[access_token_key])
|
||||
else
|
||||
Gitlab::LegacyGithubImport::Client.new(session[access_token_key], client_options)
|
||||
end
|
||||
end
|
||||
|
||||
def client_repos
|
||||
@client_repos ||= filtered(client.repos)
|
||||
@client_repos ||= if Feature.enabled?(:remove_legacy_github_client)
|
||||
filtered(concatenated_repos)
|
||||
else
|
||||
filtered(client.repos)
|
||||
end
|
||||
end
|
||||
|
||||
def concatenated_repos
|
||||
return [] unless client.respond_to?(:each_page)
|
||||
|
||||
client.each_page(:repos).flat_map(&:objects)
|
||||
end
|
||||
|
||||
def oauth_client
|
||||
raise OAuthConfigMissingError unless oauth_config
|
||||
|
||||
@oauth_client ||= ::OAuth2::Client.new(
|
||||
oauth_config.app_id,
|
||||
oauth_config.app_secret,
|
||||
oauth_options.merge(ssl: { verify: oauth_config['verify_ssl'] })
|
||||
)
|
||||
end
|
||||
|
||||
def oauth_config
|
||||
@oauth_config ||= Gitlab::Auth::OAuth::Provider.config_for('github')
|
||||
end
|
||||
|
||||
def oauth_options
|
||||
if oauth_config
|
||||
oauth_config.dig('args', 'client_options').deep_symbolize_keys
|
||||
else
|
||||
OmniAuth::Strategies::GitHub.default_options[:client_options].symbolize_keys
|
||||
end
|
||||
end
|
||||
|
||||
def authorize_url
|
||||
if Feature.enabled?(:remove_legacy_github_client)
|
||||
oauth_client.auth_code.authorize_url(
|
||||
redirect_uri: callback_import_url,
|
||||
scope: 'repo, user, user:email'
|
||||
)
|
||||
else
|
||||
client.authorize_url(callback_import_url)
|
||||
end
|
||||
end
|
||||
|
||||
def get_token(code)
|
||||
if Feature.enabled?(:remove_legacy_github_client)
|
||||
oauth_client.auth_code.get_token(code).token
|
||||
else
|
||||
client.get_token(code)
|
||||
end
|
||||
end
|
||||
|
||||
def verify_import_enabled
|
||||
|
|
@ -116,7 +172,7 @@ class Import::GithubController < Import::BaseController
|
|||
end
|
||||
|
||||
def go_to_provider_for_permissions
|
||||
redirect_to client.authorize_url(callback_import_url)
|
||||
redirect_to authorize_url
|
||||
end
|
||||
|
||||
def import_enabled?
|
||||
|
|
@ -152,6 +208,12 @@ class Import::GithubController < Import::BaseController
|
|||
alert: _("GitHub API rate limit exceeded. Try again after %{reset_time}") % { reset_time: reset_time }
|
||||
end
|
||||
|
||||
def missing_oauth_config
|
||||
session[access_token_key] = nil
|
||||
redirect_to new_import_url,
|
||||
alert: _('Missing OAuth configuration for GitHub.')
|
||||
end
|
||||
|
||||
def access_token_key
|
||||
:"#{provider_name}_access_token"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ module PackagesHelper
|
|||
full_url.sub!('://', '://__token__:<your_personal_token>@')
|
||||
end
|
||||
|
||||
def composer_registry_url(group_id)
|
||||
expose_url(api_v4_group___packages_composer_packages_path(id: group_id, format: '.json'))
|
||||
end
|
||||
|
||||
def packages_coming_soon_enabled?(resource)
|
||||
::Feature.enabled?(:packages_coming_soon, resource) && ::Gitlab.dev_env_or_com?
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
class PersonalAccessTokenPolicy < BasePolicy
|
||||
condition(:is_owner) { user && subject.user_id == user.id }
|
||||
|
||||
rule { is_owner | admin & ~blocked }.policy do
|
||||
rule { (is_owner | admin) & ~blocked }.policy do
|
||||
enable :read_token
|
||||
enable :revoke_token
|
||||
end
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ module Import
|
|||
end
|
||||
|
||||
def repo
|
||||
@repo ||= client.repo(params[:repo_id].to_i)
|
||||
@repo ||= client.repository(params[:repo_id].to_i)
|
||||
end
|
||||
|
||||
def project_name
|
||||
|
|
|
|||
|
|
@ -20,4 +20,6 @@
|
|||
pypi_path: pypi_registry_url(@project.id),
|
||||
pypi_setup_path: package_registry_project_url(@project.id, :pypi),
|
||||
pypi_help_path: help_page_path('user/packages/pypi_repository/index'),
|
||||
composer_path: composer_registry_url(@project&.group&.id),
|
||||
composer_help_path: help_page_path('user/packages/composer_repository/index'),
|
||||
project_name: @project.name} }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add installation instructions for Composer
|
||||
merge_request: 38779
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Change UI and add new actions to monitor dashboard actions menu
|
||||
merge_request: 38946
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -2391,7 +2391,7 @@ input DastOnDemandScanCreateInput {
|
|||
"""
|
||||
ID of the site profile to be used for the scan.
|
||||
"""
|
||||
dastSiteProfileId: ID!
|
||||
dastSiteProfileId: DastSiteProfileID!
|
||||
|
||||
"""
|
||||
The project the site profile belongs to.
|
||||
|
|
|
|||
|
|
@ -6374,7 +6374,7 @@
|
|||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"name": "DastSiteProfileID",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
|
@ -20,7 +20,7 @@ The metrics as defined below do not support alerts, unlike
|
|||
|
||||
## Add a new dashboard to your project
|
||||
|
||||
> UI option [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/223204) in GitLab 13.2.
|
||||
> UI option [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/228856) in GitLab 13.3.
|
||||
|
||||
You can configure a custom dashboard by adding a new YAML file into your project's
|
||||
`.gitlab/dashboards/` directory. For the dashboard to display on your project's **Operations > Metrics** page, the files must have a `.yml`
|
||||
|
|
@ -31,9 +31,9 @@ To create a new dashboard from the GitLab user interface:
|
|||
1. Sign in to GitLab as a user with Maintainer or Owner
|
||||
[permissions](../../../user/permissions.md#project-members-permissions).
|
||||
1. Navigate to your dashboard at **Operations > Metrics**.
|
||||
1. In the top-right corner of your dashboard, click the **{file-addition-solid}** **Actions** menu,
|
||||
1. In the top-right corner of your dashboard, click the **{{ellipsis_v}}** **More actions** menu,
|
||||
and select **Create new**:
|
||||

|
||||

|
||||
1. In the modal window, click **Open Repository**, then follow the instructions
|
||||
for creating a new dashboard from the command line.
|
||||
|
||||
|
|
@ -82,7 +82,7 @@ The resulting `.yml` file can be customized and adapted to your project.
|
|||
You can decide to save the dashboard `.yml` file in the project's **default** branch or in a
|
||||
new branch.
|
||||
|
||||
1. Click **Duplicate dashboard** in the actions menu.
|
||||
1. Click **Duplicate current dashboard** in the **{{ellipsis_v}}** **More actions** menu.
|
||||
|
||||
NOTE: **Note:**
|
||||
You can duplicate only GitLab-defined dashboards.
|
||||
|
|
@ -105,7 +105,7 @@ To manage the settings for your metrics dashboard:
|
|||
1. Navigate to your dashboard at **Operations > Metrics**.
|
||||
1. In the top-right corner of your dashboard, click **Metrics Settings**:
|
||||
|
||||

|
||||

|
||||
|
||||
## Chart Context Menu
|
||||
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
|
@ -25,7 +25,7 @@ To view the metrics dashboard for an environment that has
|
|||
GitLab displays the default metrics dashboard for the environment, like the
|
||||
following example:
|
||||
|
||||

|
||||

|
||||
|
||||
The top of the dashboard contains a navigation bar. From left to right, the
|
||||
navigation bar contains:
|
||||
|
|
@ -37,15 +37,19 @@ navigation bar contains:
|
|||
- **Range** - The time period of data to display.
|
||||
- **Refresh dashboard** **{retry}** - Reload the dashboard with current data.
|
||||
- **Set refresh rate** - Set a time frame for refreshing the data displayed.
|
||||
- **Star dashboard** **{star-o}** - Click to mark a dashboard as a favorite.
|
||||
- **More actions** **{ellipsis_v}** - More dashboard actions
|
||||
- **Add metric** - Adds a [custom metric](#adding-custom-metrics). Only available on GitLab-defined dashboards.
|
||||
([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/34779) in GitLab 12.5.)
|
||||
- **Edit dashboard YAML** - Edit the source YAML file of a custom dashboard. Only available on
|
||||
[custom dashboards](dashboards/index.md).
|
||||
([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/34779) in GitLab 12.5.)
|
||||
- **Duplicate current dashboard** - Save a [complete copy of a dashboard](dashboards/index.md#duplicate-a-gitlab-defined-dashboard). Only available on GitLab-defined dashboards.
|
||||
- **Star dashboard** **{star-o}** - Click to mark a dashboard as a favorite.
|
||||
Starred dashboards display a solid star **{star}** button, and display first
|
||||
in the **Dashboard** dropdown list.
|
||||
([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214582) in GitLab 13.0.)
|
||||
- **Edit dashboard** - Edit the source YAML file of a custom dashboard. Only available on
|
||||
[custom dashboards](dashboards/index.md).
|
||||
([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/34779) in GitLab 12.5.)
|
||||
- **Create dashboard** **{file-addition-solid}** - Create a
|
||||
[new custom dashboard for your project](dashboards/index.md#add-a-new-dashboard-to-your-project).
|
||||
- **Create new dashboard** - Create a [new custom dashboard for your project](dashboards/index.md#add-a-new-dashboard-to-your-project).
|
||||
([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/228856) in GitLab 13.3.)
|
||||
- **Metrics settings** - Configure the
|
||||
[settings for this dashboard](dashboards/index.md#manage-the-metrics-dashboard-settings).
|
||||
|
||||
|
|
@ -70,7 +74,7 @@ helps quickly create a deployment:
|
|||
1. When the pipeline has run successfully, graphs are available on the
|
||||
**Operations > Metrics** page.
|
||||
|
||||

|
||||

|
||||
|
||||
## Customize your metrics dashboard
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,11 @@ module API
|
|||
|
||||
helpers do
|
||||
def client
|
||||
@client ||= Gitlab::LegacyGithubImport::Client.new(params[:personal_access_token], client_options)
|
||||
@client ||= if Feature.enabled?(:remove_legacy_github_client)
|
||||
Gitlab::GithubImport::Client.new(params[:personal_access_token])
|
||||
else
|
||||
Gitlab::LegacyGithubImport::Client.new(params[:personal_access_token], client_options)
|
||||
end
|
||||
end
|
||||
|
||||
def access_params
|
||||
|
|
|
|||
|
|
@ -8804,9 +8804,6 @@ msgstr ""
|
|||
msgid "Edit comment"
|
||||
msgstr ""
|
||||
|
||||
msgid "Edit dashboard"
|
||||
msgstr ""
|
||||
|
||||
msgid "Edit description"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15236,9 +15233,6 @@ msgstr ""
|
|||
msgid "Metrics|Create custom dashboard %{fileName}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Metrics|Create dashboard"
|
||||
msgstr ""
|
||||
|
||||
msgid "Metrics|Create metric"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15272,9 +15266,15 @@ msgstr ""
|
|||
msgid "Metrics|Duplicate dashboard"
|
||||
msgstr ""
|
||||
|
||||
msgid "Metrics|Duplicate this dashboard to edit dashboard YAML"
|
||||
msgstr ""
|
||||
|
||||
msgid "Metrics|Duplicating..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Metrics|Edit dashboard YAML"
|
||||
msgstr ""
|
||||
|
||||
msgid "Metrics|Edit metric"
|
||||
msgid_plural "Metrics|Edit metrics"
|
||||
msgstr[0] ""
|
||||
|
|
@ -15316,6 +15316,9 @@ msgstr ""
|
|||
msgid "Metrics|Min"
|
||||
msgstr ""
|
||||
|
||||
msgid "Metrics|More actions"
|
||||
msgstr ""
|
||||
|
||||
msgid "Metrics|Must be a valid PromQL query."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15639,6 +15642,9 @@ msgstr ""
|
|||
msgid "Mirroring will only be available if the feature is included in the plan of the selected group or user."
|
||||
msgstr ""
|
||||
|
||||
msgid "Missing OAuth configuration for GitHub."
|
||||
msgstr ""
|
||||
|
||||
msgid "Missing commit signatures endpoint!"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -17122,6 +17128,12 @@ msgstr ""
|
|||
msgid "PackageRegistry|Copy npm setup command"
|
||||
msgstr ""
|
||||
|
||||
msgid "PackageRegistry|Copy registry include"
|
||||
msgstr ""
|
||||
|
||||
msgid "PackageRegistry|Copy require package include"
|
||||
msgstr ""
|
||||
|
||||
msgid "PackageRegistry|Copy yarn command"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -17137,6 +17149,9 @@ msgstr ""
|
|||
msgid "PackageRegistry|Filter by name"
|
||||
msgstr ""
|
||||
|
||||
msgid "PackageRegistry|For more information on Composer packages in GitLab, %{linkStart}see the documentation.%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "PackageRegistry|For more information on the Conan registry, %{linkStart}see the documentation%{linkEnd}."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -17257,6 +17272,12 @@ msgstr ""
|
|||
msgid "PackageRegistry|You may also need to setup authentication using an auth token. %{linkStart}See the documentation%{linkEnd} to find out more."
|
||||
msgstr ""
|
||||
|
||||
msgid "PackageRegistry|composer.json registry include"
|
||||
msgstr ""
|
||||
|
||||
msgid "PackageRegistry|composer.json require package include"
|
||||
msgstr ""
|
||||
|
||||
msgid "PackageRegistry|npm"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ module QA
|
|||
end
|
||||
end
|
||||
|
||||
def enable_saml_sso(group, saml_idp_service)
|
||||
def enable_saml_sso(group, saml_idp_service, default_membership_role = 'Guest')
|
||||
page.visit Runtime::Scenario.gitlab_address
|
||||
|
||||
Page::Main::Login.perform(&:sign_in_using_credentials) unless Page::Main::Menu.perform(&:signed_in?)
|
||||
|
|
@ -29,6 +29,7 @@ module QA
|
|||
EE::Page::Group::Settings::SamlSSO.perform do |saml_sso|
|
||||
saml_sso.set_id_provider_sso_url(saml_idp_service.idp_sso_url)
|
||||
saml_sso.set_cert_fingerprint(saml_idp_service.idp_certificate_fingerprint)
|
||||
saml_sso.set_default_membership_role(default_membership_role)
|
||||
saml_sso.click_save_changes
|
||||
|
||||
saml_sso.user_login_url_link_text
|
||||
|
|
|
|||
|
|
@ -18,9 +18,12 @@ module QA
|
|||
view 'app/assets/javascripts/monitoring/components/dashboard_header.vue' do
|
||||
element :dashboards_filter_dropdown
|
||||
element :environments_dropdown
|
||||
element :edit_dashboard_button
|
||||
element :range_picker_dropdown
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue' do
|
||||
element :actions_menu_dropdown
|
||||
element :edit_dashboard_button_enabled
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue' do
|
||||
|
|
@ -56,7 +59,7 @@ module QA
|
|||
|
||||
def has_edit_dashboard_enabled?
|
||||
within_element :prometheus_graphs do
|
||||
has_element? :edit_dashboard_button
|
||||
has_element? :edit_dashboard_button_enabled
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,14 @@ RSpec.describe Import::GiteaController do
|
|||
assign_host_url
|
||||
end
|
||||
|
||||
it "requests provider repos list" do
|
||||
expect(stub_client(repos: [], orgs: [])).to receive(:repos)
|
||||
|
||||
get :status
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
|
||||
context 'when host url is local or not http' do
|
||||
%w[https://localhost:3000 http://192.168.0.1 ftp://testing].each do |url|
|
||||
let(:host_url) { url }
|
||||
|
|
|
|||
|
|
@ -15,10 +15,7 @@ RSpec.describe Import::GithubController do
|
|||
it "redirects to GitHub for an access token if logged in with GitHub" do
|
||||
allow(controller).to receive(:logged_in_with_provider?).and_return(true)
|
||||
expect(controller).to receive(:go_to_provider_for_permissions).and_call_original
|
||||
allow_any_instance_of(Gitlab::LegacyGithubImport::Client)
|
||||
.to receive(:authorize_url)
|
||||
.with(users_import_github_callback_url)
|
||||
.and_call_original
|
||||
allow(controller).to receive(:authorize_url).and_call_original
|
||||
|
||||
get :new
|
||||
|
||||
|
|
@ -46,13 +43,15 @@ RSpec.describe Import::GithubController do
|
|||
end
|
||||
|
||||
describe "GET callback" do
|
||||
before do
|
||||
allow(controller).to receive(:get_token).and_return(token)
|
||||
allow(controller).to receive(:oauth_options).and_return({})
|
||||
|
||||
stub_omniauth_provider('github')
|
||||
end
|
||||
|
||||
it "updates access token" do
|
||||
token = "asdasd12345"
|
||||
allow_any_instance_of(Gitlab::LegacyGithubImport::Client)
|
||||
.to receive(:get_token).and_return(token)
|
||||
allow_any_instance_of(Gitlab::LegacyGithubImport::Client)
|
||||
.to receive(:github_options).and_return({})
|
||||
stub_omniauth_provider('github')
|
||||
|
||||
get :callback
|
||||
|
||||
|
|
@ -66,7 +65,86 @@ RSpec.describe Import::GithubController do
|
|||
end
|
||||
|
||||
describe "GET status" do
|
||||
it_behaves_like 'a GitHub-ish import controller: GET status'
|
||||
context 'when using OAuth' do
|
||||
before do
|
||||
allow(controller).to receive(:logged_in_with_provider?).and_return(true)
|
||||
end
|
||||
|
||||
context 'when OAuth config is missing' do
|
||||
let(:new_import_url) { public_send("new_import_#{provider}_url") }
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:oauth_config).and_return(nil)
|
||||
end
|
||||
|
||||
it 'returns missing config error' do
|
||||
expect(controller).to receive(:go_to_provider_for_permissions).and_call_original
|
||||
|
||||
get :status
|
||||
|
||||
expect(session[:"#{provider}_access_token"]).to be_nil
|
||||
expect(controller).to redirect_to(new_import_url)
|
||||
expect(flash[:alert]).to eq('Missing OAuth configuration for GitHub.')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when feature remove_legacy_github_client is disabled' do
|
||||
before do
|
||||
stub_feature_flags(remove_legacy_github_client: false)
|
||||
session[:"#{provider}_access_token"] = 'asdasd12345'
|
||||
end
|
||||
|
||||
it_behaves_like 'a GitHub-ish import controller: GET status'
|
||||
|
||||
it 'uses Gitlab::LegacyGitHubImport::Client' do
|
||||
expect(controller.send(:client)).to be_instance_of(Gitlab::LegacyGithubImport::Client)
|
||||
end
|
||||
|
||||
it 'fetches repos using legacy client' do
|
||||
expect_next_instance_of(Gitlab::LegacyGithubImport::Client) do |client|
|
||||
expect(client).to receive(:repos)
|
||||
end
|
||||
|
||||
get :status
|
||||
end
|
||||
end
|
||||
|
||||
context 'when feature remove_legacy_github_client is enabled' do
|
||||
before do
|
||||
stub_feature_flags(remove_legacy_github_client: true)
|
||||
session[:"#{provider}_access_token"] = 'asdasd12345'
|
||||
end
|
||||
|
||||
it_behaves_like 'a GitHub-ish import controller: GET status'
|
||||
|
||||
it 'uses Gitlab::GithubImport::Client' do
|
||||
expect(controller.send(:client)).to be_instance_of(Gitlab::GithubImport::Client)
|
||||
end
|
||||
|
||||
it 'fetches repos using latest github client' do
|
||||
expect_next_instance_of(Gitlab::GithubImport::Client) do |client|
|
||||
expect(client).to receive(:each_page).with(:repos).and_return([].to_enum)
|
||||
end
|
||||
|
||||
get :status
|
||||
end
|
||||
|
||||
it 'concatenates list of repos from multiple pages' do
|
||||
repo_1 = OpenStruct.new(login: 'emacs', full_name: 'asd/emacs', name: 'emacs', owner: { login: 'owner' })
|
||||
repo_2 = OpenStruct.new(login: 'vim', full_name: 'asd/vim', name: 'vim', owner: { login: 'owner' })
|
||||
repos = [OpenStruct.new(objects: [repo_1]), OpenStruct.new(objects: [repo_2])].to_enum
|
||||
|
||||
allow(stub_client).to receive(:each_page).and_return(repos)
|
||||
|
||||
get :status, format: :json
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response.dig('provider_repos').count).to eq(2)
|
||||
expect(json_response.dig('provider_repos', 0, 'id')).to eq(repo_1.id)
|
||||
expect(json_response.dig('provider_repos', 1, 'id')).to eq(repo_2.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST create" do
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ RSpec.describe 'issuable list', :js do
|
|||
end
|
||||
|
||||
issuable_types.each do |issuable_type|
|
||||
it "avoids N+1 database queries for #{issuable_type.to_s.humanize.pluralize}" do
|
||||
it "avoids N+1 database queries for #{issuable_type.to_s.humanize.pluralize}", quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/231426' } do
|
||||
control_count = ActiveRecord::QueryRecorder.new { visit_issuable_list(issuable_type) }.count
|
||||
|
||||
create_issuables(issuable_type)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,10 @@
|
|||
import flash, { createFlashEl, createAction, hideFlash, removeFlashClickListener } from '~/flash';
|
||||
import flash, {
|
||||
newCreateFlash,
|
||||
createFlashEl,
|
||||
createAction,
|
||||
hideFlash,
|
||||
removeFlashClickListener,
|
||||
} from '~/flash';
|
||||
|
||||
describe('Flash', () => {
|
||||
describe('createFlashEl', () => {
|
||||
|
|
@ -205,6 +211,109 @@ describe('Flash', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('newCreateFlash', () => {
|
||||
const message = 'test';
|
||||
const type = 'alert';
|
||||
const parent = document;
|
||||
const fadeTransition = false;
|
||||
const addBodyClass = true;
|
||||
const defaultParams = {
|
||||
message,
|
||||
type,
|
||||
parent,
|
||||
actionConfig: null,
|
||||
fadeTransition,
|
||||
addBodyClass,
|
||||
};
|
||||
|
||||
describe('no flash-container', () => {
|
||||
it('does not add to the DOM', () => {
|
||||
const flashEl = newCreateFlash({ message });
|
||||
|
||||
expect(flashEl).toBeNull();
|
||||
|
||||
expect(document.querySelector('.flash-alert')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with flash-container', () => {
|
||||
beforeEach(() => {
|
||||
setFixtures(
|
||||
'<div class="content-wrapper js-content-wrapper"><div class="flash-container"></div></div>',
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.querySelector('.js-content-wrapper').remove();
|
||||
});
|
||||
|
||||
it('adds flash element into container', () => {
|
||||
newCreateFlash({ ...defaultParams });
|
||||
|
||||
expect(document.querySelector('.flash-alert')).not.toBeNull();
|
||||
|
||||
expect(document.body.className).toContain('flash-shown');
|
||||
});
|
||||
|
||||
it('adds flash into specified parent', () => {
|
||||
newCreateFlash({ ...defaultParams, parent: document.querySelector('.content-wrapper') });
|
||||
|
||||
expect(document.querySelector('.content-wrapper .flash-alert')).not.toBeNull();
|
||||
expect(document.querySelector('.content-wrapper').innerText.trim()).toEqual(message);
|
||||
});
|
||||
|
||||
it('adds container classes when inside content-wrapper', () => {
|
||||
newCreateFlash(defaultParams);
|
||||
|
||||
expect(document.querySelector('.flash-text').className).toBe('flash-text');
|
||||
expect(document.querySelector('.content-wrapper').innerText.trim()).toEqual(message);
|
||||
});
|
||||
|
||||
it('does not add container when outside of content-wrapper', () => {
|
||||
document.querySelector('.content-wrapper').className = 'js-content-wrapper';
|
||||
newCreateFlash(defaultParams);
|
||||
|
||||
expect(document.querySelector('.flash-text').className.trim()).toContain('flash-text');
|
||||
});
|
||||
|
||||
it('removes element after clicking', () => {
|
||||
newCreateFlash({ ...defaultParams });
|
||||
|
||||
document.querySelector('.flash-alert .js-close-icon').click();
|
||||
|
||||
expect(document.querySelector('.flash-alert')).toBeNull();
|
||||
|
||||
expect(document.body.className).not.toContain('flash-shown');
|
||||
});
|
||||
|
||||
describe('with actionConfig', () => {
|
||||
it('adds action link', () => {
|
||||
newCreateFlash({
|
||||
...defaultParams,
|
||||
actionConfig: {
|
||||
title: 'test',
|
||||
},
|
||||
});
|
||||
|
||||
expect(document.querySelector('.flash-action')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('calls actionConfig clickHandler on click', () => {
|
||||
const actionConfig = {
|
||||
title: 'test',
|
||||
clickHandler: jest.fn(),
|
||||
};
|
||||
|
||||
newCreateFlash({ ...defaultParams, actionConfig });
|
||||
|
||||
document.querySelector('.flash-action').click();
|
||||
|
||||
expect(actionConfig.clickHandler).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeFlashClickListener', () => {
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML += `
|
||||
|
|
|
|||
|
|
@ -99,36 +99,21 @@ exports[`Dashboard template matches the default snapshot 1`] = `
|
|||
<div
|
||||
class="d-sm-flex"
|
||||
>
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
|
||||
<div
|
||||
class="mb-2 mr-2 d-flex"
|
||||
class="gl-mb-3 gl-mr-3 d-flex d-sm-block"
|
||||
>
|
||||
<div
|
||||
class="flex-grow-1"
|
||||
title="Star dashboard"
|
||||
>
|
||||
<gl-button-stub
|
||||
category="tertiary"
|
||||
class="w-100"
|
||||
icon="star-o"
|
||||
size="medium"
|
||||
variant="default"
|
||||
/>
|
||||
</div>
|
||||
<actions-menu-stub
|
||||
custommetricspath="/monitoring/monitor-project/prometheus/metrics"
|
||||
defaultbranch="master"
|
||||
validatequerypath="/monitoring/monitor-project/prometheus/metrics/validate_query"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,388 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { createStore } from '~/monitoring/stores';
|
||||
import { setupAllDashboards, setupStoreWithData } from '../store_utils';
|
||||
import { redirectTo } from '~/lib/utils/url_utility';
|
||||
import Tracking from '~/tracking';
|
||||
import ActionsMenu from '~/monitoring/components/dashboard_actions_menu.vue';
|
||||
import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
|
||||
import { dashboardActionsMenuProps, dashboardGitResponse } from '../mock_data';
|
||||
import * as types from '~/monitoring/stores/mutation_types';
|
||||
|
||||
jest.mock('~/lib/utils/url_utility', () => ({
|
||||
redirectTo: jest.fn(),
|
||||
queryToObject: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Actions menu', () => {
|
||||
const ootbDashboards = [dashboardGitResponse[0], dashboardGitResponse[2]];
|
||||
const customDashboard = dashboardGitResponse[1];
|
||||
|
||||
let store;
|
||||
let wrapper;
|
||||
|
||||
const findAddMetricItem = () => wrapper.find('[data-testid="add-metric-item"]');
|
||||
const findAddMetricModal = () => wrapper.find('[data-testid="add-metric-modal"]');
|
||||
const findAddMetricModalSubmitButton = () =>
|
||||
wrapper.find('[data-testid="add-metric-modal-submit-button"]');
|
||||
const findStarDashboardItem = () => wrapper.find('[data-testid="star-dashboard-item"]');
|
||||
const findEditDashboardItemEnabled = () =>
|
||||
wrapper.find('[data-testid="edit-dashboard-item-enabled"]');
|
||||
const findEditDashboardItemDisabled = () =>
|
||||
wrapper.find('[data-testid="edit-dashboard-item-disabled"]');
|
||||
const findDuplicateDashboardItem = () => wrapper.find('[data-testid="duplicate-dashboard-item"]');
|
||||
const findDuplicateDashboardModal = () =>
|
||||
wrapper.find('[data-testid="duplicate-dashboard-modal"]');
|
||||
const findCreateDashboardItem = () => wrapper.find('[data-testid="create-dashboard-item"]');
|
||||
const findCreateDashboardModal = () => wrapper.find('[data-testid="create-dashboard-modal"]');
|
||||
|
||||
const createShallowWrapper = (props = {}, options = {}) => {
|
||||
wrapper = shallowMount(ActionsMenu, {
|
||||
propsData: { ...dashboardActionsMenuProps, ...props },
|
||||
store,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
describe('add metric item', () => {
|
||||
it('is rendered when custom metrics are available', () => {
|
||||
createShallowWrapper();
|
||||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
expect(findAddMetricItem().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('is not rendered when custom metrics are not available', () => {
|
||||
createShallowWrapper({
|
||||
addingMetricsAvailable: false,
|
||||
});
|
||||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
expect(findAddMetricItem().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when available', () => {
|
||||
beforeEach(() => {
|
||||
createShallowWrapper();
|
||||
});
|
||||
|
||||
it('modal for custom metrics form is rendered', () => {
|
||||
expect(findAddMetricModal().exists()).toBe(true);
|
||||
expect(findAddMetricModal().attributes().modalid).toBe('addMetric');
|
||||
});
|
||||
|
||||
it('add metric modal submit button exists', () => {
|
||||
expect(findAddMetricModalSubmitButton().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders custom metrics form fields', () => {
|
||||
expect(wrapper.find(CustomMetricsFormFields).exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when not available', () => {
|
||||
beforeEach(() => {
|
||||
createShallowWrapper({ addingMetricsAvailable: false });
|
||||
});
|
||||
|
||||
it('modal for custom metrics form is not rendered', () => {
|
||||
expect(findAddMetricModal().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('adding new metric from modal', () => {
|
||||
let origPage;
|
||||
|
||||
beforeEach(done => {
|
||||
jest.spyOn(Tracking, 'event').mockReturnValue();
|
||||
createShallowWrapper();
|
||||
|
||||
setupStoreWithData(store);
|
||||
|
||||
origPage = document.body.dataset.page;
|
||||
document.body.dataset.page = 'projects:environments:metrics';
|
||||
|
||||
wrapper.vm.$nextTick(done);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.dataset.page = origPage;
|
||||
});
|
||||
|
||||
it('is tracked', done => {
|
||||
const submitButton = findAddMetricModalSubmitButton().vm;
|
||||
|
||||
wrapper.vm.$nextTick(() => {
|
||||
submitButton.$el.click();
|
||||
wrapper.vm.$nextTick(() => {
|
||||
expect(Tracking.event).toHaveBeenCalledWith(
|
||||
document.body.dataset.page,
|
||||
'click_button',
|
||||
{
|
||||
label: 'add_new_metric',
|
||||
property: 'modal',
|
||||
value: undefined,
|
||||
},
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('edit dashboard yml item', () => {
|
||||
beforeEach(() => {
|
||||
createShallowWrapper();
|
||||
});
|
||||
|
||||
describe('when current dashboard is custom', () => {
|
||||
beforeEach(() => {
|
||||
setupAllDashboards(store, customDashboard.path);
|
||||
});
|
||||
|
||||
it('enabled item is rendered and has falsy disabled attribute', () => {
|
||||
expect(findEditDashboardItemEnabled().exists()).toBe(true);
|
||||
expect(findEditDashboardItemEnabled().attributes('disabled')).toBe(undefined);
|
||||
});
|
||||
|
||||
it('enabled item links to their edit path', () => {
|
||||
expect(findEditDashboardItemEnabled().attributes('href')).toBe(
|
||||
customDashboard.project_blob_path,
|
||||
);
|
||||
});
|
||||
|
||||
it('disabled item is not rendered', () => {
|
||||
expect(findEditDashboardItemDisabled().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each(ootbDashboards)('when current dashboard is OOTB', dashboard => {
|
||||
beforeEach(() => {
|
||||
setupAllDashboards(store, dashboard.path);
|
||||
});
|
||||
|
||||
it('disabled item is rendered and has disabled attribute set on it', () => {
|
||||
expect(findEditDashboardItemDisabled().exists()).toBe(true);
|
||||
expect(findEditDashboardItemDisabled().attributes('disabled')).toBe('');
|
||||
});
|
||||
|
||||
it('enabled item is not rendered', () => {
|
||||
expect(findEditDashboardItemEnabled().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('duplicate dashboard item', () => {
|
||||
beforeEach(() => {
|
||||
createShallowWrapper();
|
||||
});
|
||||
|
||||
describe.each(ootbDashboards)('when current dashboard is OOTB', dashboard => {
|
||||
beforeEach(() => {
|
||||
setupAllDashboards(store, dashboard.path);
|
||||
});
|
||||
|
||||
it('is rendered', () => {
|
||||
expect(findDuplicateDashboardItem().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('duplicate dashboard modal is rendered', () => {
|
||||
expect(findDuplicateDashboardModal().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('clicking on item opens up the duplicate dashboard modal', () => {
|
||||
const modalId = 'duplicateDashboard';
|
||||
const modalTrigger = findDuplicateDashboardItem();
|
||||
const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit');
|
||||
|
||||
modalTrigger.trigger('click');
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(rootEmit.mock.calls[0]).toContainEqual(modalId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when current dashboard is custom', () => {
|
||||
beforeEach(() => {
|
||||
setupAllDashboards(store, customDashboard.path);
|
||||
});
|
||||
|
||||
it('is not rendered', () => {
|
||||
expect(findDuplicateDashboardItem().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('duplicate dashboard modal is not rendered', () => {
|
||||
expect(findDuplicateDashboardModal().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when no dashboard is set', () => {
|
||||
it('is not rendered', () => {
|
||||
expect(findDuplicateDashboardItem().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('duplicate dashboard modal is not rendered', () => {
|
||||
expect(findDuplicateDashboardModal().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a dashboard has been duplicated in the duplicate dashboard modal', () => {
|
||||
beforeEach(() => {
|
||||
store.state.monitoringDashboard.projectPath = 'root/sandbox';
|
||||
|
||||
setupAllDashboards(store, dashboardGitResponse[0].path);
|
||||
});
|
||||
|
||||
it('redirects to the newly created dashboard', () => {
|
||||
delete window.location;
|
||||
window.location = new URL('https://localhost');
|
||||
|
||||
const newDashboard = dashboardGitResponse[1];
|
||||
|
||||
const newDashboardUrl = 'root/sandbox/-/metrics/dashboard.yml';
|
||||
findDuplicateDashboardModal().vm.$emit('dashboardDuplicated', newDashboard);
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(redirectTo).toHaveBeenCalled();
|
||||
expect(redirectTo).toHaveBeenCalledWith(newDashboardUrl);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('star dashboard item', () => {
|
||||
beforeEach(() => {
|
||||
createShallowWrapper();
|
||||
setupAllDashboards(store);
|
||||
|
||||
jest.spyOn(store, 'dispatch').mockResolvedValue();
|
||||
});
|
||||
|
||||
it('is shown', () => {
|
||||
expect(findStarDashboardItem().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('is not disabled', () => {
|
||||
expect(findStarDashboardItem().attributes('disabled')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('is disabled when starring is taking place', () => {
|
||||
store.commit(`monitoringDashboard/${types.REQUEST_DASHBOARD_STARRING}`);
|
||||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
expect(findStarDashboardItem().exists()).toBe(true);
|
||||
expect(findStarDashboardItem().attributes('disabled')).toBe('true');
|
||||
});
|
||||
});
|
||||
|
||||
it('on click it dispatches a toggle star action', () => {
|
||||
findStarDashboardItem().vm.$emit('click');
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
'monitoringDashboard/toggleStarredValue',
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when dashboard is not starred', () => {
|
||||
beforeEach(() => {
|
||||
store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
|
||||
currentDashboard: dashboardGitResponse[0].path,
|
||||
});
|
||||
return wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
it('item text shows "Star dashboard"', () => {
|
||||
expect(findStarDashboardItem().html()).toMatch(/Star dashboard/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when dashboard is starred', () => {
|
||||
beforeEach(() => {
|
||||
store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
|
||||
currentDashboard: dashboardGitResponse[1].path,
|
||||
});
|
||||
return wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
it('item text shows "Unstar dashboard"', () => {
|
||||
expect(findStarDashboardItem().html()).toMatch(/Unstar dashboard/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('create dashboard item', () => {
|
||||
beforeEach(() => {
|
||||
createShallowWrapper();
|
||||
});
|
||||
|
||||
it('is rendered by default but it is disabled', () => {
|
||||
expect(findCreateDashboardItem().attributes('disabled')).toBe('true');
|
||||
});
|
||||
|
||||
describe('when project path is set', () => {
|
||||
const mockProjectPath = 'root/sandbox';
|
||||
const mockAddDashboardDocPath = '/doc/add-dashboard';
|
||||
|
||||
beforeEach(() => {
|
||||
store.state.monitoringDashboard.projectPath = mockProjectPath;
|
||||
store.state.monitoringDashboard.addDashboardDocumentationPath = mockAddDashboardDocPath;
|
||||
});
|
||||
|
||||
it('is not disabled', () => {
|
||||
expect(findCreateDashboardItem().attributes('disabled')).toBe(undefined);
|
||||
});
|
||||
|
||||
it('renders a modal for creating a dashboard', () => {
|
||||
expect(findCreateDashboardModal().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('clicking opens up the modal', () => {
|
||||
const modalId = 'createDashboard';
|
||||
const modalTrigger = findCreateDashboardItem();
|
||||
const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit');
|
||||
|
||||
modalTrigger.trigger('click');
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(rootEmit.mock.calls[0]).toContainEqual(modalId);
|
||||
});
|
||||
});
|
||||
|
||||
it('modal gets passed correct props', () => {
|
||||
expect(findCreateDashboardModal().props('projectPath')).toBe(mockProjectPath);
|
||||
expect(findCreateDashboardModal().props('addDashboardDocumentationPath')).toBe(
|
||||
mockAddDashboardDocPath,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when project path is not set', () => {
|
||||
beforeEach(() => {
|
||||
store.state.monitoringDashboard.projectPath = null;
|
||||
});
|
||||
|
||||
it('is disabled', () => {
|
||||
expect(findCreateDashboardItem().attributes('disabled')).toBe('true');
|
||||
});
|
||||
|
||||
it('does not render a modal for creating a dashboard', () => {
|
||||
expect(findCreateDashboardModal().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -6,8 +6,7 @@ import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_p
|
|||
import RefreshButton from '~/monitoring/components/refresh_button.vue';
|
||||
import DashboardHeader from '~/monitoring/components/dashboard_header.vue';
|
||||
import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue';
|
||||
import DuplicateDashboardModal from '~/monitoring/components/duplicate_dashboard_modal.vue';
|
||||
import CreateDashboardModal from '~/monitoring/components/create_dashboard_modal.vue';
|
||||
import ActionsMenu from '~/monitoring/components/dashboard_actions_menu.vue';
|
||||
import { setupAllDashboards, setupStoreWithDashboard, setupStoreWithData } from '../store_utils';
|
||||
import {
|
||||
environmentData,
|
||||
|
|
@ -18,7 +17,6 @@ import {
|
|||
import { redirectTo } from '~/lib/utils/url_utility';
|
||||
|
||||
const mockProjectPath = 'https://path/to/project';
|
||||
const mockAddDashboardDocPath = '/doc/add-dashboard';
|
||||
|
||||
jest.mock('~/lib/utils/url_utility', () => ({
|
||||
redirectTo: jest.fn(),
|
||||
|
|
@ -41,13 +39,7 @@ describe('Dashboard header', () => {
|
|||
const findDateTimePicker = () => wrapper.find(DateTimePicker);
|
||||
const findRefreshButton = () => wrapper.find(RefreshButton);
|
||||
|
||||
const findActionsMenu = () => wrapper.find('[data-testid="actions-menu"]');
|
||||
const findCreateDashboardMenuItem = () =>
|
||||
findActionsMenu().find('[data-testid="action-create-dashboard"]');
|
||||
const findCreateDashboardDuplicateItem = () =>
|
||||
findActionsMenu().find('[data-testid="action-duplicate-dashboard"]');
|
||||
const findDuplicateDashboardModal = () => wrapper.find(DuplicateDashboardModal);
|
||||
const findCreateDashboardModal = () => wrapper.find('[data-testid="create-dashboard-modal"]');
|
||||
const findActionsMenu = () => wrapper.find(ActionsMenu);
|
||||
|
||||
const setSearchTerm = searchTerm => {
|
||||
store.commit(`monitoringDashboard/${types.SET_ENVIRONMENTS_FILTER}`, searchTerm);
|
||||
|
|
@ -264,31 +256,6 @@ describe('Dashboard header', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when a dashboard has been duplicated in the duplicate dashboard modal', () => {
|
||||
beforeEach(() => {
|
||||
store.state.monitoringDashboard.projectPath = 'root/sandbox';
|
||||
|
||||
setupAllDashboards(store, dashboardGitResponse[0].path);
|
||||
});
|
||||
|
||||
it('redirects to the newly created dashboard', () => {
|
||||
delete window.location;
|
||||
window.location = new URL('https://localhost');
|
||||
|
||||
const newDashboard = dashboardGitResponse[1];
|
||||
|
||||
createShallowWrapper();
|
||||
|
||||
const newDashboardUrl = 'root/sandbox/-/metrics/dashboard.yml';
|
||||
findDuplicateDashboardModal().vm.$emit('dashboardDuplicated', newDashboard);
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(redirectTo).toHaveBeenCalled();
|
||||
expect(redirectTo).toHaveBeenCalledWith(newDashboardUrl);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('external dashboard link', () => {
|
||||
beforeEach(() => {
|
||||
store.state.monitoringDashboard.externalDashboardUrl = '/mockUrl';
|
||||
|
|
@ -307,113 +274,97 @@ describe('Dashboard header', () => {
|
|||
});
|
||||
|
||||
describe('actions menu', () => {
|
||||
beforeEach(() => {
|
||||
store.state.monitoringDashboard.projectPath = '';
|
||||
createShallowWrapper();
|
||||
});
|
||||
|
||||
it('is rendered if projectPath is set in store', () => {
|
||||
store.state.monitoringDashboard.projectPath = mockProjectPath;
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findActionsMenu().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('is not rendered if projectPath is not set in store', () => {
|
||||
expect(findActionsMenu().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('contains the create dashboard modal', () => {
|
||||
store.state.monitoringDashboard.projectPath = mockProjectPath;
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findActionsMenu().contains(CreateDashboardModal)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
const duplicableCases = [
|
||||
null, // When no path is specified, it uses the overview dashboard path.
|
||||
const ootbDashboards = [
|
||||
dashboardGitResponse[0].path,
|
||||
dashboardGitResponse[2].path,
|
||||
selfMonitoringDashboardGitResponse[0].path,
|
||||
];
|
||||
|
||||
describe.each(duplicableCases)(
|
||||
'when the selected dashboard can be duplicated',
|
||||
dashboardPath => {
|
||||
it('contains menu items for "Create New", "Duplicate Dashboard" and a modal for duplicating dashboards', () => {
|
||||
store.state.monitoringDashboard.projectPath = mockProjectPath;
|
||||
setupAllDashboards(store, dashboardPath);
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findCreateDashboardMenuItem().exists()).toBe(true);
|
||||
expect(findCreateDashboardDuplicateItem().exists()).toBe(true);
|
||||
expect(findDuplicateDashboardModal().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const nonDuplicableCases = [
|
||||
const customDashboards = [
|
||||
dashboardGitResponse[1].path,
|
||||
selfMonitoringDashboardGitResponse[1].path,
|
||||
];
|
||||
|
||||
describe.each(nonDuplicableCases)(
|
||||
'when the selected dashboard cannot be duplicated',
|
||||
dashboardPath => {
|
||||
it('contains a "Create New" menu item, but no "Duplicate Dashboard" menu item and modal', () => {
|
||||
store.state.monitoringDashboard.projectPath = mockProjectPath;
|
||||
it('is rendered', () => {
|
||||
createShallowWrapper();
|
||||
|
||||
expect(findActionsMenu().exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('adding metrics prop', () => {
|
||||
it.each(ootbDashboards)('gets passed true if current dashboard is OOTB', dashboardPath => {
|
||||
createShallowWrapper({ customMetricsAvailable: true });
|
||||
|
||||
store.state.monitoringDashboard.emptyState = false;
|
||||
setupAllDashboards(store, dashboardPath);
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findActionsMenu().props('addingMetricsAvailable')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it.each(customDashboards)(
|
||||
'gets passed false if current dashboard is custom',
|
||||
dashboardPath => {
|
||||
createShallowWrapper({ customMetricsAvailable: true });
|
||||
|
||||
store.state.monitoringDashboard.emptyState = false;
|
||||
setupAllDashboards(store, dashboardPath);
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findCreateDashboardMenuItem().exists()).toBe(true);
|
||||
expect(findCreateDashboardDuplicateItem().exists()).toBe(false);
|
||||
expect(findDuplicateDashboardModal().exists()).toBe(false);
|
||||
expect(findActionsMenu().props('addingMetricsAvailable')).toBe(false);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('gets passed false if empty state is shown', () => {
|
||||
createShallowWrapper({ customMetricsAvailable: true });
|
||||
|
||||
store.state.monitoringDashboard.emptyState = true;
|
||||
setupAllDashboards(store, ootbDashboards[0]);
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findActionsMenu().props('addingMetricsAvailable')).toBe(false);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('actions menu modals', () => {
|
||||
beforeEach(() => {
|
||||
store.state.monitoringDashboard.projectPath = mockProjectPath;
|
||||
store.state.monitoringDashboard.addDashboardDocumentationPath = mockAddDashboardDocPath;
|
||||
setupAllDashboards(store);
|
||||
it('gets passed false if custom metrics are not available', () => {
|
||||
createShallowWrapper({ customMetricsAvailable: false });
|
||||
|
||||
createShallowWrapper();
|
||||
});
|
||||
store.state.monitoringDashboard.emptyState = false;
|
||||
setupAllDashboards(store, ootbDashboards[0]);
|
||||
|
||||
it('Clicking on "Create New" opens up a modal', () => {
|
||||
const modalId = 'createDashboard';
|
||||
const modalTrigger = findCreateDashboardMenuItem();
|
||||
const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit');
|
||||
|
||||
modalTrigger.trigger('click');
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(rootEmit.mock.calls[0]).toContainEqual(modalId);
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findActionsMenu().props('addingMetricsAvailable')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('"Create new dashboard" modal contains correct buttons', () => {
|
||||
expect(findCreateDashboardModal().props('projectPath')).toBe(mockProjectPath);
|
||||
expect(findCreateDashboardModal().props('addDashboardDocumentationPath')).toBe(
|
||||
mockAddDashboardDocPath,
|
||||
);
|
||||
});
|
||||
it('custom metrics path gets passed', () => {
|
||||
const path = 'https://path/to/customMetrics';
|
||||
|
||||
it('"Duplicate Dashboard" opens up a modal', () => {
|
||||
const modalId = 'duplicateDashboard';
|
||||
const modalTrigger = findCreateDashboardDuplicateItem();
|
||||
const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit');
|
||||
|
||||
modalTrigger.trigger('click');
|
||||
createShallowWrapper({ customMetricsPath: path });
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(rootEmit.mock.calls[0]).toContainEqual(modalId);
|
||||
expect(findActionsMenu().props('customMetricsPath')).toBe(path);
|
||||
});
|
||||
});
|
||||
|
||||
it('validate query path gets passed', () => {
|
||||
const path = 'https://path/to/validateQuery';
|
||||
|
||||
createShallowWrapper({ validateQueryPath: path });
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findActionsMenu().props('validateQueryPath')).toBe(path);
|
||||
});
|
||||
});
|
||||
|
||||
it('default branch gets passed', () => {
|
||||
const branch = 'branchName';
|
||||
|
||||
createShallowWrapper({ defaultBranch: branch });
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findActionsMenu().props('defaultBranch')).toBe(branch);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -465,72 +416,4 @@ describe('Dashboard header', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Add metric button', () => {
|
||||
const findAddMetricButton = () => wrapper.find('[data-qa-selector="add_metric_button"]');
|
||||
|
||||
it('is not rendered when custom metrics are not available', () => {
|
||||
store.state.monitoringDashboard.emptyState = false;
|
||||
|
||||
createShallowWrapper({
|
||||
customMetricsAvailable: false,
|
||||
});
|
||||
|
||||
setupAllDashboards(store, dashboardGitResponse[0].path);
|
||||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
expect(findAddMetricButton().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('is not rendered when displaying empty state', () => {
|
||||
store.state.monitoringDashboard.emptyState = true;
|
||||
|
||||
createShallowWrapper({
|
||||
customMetricsAvailable: true,
|
||||
});
|
||||
|
||||
setupAllDashboards(store, dashboardGitResponse[0].path);
|
||||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
expect(findAddMetricButton().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('system dashboards', () => {
|
||||
const systemDashboards = [
|
||||
dashboardGitResponse[0].path,
|
||||
selfMonitoringDashboardGitResponse[0].path,
|
||||
];
|
||||
const nonSystemDashboards = [
|
||||
dashboardGitResponse[1].path,
|
||||
dashboardGitResponse[2].path,
|
||||
selfMonitoringDashboardGitResponse[1].path,
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
store.state.monitoringDashboard.emptyState = false;
|
||||
|
||||
createShallowWrapper({
|
||||
customMetricsAvailable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it.each(systemDashboards)('is rendered for system dashboards', dashboardPath => {
|
||||
setupAllDashboards(store, dashboardPath);
|
||||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
expect(findAddMetricButton().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it.each(nonSystemDashboards)('is not rendered for non-system dashboards', dashboardPath => {
|
||||
setupAllDashboards(store, dashboardPath);
|
||||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
expect(findAddMetricButton().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import { shallowMount, mount } from '@vue/test-utils';
|
||||
import Tracking from '~/tracking';
|
||||
import { ESC_KEY, ESC_KEY_IE11 } from '~/lib/utils/keys';
|
||||
import { GlModal } from '@gitlab/ui';
|
||||
import { objectToQuery } from '~/lib/utils/url_utility';
|
||||
import VueDraggable from 'vuedraggable';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
|
|
@ -10,7 +8,6 @@ import { dashboardEmptyStates, metricStates } from '~/monitoring/constants';
|
|||
import Dashboard from '~/monitoring/components/dashboard.vue';
|
||||
|
||||
import DashboardHeader from '~/monitoring/components/dashboard_header.vue';
|
||||
import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
|
||||
import EmptyState from '~/monitoring/components/empty_state.vue';
|
||||
import GroupEmptyState from '~/monitoring/components/group_empty_state.vue';
|
||||
import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
|
||||
|
|
@ -42,8 +39,6 @@ describe('Dashboard', () => {
|
|||
let wrapper;
|
||||
let mock;
|
||||
|
||||
const findDashboardHeader = () => wrapper.find(DashboardHeader);
|
||||
|
||||
const createShallowWrapper = (props = {}, options = {}) => {
|
||||
wrapper = shallowMount(Dashboard, {
|
||||
propsData: { ...dashboardProps, ...props },
|
||||
|
|
@ -446,84 +441,6 @@ describe('Dashboard', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('star dashboards', () => {
|
||||
const findToggleStar = () => findDashboardHeader().find({ ref: 'toggleStarBtn' });
|
||||
|
||||
beforeEach(() => {
|
||||
createShallowWrapper();
|
||||
setupAllDashboards(store);
|
||||
});
|
||||
|
||||
it('toggle star button is shown', () => {
|
||||
expect(findToggleStar().exists()).toBe(true);
|
||||
expect(findToggleStar().props('disabled')).toBe(false);
|
||||
});
|
||||
|
||||
it('toggle star button is disabled when starring is taking place', () => {
|
||||
store.commit(`monitoringDashboard/${types.REQUEST_DASHBOARD_STARRING}`);
|
||||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
expect(findToggleStar().exists()).toBe(true);
|
||||
expect(findToggleStar().props('disabled')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the dashboard list is loaded', () => {
|
||||
// Tooltip element should wrap directly
|
||||
const getToggleTooltip = () => findToggleStar().element.parentElement.getAttribute('title');
|
||||
|
||||
beforeEach(() => {
|
||||
setupAllDashboards(store);
|
||||
jest.spyOn(store, 'dispatch');
|
||||
});
|
||||
|
||||
it('dispatches a toggle star action', () => {
|
||||
findToggleStar().vm.$emit('click');
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
'monitoringDashboard/toggleStarredValue',
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when dashboard is not starred', () => {
|
||||
beforeEach(() => {
|
||||
store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
|
||||
currentDashboard: dashboardGitResponse[0].path,
|
||||
});
|
||||
return wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
it('toggle star button shows "Star dashboard"', () => {
|
||||
expect(getToggleTooltip()).toBe('Star dashboard');
|
||||
});
|
||||
|
||||
it('toggle star button shows an unstarred state', () => {
|
||||
expect(findToggleStar().attributes('icon')).toBe('star-o');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when dashboard is starred', () => {
|
||||
beforeEach(() => {
|
||||
store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
|
||||
currentDashboard: dashboardGitResponse[1].path,
|
||||
});
|
||||
return wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
it('toggle star button shows "Star dashboard"', () => {
|
||||
expect(getToggleTooltip()).toBe('Unstar dashboard');
|
||||
});
|
||||
|
||||
it('toggle star button shows a starred state', () => {
|
||||
expect(findToggleStar().attributes('icon')).toBe('star');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('variables section', () => {
|
||||
beforeEach(() => {
|
||||
createShallowWrapper({ hasMetrics: true });
|
||||
|
|
@ -800,33 +717,6 @@ describe('Dashboard', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('dashboard edit link', () => {
|
||||
const findEditLink = () => wrapper.find('.js-edit-link');
|
||||
|
||||
beforeEach(() => {
|
||||
createShallowWrapper({ hasMetrics: true });
|
||||
|
||||
setupAllDashboards(store);
|
||||
return wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
it('is not present for the overview dashboard', () => {
|
||||
expect(findEditLink().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('is present for a custom dashboard, and links to its edit_path', () => {
|
||||
const dashboard = dashboardGitResponse[1];
|
||||
store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
|
||||
currentDashboard: dashboard.path,
|
||||
});
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findEditLink().exists()).toBe(true);
|
||||
expect(findEditLink().attributes('href')).toBe(dashboard.project_blob_path);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('document title', () => {
|
||||
const originalTitle = 'Original Title';
|
||||
const overviewDashboardName = dashboardGitResponse[0].display_name;
|
||||
|
|
@ -940,74 +830,4 @@ describe('Dashboard', () => {
|
|||
expect(dashboardPanel.exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('add custom metrics', () => {
|
||||
const findAddMetricButton = () => findDashboardHeader().find({ ref: 'addMetricBtn' });
|
||||
|
||||
describe('when not available', () => {
|
||||
beforeEach(() => {
|
||||
createShallowWrapper({
|
||||
hasMetrics: true,
|
||||
customMetricsPath: '/endpoint',
|
||||
});
|
||||
});
|
||||
it('does not render add button on the dashboard', () => {
|
||||
expect(findAddMetricButton().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when available', () => {
|
||||
let origPage;
|
||||
beforeEach(done => {
|
||||
jest.spyOn(Tracking, 'event').mockReturnValue();
|
||||
createShallowWrapper({
|
||||
hasMetrics: true,
|
||||
customMetricsPath: '/endpoint',
|
||||
customMetricsAvailable: true,
|
||||
});
|
||||
setupStoreWithData(store);
|
||||
|
||||
origPage = document.body.dataset.page;
|
||||
document.body.dataset.page = 'projects:environments:metrics';
|
||||
|
||||
wrapper.vm.$nextTick(done);
|
||||
});
|
||||
afterEach(() => {
|
||||
document.body.dataset.page = origPage;
|
||||
});
|
||||
|
||||
it('renders add button on the dashboard', () => {
|
||||
expect(findAddMetricButton()).toBeDefined();
|
||||
});
|
||||
|
||||
it('uses modal for custom metrics form', () => {
|
||||
expect(wrapper.find(GlModal).exists()).toBe(true);
|
||||
expect(wrapper.find(GlModal).attributes().modalid).toBe('addMetric');
|
||||
});
|
||||
it('adding new metric is tracked', done => {
|
||||
const submitButton = wrapper
|
||||
.find(DashboardHeader)
|
||||
.find({ ref: 'submitCustomMetricsFormBtn' }).vm;
|
||||
wrapper.vm.$nextTick(() => {
|
||||
submitButton.$el.click();
|
||||
wrapper.vm.$nextTick(() => {
|
||||
expect(Tracking.event).toHaveBeenCalledWith(
|
||||
document.body.dataset.page,
|
||||
'click_button',
|
||||
{
|
||||
label: 'add_new_metric',
|
||||
property: 'modal',
|
||||
value: undefined,
|
||||
},
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('renders custom metrics form fields', () => {
|
||||
expect(wrapper.find(CustomMetricsFormFields).exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -622,3 +622,10 @@ export const dashboardHeaderProps = {
|
|||
end: '2020-01-01T01:00:00.000Z',
|
||||
},
|
||||
};
|
||||
|
||||
export const dashboardActionsMenuProps = {
|
||||
defaultBranch: 'master',
|
||||
addingMetricsAvailable: true,
|
||||
customMetricsPath: 'https://path/to/customMetrics',
|
||||
validateQueryPath: 'https://path/to/validateQuery',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
import Vuex from 'vuex';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import { GlSprintf, GlLink } from '@gitlab/ui';
|
||||
import ComposerInstallation from '~/packages/details/components/composer_installation.vue';
|
||||
import CodeInstructions from '~/packages/details/components/code_instruction.vue';
|
||||
import { TrackingActions } from '~/packages/details/constants';
|
||||
import { registryUrl as composerHelpPath } from 'jest/packages/details/mock_data';
|
||||
import { composerPackage as packageEntity } from 'jest/packages/mock_data';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
describe('ComposerInstallation', () => {
|
||||
let wrapper;
|
||||
|
||||
const composerRegistryIncludeStr = 'foo/registry';
|
||||
const composerPackageIncludeStr = 'foo/package';
|
||||
|
||||
const store = new Vuex.Store({
|
||||
state: {
|
||||
packageEntity,
|
||||
composerHelpPath,
|
||||
},
|
||||
getters: {
|
||||
composerRegistryInclude: () => composerRegistryIncludeStr,
|
||||
composerPackageInclude: () => composerPackageIncludeStr,
|
||||
},
|
||||
});
|
||||
|
||||
const findCodeInstructions = () => wrapper.findAll(CodeInstructions);
|
||||
const findRegistryIncludeTitle = () => wrapper.find('[data-testid="registry-include-title"]');
|
||||
const findPackageIncludeTitle = () => wrapper.find('[data-testid="package-include-title"]');
|
||||
const findHelpText = () => wrapper.find('[data-testid="help-text"]');
|
||||
const findHelpLink = () => wrapper.find(GlLink);
|
||||
|
||||
function createComponent() {
|
||||
wrapper = shallowMount(ComposerInstallation, {
|
||||
localVue,
|
||||
store,
|
||||
stubs: {
|
||||
GlSprintf,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('registry include command', () => {
|
||||
it('uses code_instructions', () => {
|
||||
const registryIncludeCommand = findCodeInstructions().at(0);
|
||||
expect(registryIncludeCommand.exists()).toBe(true);
|
||||
expect(registryIncludeCommand.props()).toMatchObject({
|
||||
instruction: composerRegistryIncludeStr,
|
||||
copyText: 'Copy registry include',
|
||||
trackingAction: TrackingActions.COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND,
|
||||
});
|
||||
});
|
||||
|
||||
it('has the correct title', () => {
|
||||
expect(findRegistryIncludeTitle().text()).toBe('composer.json registry include');
|
||||
});
|
||||
});
|
||||
|
||||
describe('package include command', () => {
|
||||
it('uses code_instructions', () => {
|
||||
const registryIncludeCommand = findCodeInstructions().at(1);
|
||||
expect(registryIncludeCommand.exists()).toBe(true);
|
||||
expect(registryIncludeCommand.props()).toMatchObject({
|
||||
instruction: composerPackageIncludeStr,
|
||||
copyText: 'Copy require package include',
|
||||
trackingAction: TrackingActions.COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND,
|
||||
});
|
||||
});
|
||||
|
||||
it('has the correct title', () => {
|
||||
expect(findPackageIncludeTitle().text()).toBe('composer.json require package include');
|
||||
});
|
||||
|
||||
it('has the correct help text', () => {
|
||||
expect(findHelpText().text()).toBe(
|
||||
'For more information on Composer packages in GitLab, see the documentation.',
|
||||
);
|
||||
expect(findHelpLink().attributes()).toMatchObject({
|
||||
href: composerHelpPath,
|
||||
target: '_blank',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -6,8 +6,16 @@ import MavenInstallation from '~/packages/details/components/maven_installation.
|
|||
import ConanInstallation from '~/packages/details/components/conan_installation.vue';
|
||||
import NugetInstallation from '~/packages/details/components/nuget_installation.vue';
|
||||
import PypiInstallation from '~/packages/details/components/pypi_installation.vue';
|
||||
import ComposerInstallation from '~/packages/details/components/composer_installation.vue';
|
||||
|
||||
import { conanPackage, mavenPackage, npmPackage, nugetPackage, pypiPackage } from '../../mock_data';
|
||||
import {
|
||||
conanPackage,
|
||||
mavenPackage,
|
||||
npmPackage,
|
||||
nugetPackage,
|
||||
pypiPackage,
|
||||
composerPackage,
|
||||
} from '../../mock_data';
|
||||
|
||||
describe('InstallationCommands', () => {
|
||||
let wrapper;
|
||||
|
|
@ -23,6 +31,7 @@ describe('InstallationCommands', () => {
|
|||
const conanInstallation = () => wrapper.find(ConanInstallation);
|
||||
const nugetInstallation = () => wrapper.find(NugetInstallation);
|
||||
const pypiInstallation = () => wrapper.find(PypiInstallation);
|
||||
const composerInstallation = () => wrapper.find(ComposerInstallation);
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
|
|
@ -30,12 +39,13 @@ describe('InstallationCommands', () => {
|
|||
|
||||
describe('installation instructions', () => {
|
||||
describe.each`
|
||||
packageEntity | selector
|
||||
${conanPackage} | ${conanInstallation}
|
||||
${mavenPackage} | ${mavenInstallation}
|
||||
${npmPackage} | ${npmInstallation}
|
||||
${nugetPackage} | ${nugetInstallation}
|
||||
${pypiPackage} | ${pypiInstallation}
|
||||
packageEntity | selector
|
||||
${conanPackage} | ${conanInstallation}
|
||||
${mavenPackage} | ${mavenInstallation}
|
||||
${npmPackage} | ${npmInstallation}
|
||||
${nugetPackage} | ${nugetInstallation}
|
||||
${pypiPackage} | ${pypiInstallation}
|
||||
${composerPackage} | ${composerInstallation}
|
||||
`('renders', ({ packageEntity, selector }) => {
|
||||
it(`${packageEntity.package_type} instructions exist`, () => {
|
||||
createComponent({ packageEntity });
|
||||
|
|
|
|||
|
|
@ -44,6 +44,14 @@ RSpec.describe PackagesHelper do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'composer_registry_url' do
|
||||
it 'return the composer registry url' do
|
||||
url = helper.composer_registry_url(1)
|
||||
|
||||
expect(url).to eq("#{base_url}group/1/-/packages/composer/packages.json")
|
||||
end
|
||||
end
|
||||
|
||||
describe 'packages_coming_soon_enabled?' do
|
||||
it 'returns false when the feature flag is disabled' do
|
||||
stub_feature_flags(packages_coming_soon: false)
|
||||
|
|
|
|||
|
|
@ -5,38 +5,59 @@ require 'spec_helper'
|
|||
RSpec.describe PersonalAccessTokenPolicy do
|
||||
include AdminModeHelper
|
||||
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
subject { described_class.new(current_user, token) }
|
||||
|
||||
where(:user_type, :owned_by_same_user, :expected_permitted?) do
|
||||
:user | true | true
|
||||
:user | false | false
|
||||
:admin | false | true
|
||||
context 'current_user is an administrator', :enable_admin_mode do
|
||||
let_it_be(:current_user) { build(:admin) }
|
||||
|
||||
context 'not the owner of the token' do
|
||||
let_it_be(:token) { build(:personal_access_token) }
|
||||
|
||||
it { is_expected.to be_allowed(:read_token) }
|
||||
it { is_expected.to be_allowed(:revoke_token) }
|
||||
end
|
||||
|
||||
context 'owner of the token' do
|
||||
let_it_be(:token) { build(:personal_access_token, user: current_user) }
|
||||
|
||||
it { is_expected.to be_allowed(:read_token) }
|
||||
it { is_expected.to be_allowed(:revoke_token) }
|
||||
end
|
||||
end
|
||||
|
||||
with_them do
|
||||
context 'determine if a token is readable or revocable by a user' do
|
||||
let(:user) { build_stubbed(user_type) }
|
||||
let(:token_owner) { owned_by_same_user ? user : build(:user) }
|
||||
let(:token) { build(:personal_access_token, user: token_owner) }
|
||||
context 'current_user is not an administrator' do
|
||||
let_it_be(:current_user) { build(:user) }
|
||||
|
||||
subject { described_class.new(user, token) }
|
||||
context 'not the owner of the token' do
|
||||
let_it_be(:token) { build(:personal_access_token) }
|
||||
|
||||
before do
|
||||
enable_admin_mode!(user) if user.admin?
|
||||
end
|
||||
it { is_expected.to be_disallowed(:read_token) }
|
||||
it { is_expected.to be_disallowed(:revoke_token) }
|
||||
end
|
||||
|
||||
it { is_expected.to(expected_permitted? ? be_allowed(:read_token) : be_disallowed(:read_token)) }
|
||||
it { is_expected.to(expected_permitted? ? be_allowed(:revoke_token) : be_disallowed(:revoke_token)) }
|
||||
context 'owner of the token' do
|
||||
let_it_be(:token) { build(:personal_access_token, user: current_user) }
|
||||
|
||||
it { is_expected.to be_allowed(:read_token) }
|
||||
it { is_expected.to be_allowed(:revoke_token) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'current_user is a blocked administrator', :enable_admin_mode do
|
||||
subject { described_class.new(current_user, token) }
|
||||
let_it_be(:current_user) { build(:admin, :blocked) }
|
||||
|
||||
let(:current_user) { create(:user, :admin, :blocked) }
|
||||
let(:token) { create(:personal_access_token) }
|
||||
context 'owner of the token' do
|
||||
let_it_be(:token) { build(:personal_access_token, user: current_user) }
|
||||
|
||||
it { is_expected.to be_disallowed(:revoke_token) }
|
||||
it { is_expected.to be_disallowed(:read_token) }
|
||||
it { is_expected.to be_disallowed(:read_token) }
|
||||
it { is_expected.to be_disallowed(:revoke_token) }
|
||||
end
|
||||
|
||||
context 'not the owner of the token' do
|
||||
let_it_be(:token) { build(:personal_access_token) }
|
||||
|
||||
it { is_expected.to be_disallowed(:read_token) }
|
||||
it { is_expected.to be_disallowed(:revoke_token) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ RSpec.describe API::ImportGithub do
|
|||
|
||||
before do
|
||||
Grape::Endpoint.before_each do |endpoint|
|
||||
allow(endpoint).to receive(:client).and_return(double('client', user: provider_user, repo: provider_repo).as_null_object)
|
||||
allow(endpoint).to receive(:client).and_return(double('client', user: provider_user, repository: provider_repo).as_null_object)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ RSpec.describe Import::GithubService do
|
|||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:token) { 'complex-token' }
|
||||
let_it_be(:access_params) { { github_access_token: 'github-complex-token' } }
|
||||
let_it_be(:client) { Gitlab::LegacyGithubImport::Client.new(token) }
|
||||
let_it_be(:params) { { repo_id: 123, new_name: 'new_repo', target_namespace: 'root' } }
|
||||
|
||||
let(:subject) { described_class.new(client, user, params) }
|
||||
|
|
@ -15,41 +14,61 @@ RSpec.describe Import::GithubService do
|
|||
allow(subject).to receive(:authorized?).and_return(true)
|
||||
end
|
||||
|
||||
context 'do not raise an exception on input error' do
|
||||
let(:exception) { Octokit::ClientError.new(status: 404, body: 'Not Found') }
|
||||
shared_examples 'handles errors' do |klass|
|
||||
let(:client) { klass.new(token) }
|
||||
|
||||
before do
|
||||
expect(client).to receive(:repo).and_raise(exception)
|
||||
context 'do not raise an exception on input error' do
|
||||
let(:exception) { Octokit::ClientError.new(status: 404, body: 'Not Found') }
|
||||
|
||||
before do
|
||||
expect(client).to receive(:repository).and_raise(exception)
|
||||
end
|
||||
|
||||
it 'logs the original error' do
|
||||
expect(Gitlab::Import::Logger).to receive(:error).with({
|
||||
message: 'Import failed due to a GitHub error',
|
||||
status: 404,
|
||||
error: 'Not Found'
|
||||
}).and_call_original
|
||||
|
||||
subject.execute(access_params, :github)
|
||||
end
|
||||
|
||||
it 'returns an error' do
|
||||
result = subject.execute(access_params, :github)
|
||||
|
||||
expect(result).to include(
|
||||
message: 'Import failed due to a GitHub error: Not Found',
|
||||
status: :error,
|
||||
http_status: :unprocessable_entity
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it 'logs the original error' do
|
||||
expect(Gitlab::Import::Logger).to receive(:error).with({
|
||||
message: 'Import failed due to a GitHub error',
|
||||
status: 404,
|
||||
error: 'Not Found'
|
||||
}).and_call_original
|
||||
it 'raises an exception for unknown error causes' do
|
||||
exception = StandardError.new('Not Implemented')
|
||||
|
||||
subject.execute(access_params, :github)
|
||||
end
|
||||
expect(client).to receive(:repository).and_raise(exception)
|
||||
|
||||
it 'returns an error' do
|
||||
result = subject.execute(access_params, :github)
|
||||
expect(Gitlab::Import::Logger).not_to receive(:error)
|
||||
|
||||
expect(result).to include(
|
||||
message: 'Import failed due to a GitHub error: Not Found',
|
||||
status: :error,
|
||||
http_status: :unprocessable_entity
|
||||
)
|
||||
expect { subject.execute(access_params, :github) }.to raise_error(exception)
|
||||
end
|
||||
end
|
||||
|
||||
it 'raises an exception for unknown error causes' do
|
||||
exception = StandardError.new('Not Implemented')
|
||||
context 'when remove_legacy_github_client feature flag is enabled' do
|
||||
before do
|
||||
stub_feature_flags(remove_legacy_github_client: true)
|
||||
end
|
||||
|
||||
expect(client).to receive(:repo).and_raise(exception)
|
||||
include_examples 'handles errors', Gitlab::GithubImport::Client
|
||||
end
|
||||
|
||||
expect(Gitlab::Import::Logger).not_to receive(:error)
|
||||
context 'when remove_legacy_github_client feature flag is enabled' do
|
||||
before do
|
||||
stub_feature_flags(remove_legacy_github_client: false)
|
||||
end
|
||||
|
||||
expect { subject.execute(access_params, :github) }.to raise_error(exception)
|
||||
include_examples 'handles errors', Gitlab::LegacyGithubImport::Client
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do
|
|||
project = create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'example/repo')
|
||||
group = create(:group)
|
||||
group.add_owner(user)
|
||||
stub_client(repos: [repo, org_repo], orgs: [org], org_repos: [org_repo])
|
||||
stub_client(repos: [repo, org_repo], orgs: [org], org_repos: [org_repo], each_page: [OpenStruct.new(objects: [repo, org_repo])].to_enum)
|
||||
|
||||
get :status, format: :json
|
||||
|
||||
|
|
@ -85,7 +85,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do
|
|||
|
||||
it "does not show already added project" do
|
||||
project = create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'asd/vim')
|
||||
stub_client(repos: [repo], orgs: [])
|
||||
stub_client(repos: [repo], orgs: [], each_page: [OpenStruct.new(objects: [repo])].to_enum)
|
||||
|
||||
get :status, format: :json
|
||||
|
||||
|
|
@ -94,7 +94,8 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do
|
|||
end
|
||||
|
||||
it "touches the etag cache store" do
|
||||
expect(stub_client(repos: [], orgs: [])).to receive(:repos)
|
||||
stub_client(repos: [], orgs: [], each_page: [])
|
||||
|
||||
expect_next_instance_of(Gitlab::EtagCaching::Store) do |store|
|
||||
expect(store).to receive(:touch) { "realtime_changes_import_#{provider}_path" }
|
||||
end
|
||||
|
|
@ -102,17 +103,11 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do
|
|||
get :status, format: :json
|
||||
end
|
||||
|
||||
it "requests provider repos list" do
|
||||
expect(stub_client(repos: [], orgs: [])).to receive(:repos)
|
||||
|
||||
get :status
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
|
||||
it "handles an invalid access token" do
|
||||
allow_any_instance_of(Gitlab::LegacyGithubImport::Client)
|
||||
.to receive(:repos).and_raise(Octokit::Unauthorized)
|
||||
client = stub_client(repos: [], orgs: [], each_page: [])
|
||||
|
||||
allow(client).to receive(:repos).and_raise(Octokit::Unauthorized)
|
||||
allow(client).to receive(:each_page).and_raise(Octokit::Unauthorized)
|
||||
|
||||
get :status
|
||||
|
||||
|
|
@ -122,7 +117,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do
|
|||
end
|
||||
|
||||
it "does not produce N+1 database queries" do
|
||||
stub_client(repos: [repo], orgs: [])
|
||||
stub_client(repos: [repo], orgs: [], each_page: [].to_enum)
|
||||
group_a = create(:group)
|
||||
group_a.add_owner(user)
|
||||
create(:project, :import_started, import_type: provider, namespace: user.namespace)
|
||||
|
|
@ -144,10 +139,12 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do
|
|||
let(:repo_2) { OpenStruct.new(login: 'emacs', full_name: 'asd/emacs', name: 'emacs', owner: { login: 'owner' }) }
|
||||
let(:project) { create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'example/repo') }
|
||||
let(:group) { create(:group) }
|
||||
let(:repos) { [repo, repo_2, org_repo] }
|
||||
|
||||
before do
|
||||
group.add_owner(user)
|
||||
stub_client(repos: [repo, repo_2, org_repo], orgs: [org], org_repos: [org_repo])
|
||||
client = stub_client(repos: repos, orgs: [org], org_repos: [org_repo])
|
||||
allow(client).to receive(:each_page).and_return([OpenStruct.new(objects: repos)].to_enum)
|
||||
end
|
||||
|
||||
it 'filters list of repositories by name' do
|
||||
|
|
@ -187,14 +184,14 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
|
|||
end
|
||||
|
||||
before do
|
||||
stub_client(user: provider_user, repo: provider_repo)
|
||||
stub_client(user: provider_user, repo: provider_repo, repository: provider_repo)
|
||||
assign_session_token(provider)
|
||||
end
|
||||
|
||||
it 'returns 200 response when the project is imported successfully' do
|
||||
allow(Gitlab::LegacyGithubImport::ProjectCreator)
|
||||
.to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider)
|
||||
.and_return(double(execute: project))
|
||||
.and_return(double(execute: project))
|
||||
|
||||
post :create, format: :json
|
||||
|
||||
|
|
@ -208,7 +205,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
|
|||
|
||||
allow(Gitlab::LegacyGithubImport::ProjectCreator)
|
||||
.to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider)
|
||||
.and_return(double(execute: project))
|
||||
.and_return(double(execute: project))
|
||||
|
||||
post :create, format: :json
|
||||
|
||||
|
|
@ -219,7 +216,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
|
|||
it "touches the etag cache store" do
|
||||
allow(Gitlab::LegacyGithubImport::ProjectCreator)
|
||||
.to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider)
|
||||
.and_return(double(execute: project))
|
||||
.and_return(double(execute: project))
|
||||
expect_next_instance_of(Gitlab::EtagCaching::Store) do |store|
|
||||
expect(store).to receive(:touch) { "realtime_changes_import_#{provider}_path" }
|
||||
end
|
||||
|
|
@ -232,7 +229,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
|
|||
it "takes the current user's namespace" do
|
||||
expect(Gitlab::LegacyGithubImport::ProjectCreator)
|
||||
.to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider)
|
||||
.and_return(double(execute: project))
|
||||
.and_return(double(execute: project))
|
||||
|
||||
post :create, format: :json
|
||||
end
|
||||
|
|
@ -244,7 +241,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
|
|||
it "takes the current user's namespace" do
|
||||
expect(Gitlab::LegacyGithubImport::ProjectCreator)
|
||||
.to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider)
|
||||
.and_return(double(execute: project))
|
||||
.and_return(double(execute: project))
|
||||
|
||||
post :create, format: :json
|
||||
end
|
||||
|
|
@ -271,7 +268,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
|
|||
it "takes the existing namespace" do
|
||||
expect(Gitlab::LegacyGithubImport::ProjectCreator)
|
||||
.to receive(:new).with(provider_repo, provider_repo.name, existing_namespace, user, access_params, type: provider)
|
||||
.and_return(double(execute: project))
|
||||
.and_return(double(execute: project))
|
||||
|
||||
post :create, format: :json
|
||||
end
|
||||
|
|
@ -283,7 +280,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
|
|||
|
||||
expect(Gitlab::LegacyGithubImport::ProjectCreator)
|
||||
.to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider)
|
||||
.and_return(double(execute: project))
|
||||
.and_return(double(execute: project))
|
||||
|
||||
post :create, format: :json
|
||||
end
|
||||
|
|
@ -302,7 +299,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
|
|||
it "takes the new namespace" do
|
||||
expect(Gitlab::LegacyGithubImport::ProjectCreator)
|
||||
.to receive(:new).with(provider_repo, provider_repo.name, an_instance_of(Group), user, access_params, type: provider)
|
||||
.and_return(double(execute: project))
|
||||
.and_return(double(execute: project))
|
||||
|
||||
post :create, params: { target_namespace: provider_repo.name }, format: :json
|
||||
end
|
||||
|
|
@ -323,7 +320,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
|
|||
it "takes the current user's namespace" do
|
||||
expect(Gitlab::LegacyGithubImport::ProjectCreator)
|
||||
.to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider)
|
||||
.and_return(double(execute: project))
|
||||
.and_return(double(execute: project))
|
||||
|
||||
post :create, format: :json
|
||||
end
|
||||
|
|
@ -341,7 +338,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
|
|||
it 'takes the selected namespace and name' do
|
||||
expect(Gitlab::LegacyGithubImport::ProjectCreator)
|
||||
.to receive(:new).with(provider_repo, test_name, test_namespace, user, access_params, type: provider)
|
||||
.and_return(double(execute: project))
|
||||
.and_return(double(execute: project))
|
||||
|
||||
post :create, params: { target_namespace: test_namespace.name, new_name: test_name }, format: :json
|
||||
end
|
||||
|
|
@ -349,7 +346,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
|
|||
it 'takes the selected name and default namespace' do
|
||||
expect(Gitlab::LegacyGithubImport::ProjectCreator)
|
||||
.to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider)
|
||||
.and_return(double(execute: project))
|
||||
.and_return(double(execute: project))
|
||||
|
||||
post :create, params: { new_name: test_name }, format: :json
|
||||
end
|
||||
|
|
@ -368,7 +365,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
|
|||
it 'takes the selected namespace and name' do
|
||||
expect(Gitlab::LegacyGithubImport::ProjectCreator)
|
||||
.to receive(:new).with(provider_repo, test_name, nested_namespace, user, access_params, type: provider)
|
||||
.and_return(double(execute: project))
|
||||
.and_return(double(execute: project))
|
||||
|
||||
post :create, params: { target_namespace: nested_namespace.full_path, new_name: test_name }, format: :json
|
||||
end
|
||||
|
|
@ -380,7 +377,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
|
|||
it 'takes the selected namespace and name' do
|
||||
expect(Gitlab::LegacyGithubImport::ProjectCreator)
|
||||
.to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider)
|
||||
.and_return(double(execute: project))
|
||||
.and_return(double(execute: project))
|
||||
|
||||
post :create, params: { target_namespace: 'foo/bar', new_name: test_name }, format: :json
|
||||
end
|
||||
|
|
@ -388,7 +385,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
|
|||
it 'creates the namespaces' do
|
||||
allow(Gitlab::LegacyGithubImport::ProjectCreator)
|
||||
.to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider)
|
||||
.and_return(double(execute: project))
|
||||
.and_return(double(execute: project))
|
||||
|
||||
expect { post :create, params: { target_namespace: 'foo/bar', new_name: test_name }, format: :json }
|
||||
.to change { Namespace.count }.by(2)
|
||||
|
|
@ -397,7 +394,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
|
|||
it 'new namespace has the right parent' do
|
||||
allow(Gitlab::LegacyGithubImport::ProjectCreator)
|
||||
.to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider)
|
||||
.and_return(double(execute: project))
|
||||
.and_return(double(execute: project))
|
||||
|
||||
post :create, params: { target_namespace: 'foo/bar', new_name: test_name }, format: :json
|
||||
|
||||
|
|
@ -416,7 +413,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
|
|||
it 'takes the selected namespace and name' do
|
||||
expect(Gitlab::LegacyGithubImport::ProjectCreator)
|
||||
.to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider)
|
||||
.and_return(double(execute: project))
|
||||
.and_return(double(execute: project))
|
||||
|
||||
post :create, params: { target_namespace: 'foo/foobar/bar', new_name: test_name }, format: :json
|
||||
end
|
||||
|
|
@ -424,7 +421,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
|
|||
it 'creates the namespaces' do
|
||||
allow(Gitlab::LegacyGithubImport::ProjectCreator)
|
||||
.to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider)
|
||||
.and_return(double(execute: project))
|
||||
.and_return(double(execute: project))
|
||||
|
||||
expect { post :create, params: { target_namespace: 'foo/foobar/bar', new_name: test_name }, format: :json }
|
||||
.to change { Namespace.count }.by(2)
|
||||
|
|
@ -432,11 +429,11 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
|
|||
|
||||
it 'does not create a new namespace under the user namespace' do
|
||||
expect(Gitlab::LegacyGithubImport::ProjectCreator)
|
||||
.to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider)
|
||||
.and_return(double(execute: project))
|
||||
.to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider)
|
||||
.and_return(double(execute: project))
|
||||
|
||||
expect { post :create, params: { target_namespace: "#{user.namespace_path}/test_group", new_name: test_name }, format: :js }
|
||||
.not_to change { Namespace.count }
|
||||
.not_to change { Namespace.count }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -446,19 +443,19 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
|
|||
|
||||
it 'does not take the selected namespace and name' do
|
||||
expect(Gitlab::LegacyGithubImport::ProjectCreator)
|
||||
.to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider)
|
||||
.and_return(double(execute: project))
|
||||
.to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider)
|
||||
.and_return(double(execute: project))
|
||||
|
||||
post :create, params: { target_namespace: 'foo/foobar/bar', new_name: test_name }, format: :js
|
||||
end
|
||||
|
||||
it 'does not create the namespaces' do
|
||||
allow(Gitlab::LegacyGithubImport::ProjectCreator)
|
||||
.to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider)
|
||||
.and_return(double(execute: project))
|
||||
.to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider)
|
||||
.and_return(double(execute: project))
|
||||
|
||||
expect { post :create, params: { target_namespace: 'foo/foobar/bar', new_name: test_name }, format: :js }
|
||||
.not_to change { Namespace.count }
|
||||
.not_to change { Namespace.count }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -471,8 +468,8 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
|
|||
user.update!(can_create_group: false)
|
||||
|
||||
expect(Gitlab::LegacyGithubImport::ProjectCreator)
|
||||
.to receive(:new).with(provider_repo, test_name, group, user, access_params, type: provider)
|
||||
.and_return(double(execute: project))
|
||||
.to receive(:new).with(provider_repo, test_name, group, user, access_params, type: provider)
|
||||
.and_return(double(execute: project))
|
||||
|
||||
post :create, params: { target_namespace: 'foo', new_name: test_name }, format: :js
|
||||
end
|
||||
|
|
|
|||