Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
e9ab418709
commit
8a0a54ab92
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
fragment PackageData on Package {
|
||||
id
|
||||
name
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
export const FILTERED_SEARCH_TERM = 'filtered-search-term';
|
||||
export const FILTERED_SEARCH_TYPE = 'type';
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: -> {
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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 |
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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 |
|
||||
| ------ | ------ | ------ | ------ |
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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>
|
||||
`;
|
||||
|
|
|
@ -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]: '' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue