Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
577bb49691
commit
02c3b2af44
46 changed files with 631 additions and 143 deletions
|
@ -6,7 +6,7 @@ import pipelinesMixin from '~/pipelines/mixins/pipelines';
|
|||
import eventHub from '~/pipelines/event_hub';
|
||||
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
|
||||
import { getParameterByName } from '~/lib/utils/common_utils';
|
||||
import CIPaginationMixin from '~/vue_shared/mixins/ci_pagination_api_mixin';
|
||||
import PipelinesPaginationApiMixin from '~/pipelines/mixins/pipelines_pagination_api_mixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -16,7 +16,7 @@ export default {
|
|||
GlModal,
|
||||
GlLink,
|
||||
},
|
||||
mixins: [pipelinesMixin, CIPaginationMixin],
|
||||
mixins: [pipelinesMixin, PipelinesPaginationApiMixin],
|
||||
props: {
|
||||
endpoint: {
|
||||
type: String,
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
import { GlBadge, GlButton, GlModalDirective, GlTab, GlTabs } from '@gitlab/ui';
|
||||
import { deprecatedCreateFlash as Flash } from '~/flash';
|
||||
import { s__ } from '~/locale';
|
||||
import CIPaginationMixin from '~/vue_shared/mixins/ci_pagination_api_mixin';
|
||||
import eventHub from '../event_hub';
|
||||
import environmentsMixin from '../mixins/environments_mixin';
|
||||
import EnvironmentsPaginationApiMixin from '../mixins/environments_pagination_api_mixin';
|
||||
import emptyState from './empty_state.vue';
|
||||
import EnableReviewAppModal from './enable_review_app_modal.vue';
|
||||
import StopEnvironmentModal from './stop_environment_modal.vue';
|
||||
|
@ -33,7 +33,7 @@ export default {
|
|||
directives: {
|
||||
'gl-modal': GlModalDirective,
|
||||
},
|
||||
mixins: [CIPaginationMixin, environmentsMixin],
|
||||
mixins: [EnvironmentsPaginationApiMixin, environmentsMixin],
|
||||
props: {
|
||||
endpoint: {
|
||||
type: String,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { GlBadge, GlTab, GlTabs } from '@gitlab/ui';
|
||||
import environmentsMixin from '../mixins/environments_mixin';
|
||||
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
|
||||
import EnvironmentsPaginationApiMixin from '../mixins/environments_pagination_api_mixin';
|
||||
import StopEnvironmentModal from '../components/stop_environment_modal.vue';
|
||||
import DeleteEnvironmentModal from '../components/delete_environment_modal.vue';
|
||||
|
||||
|
@ -14,7 +14,7 @@ export default {
|
|||
StopEnvironmentModal,
|
||||
},
|
||||
|
||||
mixins: [environmentsMixin, CIPaginationMixin],
|
||||
mixins: [environmentsMixin, EnvironmentsPaginationApiMixin],
|
||||
|
||||
props: {
|
||||
endpoint: {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/**
|
||||
* API callbacks for pagination and tabs
|
||||
* shared between Pipelines and Environments table.
|
||||
*
|
||||
* Components need to have `scope`, `page` and `requestData`
|
||||
*/
|
|
@ -6,7 +6,7 @@ import { deprecatedCreateFlash as createFlash } from '~/flash';
|
|||
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
|
||||
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
|
||||
import { getParameterByName } from '~/lib/utils/common_utils';
|
||||
import CIPaginationMixin from '~/vue_shared/mixins/ci_pagination_api_mixin';
|
||||
import PipelinesPaginationApiMixin from '../../mixins/pipelines_pagination_api_mixin';
|
||||
import pipelinesMixin from '../../mixins/pipelines';
|
||||
import PipelinesService from '../../services/pipelines_service';
|
||||
import { validateParams } from '../../utils';
|
||||
|
@ -22,7 +22,7 @@ export default {
|
|||
PipelinesFilteredSearch,
|
||||
GlIcon,
|
||||
},
|
||||
mixins: [pipelinesMixin, CIPaginationMixin],
|
||||
mixins: [pipelinesMixin, PipelinesPaginationApiMixin],
|
||||
props: {
|
||||
store: {
|
||||
type: Object,
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
GlTooltipDirective,
|
||||
GlFriendlyWrap,
|
||||
GlIcon,
|
||||
GlLink,
|
||||
GlButton,
|
||||
GlPagination,
|
||||
} from '@gitlab/ui';
|
||||
|
@ -16,6 +17,7 @@ export default {
|
|||
components: {
|
||||
GlIcon,
|
||||
GlFriendlyWrap,
|
||||
GlLink,
|
||||
GlButton,
|
||||
GlPagination,
|
||||
TestCaseDetails,
|
||||
|
@ -97,11 +99,9 @@ export default {
|
|||
<div class="table-section section-10 section-wrap">
|
||||
<div role="rowheader" class="table-mobile-header">{{ __('Filename') }}</div>
|
||||
<div class="table-mobile-content gl-md-pr-2 gl-overflow-wrap-break">
|
||||
<gl-friendly-wrap
|
||||
v-if="testCase.file"
|
||||
:symbols="$options.wrapSymbols"
|
||||
:text="testCase.file"
|
||||
/>
|
||||
<gl-link v-if="testCase.file" :href="testCase.filePath" target="_blank">
|
||||
<gl-friendly-wrap :symbols="$options.wrapSymbols" :text="testCase.file" />
|
||||
</gl-link>
|
||||
<gl-button
|
||||
v-if="testCase.file"
|
||||
v-gl-tooltip
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* API callbacks for pagination and tabs
|
||||
*
|
||||
* Components need to have `scope`, `page` and `requestData`
|
||||
*/
|
||||
import { validateParams } from '~/pipelines/utils';
|
||||
import { historyPushState, buildUrlWithCurrentLocation } from '../../lib/utils/common_utils';
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
onChangeTab(scope) {
|
||||
if (this.scope === scope) {
|
||||
return;
|
||||
}
|
||||
|
||||
let params = {
|
||||
scope,
|
||||
page: '1',
|
||||
};
|
||||
|
||||
params = this.onChangeWithFilter(params);
|
||||
|
||||
this.updateContent(params);
|
||||
},
|
||||
|
||||
onChangePage(page) {
|
||||
/* URLS parameters are strings, we need to parse to match types */
|
||||
let params = {
|
||||
page: Number(page).toString(),
|
||||
};
|
||||
|
||||
if (this.scope) {
|
||||
params.scope = this.scope;
|
||||
}
|
||||
|
||||
params = this.onChangeWithFilter(params);
|
||||
|
||||
this.updateContent(params);
|
||||
},
|
||||
|
||||
onChangeWithFilter(params) {
|
||||
return { ...params, ...validateParams(this.requestData) };
|
||||
},
|
||||
|
||||
updateInternalState(parameters) {
|
||||
// stop polling
|
||||
this.poll.stop();
|
||||
|
||||
const queryString = Object.keys(parameters)
|
||||
.map((parameter) => {
|
||||
const value = parameters[parameter];
|
||||
// update internal state for UI
|
||||
this[parameter] = value;
|
||||
return `${parameter}=${encodeURIComponent(value)}`;
|
||||
})
|
||||
.join('&');
|
||||
|
||||
// update polling parameters
|
||||
this.requestData = parameters;
|
||||
|
||||
historyPushState(buildUrlWithCurrentLocation(`?${queryString}`));
|
||||
|
||||
this.isLoading = true;
|
||||
},
|
||||
},
|
||||
};
|
|
@ -58,8 +58,9 @@ const createLegacyPipelinesDetailApp = (mediator) => {
|
|||
|
||||
const createTestDetails = () => {
|
||||
const el = document.querySelector(SELECTORS.PIPELINE_TESTS);
|
||||
const { summaryEndpoint, suiteEndpoint } = el?.dataset || {};
|
||||
const { blobPath, summaryEndpoint, suiteEndpoint } = el?.dataset || {};
|
||||
const testReportsStore = createTestReportsStore({
|
||||
blobPath,
|
||||
summaryEndpoint,
|
||||
suiteEndpoint,
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { addIconStatus, formattedTime } from './utils';
|
||||
import { addIconStatus, formatFilePath, formattedTime } from './utils';
|
||||
|
||||
export const getTestSuites = (state) => {
|
||||
const { test_suites: testSuites = [] } = state.testReports;
|
||||
|
@ -17,7 +17,13 @@ export const getSuiteTests = (state) => {
|
|||
const { page, perPage } = state.pageInfo;
|
||||
const start = (page - 1) * perPage;
|
||||
|
||||
return testCases.map(addIconStatus).slice(start, start + perPage);
|
||||
return testCases
|
||||
.map((testCase) => ({
|
||||
...testCase,
|
||||
filePath: testCase.file ? `${state.blobPath}/${formatFilePath(testCase.file)}` : null,
|
||||
}))
|
||||
.map(addIconStatus)
|
||||
.slice(start, start + perPage);
|
||||
};
|
||||
|
||||
export const getSuiteTestCount = (state) => getSelectedSuite(state)?.test_cases?.length || 0;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
export default ({ summaryEndpoint = '', suiteEndpoint = '' }) => ({
|
||||
export default ({ blobPath = '', summaryEndpoint = '', suiteEndpoint = '' }) => ({
|
||||
blobPath,
|
||||
summaryEndpoint,
|
||||
suiteEndpoint,
|
||||
testReports: {},
|
||||
|
|
|
@ -1,6 +1,15 @@
|
|||
import { __, sprintf } from '../../../locale';
|
||||
import { TestStatus } from '../../constants';
|
||||
|
||||
/**
|
||||
* Removes `./` from the beginning of a file path so it can be appended onto a blob path
|
||||
* @param {String} file
|
||||
* @returns {String} - formatted value
|
||||
*/
|
||||
export function formatFilePath(file) {
|
||||
return file.replace(/^\.?\/*/, '');
|
||||
}
|
||||
|
||||
export function iconForTestStatus(status) {
|
||||
switch (status) {
|
||||
case TestStatus.SUCCESS:
|
||||
|
|
|
@ -3,20 +3,37 @@ import { GlSearchBoxByType } from '@gitlab/ui';
|
|||
import { uniq } from 'lodash';
|
||||
import { EXCLUDED_NODES, HIDE_CLASS, HIGHLIGHT_CLASS, TYPING_DELAY } from '../constants';
|
||||
|
||||
const origExpansions = new Map();
|
||||
|
||||
const findSettingsSection = (sectionSelector, node) => {
|
||||
return node.parentElement.closest(sectionSelector);
|
||||
};
|
||||
|
||||
const resetSections = ({ sectionSelector, expandSection, collapseSection }) => {
|
||||
document.querySelectorAll(sectionSelector).forEach((section, index) => {
|
||||
section.classList.remove(HIDE_CLASS);
|
||||
|
||||
if (index === 0) {
|
||||
const restoreExpansionState = ({ expandSection, collapseSection }) => {
|
||||
origExpansions.forEach((isExpanded, section) => {
|
||||
if (isExpanded) {
|
||||
expandSection(section);
|
||||
} else {
|
||||
collapseSection(section);
|
||||
}
|
||||
});
|
||||
|
||||
origExpansions.clear();
|
||||
};
|
||||
|
||||
const saveExpansionState = (sections, { isExpanded }) => {
|
||||
// If we've saved expansions before, don't override it.
|
||||
if (origExpansions.size > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
sections.forEach((section) => origExpansions.set(section, isExpanded(section)));
|
||||
};
|
||||
|
||||
const resetSections = ({ sectionSelector }) => {
|
||||
document.querySelectorAll(sectionSelector).forEach((section) => {
|
||||
section.classList.remove(HIDE_CLASS);
|
||||
});
|
||||
};
|
||||
|
||||
const clearHighlights = () => {
|
||||
|
@ -85,6 +102,12 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isExpandedFn: {
|
||||
type: Function,
|
||||
required: false,
|
||||
// default to a function that returns false
|
||||
default: () => () => false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -97,6 +120,7 @@ export default {
|
|||
sectionSelector: this.sectionSelector,
|
||||
expandSection: this.expandSection,
|
||||
collapseSection: this.collapseSection,
|
||||
isExpanded: this.isExpandedFn,
|
||||
};
|
||||
|
||||
this.searchTerm = value;
|
||||
|
@ -104,7 +128,11 @@ export default {
|
|||
clearResults(displayOptions);
|
||||
|
||||
if (value.length) {
|
||||
saveExpansionState(document.querySelectorAll(this.sectionSelector), displayOptions);
|
||||
|
||||
displayResults(displayOptions, search(this.searchRoot, value));
|
||||
} else {
|
||||
restoreExpansionState(displayOptions);
|
||||
}
|
||||
},
|
||||
expandSection(section) {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import Vue from 'vue';
|
||||
import $ from 'jquery';
|
||||
import { expandSection, closeSection } from '~/settings_panels';
|
||||
import { expandSection, closeSection, isExpanded } from '~/settings_panels';
|
||||
import SearchSettings from '~/search_settings/components/search_settings.vue';
|
||||
|
||||
const mountSearch = ({ el }) =>
|
||||
|
@ -12,10 +11,11 @@ const mountSearch = ({ el }) =>
|
|||
props: {
|
||||
searchRoot: document.querySelector('#content-body'),
|
||||
sectionSelector: '.js-search-settings-section, section.settings',
|
||||
isExpandedFn: isExpanded,
|
||||
},
|
||||
on: {
|
||||
collapse: (section) => closeSection($(section)),
|
||||
expand: (section) => expandSection($(section)),
|
||||
collapse: closeSection,
|
||||
expand: expandSection,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -1,7 +1,22 @@
|
|||
import $ from 'jquery';
|
||||
import { __ } from './locale';
|
||||
|
||||
export function expandSection($section) {
|
||||
/**
|
||||
* Returns true if the given section is expanded or not
|
||||
*
|
||||
* For legacy consistency, it supports both jQuery and DOM elements
|
||||
*
|
||||
* @param {jQuery | Element} section
|
||||
*/
|
||||
export function isExpanded(sectionArg) {
|
||||
const section = sectionArg instanceof $ ? sectionArg[0] : sectionArg;
|
||||
|
||||
return section.classList.contains('expanded');
|
||||
}
|
||||
|
||||
export function expandSection(sectionArg) {
|
||||
const $section = $(sectionArg);
|
||||
|
||||
$section.find('.js-settings-toggle:not(.js-settings-toggle-trigger-only)').text(__('Collapse'));
|
||||
// eslint-disable-next-line @gitlab/no-global-event-off
|
||||
$section.find('.settings-content').off('scroll.expandSection').scrollTop(0);
|
||||
|
@ -13,7 +28,9 @@ export function expandSection($section) {
|
|||
}
|
||||
}
|
||||
|
||||
export function closeSection($section) {
|
||||
export function closeSection(sectionArg) {
|
||||
const $section = $(sectionArg);
|
||||
|
||||
$section.find('.js-settings-toggle:not(.js-settings-toggle-trigger-only)').text(__('Expand'));
|
||||
$section.find('.settings-content').on('scroll.expandSection', () => expandSection($section));
|
||||
$section.removeClass('expanded');
|
||||
|
@ -26,7 +43,7 @@ export function closeSection($section) {
|
|||
|
||||
export function toggleSection($section) {
|
||||
$section.removeClass('no-animate');
|
||||
if ($section.hasClass('expanded')) {
|
||||
if (isExpanded($section)) {
|
||||
closeSection($section);
|
||||
} else {
|
||||
expandSection($section);
|
||||
|
@ -38,7 +55,7 @@ export default function initSettingsPanels() {
|
|||
const $section = $(elm);
|
||||
$section.on('click.toggleSection', '.js-settings-toggle', () => toggleSection($section));
|
||||
|
||||
if (!$section.hasClass('expanded')) {
|
||||
if (!isExpanded($section)) {
|
||||
$section.find('.settings-content').on('scroll.expandSection', () => {
|
||||
$section.removeClass('no-animate');
|
||||
expandSection($section);
|
||||
|
|
|
@ -326,7 +326,7 @@
|
|||
color: $gl-text-color-secondary;
|
||||
}
|
||||
|
||||
.badge.badge-pill + span:not(.badge.badge-pill) {
|
||||
.badge.badge-pill + span:not(.badge):not(.badge-pill) {
|
||||
// Expects up to 3 digits on the badge
|
||||
margin-right: 40px;
|
||||
}
|
||||
|
|
|
@ -903,7 +903,7 @@ table a code {
|
|||
padding: 0;
|
||||
background-color: #4f4f4f;
|
||||
}
|
||||
.dropdown-menu .badge.badge-pill + span:not(.badge.badge-pill) {
|
||||
.dropdown-menu .badge.badge-pill + span:not(.badge):not(.badge-pill) {
|
||||
margin-right: 40px;
|
||||
}
|
||||
.dropdown-select {
|
||||
|
|
|
@ -902,7 +902,7 @@ table a code {
|
|||
padding: 0;
|
||||
background-color: #dbdbdb;
|
||||
}
|
||||
.dropdown-menu .badge.badge-pill + span:not(.badge.badge-pill) {
|
||||
.dropdown-menu .badge.badge-pill + span:not(.badge):not(.badge-pill) {
|
||||
margin-right: 40px;
|
||||
}
|
||||
.dropdown-select {
|
||||
|
|
|
@ -1174,7 +1174,7 @@ table a code {
|
|||
padding: 0;
|
||||
background-color: #dbdbdb;
|
||||
}
|
||||
.dropdown-menu .badge.badge-pill + span:not(.badge.badge-pill) {
|
||||
.dropdown-menu .badge.badge-pill + span:not(.badge):not(.badge-pill) {
|
||||
margin-right: 40px;
|
||||
}
|
||||
.dropdown-select {
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
<!-- Matomo -->
|
||||
- matomo_disable_cookies = extra_config.has_key?('matomo_disable_cookies') && extra_config.matomo_disable_cookies
|
||||
= javascript_tag do
|
||||
:plain
|
||||
var _paq = window._paq = window._paq || [];
|
||||
_paq.push(['trackPageView']);
|
||||
_paq.push(['enableLinkTracking']);
|
||||
#{matomo_disable_cookies ? '_paq.push(["disableCookies"])' : ""};
|
||||
#{extra_config.matomo_disable_cookies ? '_paq.push(["disableCookies"])' : ""};
|
||||
(function() {
|
||||
var u="//#{extra_config.matomo_url}/";
|
||||
_paq.push(['setTrackerUrl', u+'matomo.php']);
|
||||
|
|
|
@ -82,5 +82,6 @@
|
|||
|
||||
#js-tab-tests.tab-pane
|
||||
#js-pipeline-tests-detail{ data: { summary_endpoint: summary_project_pipeline_tests_path(@project, @pipeline, format: :json),
|
||||
suite_endpoint: project_pipeline_test_path(@project, @pipeline, suite_name: 'suite', format: :json) } }
|
||||
suite_endpoint: project_pipeline_test_path(@project, @pipeline, suite_name: 'suite', format: :json),
|
||||
blob_path: project_blob_path(@project, @pipeline.sha) } }
|
||||
= render_if_exists "projects/pipelines/tabs_content", pipeline: @pipeline, project: @project
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
= f.hidden_field 'milestone_id', value: milestone[:id], id: nil
|
||||
= dropdown_tag('Milestone', options: { title: _('Assign milestone'), toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: _('Search milestones'), data: { show_no: true, field_name: "#{issuable_type}[milestone_id]", project_id: issuable_sidebar[:project_id], issuable_id: issuable_sidebar[:id], ability_name: issuable_type, issue_update: issuable_sidebar[:issuable_json_path], use_id: true, default_no: true, selected: milestone[:title], null_default: true, display: 'static' }})
|
||||
- if @project.group.present? && issuable_sidebar[:supports_iterations]
|
||||
= render_if_exists 'shared/issuable/iteration_select', can_edit: can_edit_issuable, group_path: @project.group.full_path, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid], issuable_type: issuable_type
|
||||
= render_if_exists 'shared/issuable/iteration_select', can_edit: can_edit_issuable.to_s, group_path: @project.group.full_path, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid], issuable_type: issuable_type
|
||||
|
||||
- if issuable_sidebar[:supports_time_tracking]
|
||||
#issuable-time-tracker.block
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add link to test case file in pipeline test report
|
||||
merge_request: 53650
|
||||
author:
|
||||
type: added
|
|
@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/292601
|
|||
milestone: '13.8'
|
||||
type: development
|
||||
group: group::release
|
||||
default_enabled: false
|
||||
default_enabled: true
|
||||
|
|
|
@ -819,6 +819,7 @@ Settings.forti_token_cloud['enabled'] = false if Settings.forti_token_cloud['ena
|
|||
Settings['extra'] ||= Settingslogic.new({})
|
||||
Settings.extra['matomo_site_id'] ||= Settings.extra['piwik_site_id'] if Settings.extra['piwik_site_id'].present?
|
||||
Settings.extra['matomo_url'] ||= Settings.extra['piwik_url'] if Settings.extra['piwik_url'].present?
|
||||
Settings.extra['matomo_disable_cookies'] = false if Settings.extra['matomo_disable_cookies'].nil?
|
||||
|
||||
#
|
||||
# Rack::Attack settings
|
||||
|
|
|
@ -811,3 +811,38 @@ You can set them globally or per-job in the [`variables`](../yaml/README.md#vari
|
|||
## System calls not available on GitLab.com shared runners
|
||||
|
||||
GitLab.com shared runners run on CoreOS. This means that you cannot use some system calls, like `getlogin`, from the C standard library.
|
||||
|
||||
## Artifact and cache settings
|
||||
|
||||
> Introduced in GitLab Runner 13.9.
|
||||
|
||||
Artifact and cache settings control the compression ratio of artifacts and caches.
|
||||
Use these settings to specify the size of the archive produced by a job.
|
||||
|
||||
- On a slow network, uploads might be faster for smaller archives.
|
||||
- On a fast network where bandwidth and storage are not a concern, uploads might be faster using the fastest compression ratio, despite the archive produced being larger.
|
||||
|
||||
For [GitLab Pages](../../user/project/pages/index.md) to serve
|
||||
[HTTP Range requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests), artifacts
|
||||
should use the `ARTIFACT_COMPRESSION_LEVEL: fastest` setting, as only uncompressed zip archives
|
||||
support this feature.
|
||||
|
||||
A meter can also be enabled to provide the rate of transfer for uploads and downloads.
|
||||
|
||||
```yaml
|
||||
variables:
|
||||
# output upload and download progress every 2 seconds
|
||||
TRANSFER_METER_FREQUENCY: "2s"
|
||||
|
||||
# Use fast compression for artifacts, resulting in larger archives
|
||||
ARTIFACT_COMPRESSION_LEVEL: "fast"
|
||||
|
||||
# Use no compression for caches
|
||||
CACHE_COMPRESSION_LEVEL: "fastest"
|
||||
```
|
||||
|
||||
| Variable | Description |
|
||||
|---------------------------------|--------------------------------------------------------|
|
||||
| `TRANSFER_METER_FREQUENCY` | Specify how often to print the meter's transfer rate. It can be set to a duration (for example, `1s` or `1m30s`). A duration of `0` disables the meter (default). When a value is set, the pipeline shows a progress meter for artifact and cache uploads and downloads. |
|
||||
| `ARTIFACT_COMPRESSION_LEVEL` | To adjust compression ratio, set to `fastest`, `fast`, `default`, `slow`, or `slowest`. This setting works with the fastzip archiver only, so the GitLab Runner feature flag [`FF_USE_FASTZIP`](https://docs.gitlab.com/runner/configuration/feature-flags.html#available-feature-flags) must also be enabled. |
|
||||
| `CACHE_COMPRESSION_LEVEL` | To adjust compression ratio, set to `fastest`, `fast`, `default`, `slow`, or `slowest`. This setting works with the fastzip archiver only, so the GitLab Runner feature flag [`FF_USE_FASTZIP`](https://docs.gitlab.com/runner/configuration/feature-flags.html#available-feature-flags) must also be enabled. |
|
||||
|
|
|
@ -4358,6 +4358,9 @@ You can use [CI/CD variables](../variables/README.md) to configure runner Git be
|
|||
- [`GIT_FETCH_EXTRA_FLAGS`](../runners/README.md#git-fetch-extra-flags)
|
||||
- [`GIT_DEPTH`](../runners/README.md#shallow-cloning) (shallow cloning)
|
||||
- [`GIT_CLONE_PATH`](../runners/README.md#custom-build-directories) (custom build directories)
|
||||
- [`TRANSFER_METER_FREQUENCY`](../runners/README.md#artifact-and-cache-settings) (artifact/cache meter update frequency)
|
||||
- [`ARTIFACT_COMPRESSION_LEVEL`](../runners/README.md#artifact-and-cache-settings) (artifact archiver compression level)
|
||||
- [`CACHE_COMPRESSION_LEVEL`](../runners/README.md#artifact-and-cache-settings) (cache archiver compression level)
|
||||
|
||||
You can also use variables to configure how many times a runner
|
||||
[attempts certain stages of job execution](../runners/README.md#job-stages-attempts).
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
stage: Fulfillment
|
||||
group: License
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
|
|
|
@ -8,6 +8,15 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
This page will cover architectural patterns and tips for developers to follow to prevent [transient bugs.](https://about.gitlab.com/handbook/engineering/quality/issue-triage/#transient-bugs)
|
||||
|
||||
## Common root causes
|
||||
|
||||
We've noticed a few root causes that come up frequently when addressing transient bugs.
|
||||
|
||||
- Needs better state management in the backend or frontend.
|
||||
- Frontend code needs improvements.
|
||||
- Lack of test coverage.
|
||||
- Race conditions.
|
||||
|
||||
## Frontend
|
||||
|
||||
### Don't rely on response order
|
||||
|
|
|
@ -43,7 +43,7 @@ receive alert payloads in JSON format. You can always
|
|||
1. Toggle the **Active** alert setting to display the **URL** and **Authorization Key**
|
||||
for the webhook configuration.
|
||||
|
||||
### HTTP Endpoints **PREMIUM**
|
||||
### HTTP Endpoints **(PREMIUM)**
|
||||
|
||||
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/4442) in GitLab Premium 13.6.
|
||||
|
||||
|
@ -72,7 +72,7 @@ side of the integrations list.
|
|||
|
||||
### External Prometheus integration
|
||||
|
||||
For GitLab versions 13.1 and greater, please read
|
||||
For GitLab versions 13.1 and greater, read
|
||||
[External Prometheus Instances](../metrics/alerts.md#external-prometheus-instances)
|
||||
to configure alerts for this integration.
|
||||
|
||||
|
|
BIN
doc/user/admin_area/img/impersonate_user_button_v13_8.png
Normal file
BIN
doc/user/admin_area/img/impersonate_user_button_v13_8.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
|
@ -144,6 +144,19 @@ To search for users, enter your criteria in the search field. The user search is
|
|||
insensitive, and applies partial matching to name and username. To search for an email address,
|
||||
you must provide the complete email address.
|
||||
|
||||
#### User impersonation
|
||||
|
||||
An administrator can "impersonate" any other user, including other administrator users.
|
||||
This allows the administrator to "see what the user sees," and take actions on behalf of the user.
|
||||
You can impersonate a user in the following ways:
|
||||
|
||||
- Through the UI, by selecting **Admin Area > Overview > Users > [Select a user] > Impersonate**.
|
||||
- With the API, using [impersonation tokens](../../api/README.md#impersonation-tokens).
|
||||
|
||||
All impersonation activities are [captured with audit events](../../administration/audit_events.md#impersonation-data).
|
||||
|
||||
![user impersonation button](img/impersonate_user_button_v13_8.png)
|
||||
|
||||
#### Users statistics
|
||||
|
||||
The **Users statistics** page provides an overview of user accounts by role. These statistics are
|
||||
|
|
|
@ -776,7 +776,7 @@ A site profile contains the following:
|
|||
- **Profile name**: A name you assign to the site to be scanned.
|
||||
- **Target URL**: The URL that DAST runs against.
|
||||
|
||||
## Site profile validation
|
||||
### Site profile validation
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/233020) in GitLab 13.8.
|
||||
|
||||
|
@ -799,37 +799,51 @@ To create a site profile:
|
|||
|
||||
1. From your project's home page, go to **Security & Compliance > Configuration**.
|
||||
1. Select **Manage** in the **DAST Profiles** row.
|
||||
1. Select **New Profile > Site Profile**.
|
||||
1. Type in a unique **Profile name** and **Target URL** then select **Save profile**.
|
||||
1. Select **New > Site Profile**.
|
||||
1. Complete the fields then select **Save profile**.
|
||||
|
||||
The site profile is created.
|
||||
|
||||
### Edit a site profile
|
||||
|
||||
To edit an existing site profile:
|
||||
|
||||
1. From your project's home page, go to **Security & Compliance > Configuration**.
|
||||
1. Select **Manage** in the **DAST Profiles** row.
|
||||
1. Select **Edit** in the row of the profile to edit.
|
||||
1. Edit the **Profile name** and **Target URL**, then select **Save profile**.
|
||||
1. In the **DAST Profiles** row select **Manage**.
|
||||
1. Select the **Site Profiles** tab.
|
||||
1. In the profile's row select the **More actions** (**{ellipsis_v}**) menu, then select **Edit**.
|
||||
1. Edit the fields then select **Save profile**.
|
||||
|
||||
The site profile is updated with the edited details.
|
||||
|
||||
### Delete a site profile
|
||||
|
||||
To delete an existing site profile:
|
||||
|
||||
1. From your project's home page, go to **Security & Compliance > Configuration**.
|
||||
1. Select **Manage** in the **DAST Profiles** row.
|
||||
1. Select **{remove}** (Delete profile) in the row of the profile to delete.
|
||||
1. In the **DAST Profiles** row select **Manage**.
|
||||
1. Select the **Site Profiles** tab.
|
||||
1. In the profile's row select the **More actions** (**{ellipsis_v}**) menu, then select **Delete**.
|
||||
1. Select **Delete** to confirm the deletion.
|
||||
|
||||
The site profile is deleted.
|
||||
|
||||
### Validate a site profile
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- A site profile.
|
||||
|
||||
To validate a site profile:
|
||||
|
||||
1. From your project's home page, go to **Security & Compliance > Configuration**.
|
||||
1. Select **Manage** in the **DAST Profiles** row.
|
||||
1. Select **Validate target site** beside the profile to validate.
|
||||
1. In the **DAST Profiles** row select **Manage**.
|
||||
1. Select the **Site Profiles** tab.
|
||||
1. In the profile's row select **Validate** or **Retry validation**.
|
||||
1. Select the validation method.
|
||||
1. For **Text file validation**:
|
||||
1. Download the validation file listed in **Step 2**.
|
||||
1. Upload the validation file to the host. You can upload the file to the location in
|
||||
1. Upload the validation file to the host. Upload the file to the location in
|
||||
**Step 3** or any location you prefer.
|
||||
1. Select **Validate**.
|
||||
1. For **Header validation**:
|
||||
|
@ -840,22 +854,23 @@ To validate a site profile:
|
|||
|
||||
The site is validated and an active scan can run against it.
|
||||
|
||||
If a validated site profile's target URL is edited, the site is no longer validated.
|
||||
If a validated site profile's target URL is edited, the site's validation status is revoked.
|
||||
|
||||
### Revoke a site validation
|
||||
### Revoke a site profile's validation status
|
||||
|
||||
To revoke validation from a site profile:
|
||||
Note that all site profiles with the same URL have their validation status revoked.
|
||||
|
||||
To revoke a site profile's validation status:
|
||||
|
||||
1. From your project's home page, go to **Security & Compliance > Configuration**.
|
||||
1. Select **Manage** in the **DAST Profiles** row.
|
||||
1. In the **DAST Profiles** row select **Manage**.
|
||||
1. Select **Revoke validation** beside the validated profile.
|
||||
1. Select **Revoke validation**.
|
||||
|
||||
The site profile's validation is revoked. An active scan cannot be run against it or any other profile with the same URL.
|
||||
The site profile's validation status is revoked.
|
||||
|
||||
#### Validated site profile headers
|
||||
|
||||
The following are code samples of how you could provide the required site profile header in your
|
||||
The following are code samples of how you can provide the required site profile header in your
|
||||
application.
|
||||
|
||||
##### Ruby on Rails example for on-demand scan
|
||||
|
@ -900,27 +915,26 @@ app.get('/dast-website-target', function(req, res) {
|
|||
## Scanner profile
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/222767) in GitLab 13.4.
|
||||
> - [Added](https://gitlab.com/gitlab-org/gitlab/-/issues/225804) in GitLab 13.5: scan mode, AJAX spider, debug messages.
|
||||
|
||||
A scanner profile defines the scanner settings used to run an on-demand scan:
|
||||
|
||||
- **Profile name:** A name you give the scanner profile. For example, "Spider_15".
|
||||
- **Scan mode:** A passive scan monitors all HTTP messages (requests and responses) sent to the target. An active scan attacks the target to find potential vulnerabilities.
|
||||
- **Spider timeout:** The maximum number of minutes allowed for the spider to traverse the site.
|
||||
- **Target timeout:** The maximum number of seconds DAST waits for the site to be available before
|
||||
starting the scan.
|
||||
- **Scan mode:** A passive scan monitors all HTTP messages (requests and responses) sent to the target. An active scan attacks the target to find potential vulnerabilities.
|
||||
- **AJAX spider:** Run the AJAX spider, in addition to the traditional spider, to crawl the target site.
|
||||
- **Debug messages:** Include debug messages in the DAST console output.
|
||||
|
||||
Scan mode, AJAX spider, Debug messages are [added in GitLab 13.5](https://gitlab.com/gitlab-org/gitlab/-/issues/225804)
|
||||
|
||||
### Create a scanner profile
|
||||
|
||||
To create a scanner profile:
|
||||
|
||||
1. From your project's home page, go to **Security & Compliance > Configuration**.
|
||||
1. Click **Manage** in the **DAST Profiles** row.
|
||||
1. Click **New Profile > Scanner Profile**.
|
||||
1. Enter a unique **Profile name**, the desired **Spider timeout**, and the **Target timeout**.
|
||||
1. In the **DAST Profiles** row select **Manage**.
|
||||
1. Select **New > Scanner Profile**.
|
||||
1. Complete the form. For details of each field, see [Scanner profile](#scanner-profile).
|
||||
1. Click **Save profile**.
|
||||
|
||||
### Edit a scanner profile
|
||||
|
@ -929,7 +943,12 @@ To edit a scanner profile:
|
|||
|
||||
1. From your project's home page, go to **Security & Compliance > Configuration**.
|
||||
1. Click **Manage** in the **DAST Profiles** row.
|
||||
1. Click **Edit** in the scanner profile's row.
|
||||
1. Select the **Scanner Profiles** tab.
|
||||
1. In the scanner's row select the **More actions** (**{ellipsis_v}**) menu, then select **Edit**.
|
||||
1. Edit the form.
|
||||
1. Select **Save profile**.
|
||||
|
||||
The scanner profile is updated with the edited details.
|
||||
|
||||
### Delete a scanner profile
|
||||
|
||||
|
@ -937,7 +956,11 @@ To delete a scanner profile:
|
|||
|
||||
1. From your project's home page, go to **Security & Compliance > Configuration**.
|
||||
1. Click **Manage** in the **DAST Profiles** row.
|
||||
1. Click **{remove}** (Delete profile) in the scanner profile's row.
|
||||
1. Select the **Scanner Profiles** tab.
|
||||
1. In the scanner's row select the **More actions** (**{ellipsis_v}**) menu, then select **Delete**.
|
||||
1. Select **Delete**.
|
||||
|
||||
The scanner profile is deleted.
|
||||
|
||||
## Reports
|
||||
|
||||
|
|
|
@ -387,3 +387,13 @@ If you encounter this error while adding a Kubernetes cluster, ensure you're
|
|||
properly pasting the service token. Some shells may add a line break to the
|
||||
service token, making it invalid. Ensure that there are no line breaks by
|
||||
pasting your token into an editor and removing any additional spaces.
|
||||
|
||||
You may also experience this error if your certificate is not valid. To check that your certificate's
|
||||
subject alternative names contain the correct domain for your cluster's API, run this:
|
||||
|
||||
```shell
|
||||
echo | openssl s_client -showcerts -connect kubernetes.example.com:443 2>/dev/null |
|
||||
openssl x509 -inform pem -noout -text
|
||||
```
|
||||
|
||||
Note that the `-connect` argument expects a `host:port` combination. For example, `https://kubernetes.example.com` would be `kubernetes.example.com:443`.
|
||||
|
|
|
@ -475,6 +475,16 @@ terminal.
|
|||
Read the [Release CLI documentation](https://gitlab.com/gitlab-org/release-cli/-/blob/master/docs/index.md)
|
||||
for details.
|
||||
|
||||
## Release Metrics **(PREMIUM)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/259703) in GitLab Premium 13.9.
|
||||
|
||||
Group-level release metrics are available by navigating to **Group > Analytics > CI/CD**.
|
||||
These metrics include:
|
||||
|
||||
- Total number of releases in the group
|
||||
- Percentage of projects in the group that have at least one release
|
||||
|
||||
<!-- ## Troubleshooting
|
||||
|
||||
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
|
||||
|
|
47
lib/gitlab/database/migrations/instrumentation.rb
Normal file
47
lib/gitlab/database/migrations/instrumentation.rb
Normal file
|
@ -0,0 +1,47 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Database
|
||||
module Migrations
|
||||
Observation = Struct.new(
|
||||
:migration,
|
||||
:walltime,
|
||||
:success
|
||||
)
|
||||
|
||||
class Instrumentation
|
||||
attr_reader :observations
|
||||
|
||||
def initialize
|
||||
@observations = []
|
||||
end
|
||||
|
||||
def observe(migration, &block)
|
||||
observation = Observation.new(migration)
|
||||
observation.success = true
|
||||
|
||||
exception = nil
|
||||
|
||||
observation.walltime = Benchmark.realtime do
|
||||
yield
|
||||
rescue => e
|
||||
exception = e
|
||||
observation.success = false
|
||||
end
|
||||
|
||||
record_observation(observation)
|
||||
|
||||
raise exception if exception
|
||||
|
||||
observation
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def record_observation(observation)
|
||||
@observations << observation
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -231,5 +231,37 @@ namespace :gitlab do
|
|||
puts "Found user created projects. Database active"
|
||||
exit 0
|
||||
end
|
||||
|
||||
desc 'Run migrations with instrumentation'
|
||||
task :migration_testing, [:result_file] => :environment do |_, args|
|
||||
result_file = args[:result_file] || raise("Please specify result_file argument")
|
||||
raise "File exists already, won't overwrite: #{result_file}" if File.exist?(result_file)
|
||||
|
||||
verbose_was, ActiveRecord::Migration.verbose = ActiveRecord::Migration.verbose, true
|
||||
|
||||
ctx = ActiveRecord::Base.connection.migration_context
|
||||
existing_versions = ctx.get_all_versions.to_set
|
||||
|
||||
pending_migrations = ctx.migrations.reject do |migration|
|
||||
existing_versions.include?(migration.version)
|
||||
end
|
||||
|
||||
instrumentation = Gitlab::Database::Migrations::Instrumentation.new
|
||||
|
||||
pending_migrations.each do |migration|
|
||||
instrumentation.observe(migration.version) do
|
||||
ActiveRecord::Migrator.new(:up, ctx.migrations, ctx.schema_migration, migration.version).run
|
||||
end
|
||||
end
|
||||
ensure
|
||||
if instrumentation
|
||||
File.open(result_file, 'wb+') do |io|
|
||||
io << instrumentation.observations.to_json
|
||||
end
|
||||
end
|
||||
|
||||
ActiveRecord::Base.clear_cache!
|
||||
ActiveRecord::Migration.verbose = verbose_was
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12929,7 +12929,7 @@ msgstr ""
|
|||
msgid "Free Trial"
|
||||
msgstr ""
|
||||
|
||||
msgid "Free Trial of GitLab.com Gold"
|
||||
msgid "Free Trial of GitLab.com Ultimate"
|
||||
msgstr ""
|
||||
|
||||
msgid "Freeze end"
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import { getJSONFixture } from 'helpers/fixtures';
|
||||
import * as getters from '~/pipelines/stores/test_reports/getters';
|
||||
import { iconForTestStatus, formattedTime } from '~/pipelines/stores/test_reports/utils';
|
||||
import {
|
||||
iconForTestStatus,
|
||||
formatFilePath,
|
||||
formattedTime,
|
||||
} from '~/pipelines/stores/test_reports/utils';
|
||||
|
||||
describe('Getters TestReports Store', () => {
|
||||
let state;
|
||||
|
@ -8,6 +12,7 @@ describe('Getters TestReports Store', () => {
|
|||
const testReports = getJSONFixture('pipelines/test_report.json');
|
||||
|
||||
const defaultState = {
|
||||
blobPath: '/test/blob/path',
|
||||
testReports,
|
||||
selectedSuiteIndex: 0,
|
||||
pageInfo: {
|
||||
|
@ -17,6 +22,7 @@ describe('Getters TestReports Store', () => {
|
|||
};
|
||||
|
||||
const emptyState = {
|
||||
blobPath: '',
|
||||
testReports: {},
|
||||
selectedSuite: null,
|
||||
pageInfo: {
|
||||
|
@ -74,6 +80,7 @@ describe('Getters TestReports Store', () => {
|
|||
const expected = testReports.test_suites[0].test_cases
|
||||
.map((x) => ({
|
||||
...x,
|
||||
filePath: `${state.blobPath}/${formatFilePath(x.file)}`,
|
||||
formattedTime: formattedTime(x.execution_time),
|
||||
icon: iconForTestStatus(x.status),
|
||||
}))
|
||||
|
|
|
@ -1,6 +1,20 @@
|
|||
import { formattedTime } from '~/pipelines/stores/test_reports/utils';
|
||||
import { formatFilePath, formattedTime } from '~/pipelines/stores/test_reports/utils';
|
||||
|
||||
describe('Test reports utils', () => {
|
||||
describe('formatFilePath', () => {
|
||||
it.each`
|
||||
file | expected
|
||||
${'./test.js'} | ${'test.js'}
|
||||
${'/test.js'} | ${'test.js'}
|
||||
${'.//////////////test.js'} | ${'test.js'}
|
||||
${'test.js'} | ${'test.js'}
|
||||
${'mock/path./test.js'} | ${'mock/path./test.js'}
|
||||
${'./mock/path./test.js'} | ${'mock/path./test.js'}
|
||||
`('should format $file to be $expected', ({ file, expected }) => {
|
||||
expect(formatFilePath(file)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formattedTime', () => {
|
||||
describe('when time is smaller than a second', () => {
|
||||
it('should return time in milliseconds fixed to 2 decimals', () => {
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import Vuex from 'vuex';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import { GlButton, GlFriendlyWrap, GlPagination } from '@gitlab/ui';
|
||||
import { GlButton, GlFriendlyWrap, GlLink, GlPagination } from '@gitlab/ui';
|
||||
import { getJSONFixture } from 'helpers/fixtures';
|
||||
import SuiteTable from '~/pipelines/components/test_reports/test_suite_table.vue';
|
||||
import * as getters from '~/pipelines/stores/test_reports/getters';
|
||||
import { formatFilePath } from '~/pipelines/stores/test_reports/utils';
|
||||
import { TestStatus } from '~/pipelines/constants';
|
||||
import skippedTestCases from './mock_data';
|
||||
|
||||
|
@ -20,15 +21,18 @@ describe('Test reports suite table', () => {
|
|||
|
||||
testSuite.test_cases = [...testSuite.test_cases, ...skippedTestCases];
|
||||
const testCases = testSuite.test_cases;
|
||||
const blobPath = '/test/blob/path';
|
||||
|
||||
const noCasesMessage = () => wrapper.find('.js-no-test-cases');
|
||||
const allCaseRows = () => wrapper.findAll('.js-case-row');
|
||||
const findCaseRowAtIndex = (index) => wrapper.findAll('.js-case-row').at(index);
|
||||
const findLinkForRow = (row) => row.find(GlLink);
|
||||
const findIconForRow = (row, status) => row.find(`.ci-status-icon-${status}`);
|
||||
|
||||
const createComponent = (suite = testSuite, perPage = 20) => {
|
||||
store = new Vuex.Store({
|
||||
state: {
|
||||
blobPath,
|
||||
testReports: {
|
||||
test_suites: [suite],
|
||||
},
|
||||
|
@ -82,9 +86,13 @@ describe('Test reports suite table', () => {
|
|||
|
||||
it('renders the file name for the test with a copy button', () => {
|
||||
const { file } = testCases[0];
|
||||
const relativeFile = formatFilePath(file);
|
||||
const filePath = `${blobPath}/${relativeFile}`;
|
||||
const row = findCaseRowAtIndex(0);
|
||||
const fileLink = findLinkForRow(row);
|
||||
const button = row.find(GlButton);
|
||||
|
||||
expect(fileLink.attributes('href')).toBe(filePath);
|
||||
expect(row.text()).toContain(file);
|
||||
expect(button.exists()).toBe(true);
|
||||
expect(button.attributes('data-clipboard-text')).toBe(file);
|
||||
|
|
|
@ -2,6 +2,7 @@ import { GlSearchBoxByType } from '@gitlab/ui';
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import SearchSettings from '~/search_settings/components/search_settings.vue';
|
||||
import { HIGHLIGHT_CLASS, HIDE_CLASS } from '~/search_settings/constants';
|
||||
import { isExpanded, expandSection, closeSection } from '~/settings_panels';
|
||||
|
||||
describe('search_settings/components/search_settings.vue', () => {
|
||||
const ROOT_ID = 'content-body';
|
||||
|
@ -9,6 +10,8 @@ describe('search_settings/components/search_settings.vue', () => {
|
|||
const SEARCH_TERM = 'Delete project';
|
||||
const GENERAL_SETTINGS_ID = 'js-general-settings';
|
||||
const ADVANCED_SETTINGS_ID = 'js-advanced-settings';
|
||||
const EXTRA_SETTINGS_ID = 'js-extra-settings';
|
||||
|
||||
let wrapper;
|
||||
|
||||
const buildWrapper = () => {
|
||||
|
@ -16,10 +19,15 @@ describe('search_settings/components/search_settings.vue', () => {
|
|||
propsData: {
|
||||
searchRoot: document.querySelector(`#${ROOT_ID}`),
|
||||
sectionSelector: SECTION_SELECTOR,
|
||||
isExpandedFn: isExpanded,
|
||||
},
|
||||
// Add real listeners so we can simplify and strengthen some tests.
|
||||
listeners: {
|
||||
expand: expandSection,
|
||||
collapse: closeSection,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const sections = () => Array.from(document.querySelectorAll(SECTION_SELECTOR));
|
||||
const sectionsCount = () => sections().length;
|
||||
const visibleSectionsCount = () =>
|
||||
|
@ -39,7 +47,10 @@ describe('search_settings/components/search_settings.vue', () => {
|
|||
<section id="${GENERAL_SETTINGS_ID}" class="settings">
|
||||
<span>General</span>
|
||||
</section>
|
||||
<section id="${ADVANCED_SETTINGS_ID}" class="settings">
|
||||
<section id="${ADVANCED_SETTINGS_ID}" class="settings expanded">
|
||||
<span>Advanced</span>
|
||||
</section>
|
||||
<section id="${EXTRA_SETTINGS_ID}" class="settings">
|
||||
<span>${SEARCH_TERM}</span>
|
||||
</section>
|
||||
</div>
|
||||
|
@ -52,17 +63,6 @@ describe('search_settings/components/search_settings.vue', () => {
|
|||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('expands first section and collapses the rest', () => {
|
||||
clearSearch();
|
||||
|
||||
const [firstSection, ...otherSections] = sections();
|
||||
|
||||
expect(wrapper.emitted()).toEqual({
|
||||
expand: [[firstSection]],
|
||||
collapse: otherSections.map((x) => [x]),
|
||||
});
|
||||
});
|
||||
|
||||
it('hides sections that do not match the search term', () => {
|
||||
const hiddenSection = document.querySelector(`#${GENERAL_SETTINGS_ID}`);
|
||||
search(SEARCH_TERM);
|
||||
|
@ -72,12 +72,11 @@ describe('search_settings/components/search_settings.vue', () => {
|
|||
});
|
||||
|
||||
it('expands section that matches the search term', () => {
|
||||
const section = document.querySelector(`#${ADVANCED_SETTINGS_ID}`);
|
||||
const section = document.querySelector(`#${EXTRA_SETTINGS_ID}`);
|
||||
|
||||
search(SEARCH_TERM);
|
||||
|
||||
// Last called because expand is always called once to reset the page state
|
||||
expect(wrapper.emitted().expand[1][0]).toBe(section);
|
||||
expect(wrapper.emitted('expand')).toEqual([[section]]);
|
||||
});
|
||||
|
||||
it('highlight elements that match the search term', () => {
|
||||
|
@ -86,21 +85,64 @@ describe('search_settings/components/search_settings.vue', () => {
|
|||
expect(highlightedElementsCount()).toBe(1);
|
||||
});
|
||||
|
||||
describe('when search term is cleared', () => {
|
||||
beforeEach(() => {
|
||||
search(SEARCH_TERM);
|
||||
describe('default', () => {
|
||||
it('test setup starts with expansion state', () => {
|
||||
expect(sections().map(isExpanded)).toEqual([false, true, false]);
|
||||
});
|
||||
|
||||
it('displays all sections', () => {
|
||||
expect(visibleSectionsCount()).toBe(1);
|
||||
clearSearch();
|
||||
expect(visibleSectionsCount()).toBe(sectionsCount());
|
||||
});
|
||||
describe('when searched and cleared', () => {
|
||||
beforeEach(() => {
|
||||
search('Test');
|
||||
clearSearch();
|
||||
});
|
||||
|
||||
it('removes the highlight from all elements', () => {
|
||||
expect(highlightedElementsCount()).toBe(1);
|
||||
clearSearch();
|
||||
expect(highlightedElementsCount()).toBe(0);
|
||||
it('displays all sections', () => {
|
||||
expect(visibleSectionsCount()).toBe(sectionsCount());
|
||||
});
|
||||
|
||||
it('removes the highlight from all elements', () => {
|
||||
expect(highlightedElementsCount()).toBe(0);
|
||||
});
|
||||
|
||||
it('should preserve original expansion state', () => {
|
||||
expect(sections().map(isExpanded)).toEqual([false, true, false]);
|
||||
});
|
||||
|
||||
it('should preserve state by emitting events', () => {
|
||||
const [first, mid, last] = sections();
|
||||
|
||||
expect(wrapper.emitted()).toEqual({
|
||||
expand: [[mid]],
|
||||
collapse: [[first], [last]],
|
||||
});
|
||||
});
|
||||
|
||||
describe('after multiple searches and clear', () => {
|
||||
beforeEach(() => {
|
||||
search('Test');
|
||||
search(SEARCH_TERM);
|
||||
clearSearch();
|
||||
});
|
||||
|
||||
it('should preserve last expansion state', () => {
|
||||
expect(sections().map(isExpanded)).toEqual([false, true, false]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('after user expands and collapses, search, and clear', () => {
|
||||
beforeEach(() => {
|
||||
const [first, mid] = sections();
|
||||
closeSection(mid);
|
||||
expandSection(first);
|
||||
|
||||
search(SEARCH_TERM);
|
||||
clearSearch();
|
||||
});
|
||||
|
||||
it('should preserve last expansion state', () => {
|
||||
expect(sections().map(isExpanded)).toEqual([true, false, false]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import $ from 'jquery';
|
||||
import { setHTMLFixture } from 'helpers/fixtures';
|
||||
import mount from '~/search_settings/mount';
|
||||
import { expandSection, closeSection } from '~/settings_panels';
|
||||
|
@ -24,13 +23,13 @@ describe('search_settings/mount', () => {
|
|||
const section = { name: 'section' };
|
||||
app.$refs.searchSettings.$emit('expand', section);
|
||||
|
||||
expect(expandSection).toHaveBeenCalledWith($(section));
|
||||
expect(expandSection).toHaveBeenCalledWith(section);
|
||||
});
|
||||
|
||||
it('calls settings_panel.closeSection when collapse event is emitted', () => {
|
||||
const section = { name: 'section' };
|
||||
app.$refs.searchSettings.$emit('collapse', section);
|
||||
|
||||
expect(closeSection).toHaveBeenCalledWith($(section));
|
||||
expect(closeSection).toHaveBeenCalledWith(section);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import $ from 'jquery';
|
||||
import initSettingsPanels from '~/settings_panels';
|
||||
import initSettingsPanels, { isExpanded } from '~/settings_panels';
|
||||
|
||||
describe('Settings Panels', () => {
|
||||
preloadFixtures('groups/edit.html');
|
||||
|
@ -20,11 +20,11 @@ describe('Settings Panels', () => {
|
|||
// Our test environment automatically expands everything so we need to clear that out first
|
||||
panel.classList.remove('expanded');
|
||||
|
||||
expect(panel.classList.contains('expanded')).toBe(false);
|
||||
expect(isExpanded(panel)).toBe(false);
|
||||
|
||||
initSettingsPanels();
|
||||
|
||||
expect(panel.classList.contains('expanded')).toBe(true);
|
||||
expect(isExpanded(panel)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -35,11 +35,11 @@ describe('Settings Panels', () => {
|
|||
|
||||
initSettingsPanels();
|
||||
|
||||
expect(panel.classList.contains('expanded')).toBe(true);
|
||||
expect(isExpanded(panel)).toBe(true);
|
||||
|
||||
$(trigger).click();
|
||||
|
||||
expect(panel.classList.contains('expanded')).toBe(false);
|
||||
expect(isExpanded(panel)).toBe(false);
|
||||
expect(trigger.textContent).toEqual(originalText);
|
||||
});
|
||||
});
|
||||
|
|
76
spec/lib/gitlab/database/migrations/instrumentation_spec.rb
Normal file
76
spec/lib/gitlab/database/migrations/instrumentation_spec.rb
Normal file
|
@ -0,0 +1,76 @@
|
|||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Database::Migrations::Instrumentation do
|
||||
describe '#observe' do
|
||||
subject { described_class.new }
|
||||
|
||||
let(:migration) { 1234 }
|
||||
|
||||
it 'executes the given block' do
|
||||
expect { |b| subject.observe(migration, &b) }.to yield_control
|
||||
end
|
||||
|
||||
context 'on successful execution' do
|
||||
subject { described_class.new.observe(migration) {} }
|
||||
|
||||
it 'records walltime' do
|
||||
expect(subject.walltime).not_to be_nil
|
||||
end
|
||||
|
||||
it 'records success' do
|
||||
expect(subject.success).to be_truthy
|
||||
end
|
||||
|
||||
it 'records the migration version' do
|
||||
expect(subject.migration).to eq(migration)
|
||||
end
|
||||
end
|
||||
|
||||
context 'upon failure' do
|
||||
subject { described_class.new.observe(migration) { raise 'something went wrong' } }
|
||||
|
||||
it 'raises the exception' do
|
||||
expect { subject }.to raise_error(/something went wrong/)
|
||||
end
|
||||
|
||||
context 'retrieving observations' do
|
||||
subject { instance.observations.first }
|
||||
|
||||
before do
|
||||
instance.observe(migration) { raise 'something went wrong' }
|
||||
rescue
|
||||
# ignore
|
||||
end
|
||||
|
||||
let(:instance) { described_class.new }
|
||||
|
||||
it 'records walltime' do
|
||||
expect(subject.walltime).not_to be_nil
|
||||
end
|
||||
|
||||
it 'records failure' do
|
||||
expect(subject.success).to be_falsey
|
||||
end
|
||||
|
||||
it 'records the migration version' do
|
||||
expect(subject.migration).to eq(migration)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'sequence of migrations with failures' do
|
||||
subject { described_class.new }
|
||||
|
||||
let(:migration1) { double('migration1', call: nil) }
|
||||
let(:migration2) { double('migration2', call: nil) }
|
||||
|
||||
it 'records observations for all migrations' do
|
||||
subject.observe('migration1') {}
|
||||
subject.observe('migration2') { raise 'something went wrong' } rescue nil
|
||||
|
||||
expect(subject.observations.size).to eq(2)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -297,6 +297,57 @@ RSpec.describe 'gitlab:db namespace rake task' do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#migrate_with_instrumentation' do
|
||||
subject { run_rake_task('gitlab:db:migration_testing', "[#{filename}]") }
|
||||
|
||||
let(:ctx) { double('ctx', migrations: all_migrations, schema_migration: double, get_all_versions: existing_versions) }
|
||||
let(:instrumentation) { instance_double(Gitlab::Database::Migrations::Instrumentation, observations: observations) }
|
||||
let(:existing_versions) { [1] }
|
||||
let(:all_migrations) { [double('migration1', version: 1), pending_migration] }
|
||||
let(:pending_migration) { double('migration2', version: 2) }
|
||||
let(:filename) { 'results-file.json'}
|
||||
let(:buffer) { StringIO.new }
|
||||
let(:observations) { %w[some data] }
|
||||
|
||||
before do
|
||||
allow(ActiveRecord::Base.connection).to receive(:migration_context).and_return(ctx)
|
||||
allow(Gitlab::Database::Migrations::Instrumentation).to receive(:new).and_return(instrumentation)
|
||||
allow(ActiveRecord::Migrator).to receive_message_chain('new.run').with(any_args).with(no_args)
|
||||
|
||||
allow(instrumentation).to receive(:observe).and_yield
|
||||
|
||||
allow(File).to receive(:open).with(filename, 'wb+').and_yield(buffer)
|
||||
end
|
||||
|
||||
it 'fails when given no filename argument' do
|
||||
expect { run_rake_task('gitlab:db:migration_testing') }.to raise_error(/specify result_file/)
|
||||
end
|
||||
|
||||
it 'fails when the given file already exists' do
|
||||
expect(File).to receive(:exist?).with(filename).and_return(true)
|
||||
|
||||
expect { subject }.to raise_error(/File exists/)
|
||||
end
|
||||
|
||||
it 'instruments the pending migration' do
|
||||
expect(instrumentation).to receive(:observe).with(2).and_yield
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it 'executes the pending migration' do
|
||||
expect(ActiveRecord::Migrator).to receive_message_chain('new.run').with(:up, ctx.migrations, ctx.schema_migration, pending_migration.version).with(no_args)
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it 'writes observations out to JSON file' do
|
||||
subject
|
||||
|
||||
expect(buffer.string).to eq(observations.to_json)
|
||||
end
|
||||
end
|
||||
|
||||
def run_rake_task(task_name, arguments = '')
|
||||
Rake::Task[task_name].reenable
|
||||
Rake.application.invoke_task("#{task_name}#{arguments}")
|
||||
|
|
|
@ -92,7 +92,8 @@ RSpec.describe 'layouts/_head' do
|
|||
before do
|
||||
stub_config(extra: {
|
||||
matomo_url: matomo_host,
|
||||
matomo_site_id: 12345
|
||||
matomo_site_id: 12345,
|
||||
matomo_disable_cookies: false
|
||||
})
|
||||
end
|
||||
|
||||
|
@ -101,43 +102,18 @@ RSpec.describe 'layouts/_head' do
|
|||
|
||||
expect(rendered).to match(/<script.*>.*var u="\/\/#{matomo_host}\/".*<\/script>/m)
|
||||
expect(rendered).to match(%r(<noscript>.*<img src="//#{matomo_host}/matomo.php.*</noscript>))
|
||||
expect(rendered).not_to include('_paq.push(["disableCookies"])')
|
||||
end
|
||||
|
||||
context 'matomo_disable_cookies' do
|
||||
context 'when true' do
|
||||
before do
|
||||
stub_config(extra: { matomo_url: matomo_host, matomo_site_id: 12345, matomo_disable_cookies: true })
|
||||
end
|
||||
|
||||
it 'disables cookies' do
|
||||
render
|
||||
|
||||
expect(rendered).to include('_paq.push(["disableCookies"])')
|
||||
end
|
||||
context 'when matomo_disable_cookies is true' do
|
||||
before do
|
||||
stub_config(extra: { matomo_url: matomo_host, matomo_site_id: 12345, matomo_disable_cookies: true })
|
||||
end
|
||||
|
||||
context 'when false' do
|
||||
before do
|
||||
stub_config(extra: { matomo_url: matomo_host, matomo_site_id: 12345, matomo_disable_cookies: false })
|
||||
end
|
||||
it 'disables cookies' do
|
||||
render
|
||||
|
||||
it 'does not disable cookies' do
|
||||
render
|
||||
|
||||
expect(rendered).not_to include('_paq.push(["disableCookies"])')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when absent' do
|
||||
before do
|
||||
stub_config(extra: { matomo_url: matomo_host, matomo_site_id: 12345 })
|
||||
end
|
||||
|
||||
it 'does not disable cookies' do
|
||||
render
|
||||
|
||||
expect(rendered).not_to include('_paq.push(["disableCookies"])')
|
||||
end
|
||||
expect(rendered).to include('_paq.push(["disableCookies"])')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue