Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-10-04 18:12:46 +00:00
parent e9ab418709
commit 8a0a54ab92
79 changed files with 1584 additions and 561 deletions

View File

@ -130,11 +130,13 @@ update-storybook-yarn-cache:
- tmp/tests/frontend/
- knapsack/
rspec frontend_fixture:
# In gitlab-foss, generates FOSS fixtures. In gitlab, generates FOSS & EE fixtures.
# That way, we don't need to have two separate jobs.
rspec-all frontend_fixture:
extends:
- .frontend-fixtures-base
- .frontend:rules:default-frontend-jobs
parallel: 2
parallel: 5
rspec frontend_fixture as-if-foss:
extends:
@ -142,12 +144,6 @@ rspec frontend_fixture as-if-foss:
- .frontend:rules:default-frontend-jobs-as-if-foss
- .as-if-foss
rspec-ee frontend_fixture:
extends:
- .frontend-fixtures-base
- .frontend:rules:default-frontend-jobs-ee
parallel: 3
graphql-schema-dump:
variables:
SETUP_DB: "false"
@ -198,9 +194,7 @@ jest:
- .frontend:rules:jest
needs:
- job: "detect-tests"
- job: "rspec frontend_fixture"
- job: "rspec-ee frontend_fixture"
optional: true
- job: "rspec-all frontend_fixture"
artifacts:
name: coverage-frontend
expire_in: 31d
@ -227,9 +221,7 @@ jest-integration:
script:
- run_timed_command "yarn jest:integration --ci"
needs:
- job: "rspec frontend_fixture"
- job: "rspec-ee frontend_fixture"
optional: true
- job: "rspec-all frontend_fixture"
- job: "graphql-schema-dump"
jest-as-if-foss:
@ -343,9 +335,7 @@ startup-css-check:
- .frontend:rules:default-frontend-jobs
needs:
- job: "compile-test-assets"
- job: "rspec frontend_fixture"
- job: "rspec-ee frontend_fixture"
optional: true
- job: "rspec-all frontend_fixture"
startup-css-check as-if-foss:
extends:
@ -369,9 +359,7 @@ compile-storybook:
- .compile-storybook-base
- .frontend:rules:default-frontend-jobs
needs:
- job: "rspec frontend_fixture"
- job: "rspec-ee frontend_fixture"
optional: true
- job: "rspec-all frontend_fixture"
- job: "graphql-schema-dump"
artifacts:
name: storybook

View File

@ -354,6 +354,10 @@
- "danger/**/*"
- "tooling/danger/**/*"
.core-backend-patterns: &core-backend-patterns
- "{,jh/}Gemfile{,.lock}"
- "{,ee/,jh/}config/**/*.rb"
.core-frontend-patterns: &core-frontend-patterns
- "{package.json,yarn.lock}"
- "babel.config.js"
@ -495,13 +499,6 @@
- <<: *if-default-refs
changes: *code-backstage-patterns
.frontend:rules:default-frontend-jobs-ee:
rules:
- <<: *if-not-ee
when: never
- <<: *if-default-refs
changes: *code-backstage-patterns
.frontend:rules:default-frontend-jobs-as-if-foss:
rules:
- <<: *if-not-ee
@ -657,6 +654,8 @@
.rails:rules:ee-and-foss-migration:
rules:
- <<: *if-merge-request-labels-run-all-rspec
- <<: *if-merge-request
changes: *core-backend-patterns
- <<: *if-merge-request
changes: *ci-patterns
- <<: *if-merge-request
@ -675,6 +674,9 @@
when: never
- <<: *if-merge-request-labels-run-all-rspec
when: never
- <<: *if-merge-request
changes: *core-backend-patterns
when: never
- <<: *if-merge-request
changes: *ci-patterns
when: never
@ -699,6 +701,8 @@
.rails:rules:ee-and-foss-unit:
rules:
- <<: *if-merge-request-labels-run-all-rspec
- <<: *if-merge-request
changes: *core-backend-patterns
- <<: *if-merge-request
changes: *ci-patterns
- <<: *if-automated-merge-request
@ -715,6 +719,9 @@
when: never
- <<: *if-merge-request-labels-run-all-rspec
when: never
- <<: *if-merge-request
changes: *core-backend-patterns
when: never
- <<: *if-merge-request
changes: *ci-patterns
when: never
@ -724,6 +731,8 @@
.rails:rules:ee-and-foss-integration:
rules:
- <<: *if-merge-request-labels-run-all-rspec
- <<: *if-merge-request
changes: *core-backend-patterns
- <<: *if-merge-request
changes: *ci-patterns
- <<: *if-automated-merge-request
@ -740,6 +749,9 @@
when: never
- <<: *if-merge-request-labels-run-all-rspec
when: never
- <<: *if-merge-request
changes: *core-backend-patterns
when: never
- <<: *if-merge-request
changes: *ci-patterns
when: never
@ -749,6 +761,8 @@
.rails:rules:ee-and-foss-system:
rules:
- <<: *if-merge-request-labels-run-all-rspec
- <<: *if-merge-request
changes: *core-backend-patterns
- <<: *if-merge-request
changes: *ci-patterns
- <<: *if-automated-merge-request
@ -765,6 +779,9 @@
when: never
- <<: *if-merge-request-labels-run-all-rspec
when: never
- <<: *if-merge-request
changes: *core-backend-patterns
when: never
- <<: *if-merge-request
changes: *ci-patterns
when: never
@ -774,6 +791,8 @@
.rails:rules:ee-and-foss-fast_spec_helper:
rules:
- <<: *if-merge-request-labels-run-all-rspec
- <<: *if-merge-request
changes: *core-backend-patterns
- <<: *if-merge-request
changes: *ci-patterns
- <<: *if-automated-merge-request
@ -790,6 +809,9 @@
when: never
- <<: *if-merge-request-labels-run-all-rspec
when: never
- <<: *if-merge-request
changes: *core-backend-patterns
when: never
- <<: *if-merge-request
changes: *ci-patterns
when: never
@ -806,6 +828,8 @@
- <<: *if-not-ee
when: never
- <<: *if-merge-request-labels-run-all-rspec
- <<: *if-merge-request
changes: *core-backend-patterns
- <<: *if-merge-request
changes: *ci-patterns
- <<: *if-merge-request
@ -826,6 +850,9 @@
when: never
- <<: *if-merge-request-labels-run-all-rspec
when: never
- <<: *if-merge-request
changes: *core-backend-patterns
when: never
- <<: *if-merge-request
changes: *ci-patterns
when: never
@ -838,6 +865,8 @@
- <<: *if-not-ee
when: never
- <<: *if-merge-request-labels-run-all-rspec
- <<: *if-merge-request
changes: *core-backend-patterns
- <<: *if-merge-request
changes: *ci-patterns
- <<: *if-automated-merge-request
@ -856,6 +885,9 @@
when: never
- <<: *if-merge-request-labels-run-all-rspec
when: never
- <<: *if-merge-request
changes: *core-backend-patterns
when: never
- <<: *if-merge-request
changes: *ci-patterns
when: never
@ -867,6 +899,8 @@
- <<: *if-not-ee
when: never
- <<: *if-merge-request-labels-run-all-rspec
- <<: *if-merge-request
changes: *core-backend-patterns
- <<: *if-merge-request
changes: *ci-patterns
- <<: *if-automated-merge-request
@ -885,6 +919,9 @@
when: never
- <<: *if-merge-request-labels-run-all-rspec
when: never
- <<: *if-merge-request
changes: *core-backend-patterns
when: never
- <<: *if-merge-request
changes: *ci-patterns
when: never
@ -896,6 +933,8 @@
- <<: *if-not-ee
when: never
- <<: *if-merge-request-labels-run-all-rspec
- <<: *if-merge-request
changes: *core-backend-patterns
- <<: *if-merge-request
changes: *ci-patterns
- <<: *if-automated-merge-request
@ -914,6 +953,9 @@
when: never
- <<: *if-merge-request-labels-run-all-rspec
when: never
- <<: *if-merge-request
changes: *core-backend-patterns
when: never
- <<: *if-merge-request
changes: *ci-patterns
when: never
@ -925,6 +967,8 @@
- <<: *if-not-ee
when: never
- <<: *if-merge-request-labels-run-all-rspec
- <<: *if-merge-request
changes: *core-backend-patterns
- <<: *if-merge-request
changes: *ci-patterns
- <<: *if-security-merge-request
@ -944,6 +988,9 @@
when: never
- <<: *if-automated-merge-request
when: never
- <<: *if-merge-request
changes: *core-backend-patterns
when: never
- <<: *if-merge-request
changes: *ci-patterns
when: never
@ -959,6 +1006,8 @@
- <<: *if-not-ee
when: never
- <<: *if-merge-request-labels-run-all-rspec
- <<: *if-merge-request
changes: *core-backend-patterns
- <<: *if-merge-request
changes: *ci-patterns
- <<: *if-automated-merge-request
@ -978,6 +1027,9 @@
when: never
- <<: *if-automated-merge-request
when: never
- <<: *if-merge-request
changes: *core-backend-patterns
when: never
- <<: *if-merge-request
changes: *ci-patterns
when: never
@ -991,6 +1043,8 @@
- <<: *if-not-ee
when: never
- <<: *if-merge-request-labels-run-all-rspec
- <<: *if-merge-request
changes: *core-backend-patterns
- <<: *if-merge-request
changes: *ci-patterns
- <<: *if-automated-merge-request
@ -1010,6 +1064,9 @@
when: never
- <<: *if-automated-merge-request
when: never
- <<: *if-merge-request
changes: *core-backend-patterns
when: never
- <<: *if-merge-request
changes: *ci-patterns
when: never
@ -1023,6 +1080,8 @@
- <<: *if-not-ee
when: never
- <<: *if-merge-request-labels-run-all-rspec
- <<: *if-merge-request
changes: *core-backend-patterns
- <<: *if-merge-request
changes: *ci-patterns
- <<: *if-automated-merge-request
@ -1042,6 +1101,9 @@
when: never
- <<: *if-automated-merge-request
when: never
- <<: *if-merge-request
changes: *core-backend-patterns
when: never
- <<: *if-merge-request
changes: *ci-patterns
when: never

View File

@ -29,8 +29,7 @@ update-tests-metadata:
- retrieve-tests-metadata
- setup-test-env
- rspec migration pg12
- rspec frontend_fixture
- rspec-ee frontend_fixture
- rspec-all frontend_fixture
- rspec unit pg12
- rspec integration pg12
- rspec system pg12

View File

@ -4,17 +4,15 @@
* Used in the environments table.
*/
import { GlTooltipDirective, GlButton, GlModalDirective } from '@gitlab/ui';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { GlDropdownItem, GlModalDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
export default {
components: {
GlButton,
GlDropdownItem,
},
directives: {
GlTooltip: GlTooltipDirective,
GlModalDirective,
},
props: {
@ -28,10 +26,8 @@ export default {
isLoading: false,
};
},
computed: {
title() {
return s__('Environments|Delete environment');
},
i18n: {
title: s__('Environments|Delete environment'),
},
mounted() {
eventHub.$on('deleteEnvironment', this.onDeleteEnvironment);
@ -41,7 +37,6 @@ export default {
},
methods: {
onClick() {
this.$root.$emit(BV_HIDE_TOOLTIP, this.$options.deleteEnvironmentTooltipId);
eventHub.$emit('requestDeleteEnvironment', this.environment);
},
onDeleteEnvironment(environment) {
@ -50,20 +45,15 @@ export default {
}
},
},
deleteEnvironmentTooltipId: 'delete-environment-button-tooltip',
};
</script>
<template>
<gl-button
v-gl-tooltip="{ id: $options.deleteEnvironmentTooltipId }"
v-gl-modal-directive="'delete-environment-modal'"
<gl-dropdown-item
v-gl-modal-directive.delete-environment-modal
:loading="isLoading"
:title="title"
:aria-label="title"
class="gl-display-none gl-md-display-block"
variant="danger"
category="primary"
icon="remove"
@click="onClick"
/>
>
{{ $options.i18n.title }}
</gl-dropdown-item>
</template>

View File

@ -18,22 +18,23 @@ export default {
required: true,
},
},
computed: {
title() {
return s__('Environments|Open live environment');
},
i18n: {
title: s__('Environments|Open live environment'),
open: s__('Environments|Open'),
},
};
</script>
<template>
<gl-button
v-gl-tooltip
:title="title"
:aria-label="title"
:title="$options.i18n.title"
:aria-label="$options.i18n.title"
:href="externalUrl"
class="external-url"
target="_blank"
icon="external-link"
rel="noopener noreferrer nofollow"
/>
>
{{ $options.i18n.open }}
</gl-button>
</template>

View File

@ -1,5 +1,5 @@
<script>
import { GlTooltipDirective, GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
import { GlDropdown, GlTooltipDirective, GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { __, s__, sprintf } from '~/locale';
@ -29,6 +29,7 @@ export default {
ActionsComponent,
CommitComponent,
ExternalUrlComponent,
GlDropdown,
GlIcon,
GlLink,
GlSprintf,
@ -521,6 +522,10 @@ export default {
return this.model.metrics_path || '';
},
terminalPath() {
return this.model?.terminal_path ?? '';
},
autoStopUrl() {
return this.model.cancel_auto_stop_path || '';
},
@ -549,6 +554,15 @@ export default {
tableNameSpacingClass() {
return this.isFolder ? 'section-100' : this.tableData.name.spacing;
},
hasExtraActions() {
return Boolean(
this.canRetry ||
this.canShowAutoStopDate ||
this.monitoringUrl ||
this.terminalPath ||
this.canDeleteEnvironment,
);
},
},
methods: {
@ -776,13 +790,6 @@ export default {
role="gridcell"
>
<div class="btn-group table-action-buttons" role="group">
<pin-component
v-if="canShowAutoStopDate"
:auto-stop-url="autoStopUrl"
data-track-action="click_button"
data-track-label="environment_pin"
/>
<external-url-component
v-if="externalURL"
:external-url="externalURL"
@ -790,13 +797,6 @@ export default {
data-track-label="environment_url"
/>
<monitoring-button-component
v-if="monitoringUrl"
:monitoring-url="monitoringUrl"
data-track-action="click_button"
data-track-label="environment_monitoring"
/>
<actions-component
v-if="actions.length > 0"
:actions="actions"
@ -804,35 +804,59 @@ export default {
data-track-label="environment_actions"
/>
<terminal-button-component
v-if="model && model.terminal_path"
:terminal-path="model.terminal_path"
data-track-action="click_button"
data-track-label="environment_terminal"
/>
<rollback-component
v-if="canRetry"
:environment="model"
:is-last-deployment="isLastDeployment"
:retry-url="retryUrl"
data-track-action="click_button"
data-track-label="environment_rollback"
/>
<stop-component
v-if="canStopEnvironment"
:environment="model"
class="gl-z-index-2"
data-track-action="click_button"
data-track-label="environment_stop"
/>
<delete-component
v-if="canDeleteEnvironment"
:environment="model"
data-track-action="click_button"
data-track-label="environment_delete"
/>
<gl-dropdown
v-if="hasExtraActions"
icon="ellipsis_v"
text-sr-only
:text="__('More actions')"
category="secondary"
no-caret
>
<rollback-component
v-if="canRetry"
:environment="model"
:is-last-deployment="isLastDeployment"
:retry-url="retryUrl"
data-track-action="click_button"
data-track-label="environment_rollback"
/>
<pin-component
v-if="canShowAutoStopDate"
:auto-stop-url="autoStopUrl"
data-track-action="click_button"
data-track-label="environment_pin"
/>
<monitoring-button-component
v-if="monitoringUrl"
:monitoring-url="monitoringUrl"
data-track-action="click_button"
data-track-label="environment_monitoring"
/>
<terminal-button-component
v-if="terminalPath"
:terminal-path="terminalPath"
data-track-action="click_button"
data-track-label="environment_terminal"
/>
<delete-component
v-if="canDeleteEnvironment"
:environment="model"
data-track-action="click_button"
data-track-label="environment_delete"
/>
</gl-dropdown>
</div>
</div>
</div>

View File

@ -1,15 +1,12 @@
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { GlDropdownItem } from '@gitlab/ui';
import { __ } from '~/locale';
/**
* Renders the Monitoring (Metrics) link in environments table.
*/
export default {
components: {
GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
GlDropdownItem,
},
props: {
monitoringUrl: {
@ -17,22 +14,11 @@ export default {
required: true,
},
},
computed: {
title() {
return __('Monitoring');
},
},
title: __('Monitoring'),
};
</script>
<template>
<gl-button
v-gl-tooltip
:href="monitoringUrl"
:title="title"
:aria-label="title"
class="monitoring-url gl-display-none gl-sm-display-none gl-md-display-block"
icon="chart"
rel="noopener noreferrer nofollow"
variant="default"
/>
<gl-dropdown-item :href="monitoringUrl" rel="noopener noreferrer nofollow" target="_blank">
{{ $options.title }}
</gl-dropdown-item>
</template>

View File

@ -3,17 +3,13 @@
* Renders a prevent auto-stop button.
* Used in environments table.
*/
import { GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { GlDropdownItem } from '@gitlab/ui';
import { __ } from '~/locale';
import eventHub from '../event_hub';
export default {
components: {
GlIcon,
GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
GlDropdownItem,
},
props: {
autoStopUrl: {
@ -26,11 +22,11 @@ export default {
eventHub.$emit('cancelAutoStop', this.autoStopUrl);
},
},
title: __('Prevent environment from auto-stopping'),
title: __('Prevent auto-stopping'),
};
</script>
<template>
<gl-button v-gl-tooltip :title="$options.title" :aria-label="$options.title" @click="onPinClick">
<gl-icon name="thumbtack" />
</gl-button>
<gl-dropdown-item @click="onPinClick">
{{ $options.title }}
</gl-dropdown-item>
</template>

View File

@ -5,16 +5,15 @@
*
* Makes a post request when the button is clicked.
*/
import { GlTooltipDirective, GlModalDirective, GlButton } from '@gitlab/ui';
import { GlModalDirective, GlDropdownItem } from '@gitlab/ui';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
export default {
components: {
GlButton,
GlDropdownItem,
},
directives: {
GlTooltip: GlTooltipDirective,
GlModal: GlModalDirective,
},
props: {
@ -65,14 +64,7 @@ export default {
};
</script>
<template>
<gl-button
v-gl-tooltip
v-gl-modal.confirm-rollback-modal
class="gl-display-none gl-md-display-block text-secondary"
:loading="isLoading"
:title="title"
:aria-label="title"
:icon="isLastDeployment ? 'repeat' : 'redo'"
@click="onClick"
/>
<gl-dropdown-item v-gl-modal.confirm-rollback-modal @click="onClick">
{{ title }}
</gl-dropdown-item>
</template>

View File

@ -23,16 +23,15 @@ export default {
required: true,
},
},
i18n: {
title: s__('Environments|Stop environment'),
stop: s__('Environments|Stop'),
},
data() {
return {
isLoading: false,
};
},
computed: {
title() {
return s__('Environments|Stop environment');
},
},
mounted() {
eventHub.$on('stopEnvironment', this.onStopEnvironment);
},
@ -58,11 +57,13 @@ export default {
v-gl-tooltip="{ id: $options.stopEnvironmentTooltipId }"
v-gl-modal-directive="'stop-environment-modal'"
:loading="isLoading"
:title="title"
:aria-label="title"
:title="$options.i18n.title"
:aria-label="$options.i18n.title"
icon="stop"
category="primary"
category="secondary"
variant="danger"
@click="onClick"
/>
>
{{ $options.i18n.stop }}
</gl-button>
</template>

View File

@ -3,15 +3,12 @@
* Renders a terminal button to open a web terminal.
* Used in environments table.
*/
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { GlDropdownItem } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: {
GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
GlDropdownItem,
},
props: {
terminalPath: {
@ -25,22 +22,11 @@ export default {
default: false,
},
},
computed: {
title() {
return __('Terminal');
},
},
title: __('Terminal'),
};
</script>
<template>
<a
v-gl-tooltip
:title="title"
:aria-label="title"
:href="terminalPath"
:class="{ disabled: disabled }"
class="btn terminal-button d-none d-md-block text-secondary"
>
<gl-icon name="terminal" />
</a>
<gl-dropdown-item :href="terminalPath" :disabled="disabled">
{{ $options.title }}
</gl-dropdown-item>
</template>

View File

@ -10,10 +10,14 @@ import { historyReplaceState } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages/list/constants';
import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import { getQueryParams, extractFilterAndSorting } from '~/packages_and_registries/shared/utils';
import getPackagesQuery from '~/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql';
import {
PROJECT_RESOURCE_TYPE,
GROUP_RESOURCE_TYPE,
LIST_QUERY_DEBOUNCE_TIME,
} from '~/packages_and_registries/package_registry/constants';
import PackageTitle from './package_title.vue';
// import PackageSearch from './package_search.vue';
import PackageSearch from './package_search.vue';
// import PackageList from './packages_list.vue';
export default {
@ -23,28 +27,53 @@ export default {
// GlSprintf,
// PackageList,
PackageTitle,
// PackageSearch,
PackageSearch,
},
inject: ['packageHelpUrl', 'emptyListIllustration', 'emptyListHelpUrl'],
inject: [
'packageHelpUrl',
'emptyListIllustration',
'emptyListHelpUrl',
'isGroupPage',
'fullPath',
],
data() {
return {
filter: [],
sorting: {
sort: 'desc',
orderBy: 'created_at',
},
selectedType: '',
pagination: {},
packages: {},
sort: '',
filters: {},
};
},
computed: {
packagesCount() {
return 0;
apollo: {
packages: {
query: getPackagesQuery,
variables() {
return this.queryVariables;
},
update(data) {
return data[this.graphqlResource].packages;
},
debounce: LIST_QUERY_DEBOUNCE_TIME,
},
emptySearch() {
return (
this.filter.filter((f) => f.type !== FILTERED_SEARCH_TERM || f.value?.data).length === 0
);
},
computed: {
queryVariables() {
return {
isGroupPage: this.isGroupPage,
fullPath: this.fullPath,
sort: this.isGroupPage ? undefined : this.sort,
groupSort: this.isGroupPage ? this.sort : undefined,
packageName: this.filters?.packageName,
packageType: this.filters?.packageType,
};
},
graphqlResource() {
return this.isGroupPage ? GROUP_RESOURCE_TYPE : PROJECT_RESOURCE_TYPE;
},
packagesCount() {
return this.packages?.count;
},
hasFilters() {
return this.filters.packageName && this.filters.packageType;
},
emptyStateTitle() {
return this.emptySearch
@ -53,19 +82,9 @@ export default {
},
},
mounted() {
const queryParams = getQueryParams(window.document.location.search);
const { sorting, filters } = extractFilterAndSorting(queryParams);
this.sorting = { ...sorting };
this.filter = [...filters];
this.checkDeleteAlert();
},
methods: {
onPageChanged(page) {
return this.requestPackagesList({ page });
},
onPackageDeleteRequest(item) {
return this.requestDeletePackage(item);
},
checkDeleteAlert() {
const urlParams = new URLSearchParams(window.location.search);
const showAlert = urlParams.get(SHOW_DELETE_SUCCESS_ALERT);
@ -76,6 +95,10 @@ export default {
historyReplaceState(cleanUrl);
}
},
handleSearchUpdate({ sort, filters }) {
this.sort = sort;
this.filters = { ...filters };
},
},
i18n: {
widenFilters: s__('PackageRegistry|To widen your search, change or remove the filters above.'),
@ -91,13 +114,13 @@ export default {
<template>
<div>
<package-title :help-url="packageHelpUrl" :count="packagesCount" />
<!-- <package-search @update="requestPackagesList" />
<package-search @update="handleSearchUpdate" />
<package-list @page:changed="onPageChanged" @package:delete="onPackageDeleteRequest">
<!-- <package-list @page:changed="onPageChanged" @package:delete="onPackageDeleteRequest">
<template #empty-state>
<gl-empty-state :title="emptyStateTitle" :svg-path="emptyListIllustration">
<template #description>
<gl-sprintf v-if="!emptySearch" :message="$options.i18n.widenFilters" />
<gl-sprintf v-if="hasFilters" :message="$options.i18n.widenFilters" />
<gl-sprintf v-else :message="$options.i18n.noResultsText">
<template #noPackagesLink="{ content }">
<gl-link :href="emptyListHelpUrl" target="_blank">{{ content }}</gl-link>

View File

@ -1,10 +1,14 @@
<script>
import { mapState, mapActions } from 'vuex';
import { s__ } from '~/locale';
import { sortableFields } from '~/packages/list/utils';
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
import UrlSync from '~/vue_shared/components/url_sync.vue';
import { getQueryParams, extractFilterAndSorting } from '~/packages_and_registries/shared/utils';
import {
FILTERED_SEARCH_TERM,
FILTERED_SEARCH_TYPE,
} from '~/packages_and_registries/shared/constants';
import PackageTypeToken from './tokens/package_type_token.vue';
export default {
@ -19,21 +23,71 @@ export default {
},
],
components: { RegistrySearch, UrlSync },
inject: ['isGroupPage'],
data() {
return {
filters: [],
sorting: {
orderBy: 'name',
sort: 'desc',
},
mountRegistrySearch: false,
};
},
computed: {
...mapState({
isGroupPage: (state) => state.config.isGroupPage,
sorting: (state) => state.sorting,
filter: (state) => state.filter,
}),
sortableFields() {
return sortableFields(this.isGroupPage);
},
parsedSorting() {
const cleanOrderBy = this.sorting?.orderBy.replace('_at', '');
return `${cleanOrderBy}_${this.sorting?.sort}`.toUpperCase();
},
parsedFilters() {
const parsed = {
packageName: '',
packageType: undefined,
};
return this.filters.reduce((acc, filter) => {
if (filter.type === FILTERED_SEARCH_TYPE && filter.value?.data) {
return {
...acc,
packageType: filter.value.data.toUpperCase(),
};
}
if (filter.type === FILTERED_SEARCH_TERM) {
return {
...acc,
packageName: `${acc.packageName} ${filter.value.data}`.trim(),
};
}
return acc;
}, parsed);
},
},
mounted() {
const queryParams = getQueryParams(window.document.location.search);
const { sorting, filters } = extractFilterAndSorting(queryParams);
this.updateSorting(sorting);
this.updateFilters(filters);
this.mountRegistrySearch = true;
this.emitUpdate();
},
methods: {
...mapActions(['setSorting', 'setFilter']),
updateFilters(newValue) {
this.filters = newValue;
},
updateSorting(newValue) {
this.setSorting(newValue);
this.$emit('update');
this.sorting = { ...this.sorting, ...newValue };
},
updateSortingAndEmitUpdate(newValue) {
this.updateSorting(newValue);
this.emitUpdate();
},
emitUpdate() {
this.$emit('update', { sort: this.parsedSorting, filters: this.parsedFilters });
},
},
};
@ -43,13 +97,14 @@ export default {
<url-sync>
<template #default="{ updateQuery }">
<registry-search
:filter="filter"
v-if="mountRegistrySearch"
:filter="filters"
:sorting="sorting"
:tokens="$options.tokens"
:sortable-fields="sortableFields"
@sorting:changed="updateSorting"
@filter:changed="setFilter"
@filter:submit="$emit('update')"
@sorting:changed="updateSortingAndEmitUpdate"
@filter:changed="updateFilters"
@filter:submit="emitUpdate"
@query:changed="updateQuery"
/>
</template>

View File

@ -89,3 +89,7 @@ export const YARN_PACKAGE_MANAGER = 'yarn';
export const PROJECT_PACKAGE_ENDPOINT_TYPE = 'project';
export const INSTANCE_PACKAGE_ENDPOINT_TYPE = 'instance';
export const PROJECT_RESOURCE_TYPE = 'project';
export const GROUP_RESOURCE_TYPE = 'group';
export const LIST_QUERY_DEBOUNCE_TIME = 50;

View File

@ -0,0 +1,4 @@
fragment PackageData on Package {
id
name
}

View File

@ -0,0 +1,27 @@
#import "~/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql"
query getPackages(
$fullPath: ID!
$isGroupPage: Boolean!
$sort: PackageSort
$groupSort: PackageGroupSort
$packageName: String
$packageType: PackageTypeEnum
) {
project(fullPath: $fullPath) @skip(if: $isGroupPage) {
packages(sort: $sort, packageName: $packageName, packageType: $packageType) {
count
nodes {
...PackageData
}
}
}
group(fullPath: $fullPath) @include(if: $isGroupPage) {
packages(sort: $groupSort, packageName: $packageName, packageType: $packageType) {
count
nodes {
...PackageData
}
}
}
}

View File

@ -1,5 +1,6 @@
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import { apolloProvider } from '~/packages_and_registries/package_registry/graphql/index';
import PackagesListApp from '../components/list/app.vue';
Vue.use(Translate);
@ -7,10 +8,14 @@ Vue.use(Translate);
export default () => {
const el = document.getElementById('js-vue-packages-list');
const isGroupPage = el.dataset.pageType === 'groups';
return new Vue({
el,
apolloProvider,
provide: {
...el.dataset,
isGroupPage,
},
render(createElement) {
return createElement(PackagesListApp);

View File

@ -1 +1,2 @@
export const FILTERED_SEARCH_TERM = 'filtered-search-term';
export const FILTERED_SEARCH_TYPE = 'type';

View File

@ -60,6 +60,10 @@ module IssueResolverArguments
argument :my_reaction_emoji, GraphQL::Types::String,
required: false,
description: 'Filter by reaction emoji applied by the current user. Wildcard values "NONE" and "ANY" are supported.'
argument :confidential,
GraphQL::Types::Boolean,
required: false,
description: 'Filter for confidential issues. If "false", excludes confidential issues. If "true", returns only confidential issues.'
argument :not, Types::Issues::NegatedIssueFilterInputType,
description: 'Negated arguments.',
prepare: ->(negated_args, ctx) { negated_args.to_h },

View File

@ -41,6 +41,7 @@ module PackagesHelper
def packages_list_data(type, resource)
{
resource_id: resource.id,
full_path: resource.full_path,
page_type: type,
empty_list_help_url: help_page_path('user/packages/package_registry/index'),
empty_list_illustration: image_path('illustrations/no-packages.svg'),

View File

@ -27,6 +27,7 @@ module Enums
no_matching_runner: 18, # not used anymore, but cannot be deleted because of old data
trace_size_exceeded: 19,
builds_disabled: 20,
environment_creation_failure: 21,
insufficient_bridge_permissions: 1_001,
downstream_bridge_project_not_found: 1_002,
invalid_bridge_trigger: 1_003,

View File

@ -41,7 +41,7 @@ module Routable
has_one :route, as: :source, autosave: true, dependent: :destroy, inverse_of: :source # rubocop:disable Cop/ActiveRecordDependent
has_many :redirect_routes, as: :source, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
validates :route, presence: true
validates :route, presence: true, unless: -> { is_a?(Namespaces::ProjectNamespace) }
scope :with_route, -> { includes(:route) }
@ -185,6 +185,7 @@ module Routable
def prepare_route
return unless full_path_changed? || full_name_changed?
return if is_a?(Namespaces::ProjectNamespace)
route || build_route(source: self)
route.path = build_full_path

View File

@ -105,7 +105,7 @@ class Namespace < ApplicationRecord
# Legacy Storage specific hooks
after_update :move_dir, if: :saved_change_to_path_or_parent?
after_update :move_dir, if: :saved_change_to_path_or_parent?, unless: -> { is_a?(Namespaces::ProjectNamespace) }
before_destroy(prepend: true) { prepare_for_destroy }
after_destroy :rm_dir
after_commit :expire_child_caches, on: :update, if: -> {

View File

@ -98,6 +98,7 @@ class Project < ApplicationRecord
before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? }
before_save :ensure_runners_token
before_save :ensure_project_namespace_in_sync
after_save :update_project_statistics, if: :saved_change_to_namespace_id?
@ -146,7 +147,7 @@ class Project < ApplicationRecord
belongs_to :namespace
# Sync deletion via DB Trigger to ensure we do not have
# a project without a project_namespace (or vice-versa)
belongs_to :project_namespace, class_name: 'Namespaces::ProjectNamespace', foreign_key: 'project_namespace_id', inverse_of: :project
belongs_to :project_namespace, autosave: true, class_name: 'Namespaces::ProjectNamespace', foreign_key: 'project_namespace_id', inverse_of: :project
alias_method :parent, :namespace
alias_attribute :parent_id, :namespace_id
@ -2883,6 +2884,15 @@ class Project < ApplicationRecord
def online_runners_with_tags
@online_runners_with_tags ||= active_runners.with_tags.online
end
def ensure_project_namespace_in_sync
if changes.keys & [:name, :path, :namespace_id, :visibility_level] && project_namespace.present?
project_namespace.name = name
project_namespace.path = path
project_namespace.parent = namespace
project_namespace.visibility_level = visibility_level
end
end
end
Project.prepend_mod_with('Project')

View File

@ -18,6 +18,8 @@ class Upload < ApplicationRecord
before_save :calculate_checksum!, if: :foreground_checksummable?
after_commit :schedule_checksum, if: :needs_checksum?
after_commit :update_project_statistics, on: [:create, :destroy], if: :project?
# as the FileUploader is not mounted, the default CarrierWave ActiveRecord
# hooks are not executed and the file will not be deleted
after_destroy :delete_file!, if: -> { uploader_class <= FileUploader }
@ -161,6 +163,14 @@ class Upload < ApplicationRecord
def mount_point
super&.to_sym
end
def project?
model_type == "Project"
end
def update_project_statistics
ProjectCacheWorker.perform_async(model_id, [], [:uploads_size])
end
end
Upload.prepend_mod_with('Upload')

View File

@ -28,7 +28,12 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
ci_quota_exceeded: 'No more CI minutes available',
no_matching_runner: 'No matching runner available',
trace_size_exceeded: 'The job log size limit was reached',
builds_disabled: 'The CI/CD is disabled for this project'
builds_disabled: 'The CI/CD is disabled for this project',
environment_creation_failure: 'This job could not be executed because it would create an environment with an invalid parameter.'
}.freeze
TROUBLESHOOTING_DOC = {
environment_creation_failure: { path: 'ci/environments/index', anchor: 'a-deployment-job-failed-with-this-job-could-not-be-executed-because-it-would-create-an-environment-with-an-invalid-parameter-error' }
}.freeze
private_constant :CALLOUT_FAILURE_MESSAGES
@ -40,7 +45,19 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
end
def callout_failure_message
self.class.callout_failure_messages.fetch(failure_reason.to_sym)
message = self.class.callout_failure_messages.fetch(failure_reason.to_sym)
if doc = TROUBLESHOOTING_DOC[failure_reason.to_sym]
message += " #{help_page_link(doc[:path], doc[:anchor])}"
end
message
end
private
def help_page_link(path, anchor)
ActionController::Base.helpers.link_to('How do I fix it?', help_page_path(path, anchor: anchor))
end
end

View File

@ -140,6 +140,10 @@ module Groups
# these records again.
@updated_project_ids = projects_to_update.pluck(:id)
Namespaces::ProjectNamespace
.where(id: projects_to_update.select(:project_namespace_id))
.update_all(visibility_level: @new_parent_group.visibility_level)
projects_to_update
.update_all(visibility_level: @new_parent_group.visibility_level)
end

View File

@ -0,0 +1,8 @@
---
name: surface_environment_creation_failure
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69537
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/340169
milestone: '14.4'
type: development
group: group::release
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: surface_environment_creation_failure_override
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69537
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/340169
milestone: '14.4'
type: development
group: group::release
default_enabled: false

View File

@ -0,0 +1,25 @@
---
key_path: redis_hll_counters.ci_templates.p_ci_templates_implicit_jobs_build_monthly
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
value_type: number
status: active
milestone: "14.4"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71157
time_frame: 7d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
options:
events:
- p_ci_templates_implicit_jobs_build

View File

@ -0,0 +1,25 @@
---
key_path: redis_hll_counters.ci_templates.p_ci_templates_implicit_jobs_deploy_latest_monthly
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
value_type: number
status: active
milestone: "14.4"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71157
time_frame: 7d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
options:
events:
- p_ci_templates_implicit_jobs_deploy_latest

View File

@ -0,0 +1,25 @@
---
key_path: redis_hll_counters.ci_templates.p_ci_templates_implicit_jobs_deploy_monthly
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
value_type: number
status: active
milestone: "14.4"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71157
time_frame: 7d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
options:
events:
- p_ci_templates_implicit_jobs_deploy

View File

@ -0,0 +1,25 @@
---
key_path: redis_hll_counters.ci_templates.p_ci_templates_implicit_jobs_build_weekly
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
value_type: number
status: active
milestone: "14.4"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71157
time_frame: 7d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
options:
events:
- p_ci_templates_implicit_jobs_build

View File

@ -0,0 +1,25 @@
---
key_path: redis_hll_counters.ci_templates.p_ci_templates_implicit_jobs_deploy_latest_weekly
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
value_type: number
status: active
milestone: "14.4"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71157
time_frame: 7d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
options:
events:
- p_ci_templates_implicit_jobs_deploy_latest

View File

@ -0,0 +1,25 @@
---
key_path: redis_hll_counters.ci_templates.p_ci_templates_implicit_jobs_deploy_weekly
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
value_type: number
status: active
milestone: "14.4"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71157
time_frame: 7d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
options:
events:
- p_ci_templates_implicit_jobs_deploy

View File

@ -10303,6 +10303,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="groupissuesauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author of the issue. |
| <a id="groupissuesclosedafter"></a>`closedAfter` | [`Time`](#time) | Issues closed after this date. |
| <a id="groupissuesclosedbefore"></a>`closedBefore` | [`Time`](#time) | Issues closed before this date. |
| <a id="groupissuesconfidential"></a>`confidential` | [`Boolean`](#boolean) | Filter for confidential issues. If "false", excludes confidential issues. If "true", returns only confidential issues. |
| <a id="groupissuescreatedafter"></a>`createdAfter` | [`Time`](#time) | Issues created after this date. |
| <a id="groupissuescreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before this date. |
| <a id="groupissuesepicid"></a>`epicId` | [`String`](#string) | ID of an epic associated with the issues, "none" and "any" values are supported. |
@ -12738,6 +12739,7 @@ Returns [`Issue`](#issue).
| <a id="projectissueauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author of the issue. |
| <a id="projectissueclosedafter"></a>`closedAfter` | [`Time`](#time) | Issues closed after this date. |
| <a id="projectissueclosedbefore"></a>`closedBefore` | [`Time`](#time) | Issues closed before this date. |
| <a id="projectissueconfidential"></a>`confidential` | [`Boolean`](#boolean) | Filter for confidential issues. If "false", excludes confidential issues. If "true", returns only confidential issues. |
| <a id="projectissuecreatedafter"></a>`createdAfter` | [`Time`](#time) | Issues created after this date. |
| <a id="projectissuecreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before this date. |
| <a id="projectissueepicid"></a>`epicId` | [`String`](#string) | ID of an epic associated with the issues, "none" and "any" values are supported. |
@ -12774,6 +12776,7 @@ Returns [`IssueStatusCountsType`](#issuestatuscountstype).
| <a id="projectissuestatuscountsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author of the issue. |
| <a id="projectissuestatuscountsclosedafter"></a>`closedAfter` | [`Time`](#time) | Issues closed after this date. |
| <a id="projectissuestatuscountsclosedbefore"></a>`closedBefore` | [`Time`](#time) | Issues closed before this date. |
| <a id="projectissuestatuscountsconfidential"></a>`confidential` | [`Boolean`](#boolean) | Filter for confidential issues. If "false", excludes confidential issues. If "true", returns only confidential issues. |
| <a id="projectissuestatuscountscreatedafter"></a>`createdAfter` | [`Time`](#time) | Issues created after this date. |
| <a id="projectissuestatuscountscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before this date. |
| <a id="projectissuestatuscountsiid"></a>`iid` | [`String`](#string) | IID of the issue. For example, "1". |
@ -12808,6 +12811,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="projectissuesauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author of the issue. |
| <a id="projectissuesclosedafter"></a>`closedAfter` | [`Time`](#time) | Issues closed after this date. |
| <a id="projectissuesclosedbefore"></a>`closedBefore` | [`Time`](#time) | Issues closed before this date. |
| <a id="projectissuesconfidential"></a>`confidential` | [`Boolean`](#boolean) | Filter for confidential issues. If "false", excludes confidential issues. If "true", returns only confidential issues. |
| <a id="projectissuescreatedafter"></a>`createdAfter` | [`Time`](#time) | Issues created after this date. |
| <a id="projectissuescreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before this date. |
| <a id="projectissuesepicid"></a>`epicId` | [`String`](#string) | ID of an epic associated with the issues, "none" and "any" values are supported. |

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -35,7 +35,7 @@ To view a list of environments and deployments:
1. On the left sidebar, select **Deployments > Environments**.
The environments are displayed.
![Environments list](img/environments_list.png)
![Environments list](img/environments_list_v14_3.png)
1. To view a list of deployments for an environment, select the environment name,
for example, `staging`.
@ -646,9 +646,9 @@ Web terminals:
- Are available to project Maintainers and Owners only.
- Must [be enabled](../../administration/integration/terminal.md).
In the UI, you can view the Web terminal by selecting a **Terminal** button:
In the UI, you can view the Web terminal by selecting **Terminal** from the actions menu:
![Terminal button on environment index](img/environments_terminal_button_on_index_v13_10.png)
![Terminal button on environment index](img/environments_terminal_button_on_index_v14_3.png)
You can also access the terminal button from the page for a specific environment:
@ -816,3 +816,41 @@ To ensure the `action: stop` can always run when needed, you can:
action: stop
when: manual
```
### A deployment job failed with "This job could not be executed because it would create an environment with an invalid parameter" error
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/21182) in GitLab 14.4.
FLAG:
On self-managed GitLab, by default this bug fix is not available. To make it available per project or for your entire instance, ask an administrator to [enable the `surface_environment_creation_failure` flag](../../administration/feature_flags.md). On GitLab.com, this bug fix is not available, but will be rolled out shortly.
If your project is configured to [create a dynamic environment](#create-a-dynamic-environment),
such as a [Review App](../review_apps/index.md), you might encounter this error
because the dynamically generated parameter is invalid for the system.
For example, if you have the following in your `.gitlab-ci.yml`:
```yaml
review:
script: deploy review app
environment: review/$CI_COMMIT_REF_NAME
```
When you create a new merge request with a branch name `bug-fix!`,
the `review` job tries to create an environment with `review/bug-fix!`.
However, the `!` is an invalid character for environments, so the
deployment job fails since it was about to run without an environment.
To fix this, you can:
- Re-create your feature branch without the invalid characters,
such as `bug-fix`.
- Replace the `CI_COMMIT_REF_NAME`
[predefined variable](../variables/predefined_variables.md) with
`CI_COMMIT_REF_SLUG` which strips any invalid characters:
```yaml
review:
script: deploy review app
environment: review/$CI_COMMIT_REF_SLUG
```

View File

@ -252,7 +252,7 @@ graph RL;
2_1-1 & 2_1-2 & 2_1-3 & 2_1-4 --> 1-6;
end
2_2-2["rspec frontend_fixture/rspec-ee frontend_fixture (7 minutes)"];
2_2-2["rspec-all frontend_fixture (7 minutes)"];
class 2_2-2 criticalPath;
click 2_2-2 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=7910143&udv=0"
2_2-4["memory-on-boot (3.5 minutes)"];
@ -284,7 +284,7 @@ graph RL;
3_1-1["jest (14.5 minutes)"];
class 3_1-1 criticalPath;
click 3_1-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914204&udv=0"
subgraph "Needs `rspec frontend_fixture/rspec-ee frontend_fixture`";
subgraph "Needs `rspec-all frontend_fixture`";
3_1-1 --> 2_2-2;
end
@ -355,7 +355,7 @@ graph RL;
2_1-1 & 2_1-2 & 2_1-3 & 2_1-4 --> 1-6;
end
2_2-2["rspec frontend_fixture/rspec-ee frontend_fixture (7 minutes)"];
2_2-2["rspec-all frontend_fixture (7 minutes)"];
class 2_2-2 criticalPath;
click 2_2-2 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=7910143&udv=0"
2_2-4["memory-on-boot (3.5 minutes)"];
@ -395,7 +395,7 @@ graph RL;
3_1-1["jest (14.5 minutes)"];
class 3_1-1 criticalPath;
click 3_1-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914204&udv=0"
subgraph "Needs `rspec frontend_fixture/rspec-ee frontend_fixture`";
subgraph "Needs `rspec-all frontend_fixture`";
3_1-1 --> 2_2-2;
end

View File

@ -62,12 +62,10 @@ panel_groups:
query_range: 'http_requests_total'
label: '# of Requests'
unit: 'count'
metrics:
- id: anomaly_requests_upper_limit
query_range: 10000
label: 'Max # of requests'
unit: 'count'
metrics:
- id: anomaly_requests_lower_limit
query_range: 2000
label: 'Min # of requests'

View File

@ -8,7 +8,9 @@ info: To determine the technical writer assigned to the Stage/Group associated w
## Query variables
Variables can be specified using double curly braces, such as `"{{ci_environment_slug}}"` ([added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20793) in GitLab 12.7).
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20793) in GitLab 12.7.
Variables can be specified using double curly braces, such as `"{{ci_environment_slug}}"`.
Support for the `"%{ci_environment_slug}"` format was
[removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31581) in GitLab 13.0.
@ -66,5 +68,5 @@ avg(sum(container_memory_usage_bytes{container_name!="{{pod}}"}) by (job)) witho
The URL for this query would be:
```plaintext
http://gitlab.com/<user>/<project>/-/environments/<environment_id>/metrics?dashboard=.gitlab%2Fdashboards%2Fcustom.yml&pod=POD
https://gitlab.com/<user>/<project>/-/environments/<environment_id>/metrics?dashboard=.gitlab%2Fdashboards%2Fcustom.yml&pod=POD
```

View File

@ -14,7 +14,7 @@ Dashboards have several components:
The following tables outline the details of expected properties.
## **Dashboard (top-level) properties**
## Dashboard (top-level) properties
| Property | Type | Required | Description |
| ------ | ------ | ------ | ------ |
@ -23,7 +23,7 @@ The following tables outline the details of expected properties.
| `templating` | hash | no | Top level key under which templating related options can be added. |
| `links` | array | no | Add links to display on the dashboard. |
## **Templating (`templating`) properties**
## Templating (`templating`) properties
| Property | Type | Required | Description |
| -------- | ---- | -------- | ----------- |
@ -31,7 +31,7 @@ The following tables outline the details of expected properties.
Read the documentation on [templating](templating_variables.md).
## **Links (`links`) properties**
## Links (`links`) properties
| Property | Type | Required | Description |
| -------- | ---- | -------- | ----------- |
@ -41,7 +41,7 @@ Read the documentation on [templating](templating_variables.md).
Read the documentation on [links](index.md#add-related-links-to-custom-dashboards).
## **Panel group (`panel_groups`) properties**
## Panel group (`panel_groups`) properties
Dashboards display panel groups in the order they are listed in the dashboard YAML file.
@ -55,7 +55,7 @@ is no longer used.
Panels in a panel group are laid out in rows consisting of two panels per row. An exception to this rule are single panels on a row: these panels take the full width of their containing row.
## **Panel (`panels`) properties**
## Panel (`panels`) properties
Dashboards display panels in the order they are listed in the dashboard YAML file.
@ -72,7 +72,7 @@ is no longer used.
| `metrics` | array | yes | The metrics which should be displayed in the panel. Any number of metrics can be displayed when `type` is `area-chart` or `line-chart`, whereas only 3 can be displayed when `type` is `anomaly-chart`. |
| `links` | array | no | Add links to display on the chart's [context menu](index.md#chart-context-menu). |
## **Axis (`panels[].y_axis`) properties**
## Axis (`panels[].y_axis`) properties
| Property | Type | Required | Description |
| ----------- | ------ | ----------------------------- | -------------------------------------------------------------------- |
@ -80,7 +80,7 @@ is no longer used.
| `format` | string | no, defaults to `engineering` | Unit format used. See the [full list of units](yaml_number_format.md). |
| `precision` | number | no, defaults to `2` | Number of decimal places to display in the number. | |
## **Metrics (`metrics`) properties**
## Metrics (`metrics`) properties
| Property | Type | Required | Description |
| ------ | ------ | ------ | ------ |

View File

@ -106,10 +106,15 @@ module Gitlab
environment = Seed::Environment.new(build).to_resource
# If there is a validation error on environment creation, such as
# the name contains invalid character, the build falls back to a
# non-environment job.
unless environment.persisted?
if Feature.enabled?(:surface_environment_creation_failure, build.project, default_enabled: :yaml) &&
Feature.disabled?(:surface_environment_creation_failure_override, build.project)
return { status: :failed, failure_reason: :environment_creation_failure }
end
# If there is a validation error on environment creation, such as
# the name contains invalid character, the build falls back to a
# non-environment job.
Gitlab::ErrorTracking.track_exception(
EnvironmentCreationFailure.new,
project_id: build.project_id,

View File

@ -33,7 +33,8 @@ module Gitlab
ci_quota_exceeded: 'no more CI minutes available',
no_matching_runner: 'no matching runner available',
trace_size_exceeded: 'log size limit exceeded',
builds_disabled: 'project builds are disabled'
builds_disabled: 'project builds are disabled',
environment_creation_failure: 'environment creation failure'
}.freeze
private_constant :REASONS

View File

@ -30,6 +30,10 @@ module Gitlab
end
end
def primary_only?
@primary_only
end
def disconnect!(timeout: 120)
host_list.hosts.each { |host| host.disconnect!(timeout: timeout) }
end
@ -151,6 +155,17 @@ module Gitlab
# Yields a block, retrying it upon error using an exponential backoff.
def retry_with_backoff(retries = 3, time = 2)
# In CI we only use the primary, but databases may not always be
# available (or take a few seconds to become available). Retrying in
# this case can slow down CI jobs. In addition, retrying with _only_
# a primary being present isn't all that helpful.
#
# To prevent this from happening, we don't make any attempt at
# retrying unless one or more replicas are used. This matches the
# behaviour from before we enabled load balancing code even if no
# replicas were configured.
return yield if primary_only?
retried = 0
last_error = nil
@ -176,6 +191,11 @@ module Gitlab
def connection_error?(error)
case error
when ActiveRecord::NoDatabaseError
# Retrying this error isn't going to magically make the database
# appear. It also slows down CI jobs that are meant to create the
# database in the first place.
false
when ActiveRecord::StatementInvalid, ActionView::Template::Error
# After connecting to the DB Rails will wrap query errors using this
# class.

View File

@ -5,23 +5,14 @@ module Gitlab::UsageDataCounters
REDIS_SLOT = 'ci_templates'
KNOWN_EVENTS_FILE_PATH = File.expand_path('known_events/ci_templates.yml', __dir__)
# NOTE: Events originating from implicit Auto DevOps pipelines get prefixed with `implicit_`
TEMPLATE_TO_EVENT = {
'5-Minute-Production-App.gitlab-ci.yml' => '5_min_production_app',
'Auto-DevOps.gitlab-ci.yml' => 'auto_devops',
'AWS/CF-Provision-and-Deploy-EC2.gitlab-ci.yml' => 'aws_cf_deploy_ec2',
'AWS/Deploy-ECS.gitlab-ci.yml' => 'aws_deploy_ecs',
'Jobs/Build.gitlab-ci.yml' => 'auto_devops_build',
'Jobs/Deploy.gitlab-ci.yml' => 'auto_devops_deploy',
'Jobs/Deploy.latest.gitlab-ci.yml' => 'auto_devops_deploy_latest',
'Security/SAST.gitlab-ci.yml' => 'security_sast',
'Security/Secret-Detection.gitlab-ci.yml' => 'security_secret_detection',
'Terraform/Base.latest.gitlab-ci.yml' => 'terraform_base_latest'
}.freeze
class << self
def track_unique_project_event(project_id:, template:, config_source:)
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(ci_template_event_name(template, config_source), values: project_id)
expanded_template_name = expand_template_name(template)
return unless expanded_template_name
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(
ci_template_event_name(expanded_template_name, config_source), values: project_id
)
end
def ci_templates(relative_base = 'lib/gitlab/ci/templates')
@ -30,9 +21,12 @@ module Gitlab::UsageDataCounters
def ci_template_event_name(template_name, config_source)
prefix = 'implicit_' if config_source.to_s == 'auto_devops_source'
template_event_name = TEMPLATE_TO_EVENT[template_name] || template_to_event_name(template_name)
"p_#{REDIS_SLOT}_#{prefix}#{template_event_name}"
"p_#{REDIS_SLOT}_#{prefix}#{template_to_event_name(template_name)}"
end
def expand_template_name(template_name)
Gitlab::Template::GitlabCiYmlTemplate.find(template_name.chomp('.gitlab-ci.yml'))&.full_name
end
private

View File

@ -1,44 +1,8 @@
# Implicit Auto DevOps pipeline events
- name: p_ci_templates_implicit_auto_devops
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
# Explicit include:template pipeline events
- name: p_ci_templates_5_min_production_app
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
- name: p_ci_templates_aws_cf_deploy_ec2
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
- name: p_ci_templates_auto_devops_build
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
- name: p_ci_templates_auto_devops_deploy
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
- name: p_ci_templates_auto_devops_deploy_latest
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
# This part of the file is generated automatically by
# This file is generated automatically by
# bin/rake gitlab:usage_data:generate_ci_template_events
#
# Do not edit it manually!
#
# The section above this should be removed once we roll out tracking all ci
# templates
# https://gitlab.com/gitlab-org/gitlab/-/issues/339684
---
- name: p_ci_templates_terraform_base_latest
category: ci_templates
redis_slot: ci_templates
@ -463,6 +427,10 @@
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
- name: p_ci_templates_implicit_auto_devops
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
- name: p_ci_templates_implicit_jobs_dast_default_branch_deploy
category: ci_templates
redis_slot: ci_templates
@ -499,11 +467,11 @@
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
- name: p_ci_templates_implicit_auto_devops_deploy
- name: p_ci_templates_implicit_jobs_deploy
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
- name: p_ci_templates_implicit_auto_devops_build
- name: p_ci_templates_implicit_jobs_build
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
@ -515,7 +483,7 @@
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
- name: p_ci_templates_implicit_auto_devops_deploy_latest
- name: p_ci_templates_implicit_jobs_deploy_latest
category: ci_templates
redis_slot: ci_templates
aggregation: weekly

View File

@ -41,20 +41,32 @@ namespace :gitlab do
repository_includes = ci_template_includes_hash(:repository_source)
auto_devops_jobs_includes = ci_template_includes_hash(:auto_devops_source, 'Jobs')
auto_devops_security_includes = ci_template_includes_hash(:auto_devops_source, 'Security')
all_includes = [*repository_includes, *auto_devops_jobs_includes, *auto_devops_security_includes]
all_includes = [
*repository_includes,
ci_template_event('p_ci_templates_implicit_auto_devops'),
*auto_devops_jobs_includes,
*auto_devops_security_includes
]
File.write(Gitlab::UsageDataCounters::CiTemplateUniqueCounter::KNOWN_EVENTS_FILE_PATH, banner + YAML.dump(all_includes).gsub(/ *$/m, ''))
end
def ci_template_includes_hash(source, template_directory = nil)
Gitlab::UsageDataCounters::CiTemplateUniqueCounter.ci_templates("lib/gitlab/ci/templates/#{template_directory}").map do |template|
{
'name' => Gitlab::UsageDataCounters::CiTemplateUniqueCounter.ci_template_event_name("#{template_directory}/#{template}", source),
'category' => 'ci_templates',
'redis_slot' => Gitlab::UsageDataCounters::CiTemplateUniqueCounter::REDIS_SLOT,
'aggregation' => 'weekly'
}
expanded_template_name = Gitlab::UsageDataCounters::CiTemplateUniqueCounter.expand_template_name("#{template_directory}/#{template}")
event_name = Gitlab::UsageDataCounters::CiTemplateUniqueCounter.ci_template_event_name(expanded_template_name, source)
ci_template_event(event_name)
end
end
def ci_template_event(event_name)
{
'name' => event_name,
'category' => 'ci_templates',
'redis_slot' => Gitlab::UsageDataCounters::CiTemplateUniqueCounter::REDIS_SLOT,
'aggregation' => 'weekly'
}
end
end
end

View File

@ -13060,6 +13060,9 @@ msgstr ""
msgid "Environments|Note that this action will stop the environment, but it will %{emphasisStart}not%{emphasisEnd} have an effect on any existing deployment due to no “stop environment action” being defined in the %{ciConfigLinkStart}.gitlab-ci.yml%{ciConfigLinkEnd} file."
msgstr ""
msgid "Environments|Open"
msgstr ""
msgid "Environments|Open live environment"
msgstr ""
@ -25726,6 +25729,9 @@ msgstr ""
msgid "Prevent adding new members to project membership within this group"
msgstr ""
msgid "Prevent auto-stopping"
msgstr ""
msgid "Prevent editing approval rules in projects and merge requests."
msgstr ""

View File

@ -109,14 +109,18 @@ function rspec_paralellized_job() {
local test_level="${job_name[1]}"
local report_name=$(echo "${CI_JOB_NAME}" | sed -E 's|[/ ]|_|g') # e.g. 'rspec unit pg12 1/24' would become 'rspec_unit_pg12_1_24'
local rspec_opts="${1}"
local spec_folder_prefix=""
local spec_folder_prefixes=""
if [[ "${test_tool}" =~ "-ee" ]]; then
spec_folder_prefix="ee/"
spec_folder_prefixes="'ee/'"
fi
if [[ "${test_tool}" =~ "-jh" ]]; then
spec_folder_prefix="jh/"
spec_folder_prefixes="'jh/'"
fi
if [[ "${test_tool}" =~ "-all" ]]; then
spec_folder_prefixes="['', 'ee/']"
fi
export KNAPSACK_LOG_LEVEL="debug"
@ -131,7 +135,7 @@ function rspec_paralellized_job() {
cp "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}" "${KNAPSACK_REPORT_PATH}"
if [[ -z "${KNAPSACK_TEST_FILE_PATTERN}" ]]; then
pattern=$(ruby -r./tooling/quality/test_level.rb -e "puts Quality::TestLevel.new(%(${spec_folder_prefix})).pattern(:${test_level})")
pattern=$(ruby -r./tooling/quality/test_level.rb -e "puts Quality::TestLevel.new(${spec_folder_prefixes}).pattern(:${test_level})")
export KNAPSACK_TEST_FILE_PATTERN="${pattern}"
fi

View File

@ -3,10 +3,11 @@
FactoryBot.define do
factory :project_namespace, class: 'Namespaces::ProjectNamespace' do
project
parent { project.namespace }
visibility_level { project.visibility_level }
name { project.name }
path { project.path }
type { Namespaces::ProjectNamespace.sti_name }
owner { nil }
parent factory: :group
end
end

View File

@ -226,6 +226,7 @@ RSpec.describe 'Environments page', :js do
end
it 'does not show terminal button' do
expect(page).not_to have_button(_('More actions'))
expect(page).not_to have_terminal_button
end
@ -273,6 +274,7 @@ RSpec.describe 'Environments page', :js do
let(:role) { :maintainer }
it 'shows the terminal button' do
click_button(_('More actions'))
expect(page).to have_terminal_button
end
end
@ -281,6 +283,7 @@ RSpec.describe 'Environments page', :js do
let(:role) { :developer }
it 'does not show terminal button' do
expect(page).not_to have_button(_('More actions'))
expect(page).not_to have_terminal_button
end
end
@ -515,7 +518,7 @@ RSpec.describe 'Environments page', :js do
end
def have_terminal_button
have_link(nil, href: terminal_project_environment_path(project, environment))
have_link(_('Terminal'), href: terminal_project_environment_path(project, environment))
end
def visit_environments(project, **opts)

View File

@ -1,4 +1,4 @@
import { GlButton } from '@gitlab/ui';
import { GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import DeleteComponent from '~/environments/components/environment_delete.vue';
@ -15,7 +15,7 @@ describe('External URL Component', () => {
});
};
const findButton = () => wrapper.find(GlButton);
const findDropdownItem = () => wrapper.find(GlDropdownItem);
beforeEach(() => {
jest.spyOn(window, 'confirm');
@ -23,14 +23,15 @@ describe('External URL Component', () => {
createWrapper();
});
it('should render a button to delete the environment', () => {
expect(findButton().exists()).toBe(true);
expect(wrapper.attributes('title')).toEqual('Delete environment');
it('should render a dropdown item to delete the environment', () => {
expect(findDropdownItem().exists()).toBe(true);
expect(wrapper.text()).toEqual('Delete environment');
expect(findDropdownItem().attributes('variant')).toBe('danger');
});
it('emits requestDeleteEnvironment in the event hub when button is clicked', () => {
jest.spyOn(eventHub, '$emit');
findButton().vm.$emit('click');
findDropdownItem().vm.$emit('click');
expect(eventHub.$emit).toHaveBeenCalledWith('requestDeleteEnvironment', wrapper.vm.environment);
});
});

View File

@ -1,6 +1,6 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import MonitoringComponent from '~/environments/components/environment_monitoring.vue';
import { __ } from '~/locale';
describe('Monitoring Component', () => {
let wrapper;
@ -8,31 +8,19 @@ describe('Monitoring Component', () => {
const monitoringUrl = 'https://gitlab.com';
const createWrapper = () => {
wrapper = shallowMount(MonitoringComponent, {
wrapper = mountExtended(MonitoringComponent, {
propsData: {
monitoringUrl,
},
});
};
const findButtons = () => wrapper.findAll(GlButton);
const findButtonsByIcon = (icon) =>
findButtons().filter((button) => button.props('icon') === icon);
beforeEach(() => {
createWrapper();
});
describe('computed', () => {
it('title', () => {
expect(wrapper.vm.title).toBe('Monitoring');
});
});
it('should render a link to environment monitoring page', () => {
expect(wrapper.attributes('href')).toEqual(monitoringUrl);
expect(findButtonsByIcon('chart').length).toBe(1);
expect(wrapper.attributes('title')).toBe('Monitoring');
expect(wrapper.attributes('aria-label')).toBe('Monitoring');
const link = wrapper.findByRole('menuitem', { name: __('Monitoring') });
expect(link.attributes('href')).toEqual(monitoringUrl);
});
});

View File

@ -1,4 +1,4 @@
import { GlButton, GlIcon } from '@gitlab/ui';
import { GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import PinComponent from '~/environments/components/environment_pin.vue';
import eventHub from '~/environments/event_hub';
@ -30,15 +30,15 @@ describe('Pin Component', () => {
wrapper.destroy();
});
it('should render the component with thumbtack icon', () => {
expect(wrapper.find(GlIcon).props('name')).toBe('thumbtack');
it('should render the component with descriptive text', () => {
expect(wrapper.text()).toBe('Prevent auto-stopping');
});
it('should emit onPinClick when clicked', () => {
const eventHubSpy = jest.spyOn(eventHub, '$emit');
const button = wrapper.find(GlButton);
const item = wrapper.find(GlDropdownItem);
button.vm.$emit('click');
item.vm.$emit('click');
expect(eventHubSpy).toHaveBeenCalledWith('cancelAutoStop', autoStopUrl);
});

View File

@ -1,5 +1,5 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
import { GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import RollbackComponent from '~/environments/components/environment_rollback.vue';
import eventHub from '~/environments/event_hub';
@ -7,7 +7,7 @@ describe('Rollback Component', () => {
const retryUrl = 'https://gitlab.com/retry';
it('Should render Re-deploy label when isLastDeployment is true', () => {
const wrapper = mount(RollbackComponent, {
const wrapper = shallowMount(RollbackComponent, {
propsData: {
retryUrl,
isLastDeployment: true,
@ -15,11 +15,11 @@ describe('Rollback Component', () => {
},
});
expect(wrapper.element).toHaveSpriteIcon('repeat');
expect(wrapper.text()).toBe('Re-deploy to environment');
});
it('Should render Rollback label when isLastDeployment is false', () => {
const wrapper = mount(RollbackComponent, {
const wrapper = shallowMount(RollbackComponent, {
propsData: {
retryUrl,
isLastDeployment: false,
@ -27,7 +27,7 @@ describe('Rollback Component', () => {
},
});
expect(wrapper.element).toHaveSpriteIcon('redo');
expect(wrapper.text()).toBe('Rollback environment');
});
it('should emit a "rollback" event on button click', () => {
@ -40,7 +40,7 @@ describe('Rollback Component', () => {
},
},
});
const button = wrapper.find(GlButton);
const button = wrapper.find(GlDropdownItem);
button.vm.$emit('click');

View File

@ -1,12 +1,13 @@
import { shallowMount } from '@vue/test-utils';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import TerminalComponent from '~/environments/components/environment_terminal_button.vue';
import { __ } from '~/locale';
describe('Stop Component', () => {
describe('Terminal Component', () => {
let wrapper;
const terminalPath = '/path';
const mountWithProps = (props) => {
wrapper = shallowMount(TerminalComponent, {
wrapper = mountExtended(TerminalComponent, {
propsData: props,
});
};
@ -15,17 +16,9 @@ describe('Stop Component', () => {
mountWithProps({ terminalPath });
});
describe('computed', () => {
it('title', () => {
expect(wrapper.vm.title).toEqual('Terminal');
});
});
it('should render a link to open a web terminal with the provided path', () => {
expect(wrapper.element.tagName).toBe('A');
expect(wrapper.attributes('title')).toBe('Terminal');
expect(wrapper.attributes('aria-label')).toBe('Terminal');
expect(wrapper.attributes('href')).toBe(terminalPath);
const link = wrapper.findByRole('menuitem', { name: __('Terminal') });
expect(link.attributes('href')).toBe(terminalPath);
});
it('should render a non-disabled button', () => {

View File

@ -1,10 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`packages_list_app renders 1`] = `
exports[`PackagesListApp renders 1`] = `
<div>
<package-title-stub
count="0"
count="2"
helpurl="packageHelpUrl"
/>
<package-search-stub />
</div>
`;

View File

@ -1,15 +1,40 @@
import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import PackageListApp from '~/packages_and_registries/package_registry/components/list/app.vue';
import PackageTitle from '~/packages_and_registries/package_registry/components/list/package_title.vue';
import PackageSearch from '~/packages_and_registries/package_registry/components/list/package_search.vue';
import * as packageUtils from '~/packages_and_registries/shared/utils';
import {
PROJECT_RESOURCE_TYPE,
GROUP_RESOURCE_TYPE,
LIST_QUERY_DEBOUNCE_TIME,
} from '~/packages_and_registries/package_registry/constants';
import getPackagesQuery from '~/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql';
import { packagesListQuery } from '../../mock_data';
jest.mock('~/lib/utils/common_utils');
jest.mock('~/flash');
describe('packages_list_app', () => {
const localVue = createLocalVue();
describe('PackagesListApp', () => {
let wrapper;
let apolloProvider;
const defaultProvide = {
packageHelpUrl: 'packageHelpUrl',
emptyListIllustration: 'emptyListIllustration',
emptyListHelpUrl: 'emptyListHelpUrl',
isGroupPage: true,
fullPath: 'gitlab-org',
};
const PackageList = {
name: 'package-list',
@ -18,9 +43,21 @@ describe('packages_list_app', () => {
const GlLoadingIcon = { name: 'gl-loading-icon', template: '<div>loading</div>' };
const findPackageTitle = () => wrapper.findComponent(PackageTitle);
const findSearch = () => wrapper.findComponent(PackageSearch);
const mountComponent = ({
resolver = jest.fn().mockResolvedValue(packagesListQuery()),
provide = defaultProvide,
} = {}) => {
localVue.use(VueApollo);
const requestHandlers = [[getPackagesQuery, resolver]];
apolloProvider = createMockApollo(requestHandlers);
const mountComponent = () => {
wrapper = shallowMountExtended(PackageListApp, {
localVue,
apolloProvider,
provide,
stubs: {
GlEmptyState,
GlLoadingIcon,
@ -28,30 +65,90 @@ describe('packages_list_app', () => {
GlSprintf,
GlLink,
},
provide: {
packageHelpUrl: 'packageHelpUrl',
emptyListIllustration: 'emptyListIllustration',
emptyListHelpUrl: 'emptyListHelpUrl',
},
});
};
beforeEach(() => {
jest.spyOn(packageUtils, 'getQueryParams').mockReturnValue({});
});
afterEach(() => {
wrapper.destroy();
});
it('renders', () => {
const waitForDebouncedApollo = () => {
jest.advanceTimersByTime(LIST_QUERY_DEBOUNCE_TIME);
return waitForPromises();
};
it('renders', async () => {
mountComponent();
await waitForDebouncedApollo();
expect(wrapper.element).toMatchSnapshot();
});
it('has a package title', () => {
it('has a package title', async () => {
mountComponent();
await waitForDebouncedApollo();
expect(findPackageTitle().exists()).toBe(true);
expect(findPackageTitle().props('count')).toBe(2);
});
describe('search component', () => {
it('exists', () => {
mountComponent();
expect(findSearch().exists()).toBe(true);
});
it('on update triggers a new query with updated values', async () => {
const resolver = jest.fn().mockResolvedValue(packagesListQuery());
mountComponent({ resolver });
const payload = {
sort: 'VERSION_DESC',
filters: { packageName: 'foo', packageType: 'CONAN' },
};
findSearch().vm.$emit('update', payload);
await waitForDebouncedApollo();
jest.advanceTimersByTime(LIST_QUERY_DEBOUNCE_TIME);
expect(resolver).toHaveBeenCalledWith(
expect.objectContaining({
groupSort: payload.sort,
...payload.filters,
}),
);
});
});
describe.each`
type | sortType
${PROJECT_RESOURCE_TYPE} | ${'sort'}
${GROUP_RESOURCE_TYPE} | ${'groupSort'}
`('$type query', ({ type, sortType }) => {
let provide;
let resolver;
const isGroupPage = type === GROUP_RESOURCE_TYPE;
beforeEach(() => {
provide = { ...defaultProvide, isGroupPage };
resolver = jest.fn().mockResolvedValue(packagesListQuery(type));
mountComponent({ provide, resolver });
return waitForDebouncedApollo();
});
it('succeeds', () => {
expect(findPackageTitle().props('count')).toBe(2);
});
it('calls the resolver with the right parameters', () => {
expect(resolver).toHaveBeenCalledWith(
expect.objectContaining({ isGroupPage, [sortType]: '' }),
);
});
});
});

View File

@ -1,113 +1,61 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { sortableFields } from '~/packages/list/utils';
import component from '~/packages_and_registries/package_registry/components/list/package_search.vue';
import PackageTypeToken from '~/packages_and_registries/package_registry/components/list/tokens/package_type_token.vue';
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
import UrlSync from '~/vue_shared/components/url_sync.vue';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import { getQueryParams, extractFilterAndSorting } from '~/packages_and_registries/shared/utils';
const localVue = createLocalVue();
localVue.use(Vuex);
jest.mock('~/packages_and_registries/shared/utils');
useMockLocationHelper();
describe('Package Search', () => {
let wrapper;
let store;
const defaultQueryParamsMock = {
filters: ['foo'],
sorting: { sort: 'desc' },
};
const findRegistrySearch = () => wrapper.findComponent(RegistrySearch);
const findUrlSync = () => wrapper.findComponent(UrlSync);
const createStore = (isGroupPage) => {
const state = {
config: {
isGroupPage,
},
sorting: {
orderBy: 'version',
sort: 'desc',
},
filter: [],
};
store = new Vuex.Store({
state,
});
store.dispatch = jest.fn();
};
const mountComponent = (isGroupPage = false) => {
createStore(isGroupPage);
wrapper = shallowMount(component, {
localVue,
store,
wrapper = shallowMountExtended(component, {
provide() {
return {
isGroupPage,
};
},
stubs: {
UrlSync,
},
});
};
beforeEach(() => {
extractFilterAndSorting.mockReturnValue(defaultQueryParamsMock);
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('has a registry search component', () => {
it('has a registry search component', async () => {
mountComponent();
await nextTick();
expect(findRegistrySearch().exists()).toBe(true);
expect(findRegistrySearch().props()).toMatchObject({
filter: store.state.filter,
sorting: store.state.sorting,
tokens: expect.arrayContaining([
expect.objectContaining({ token: PackageTypeToken, type: 'type', icon: 'package' }),
]),
sortableFields: sortableFields(),
});
});
it.each`
isGroupPage | page
${false} | ${'project'}
${true} | ${'group'}
`('in a $page page binds the right props', ({ isGroupPage }) => {
mountComponent(isGroupPage);
expect(findRegistrySearch().props()).toMatchObject({
filter: store.state.filter,
sorting: store.state.sorting,
tokens: expect.arrayContaining([
expect.objectContaining({ token: PackageTypeToken, type: 'type', icon: 'package' }),
]),
sortableFields: sortableFields(isGroupPage),
});
});
it('on sorting:changed emits update event and calls vuex setSorting', () => {
const payload = { sort: 'foo' };
it('registry search is mounted after mount', async () => {
mountComponent();
findRegistrySearch().vm.$emit('sorting:changed', payload);
expect(store.dispatch).toHaveBeenCalledWith('setSorting', payload);
expect(wrapper.emitted('update')).toEqual([[]]);
});
it('on filter:changed calls vuex setFilter', () => {
const payload = ['foo'];
mountComponent();
findRegistrySearch().vm.$emit('filter:changed', payload);
expect(store.dispatch).toHaveBeenCalledWith('setFilter', payload);
});
it('on filter:submit emits update event', () => {
mountComponent();
findRegistrySearch().vm.$emit('filter:submit');
expect(wrapper.emitted('update')).toEqual([[]]);
expect(findRegistrySearch().exists()).toBe(false);
});
it('has a UrlSync component', () => {
@ -116,13 +64,102 @@ describe('Package Search', () => {
expect(findUrlSync().exists()).toBe(true);
});
it('on query:changed calls updateQuery from UrlSync', () => {
it.each`
isGroupPage | page
${false} | ${'project'}
${true} | ${'group'}
`('in a $page page binds the right props', async ({ isGroupPage }) => {
mountComponent(isGroupPage);
await nextTick();
expect(findRegistrySearch().props()).toMatchObject({
tokens: expect.arrayContaining([
expect.objectContaining({ token: PackageTypeToken, type: 'type', icon: 'package' }),
]),
sortableFields: sortableFields(isGroupPage),
});
});
it('on sorting:changed emits update event and update internal sort', async () => {
const payload = { sort: 'foo' };
mountComponent();
await nextTick();
findRegistrySearch().vm.$emit('sorting:changed', payload);
await nextTick();
expect(findRegistrySearch().props('sorting')).toEqual({ sort: 'foo', orderBy: 'name' });
// there is always a first call on mounted that emits up default values
expect(wrapper.emitted('update')[1]).toEqual([
{
filters: {
packageName: '',
packageType: undefined,
},
sort: 'NAME_FOO',
},
]);
});
it('on filter:changed updates the filters', async () => {
const payload = ['foo'];
mountComponent();
await nextTick();
findRegistrySearch().vm.$emit('filter:changed', payload);
await nextTick();
expect(findRegistrySearch().props('filter')).toEqual(['foo']);
});
it('on filter:submit emits update event', async () => {
mountComponent();
await nextTick();
findRegistrySearch().vm.$emit('filter:submit');
expect(wrapper.emitted('update')[1]).toEqual([
{
filters: {
packageName: '',
packageType: undefined,
},
sort: 'NAME_DESC',
},
]);
});
it('on query:changed calls updateQuery from UrlSync', async () => {
jest.spyOn(UrlSync.methods, 'updateQuery').mockImplementation(() => {});
mountComponent();
await nextTick();
findRegistrySearch().vm.$emit('query:changed');
expect(UrlSync.methods.updateQuery).toHaveBeenCalled();
});
it('sets the component sorting and filtering based on the querystring', async () => {
mountComponent();
await nextTick();
expect(getQueryParams).toHaveBeenCalled();
expect(findRegistrySearch().props()).toMatchObject({
filter: defaultQueryParamsMock.filters,
sorting: defaultQueryParamsMock.sorting,
});
});
});

View File

@ -249,3 +249,27 @@ export const packageDestroyFileMutationError = () => ({
},
],
});
export const packagesListQuery = (type = 'group') => ({
data: {
[type]: {
packages: {
count: 2,
nodes: [
{
__typename: 'Package',
id: 'gid://gitlab/Packages::Package/247',
name: 'version_test1',
},
{
__typename: 'Package',
id: 'gid://gitlab/Packages::Package/246',
name: 'version_test1',
},
],
__typename: 'PackageConnection',
},
__typename: 'Group',
},
},
});

View File

@ -26,7 +26,14 @@ RSpec.describe Resolvers::IssuesResolver do
expect(described_class).to have_nullable_graphql_type(Types::IssueType.connection_type)
end
shared_context 'filtering for confidential issues' do
let_it_be(:confidential_issue1) { create(:issue, project: project, confidential: true) }
let_it_be(:confidential_issue2) { create(:issue, project: other_project, confidential: true) }
end
context "with a project" do
let(:obj) { project }
before_all do
project.add_developer(current_user)
project.add_reporter(reporter)
@ -222,6 +229,42 @@ RSpec.describe Resolvers::IssuesResolver do
end
end
context 'confidential issues' do
include_context 'filtering for confidential issues'
context "when user is allowed to view confidential issues" do
it 'returns all viewable issues by default' do
expect(resolve_issues).to contain_exactly(issue1, issue2, confidential_issue1)
end
it 'returns only the non-confidential issues for the project when filter is set to false' do
expect(resolve_issues({ confidential: false })).to contain_exactly(issue1, issue2)
end
it "returns only the confidential issues for the project when filter is set to true" do
expect(resolve_issues({ confidential: true })).to contain_exactly(confidential_issue1)
end
end
context "when user is not allowed to see confidential issues" do
before do
project.add_guest(current_user)
end
it 'returns all viewable issues by default' do
expect(resolve_issues).to contain_exactly(issue1, issue2)
end
it 'does not return the confidential issues when filter is set to false' do
expect(resolve_issues({ confidential: false })).to contain_exactly(issue1, issue2)
end
it 'does not return the confidential issues when filter is set to true' do
expect(resolve_issues({ confidential: true })).to be_empty
end
end
end
context 'when searching issues' do
it 'returns correct issues' do
expect(resolve_issues(search: 'foo')).to contain_exactly(issue2)
@ -519,32 +562,72 @@ RSpec.describe Resolvers::IssuesResolver do
end
context "with a group" do
let(:obj) { group }
before do
group.add_developer(current_user)
end
describe '#resolve' do
it 'finds all group issues' do
result = resolve(described_class, obj: group, ctx: { current_user: current_user })
expect(result).to contain_exactly(issue1, issue2, issue3)
expect(resolve_issues).to contain_exactly(issue1, issue2, issue3)
end
it 'returns issues without the specified issue_type' do
result = resolve(described_class, obj: group, ctx: { current_user: current_user }, args: { not: { types: ['issue'] } })
expect(resolve_issues({ not: { types: ['issue'] } })).to contain_exactly(issue1)
end
expect(result).to contain_exactly(issue1)
context "confidential issues" do
include_context 'filtering for confidential issues'
context "when user is allowed to view confidential issues" do
it 'returns all viewable issues by default' do
expect(resolve_issues).to contain_exactly(issue1, issue2, issue3, confidential_issue1, confidential_issue2)
end
context 'filtering for confidential issues' do
it 'returns only the non-confidential issues for the group when filter is set to false' do
expect(resolve_issues({ confidential: false })).to contain_exactly(issue1, issue2, issue3)
end
it "returns only the confidential issues for the group when filter is set to true" do
expect(resolve_issues({ confidential: true })).to contain_exactly(confidential_issue1, confidential_issue2)
end
end
end
context "when user is not allowed to see confidential issues" do
before do
group.add_guest(current_user)
end
it 'returns all viewable issues by default' do
expect(resolve_issues).to contain_exactly(issue1, issue2, issue3)
end
context 'filtering for confidential issues' do
it 'does not return the confidential issues when filter is set to false' do
expect(resolve_issues({ confidential: false })).to contain_exactly(issue1, issue2, issue3)
end
it 'does not return the confidential issues when filter is set to true' do
expect(resolve_issues({ confidential: true })).to be_empty
end
end
end
end
end
end
context "when passing a non existent, batch loaded project" do
let(:project) do
let!(:project) do
BatchLoader::GraphQL.for("non-existent-path").batch do |_fake_paths, loader, _|
loader.call("non-existent-path", nil)
end
end
let(:obj) { project }
it "returns nil without breaking" do
expect(resolve_issues(iids: ["don't", "break"])).to be_empty
end
@ -565,6 +648,6 @@ RSpec.describe Resolvers::IssuesResolver do
end
def resolve_issues(args = {}, context = { current_user: current_user })
resolve(described_class, obj: project, args: args, ctx: context)
resolve(described_class, obj: obj, args: args, ctx: context)
end
end

View File

@ -260,4 +260,34 @@ RSpec.describe PackagesHelper do
end
end
end
describe '#packages_list_data' do
let_it_be(:resource) { project }
let_it_be(:type) { 'project' }
let(:expected_result) do
{
resource_id: resource.id,
full_path: resource.full_path,
page_type: type
}
end
subject(:result) { helper.packages_list_data(type, resource) }
context 'at a project level' do
it 'populates presenter data' do
expect(result).to match(hash_including(expected_result))
end
end
context 'at a group level' do
let_it_be(:resource) { create(:group) }
let_it_be(:type) { 'group' }
it 'populates presenter data' do
expect(result).to match(hash_including(expected_result))
end
end
end
end

View File

@ -440,17 +440,30 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
context 'when the environment name is invalid' do
let(:attributes) { { name: 'deploy', ref: 'master', environment: '!!!' } }
it_behaves_like 'non-deployment job'
it_behaves_like 'ensures environment inexistence'
it 'fails the job with a failure reason and does not create an environment' do
expect(subject).to be_failed
expect(subject).to be_environment_creation_failure
expect(subject.metadata.expanded_environment_name).to be_nil
expect(Environment.exists?(name: expected_environment_name)).to eq(false)
end
it 'tracks an exception' do
expect(Gitlab::ErrorTracking).to receive(:track_exception)
.with(an_instance_of(described_class::EnvironmentCreationFailure),
project_id: project.id,
reason: %q{Name can contain only letters, digits, '-', '_', '/', '$', '{', '}', '.', and spaces, but it cannot start or end with '/'})
.once
context 'when surface_environment_creation_failure feature flag is disabled' do
before do
stub_feature_flags(surface_environment_creation_failure: false)
end
subject
it_behaves_like 'non-deployment job'
it_behaves_like 'ensures environment inexistence'
it 'tracks an exception' do
expect(Gitlab::ErrorTracking).to receive(:track_exception)
.with(an_instance_of(described_class::EnvironmentCreationFailure),
project_id: project.id,
reason: %q{Name can contain only letters, digits, '-', '_', '/', '$', '{', '}', '.', and spaces, but it cannot start or end with '/'})
.once
subject
end
end
end
end

View File

@ -274,6 +274,14 @@ RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store do
expect { lb.retry_with_backoff { raise } }.to raise_error(RuntimeError)
end
it 'skips retries when only the primary is used' do
allow(lb).to receive(:primary_only?).and_return(true)
expect(lb).not_to receive(:sleep)
expect { lb.retry_with_backoff { raise } }.to raise_error(RuntimeError)
end
end
describe '#connection_error?' do
@ -283,6 +291,12 @@ RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store do
expect(lb.connection_error?(error)).to eq(true)
end
it 'returns false for a missing database error' do
error = ActiveRecord::NoDatabaseError.new
expect(lb.connection_error?(error)).to eq(false)
end
it 'returns true for a wrapped connection error' do
wrapped = wrapped_exception(ActiveRecord::StatementInvalid, ActiveRecord::ConnectionNotEstablished)

View File

@ -6,97 +6,62 @@ RSpec.describe Gitlab::UsageDataCounters::CiTemplateUniqueCounter do
describe '.track_unique_project_event' do
using RSpec::Parameterized::TableSyntax
where(:template, :config_source, :expected_event) do
# Implicit Auto DevOps usage
'Auto-DevOps.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_auto_devops'
'Jobs/Build.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_auto_devops_build'
'Jobs/Deploy.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_auto_devops_deploy'
'Security/SAST.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_security_sast'
'Security/Secret-Detection.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_security_secret_detection'
# Explicit include:template usage
'5-Minute-Production-App.gitlab-ci.yml' | :repository_source | 'p_ci_templates_5_min_production_app'
'Auto-DevOps.gitlab-ci.yml' | :repository_source | 'p_ci_templates_auto_devops'
'AWS/CF-Provision-and-Deploy-EC2.gitlab-ci.yml' | :repository_source | 'p_ci_templates_aws_cf_deploy_ec2'
'AWS/Deploy-ECS.gitlab-ci.yml' | :repository_source | 'p_ci_templates_aws_deploy_ecs'
'Jobs/Build.gitlab-ci.yml' | :repository_source | 'p_ci_templates_auto_devops_build'
'Jobs/Deploy.gitlab-ci.yml' | :repository_source | 'p_ci_templates_auto_devops_deploy'
'Jobs/Deploy.latest.gitlab-ci.yml' | :repository_source | 'p_ci_templates_auto_devops_deploy_latest'
'Security/SAST.gitlab-ci.yml' | :repository_source | 'p_ci_templates_security_sast'
'Security/Secret-Detection.gitlab-ci.yml' | :repository_source | 'p_ci_templates_security_secret_detection'
'Terraform/Base.latest.gitlab-ci.yml' | :repository_source | 'p_ci_templates_terraform_base_latest'
end
let(:project_id) { 1 }
with_them do
it_behaves_like 'tracking unique hll events' do
subject(:request) { described_class.track_unique_project_event(project_id: project_id, template: template, config_source: config_source) }
shared_examples 'tracks template' do
it "has an event defined for template" do
expect do
described_class.track_unique_project_event(
project_id: project_id,
template: template_path,
config_source: config_source
)
end.not_to raise_error
end
let(:project_id) { 1 }
let(:target_id) { expected_event }
let(:expected_type) { instance_of(Integer) }
it "tracks template" do
expanded_template_name = described_class.expand_template_name(template_path)
expected_template_event_name = described_class.ci_template_event_name(expanded_template_name, config_source)
expect(Gitlab::UsageDataCounters::HLLRedisCounter).to(receive(:track_event)).with(expected_template_event_name, values: project_id)
described_class.track_unique_project_event(project_id: project_id, template: template_path, config_source: config_source)
end
end
context 'known_events coverage tests' do
let(:project_id) { 1 }
context 'with explicit includes' do
let(:config_source) { :repository_source }
# These tests help guard against missing "explicit" events in known_events/ci_templates.yml
context 'explicit include:template events' do
described_class::TEMPLATE_TO_EVENT.keys.each do |template|
it "does not raise error for #{template}" do
expect do
described_class.track_unique_project_event(project_id: project_id, template: template, config_source: config_source)
end.not_to raise_error
(described_class.ci_templates - ['Verify/Browser-Performance.latest.gitlab-ci.yml', 'Verify/Browser-Performance.gitlab-ci.yml']).each do |template|
context "for #{template}" do
let(:template_path) { template }
include_examples 'tracks template'
end
end
end
context 'with implicit includes' do
let(:config_source) { :auto_devops_source }
[
['', ['Auto-DevOps.gitlab-ci.yml']],
['Jobs', described_class.ci_templates('lib/gitlab/ci/templates/Jobs')],
['Security', described_class.ci_templates('lib/gitlab/ci/templates/Security')]
].each do |directory, templates|
templates.each do |template|
context "for #{template}" do
let(:template_path) { File.join(directory, template) }
include_examples 'tracks template'
end
end
end
# This test is to help guard against missing "implicit" events in known_events/ci_templates.yml
it 'does not raise error for any template in an implicit Auto DevOps pipeline' do
project = create(:project, :auto_devops)
pipeline = double(project: project)
command = double
result = Gitlab::Ci::YamlProcessor.new(
Gitlab::Ci::Pipeline::Chain::Config::Content::AutoDevops.new(pipeline, command).content,
project: project,
user: double,
sha: 'd310cc759caaa20cd05a9e0983d6017896d9c34c'
).execute
config_source = :auto_devops_source
result.included_templates.each do |template|
expect do
described_class.track_unique_project_event(project_id: project.id, template: template, config_source: config_source)
end.not_to raise_error
end
end
end
context 'templates outside of TEMPLATE_TO_EVENT' do
let(:project_id) { 1 }
let(:config_source) { :repository_source }
described_class.ci_templates.each do |template|
next if described_class::TEMPLATE_TO_EVENT.key?(template)
it "has an event defined for #{template}" do
expect do
described_class.track_unique_project_event(
project_id: project_id,
template: template,
config_source: config_source
)
end.not_to raise_error
end
it "tracks #{template}" do
expected_template_event_name = described_class.ci_template_event_name(template, :repository_source)
expect(Gitlab::UsageDataCounters::HLLRedisCounter).to(receive(:track_event)).with(expected_template_event_name, values: project_id)
described_class.track_unique_project_event(project_id: project_id, template: template, config_source: config_source)
end
end
it 'expands short template names' do
expect do
described_class.track_unique_project_event(project_id: 1, template: 'Dependency-Scanning.gitlab-ci.yml', config_source: :repository_source)
end.not_to raise_error
end
end
end

View File

@ -242,4 +242,28 @@ RSpec.describe Upload do
it { expect(subject.uploader_context).to match(a_hash_including(secret: 'secret', identifier: 'file.txt')) }
end
describe '#update_project_statistics' do
let_it_be(:project) { create(:project) }
subject do
create(:upload, model: project)
end
it 'updates project statistics when upload is added' do
expect(ProjectCacheWorker).to receive(:perform_async)
.with(project.id, [], [:uploads_size])
subject.save!
end
it 'updates project statistics when upload is removed' do
subject.save!
expect(ProjectCacheWorker).to receive(:perform_async)
.with(project.id, [], [:uploads_size])
subject.destroy!
end
end
end

View File

@ -15,6 +15,25 @@ RSpec.describe CommitStatusPresenter do
expect(described_class.superclass).to eq(Gitlab::View::Presenter::Delegated)
end
describe '#callout_failure_message' do
subject { presenter.callout_failure_message }
context 'when troubleshooting doc is available' do
let(:failure_reason) { :environment_creation_failure }
before do
build.failure_reason = failure_reason
end
it 'appends the troubleshooting link' do
doc = described_class::TROUBLESHOOTING_DOC[failure_reason]
expect(subject).to eq("#{described_class.callout_failure_messages[failure_reason]} " \
"<a href=\"#{presenter.help_page_path(doc[:path], anchor: doc[:anchor])}\">How do I fix it?</a>")
end
end
end
describe 'covers all failure reasons' do
let(:message) { presenter.callout_failure_message }

View File

@ -0,0 +1,123 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'getting an issue list for a group' do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let_it_be(:group1) { create(:group) }
let_it_be(:group2) { create(:group) }
let_it_be(:project1) { create(:project, :public, group: group1) }
let_it_be(:project2) { create(:project, :private, group: group1) }
let_it_be(:project3) { create(:project, :public, group: group2) }
let_it_be(:issue1) { create(:issue, project: project1) }
let_it_be(:issue2) { create(:issue, project: project2) }
let_it_be(:issue3) { create(:issue, project: project3) }
let(:issue1_gid) { issue1.to_global_id.to_s }
let(:issue2_gid) { issue2.to_global_id.to_s }
let(:issues_data) { graphql_data['group']['issues']['edges'] }
let(:issue_filter_params) { {} }
let(:fields) do
<<~QUERY
edges {
node {
#{all_graphql_fields_for('issues'.classify)}
}
}
QUERY
end
let(:query) do
graphql_query_for(
'group',
{ 'fullPath' => group1.full_path },
query_graphql_field('issues', issue_filter_params, fields)
)
end
it_behaves_like 'a working graphql query' do
before do
post_graphql(query, current_user: current_user)
end
end
context 'when there is a confidential issue' do
let_it_be(:confidential_issue1) { create(:issue, :confidential, project: project1) }
let_it_be(:confidential_issue2) { create(:issue, :confidential, project: project2) }
let_it_be(:confidential_issue3) { create(:issue, :confidential, project: project3) }
let(:confidential_issue1_gid) { confidential_issue1.to_global_id.to_s }
let(:confidential_issue2_gid) { confidential_issue2.to_global_id.to_s }
context 'when the user cannot see confidential issues' do
before do
group1.add_guest(current_user)
end
it 'returns issues without confidential issues for the group' do
post_graphql(query, current_user: current_user)
expect(issues_ids).to contain_exactly(issue1_gid, issue2_gid)
end
context 'filtering for confidential issues' do
let(:issue_filter_params) { { confidential: true } }
it 'returns no issues' do
post_graphql(query, current_user: current_user)
expect(issues_ids).to be_empty
end
end
context 'filtering for non-confidential issues' do
let(:issue_filter_params) { { confidential: false } }
it 'returns correctly filtered issues' do
post_graphql(query, current_user: current_user)
expect(issues_ids).to contain_exactly(issue1_gid, issue2_gid)
end
end
end
context 'when the user can see confidential issues' do
before do
group1.add_developer(current_user)
end
it 'returns issues with confidential issues for the group' do
post_graphql(query, current_user: current_user)
expect(issues_ids).to contain_exactly(issue1_gid, issue2_gid, confidential_issue1_gid, confidential_issue2_gid)
end
context 'filtering for confidential issues' do
let(:issue_filter_params) { { confidential: true } }
it 'returns correctly filtered issues' do
post_graphql(query, current_user: current_user)
expect(issues_ids).to contain_exactly(confidential_issue1_gid, confidential_issue2_gid)
end
end
context 'filtering for non-confidential issues' do
let(:issue_filter_params) { { confidential: false } }
it 'returns correctly filtered issues' do
post_graphql(query, current_user: current_user)
expect(issues_ids).to contain_exactly(issue1_gid, issue2_gid)
end
end
end
end
def issues_ids
graphql_dig_at(issues_data, :node, :id)
end
end

View File

@ -11,6 +11,8 @@ RSpec.describe 'getting an issue list for a project' do
let_it_be(:issue_b, reload: true) { create(:issue, :with_alert, project: project) }
let_it_be(:issues, reload: true) { [issue_a, issue_b] }
let(:issue_a_gid) { issue_a.to_global_id.to_s }
let(:issue_b_gid) { issue_b.to_global_id.to_s }
let(:issues_data) { graphql_data['project']['issues']['edges'] }
let(:issue_filter_params) { {} }
@ -66,9 +68,6 @@ RSpec.describe 'getting an issue list for a project' do
let_it_be(:upvote_award) { create(:award_emoji, :upvote, user: current_user, awardable: issue_a) }
let(:issue_a_gid) { issue_a.to_global_id.to_s }
let(:issue_b_gid) { issue_b.to_global_id.to_s }
where(:value, :gids) do
'thumbsup' | lazy { [issue_a_gid] }
'ANY' | lazy { [issue_a_gid] }
@ -84,7 +83,7 @@ RSpec.describe 'getting an issue list for a project' do
it 'returns correctly filtered issues' do
post_graphql(query, current_user: current_user)
expect(graphql_dig_at(issues_data, :node, :id)).to eq(gids)
expect(issues_ids).to eq(gids)
end
end
end
@ -149,6 +148,8 @@ RSpec.describe 'getting an issue list for a project' do
create(:issue, :confidential, project: project)
end
let(:confidential_issue_gid) { confidential_issue.to_global_id.to_s }
context 'when the user cannot see confidential issues' do
it 'returns issues without confidential issues' do
post_graphql(query, current_user: current_user)
@ -159,12 +160,34 @@ RSpec.describe 'getting an issue list for a project' do
expect(issue.dig('node', 'confidential')).to eq(false)
end
end
context 'filtering for confidential issues' do
let(:issue_filter_params) { { confidential: true } }
it 'returns no issues' do
post_graphql(query, current_user: current_user)
expect(issues_data.size).to eq(0)
end
end
context 'filtering for non-confidential issues' do
let(:issue_filter_params) { { confidential: false } }
it 'returns correctly filtered issues' do
post_graphql(query, current_user: current_user)
expect(issues_ids).to contain_exactly(issue_a_gid, issue_b_gid)
end
end
end
context 'when the user can see confidential issues' do
it 'returns issues with confidential issues' do
before do
project.add_developer(current_user)
end
it 'returns issues with confidential issues' do
post_graphql(query, current_user: current_user)
expect(issues_data.size).to eq(3)
@ -175,6 +198,26 @@ RSpec.describe 'getting an issue list for a project' do
expect(confidentials).to eq([true, false, false])
end
context 'filtering for confidential issues' do
let(:issue_filter_params) { { confidential: true } }
it 'returns correctly filtered issues' do
post_graphql(query, current_user: current_user)
expect(issues_ids).to contain_exactly(confidential_issue_gid)
end
end
context 'filtering for non-confidential issues' do
let(:issue_filter_params) { { confidential: false } }
it 'returns correctly filtered issues' do
post_graphql(query, current_user: current_user)
expect(issues_ids).to contain_exactly(issue_a_gid, issue_b_gid)
end
end
end
end
@ -526,4 +569,8 @@ RSpec.describe 'getting an issue list for a project' do
include_examples 'N+1 query check'
end
end
def issues_ids
graphql_dig_at(issues_data, :node, :id)
end
end

View File

@ -3,6 +3,19 @@
require 'spec_helper'
RSpec.describe Groups::TransferService do
shared_examples 'project namespace path is in sync with project path' do
it 'keeps project and project namespace attributes in sync' do
projects_with_project_namespace.each do |project|
project.reload
expect(project.full_path).to eq("#{group_full_path}/#{project.path}")
expect(project.project_namespace.full_path).to eq(project.full_path)
expect(project.project_namespace.parent).to eq(project.namespace)
expect(project.project_namespace.visibility_level).to eq(project.visibility_level)
end
end
end
let_it_be(:user) { create(:user) }
let_it_be(:new_parent_group) { create(:group, :public) }
@ -169,6 +182,18 @@ RSpec.describe Groups::TransferService do
expect(project.full_path).to eq("#{group.path}/#{project.path}")
end
end
context 'when projects have project namespaces' do
let_it_be(:project1) { create(:project, :private, namespace: group) }
let_it_be(:project_namespace1) { create(:project_namespace, project: project1) }
let_it_be(:project2) { create(:project, :private, namespace: group) }
let_it_be(:project_namespace2) { create(:project_namespace, project: project2) }
it_behaves_like 'project namespace path is in sync with project path' do
let(:group_full_path) { "#{group.path}" }
let(:projects_with_project_namespace) { [project1, project2] }
end
end
end
end
@ -222,10 +247,10 @@ RSpec.describe Groups::TransferService do
context 'when the parent group has a project with the same path' do
let_it_be_with_reload(:group) { create(:group, :public, :nested, path: 'foo') }
let_it_be(:membership) { create(:group_member, :owner, group: new_parent_group, user: user) }
let_it_be(:project) { create(:project, path: 'foo', namespace: new_parent_group) }
before do
create(:group_member, :owner, group: new_parent_group, user: user)
create(:project, path: 'foo', namespace: new_parent_group)
group.update_attribute(:path, 'foo')
end
@ -237,6 +262,19 @@ RSpec.describe Groups::TransferService do
transfer_service.execute(new_parent_group)
expect(transfer_service.error).to eq('Transfer failed: Validation failed: Group URL has already been taken')
end
context 'when projects have project namespaces' do
let!(:project_namespace) { create(:project_namespace, project: project) }
before do
transfer_service.execute(new_parent_group)
end
it_behaves_like 'project namespace path is in sync with project path' do
let(:group_full_path) { "#{new_parent_group.full_path}" }
let(:projects_with_project_namespace) { [project] }
end
end
end
context 'when the group is allowed to be transferred' do
@ -407,6 +445,8 @@ RSpec.describe Groups::TransferService do
context 'when transferring a group with project descendants' do
let!(:project1) { create(:project, :repository, :private, namespace: group) }
let!(:project2) { create(:project, :repository, :internal, namespace: group) }
let!(:project_namespace1) { create(:project_namespace, project: project1) }
let!(:project_namespace2) { create(:project_namespace, project: project2) }
before do
TestEnv.clean_test_path
@ -432,18 +472,30 @@ RSpec.describe Groups::TransferService do
expect(project1.private?).to be_truthy
expect(project2.internal?).to be_truthy
end
it_behaves_like 'project namespace path is in sync with project path' do
let(:group_full_path) { "#{new_parent_group.path}/#{group.path}" }
let(:projects_with_project_namespace) { [project1, project2] }
end
end
context 'when the new parent has a lower visibility than the projects' do
let!(:project1) { create(:project, :repository, :public, namespace: group) }
let!(:project2) { create(:project, :repository, :public, namespace: group) }
let(:new_parent_group) { create(:group, :private) }
let!(:new_parent_group) { create(:group, :private) }
let!(:project_namespace1) { create(:project_namespace, project: project1) }
let!(:project_namespace2) { create(:project_namespace, project: project2) }
it 'updates projects visibility to match the new parent' do
group.projects.each do |project|
expect(project.private?).to be_truthy
end
end
it_behaves_like 'project namespace path is in sync with project path' do
let(:group_full_path) { "#{new_parent_group.path}/#{group.path}" }
let(:projects_with_project_namespace) { [project1, project2] }
end
end
end
@ -452,6 +504,8 @@ RSpec.describe Groups::TransferService do
let!(:project2) { create(:project, :repository, :internal, namespace: group) }
let!(:subgroup1) { create(:group, :private, parent: group) }
let!(:subgroup2) { create(:group, :internal, parent: group) }
let!(:project_namespace1) { create(:project_namespace, project: project1) }
let!(:project_namespace2) { create(:project_namespace, project: project2) }
before do
TestEnv.clean_test_path
@ -480,6 +534,11 @@ RSpec.describe Groups::TransferService do
expect(project1.redirect_routes.count).to eq(1)
expect(project2.redirect_routes.count).to eq(1)
end
it_behaves_like 'project namespace path is in sync with project path' do
let(:group_full_path) { "#{new_parent_group.path}/#{group.path}" }
let(:projects_with_project_namespace) { [project1, project2] }
end
end
context 'when transferring a group with nested groups and projects' do

View File

@ -64,6 +64,33 @@ RSpec.describe Projects::TransferService do
expect(transfer_result).to be_truthy
expect(project.namespace).to eq(group)
end
context 'when project has an associated project namespace' do
let!(:project_namespace) { create(:project_namespace, project: project) }
it 'keeps project namespace in sync with project' do
transfer_result = execute_transfer
expect(transfer_result).to be_truthy
project_namespace_in_sync(group)
end
context 'when project is transferred to a deeper nested group' do
let(:parent_group) { create(:group) }
let(:sub_group) { create(:group, parent: parent_group) }
let(:sub_sub_group) { create(:group, parent: sub_group) }
let(:group) { sub_sub_group }
it 'keeps project namespace in sync with project' do
transfer_result = execute_transfer
expect(transfer_result).to be_truthy
project_namespace_in_sync(sub_sub_group)
end
end
end
end
context 'when transfer succeeds' do
@ -243,6 +270,16 @@ RSpec.describe Projects::TransferService do
expect(unrelated_pending_build.namespace_traversal_ids).to eq(other_project.namespace.traversal_ids)
end
end
context 'when project has an associated project namespace' do
let!(:project_namespace) { create(:project_namespace, project: project) }
it 'keeps project namespace in sync with project' do
attempt_project_transfer
project_namespace_in_sync(user.namespace)
end
end
end
context 'namespace -> no namespace' do
@ -255,6 +292,18 @@ RSpec.describe Projects::TransferService do
expect(project.namespace).to eq(user.namespace)
expect(project.errors.messages[:new_namespace].first).to eq 'Please select a new namespace for your project.'
end
context 'when project has an associated project namespace' do
let!(:project_namespace) { create(:project_namespace, project: project) }
it 'keeps project namespace in sync with project' do
transfer_result = execute_transfer
expect(transfer_result).to be false
project_namespace_in_sync(user.namespace)
end
end
end
context 'disallow transferring of project with tags' do
@ -655,4 +704,13 @@ RSpec.describe Projects::TransferService do
def rugged_config
rugged_repo(project.repository).config
end
def project_namespace_in_sync(group)
project.reload
expect(project.namespace).to eq(group)
expect(project.project_namespace.visibility_level).to eq(project.visibility_level)
expect(project.project_namespace.path).to eq(project.path)
expect(project.project_namespace.parent).to eq(project.namespace)
expect(project.project_namespace.traversal_ids).to eq([*project.namespace.traversal_ids, project.project_namespace.id])
end
end

View File

@ -315,6 +315,10 @@ RSpec.configure do |config|
# For more information check https://gitlab.com/gitlab-org/gitlab/-/issues/339348
stub_feature_flags(new_header_search: false)
# Disable the override flag in order to enable the feature by default.
# See https://docs.gitlab.com/ee/development/feature_flags/#selectively-disable-by-actor
stub_feature_flags(surface_environment_creation_failure_override: false)
allow(Gitlab::GitalyClient).to receive(:can_use_disk?).and_return(enable_rugged)
else
unstub_all_feature_flags

View File

@ -63,7 +63,14 @@ RSpec.describe Quality::TestLevel do
context 'with a prefix' do
it 'returns a pattern' do
expect(described_class.new('ee/').pattern(:system))
.to eq("ee/spec/{features}{,/**/}*_spec.rb")
.to eq("{ee/}spec/{features}{,/**/}*_spec.rb")
end
end
context 'with several prefixes' do
it 'returns a pattern' do
expect(described_class.new(['', 'ee/', 'jh/']).pattern(:system))
.to eq("{,ee/,jh/}spec/{features}{,/**/}*_spec.rb")
end
end
@ -138,7 +145,14 @@ RSpec.describe Quality::TestLevel do
context 'with a prefix' do
it 'returns a regexp' do
expect(described_class.new('ee/').regexp(:system))
.to eq(%r{ee/spec/(features)})
.to eq(%r{(ee/)spec/(features)})
end
end
context 'with several prefixes' do
it 'returns a regexp' do
expect(described_class.new(['', 'ee/', 'jh/']).regexp(:system))
.to eq(%r{(|ee/|jh/)spec/(features)})
end
end

View File

@ -60,20 +60,20 @@ module Quality
system: ['features']
}.freeze
attr_reader :prefix
attr_reader :prefixes
def initialize(prefix = nil)
@prefix = prefix
def initialize(prefixes = nil)
@prefixes = Array(prefixes)
@patterns = {}
@regexps = {}
end
def pattern(level)
@patterns[level] ||= "#{prefix}spec/#{folders_pattern(level)}{,/**/}*#{suffix(level)}"
@patterns[level] ||= "#{prefixes_for_pattern}spec/#{folders_pattern(level)}{,/**/}*#{suffix(level)}"
end
def regexp(level)
@regexps[level] ||= Regexp.new("#{prefix}spec/#{folders_regex(level)}").freeze
@regexps[level] ||= Regexp.new("#{prefixes_for_regex}spec/#{folders_regex(level)}").freeze
end
def level_for(file_path)
@ -102,6 +102,20 @@ module Quality
private
def prefixes_for_pattern
return '' if prefixes.empty?
"{#{prefixes.join(',')}}"
end
def prefixes_for_regex
return '' if prefixes.empty?
regex_prefix = prefixes.map(&Regexp.method(:escape)).join('|')
"(#{regex_prefix})"
end
def suffix(level)
case level
when :frontend_fixture