Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
df9890e9a7
commit
fd247970cf
|
|
@ -102,7 +102,6 @@ review-build-cng:
|
|||
name: review/${CI_COMMIT_REF_SLUG}${SCHEDULE_TYPE} # No separator for SCHEDULE_TYPE so it's compatible as before and looks nice without it
|
||||
url: https://gitlab-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}
|
||||
on_stop: review-stop
|
||||
auto_stop_in: 6 hours
|
||||
|
||||
review-deploy:
|
||||
extends:
|
||||
|
|
@ -168,7 +167,6 @@ review-deploy-sample-projects:
|
|||
extends: .review-workflow-base
|
||||
environment:
|
||||
action: stop
|
||||
dependencies: []
|
||||
variables:
|
||||
# We're cloning the repo instead of downloading the script for now
|
||||
# because some repos are private and CI_JOB_TOKEN cannot access files.
|
||||
|
|
@ -178,21 +176,20 @@ review-deploy-sample-projects:
|
|||
- source ./scripts/utils.sh
|
||||
- source ./scripts/review_apps/review-apps.sh
|
||||
- !reference [".use-kube-context", before_script]
|
||||
script:
|
||||
- delete_helm_release
|
||||
|
||||
review-delete-deployment:
|
||||
extends:
|
||||
- .review-stop-base
|
||||
- .review:rules:review-delete-deployment
|
||||
dependencies: []
|
||||
stage: prepare
|
||||
script:
|
||||
- delete_helm_release
|
||||
|
||||
review-stop:
|
||||
extends:
|
||||
- .review-stop-base
|
||||
- .review:rules:review-stop
|
||||
resource_group: review/${CI_COMMIT_REF_SLUG}${SCHEDULE_TYPE} # CI_ENVIRONMENT_SLUG is not available here and we want this to be the same as the environment
|
||||
resource_group: review/${CI_COMMIT_REF_SLUG}${SCHEDULE_TYPE} # CI_ENVIRONMENT_SLUG is not available here and we want this to be the same as the environment
|
||||
stage: deploy
|
||||
needs: []
|
||||
script:
|
||||
- delete_helm_release
|
||||
|
|
|
|||
|
|
@ -58,6 +58,21 @@ start-review-app-pipeline:
|
|||
- job: e2e-test-pipeline-generate
|
||||
- job: build-assets-image
|
||||
artifacts: false
|
||||
# We do not want to have ALL global variables passed as trigger variables,
|
||||
# as they cannot be overridden. See this issue for more context:
|
||||
#
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/387183
|
||||
inherit:
|
||||
variables:
|
||||
- CHROME_VERSION
|
||||
- REGISTRY_GROUP
|
||||
- REGISTRY_HOST
|
||||
- REVIEW_APPS_DOMAIN
|
||||
- REVIEW_APPS_GCP_PROJECT
|
||||
- REVIEW_APPS_GCP_REGION
|
||||
- REVIEW_APPS_IMAGE
|
||||
- RUBY_VERSION
|
||||
|
||||
# These variables are set in the pipeline schedules.
|
||||
# They need to be explicitly passed on to the child pipeline.
|
||||
# https://docs.gitlab.com/ee/ci/pipelines/multi_project_pipelines.html#pass-cicd-variables-to-a-downstream-pipeline-by-using-the-variables-keyword
|
||||
|
|
|
|||
|
|
@ -1,14 +1,31 @@
|
|||
## Purpose of Revert
|
||||
<!--
|
||||
IMPORTANT: Add appropriate labels BEFORE you save the merge request. CI/CD jobs
|
||||
can be skipped only if the labels are applied BEFORE the CI/CD pipeline is created.
|
||||
See https://docs.gitlab.com/ee/development/pipelines#revert-mrs for more info.
|
||||
-->
|
||||
|
||||
## Purpose of revert
|
||||
|
||||
<!-- Please link to the relevant incident -->
|
||||
|
||||
### Check-list
|
||||
### Checklist
|
||||
|
||||
- [ ] Create an issue to reinstate the merge request and assign it to the author of the reverted merge request.
|
||||
- [ ] If the revert is to resolve a ['broken master' incident](https://about.gitlab.com/handbook/engineering/workflow/#broken-master), please read through the [Responsibilities of the Broken 'Master' resolution DRI](https://about.gitlab.com/handbook/engineering/workflow/#responsibilities-of-the-resolution-dri)
|
||||
- [ ] Add the appropriate labels **before** the MR is created (we can only skip CI/CD jobs if the labels are added **before** the CI/CD pipeline gets created)
|
||||
- [ ] If the revert is to resolve a [broken `master' incident](https://about.gitlab.com/handbook/engineering/workflow/#broken-master), please read through the [Responsibilities of the Broken `master` resolution DRI](https://about.gitlab.com/handbook/engineering/workflow/#responsibilities-of-the-resolution-dri).
|
||||
- [ ] Add the appropriate labels **before** the MR is created. We can skip CI/CD jobs only if the labels are added **before** the CI/CD pipeline is created.
|
||||
|
||||
### Milestone info
|
||||
|
||||
- [ ] I am reverting something in the **current** milestone. No changelog is needed, and I've added a `~"regression:*"` label.
|
||||
- [ ] I am reverting something in a **different** milestone. A changelog is needed, and I've removed the `~"regression:*"` label.
|
||||
|
||||
### Related issues and merge requests
|
||||
|
||||
|
||||
/label ~"pipeline:expedite" ~"master:broken"
|
||||
|
||||
<!-- If applicable, specifying the regression label in the current milestone will skip additional CI/CD jobs (e.g. Danger changelog checks) -->
|
||||
<!--
|
||||
Regression label: if applicable, specify the milestone-specific regression label
|
||||
(such as ~regression:15.8) to skip additional CI/CD jobs, like Danger changelog checks. -->
|
||||
|
||||
<!-- /label ~regression: -->
|
||||
|
|
|
|||
|
|
@ -356,7 +356,6 @@ Gitlab/StrongMemoizeAttr:
|
|||
- 'ee/app/models/ee/list.rb'
|
||||
- 'ee/app/models/ee/merge_request.rb'
|
||||
- 'ee/app/models/ee/namespace.rb'
|
||||
- 'ee/app/models/ee/namespace/storage/notification.rb'
|
||||
- 'ee/app/models/ee/project.rb'
|
||||
- 'ee/app/models/ee/snippet.rb'
|
||||
- 'ee/app/models/ee/user.rb'
|
||||
|
|
|
|||
|
|
@ -71,7 +71,6 @@ Layout/LineEndStringConcatenationIndentation:
|
|||
- 'ee/app/models/app_sec/fuzzing/api/ci_configuration.rb'
|
||||
- 'ee/app/models/ci/minutes/notification.rb'
|
||||
- 'ee/app/models/ee/group_group_link.rb'
|
||||
- 'ee/app/models/ee/namespace/storage/notification.rb'
|
||||
- 'ee/app/models/ee/vulnerability.rb'
|
||||
- 'ee/app/services/boards/epic_lists/destroy_service.rb'
|
||||
- 'ee/app/services/ee/admin/set_feature_flag_service.rb'
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ RSpec/DescribedClass:
|
|||
- 'ee/spec/models/ee/group_spec.rb'
|
||||
- 'ee/spec/models/ee/iteration_spec.rb'
|
||||
- 'ee/spec/models/ee/merge_request_diff_spec.rb'
|
||||
- 'ee/spec/models/ee/namespace/storage/notification_spec.rb'
|
||||
- 'ee/spec/models/ee/vulnerability_spec.rb'
|
||||
- 'ee/spec/models/epic_issue_spec.rb'
|
||||
- 'ee/spec/models/epic_spec.rb'
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ RSpec/ExpectInHook:
|
|||
- 'ee/spec/models/concerns/geo/replicable_model_spec.rb'
|
||||
- 'ee/spec/models/container_repository_spec.rb'
|
||||
- 'ee/spec/models/dora/daily_metrics_spec.rb'
|
||||
- 'ee/spec/models/ee/namespace/storage/notification_spec.rb'
|
||||
- 'ee/spec/models/ee/namespace_spec.rb'
|
||||
- 'ee/spec/models/gitlab_subscription_spec.rb'
|
||||
- 'ee/spec/models/license_spec.rb'
|
||||
|
|
|
|||
|
|
@ -1382,7 +1382,6 @@ RSpec/MissingFeatureCategory:
|
|||
- 'ee/spec/models/ee/merge_request/metrics_spec.rb'
|
||||
- 'ee/spec/models/ee/merge_request_diff_spec.rb'
|
||||
- 'ee/spec/models/ee/namespace/root_storage_statistics_spec.rb'
|
||||
- 'ee/spec/models/ee/namespace/storage/notification_spec.rb'
|
||||
- 'ee/spec/models/ee/namespace_ci_cd_setting_spec.rb'
|
||||
- 'ee/spec/models/ee/namespace_spec.rb'
|
||||
- 'ee/spec/models/ee/namespace_statistics_spec.rb'
|
||||
|
|
|
|||
|
|
@ -198,7 +198,6 @@ Style/FormatString:
|
|||
- 'ee/app/models/dast_site_profile.rb'
|
||||
- 'ee/app/models/dast_site_validation.rb'
|
||||
- 'ee/app/models/ee/member.rb'
|
||||
- 'ee/app/models/ee/namespace/storage/notification.rb'
|
||||
- 'ee/app/models/geo/upload_registry.rb'
|
||||
- 'ee/app/models/integrations/github.rb'
|
||||
- 'ee/app/models/iterations/cadence.rb'
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
5b5abda2e69a93c5898609cd9c9aa02954c10556
|
||||
69e486270838efbbb78e6736ac6aecde5ccd8caa
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
65bbfa0b62518691961de096e4a27d7c76307b7c
|
||||
27d39b816071e9630436f8bf19740c173594631d
|
||||
|
|
|
|||
|
|
@ -1,19 +1,33 @@
|
|||
<script>
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
|
||||
import { GlAreaChart } from '@gitlab/ui/dist/charts';
|
||||
import { debounce, uniq } from 'lodash';
|
||||
import { mapActions, mapState, mapGetters } from 'vuex';
|
||||
import { visitUrl } from '~/lib/utils/url_utility';
|
||||
import { getDatesInRange } from '~/lib/utils/datetime_utility';
|
||||
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
|
||||
import { __ } from '~/locale';
|
||||
import RefSelector from '~/ref/components/ref_selector.vue';
|
||||
import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants';
|
||||
import ResizableChartContainer from '~/vue_shared/components/resizable_chart/resizable_chart_container.vue';
|
||||
import { xAxisLabelFormatter, dateFormatter } from '../utils';
|
||||
|
||||
const GRAPHS_PATH_REGEX = /^(.*?)\/-\/graphs/g;
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
history: __('History'),
|
||||
refSelectorTranslations: {
|
||||
dropdownHeader: __('Switch branch/tag'),
|
||||
searchPlaceholder: __('Search branches and tags'),
|
||||
},
|
||||
},
|
||||
components: {
|
||||
GlAreaChart,
|
||||
GlButton,
|
||||
GlLoadingIcon,
|
||||
ResizableChartContainer,
|
||||
RefSelector,
|
||||
},
|
||||
props: {
|
||||
endpoint: {
|
||||
|
|
@ -24,7 +38,16 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projectId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
commitsPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
refTypes: [REF_TYPE_BRANCHES, REF_TYPE_TAGS],
|
||||
data() {
|
||||
return {
|
||||
masterChart: null,
|
||||
|
|
@ -32,6 +55,7 @@ export default {
|
|||
svgs: {},
|
||||
masterChartHeight: 264,
|
||||
individualChartHeight: 216,
|
||||
selectedBranch: this.branch,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -190,6 +214,11 @@ export default {
|
|||
),
|
||||
);
|
||||
},
|
||||
visitBranch(selected) {
|
||||
const graphsPathPrefix = this.endpoint.match(GRAPHS_PATH_REGEX)?.[0];
|
||||
|
||||
visitUrl(`${graphsPathPrefix}/${selected}`);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -197,48 +226,67 @@ export default {
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="loading" class="gl-text-center gl-pt-13">
|
||||
<gl-loading-icon :inline="true" size="xl" />
|
||||
<gl-loading-icon :inline="true" size="xl" data-testid="loading-app-icon" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="showChart" class="contributors-charts">
|
||||
<h4 class="gl-mb-2 gl-mt-5">{{ __('Commits to') }} {{ branch }}</h4>
|
||||
<span>{{ __('Excluding merge commits. Limited to 6,000 commits.') }}</span>
|
||||
<resizable-chart-container>
|
||||
<template #default="{ width }">
|
||||
<gl-area-chart
|
||||
class="gl-mb-5"
|
||||
:width="width"
|
||||
:data="masterChartData"
|
||||
:option="masterChartOptions"
|
||||
:height="masterChartHeight"
|
||||
@created="onMasterChartCreated"
|
||||
/>
|
||||
</template>
|
||||
</resizable-chart-container>
|
||||
|
||||
<div class="row">
|
||||
<div
|
||||
v-for="(contributor, index) in individualChartsData"
|
||||
:key="index"
|
||||
class="col-lg-6 col-12 gl-my-5"
|
||||
>
|
||||
<h4 class="gl-mb-2 gl-mt-0">{{ contributor.name }}</h4>
|
||||
<p class="gl-mb-3">
|
||||
{{ n__('%d commit', '%d commits', contributor.commits) }} ({{ contributor.email }})
|
||||
</p>
|
||||
<resizable-chart-container>
|
||||
<template #default="{ width }">
|
||||
<gl-area-chart
|
||||
:width="width"
|
||||
:data="contributor.dates"
|
||||
:option="individualChartOptions"
|
||||
:height="individualChartHeight"
|
||||
@created="onIndividualChartCreated"
|
||||
/>
|
||||
</template>
|
||||
</resizable-chart-container>
|
||||
<template v-else-if="showChart">
|
||||
<div class="gl-border-b gl-border-gray-100 gl-mb-6 gl-bg-gray-10 gl-p-5">
|
||||
<div class="gl-display-flex">
|
||||
<div class="gl-mr-3">
|
||||
<ref-selector
|
||||
v-model="selectedBranch"
|
||||
:project-id="projectId"
|
||||
:enabled-ref-types="$options.refTypes"
|
||||
:translations="$options.i18n.refSelectorTranslations"
|
||||
toggle-button-class="gl-max-w-26"
|
||||
@input="visitBranch"
|
||||
/>
|
||||
</div>
|
||||
<gl-button :href="commitsPath" data-testid="history-button"
|
||||
>{{ $options.i18n.history }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-testid="contributors-charts">
|
||||
<h4 class="gl-mb-2 gl-mt-5">{{ __('Commits to') }} {{ branch }}</h4>
|
||||
<span>{{ __('Excluding merge commits. Limited to 6,000 commits.') }}</span>
|
||||
<resizable-chart-container>
|
||||
<template #default="{ width }">
|
||||
<gl-area-chart
|
||||
class="gl-mb-5"
|
||||
:width="width"
|
||||
:data="masterChartData"
|
||||
:option="masterChartOptions"
|
||||
:height="masterChartHeight"
|
||||
@created="onMasterChartCreated"
|
||||
/>
|
||||
</template>
|
||||
</resizable-chart-container>
|
||||
|
||||
<div class="row">
|
||||
<div
|
||||
v-for="(contributor, index) in individualChartsData"
|
||||
:key="index"
|
||||
class="col-lg-6 col-12 gl-my-5"
|
||||
>
|
||||
<h4 class="gl-mb-2 gl-mt-0">{{ contributor.name }}</h4>
|
||||
<p class="gl-mb-3">
|
||||
{{ n__('%d commit', '%d commits', contributor.commits) }} ({{ contributor.email }})
|
||||
</p>
|
||||
<resizable-chart-container>
|
||||
<template #default="{ width }">
|
||||
<gl-area-chart
|
||||
:width="width"
|
||||
:data="contributor.dates"
|
||||
:option="individualChartOptions"
|
||||
:height="individualChartHeight"
|
||||
@created="onIndividualChartCreated"
|
||||
/>
|
||||
</template>
|
||||
</resizable-chart-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -7,18 +7,19 @@ export default () => {
|
|||
|
||||
if (!el) return null;
|
||||
|
||||
const { projectGraphPath, projectBranch, defaultBranch } = el.dataset;
|
||||
const { projectGraphPath, projectBranch, defaultBranch, projectId, commitsPath } = el.dataset;
|
||||
const store = createStore(defaultBranch);
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
store,
|
||||
|
||||
render(createElement) {
|
||||
return createElement(ContributorsGraphs, {
|
||||
props: {
|
||||
endpoint: projectGraphPath,
|
||||
branch: projectBranch,
|
||||
projectId,
|
||||
commitsPath,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -777,7 +777,7 @@
|
|||
"properties": {
|
||||
"value": {
|
||||
"type": "string",
|
||||
"markdownDescription": "Default value of the variable. If used with `options`, `value` must be included in the array. [Learn More](https://docs.gitlab.com/ee/ci/pipelines/index.html#prefill-variables-in-manual-pipelines)"
|
||||
"markdownDescription": "Default value of the variable. If used with `options`, `value` must be included in the array. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#variablesvalue)"
|
||||
},
|
||||
"options": {
|
||||
"type": "array",
|
||||
|
|
@ -786,7 +786,7 @@
|
|||
},
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"markdownDescription": "A list of predefined values that users can select from in the **Run pipeline** page when running a pipeline manually. [Learn More](https://docs.gitlab.com/ee/ci/pipelines/index.html#configure-a-list-of-selectable-values-for-a-prefilled-variable)"
|
||||
"markdownDescription": "A list of predefined values that users can select from in the **Run pipeline** page when running a pipeline manually. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#variablesoptions)"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory';
|
|||
import initFilePickers from '~/file_pickers';
|
||||
import initTransferGroupForm from '~/groups/init_transfer_group_form';
|
||||
import { initGroupSelects } from '~/vue_shared/components/entity_select/init_group_selects';
|
||||
import { initProjectSelects } from '~/vue_shared/components/entity_select/init_project_selects';
|
||||
import { initCascadingSettingsLockPopovers } from '~/namespaces/cascading_settings';
|
||||
import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
|
||||
import projectSelect from '~/project_select';
|
||||
|
|
@ -22,6 +23,9 @@ mountBadgeSettings(GROUP_BADGE);
|
|||
// Initialize Subgroups selector
|
||||
initGroupSelects();
|
||||
|
||||
// Initialize project selectors
|
||||
initProjectSelects();
|
||||
|
||||
projectSelect();
|
||||
|
||||
initSearchSettings();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
import initUsageQuotas from '~/usage_quotas';
|
||||
|
||||
initUsageQuotas();
|
||||
|
|
@ -87,6 +87,12 @@ export default {
|
|||
required: false,
|
||||
default: '',
|
||||
},
|
||||
|
||||
toggleButtonClass: {
|
||||
type: [String, Object, Array],
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -130,11 +136,21 @@ export default {
|
|||
showSectionHeaders() {
|
||||
return this.enabledRefTypes.length > 1;
|
||||
},
|
||||
toggleButtonClass() {
|
||||
return {
|
||||
'gl-inset-border-1-red-500!': !this.state,
|
||||
'gl-font-monospace': Boolean(this.selectedRef),
|
||||
};
|
||||
extendedToggleButtonClass() {
|
||||
const classes = [
|
||||
{
|
||||
'gl-inset-border-1-red-500!': !this.state,
|
||||
'gl-font-monospace': Boolean(this.selectedRef),
|
||||
},
|
||||
];
|
||||
|
||||
if (Array.isArray(this.toggleButtonClass)) {
|
||||
classes.push(...this.toggleButtonClass);
|
||||
} else {
|
||||
classes.push(this.toggleButtonClass);
|
||||
}
|
||||
|
||||
return classes;
|
||||
},
|
||||
footerSlotProps() {
|
||||
return {
|
||||
|
|
@ -239,7 +255,7 @@ export default {
|
|||
<div>
|
||||
<gl-dropdown
|
||||
:header-text="i18n.dropdownHeader"
|
||||
:toggle-class="toggleButtonClass"
|
||||
:toggle-class="extendedToggleButtonClass"
|
||||
:text="buttonText"
|
||||
class="ref-selector gl-w-full"
|
||||
v-bind="$attrs"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
<script>
|
||||
import { GlSprintf, GlTab, GlTabs } from '@gitlab/ui';
|
||||
import { USAGE_QUOTAS_TITLE, USAGE_QUOTAS_SUBTITLE } from '../constants';
|
||||
|
||||
export default {
|
||||
name: 'UsageQuotasApp',
|
||||
components: { GlSprintf, GlTab, GlTabs },
|
||||
inject: ['namespaceName'],
|
||||
computed: {
|
||||
placeholder() {
|
||||
return `storage_app_placeholder`;
|
||||
},
|
||||
},
|
||||
USAGE_QUOTAS_TITLE,
|
||||
USAGE_QUOTAS_SUBTITLE,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section>
|
||||
<h1>{{ $options.USAGE_QUOTAS_TITLE }}</h1>
|
||||
<p data-testid="usage-quotas-page-subtitle">
|
||||
<gl-sprintf :message="$options.USAGE_QUOTAS_SUBTITLE">
|
||||
<template #namespaceName>
|
||||
<strong>
|
||||
{{ namespaceName }}
|
||||
</strong>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
<gl-tabs>
|
||||
<gl-tab title="Storage"> {{ placeholder }} </gl-tab>
|
||||
</gl-tabs>
|
||||
</section>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { s__ } from '~/locale';
|
||||
|
||||
export const USAGE_QUOTAS_TITLE = s__('UsageQuota|Usage Quotas');
|
||||
|
||||
export const USAGE_QUOTAS_SUBTITLE = s__(
|
||||
'UsageQuota|Usage of group resources across the projects in the %{namespaceName} group',
|
||||
);
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import Vue from 'vue';
|
||||
import UsageQuotasApp from './components/usage_quotas_app.vue';
|
||||
|
||||
export default () => {
|
||||
const el = document.getElementById('js-usage-quotas-view');
|
||||
|
||||
if (!el) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { namespaceName } = el.dataset;
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
name: 'UsageQuotasView',
|
||||
provide: {
|
||||
namespaceName,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(UsageQuotasApp);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -1,8 +1,16 @@
|
|||
import { __ } from '~/locale';
|
||||
import { __, s__ } from '~/locale';
|
||||
|
||||
export const TOGGLE_TEXT = __('Search for a group');
|
||||
export const HEADER_TEXT = __('Select a group');
|
||||
export const RESET_LABEL = __('Reset');
|
||||
export const QUERY_TOO_SHORT_MESSAGE = __('Enter at least three characters to search.');
|
||||
|
||||
// Groups
|
||||
export const GROUP_TOGGLE_TEXT = __('Search for a group');
|
||||
export const GROUP_HEADER_TEXT = __('Select a group');
|
||||
export const FETCH_GROUPS_ERROR = __('Unable to fetch groups. Reload the page to try again.');
|
||||
export const FETCH_GROUP_ERROR = __('Unable to fetch group. Reload the page to try again.');
|
||||
export const QUERY_TOO_SHORT_MESSAGE = __('Enter at least three characters to search.');
|
||||
|
||||
// Projects
|
||||
export const PROJECT_TOGGLE_TEXT = s__('ProjectSelect|Search for project');
|
||||
export const PROJECT_HEADER_TEXT = s__('ProjectSelect|Select a project');
|
||||
export const FETCH_PROJECTS_ERROR = __('Unable to fetch projects. Reload the page to try again.');
|
||||
export const FETCH_PROJECT_ERROR = __('Unable to fetch project. Reload the page to try again.');
|
||||
|
|
|
|||
|
|
@ -5,7 +5,12 @@ import axios from '~/lib/utils/axios_utils';
|
|||
import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
|
||||
import Api, { DEFAULT_PER_PAGE } from '~/api';
|
||||
import { groupsPath } from './utils';
|
||||
import { TOGGLE_TEXT, HEADER_TEXT, FETCH_GROUPS_ERROR, FETCH_GROUP_ERROR } from './constants';
|
||||
import {
|
||||
GROUP_TOGGLE_TEXT,
|
||||
GROUP_HEADER_TEXT,
|
||||
FETCH_GROUPS_ERROR,
|
||||
FETCH_GROUP_ERROR,
|
||||
} from './constants';
|
||||
import EntitySelect from './entity_select.vue';
|
||||
|
||||
export default {
|
||||
|
|
@ -58,7 +63,7 @@ export default {
|
|||
let groups = [];
|
||||
let totalPages = 0;
|
||||
try {
|
||||
const { data, headers } = await axios.get(
|
||||
const { data = [], headers } = await axios.get(
|
||||
Api.buildUrl(groupsPath(this.groupsFilter, this.parentGroupID)),
|
||||
{
|
||||
params: {
|
||||
|
|
@ -68,7 +73,7 @@ export default {
|
|||
},
|
||||
},
|
||||
);
|
||||
groups = (data.length ? data : data.results || []).map((group) => ({
|
||||
groups = data.map((group) => ({
|
||||
...group,
|
||||
text: group.full_name,
|
||||
value: String(group.id),
|
||||
|
|
@ -99,8 +104,8 @@ export default {
|
|||
},
|
||||
},
|
||||
i18n: {
|
||||
toggleText: TOGGLE_TEXT,
|
||||
selectGroup: HEADER_TEXT,
|
||||
toggleText: GROUP_TOGGLE_TEXT,
|
||||
selectGroup: GROUP_HEADER_TEXT,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
import Vue from 'vue';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import ProjectSelect from './project_select.vue';
|
||||
|
||||
const SELECTOR = '.js-vue-project-select';
|
||||
|
||||
export const initProjectSelects = () => {
|
||||
if (process.env.NODE_ENV !== 'production' && document.querySelector(SELECTOR) === null) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`Attempted to initialize ProjectSelect but '${SELECTOR}' not found in the page`);
|
||||
}
|
||||
|
||||
document.querySelectorAll(SELECTOR).forEach((el) => {
|
||||
const { label, inputName, inputId, groupId, selected: initialSelection } = el.dataset;
|
||||
const clearable = parseBoolean(el.dataset.clearable);
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
name: 'ProjectSelectRoot',
|
||||
render(createElement) {
|
||||
return createElement(ProjectSelect, {
|
||||
props: {
|
||||
label,
|
||||
inputName,
|
||||
inputId,
|
||||
groupId,
|
||||
initialSelection,
|
||||
clearable,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
<script>
|
||||
import { GlAlert } from '@gitlab/ui';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import Api from '~/api';
|
||||
import {
|
||||
PROJECT_TOGGLE_TEXT,
|
||||
PROJECT_HEADER_TEXT,
|
||||
FETCH_PROJECTS_ERROR,
|
||||
FETCH_PROJECT_ERROR,
|
||||
} from './constants';
|
||||
import EntitySelector from './entity_select.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlAlert,
|
||||
EntitySelector,
|
||||
},
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
inputName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
inputId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
groupId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
initialSelection: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
clearable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
errorMessage: '',
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async fetchProjects(searchString = '') {
|
||||
let projects = [];
|
||||
try {
|
||||
const { data = [] } = await Api.groupProjects(this.groupId, searchString, {
|
||||
with_shared: true,
|
||||
include_subgroups: false,
|
||||
order_by: 'similarity',
|
||||
simple: true,
|
||||
});
|
||||
projects = data.map((item) => ({
|
||||
text: item.name_with_namespace || item.name,
|
||||
value: String(item.id),
|
||||
}));
|
||||
} catch (error) {
|
||||
this.handleError({ message: FETCH_PROJECTS_ERROR, error });
|
||||
}
|
||||
return { items: projects, totalPages: 1 };
|
||||
},
|
||||
async fetchProjectName(projectId) {
|
||||
let projectName = '';
|
||||
try {
|
||||
const { data: project } = await Api.project(projectId);
|
||||
projectName = project.name_with_namespace;
|
||||
} catch (error) {
|
||||
this.handleError({ message: FETCH_PROJECT_ERROR, error });
|
||||
}
|
||||
return projectName;
|
||||
},
|
||||
handleError({ message, error }) {
|
||||
Sentry.captureException(error);
|
||||
this.errorMessage = message;
|
||||
},
|
||||
dismissError() {
|
||||
this.errorMessage = '';
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
searchForProject: PROJECT_TOGGLE_TEXT,
|
||||
selectProject: PROJECT_HEADER_TEXT,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<entity-selector
|
||||
:label="label"
|
||||
:input-name="inputName"
|
||||
:input-id="inputId"
|
||||
:initial-selection="initialSelection"
|
||||
:clearable="clearable"
|
||||
:header-text="$options.i18n.selectProject"
|
||||
:default-toggle-text="$options.i18n.searchForProject"
|
||||
:fetch-items="fetchProjects"
|
||||
:fetch-initial-selection-text="fetchProjectName"
|
||||
>
|
||||
<template #error>
|
||||
<gl-alert v-if="errorMessage" class="gl-mb-3" variant="danger" @dismiss="dismissError">{{
|
||||
errorMessage
|
||||
}}</gl-alert>
|
||||
</template>
|
||||
</entity-selector>
|
||||
</template>
|
||||
|
|
@ -36,6 +36,24 @@ input[type='number'].hide-spinners {
|
|||
}
|
||||
/* stylelint-enable property-no-vendor-prefix */
|
||||
|
||||
/**
|
||||
* When form input type is search, browsers add a clear input button inside
|
||||
* the input field. This overlaps with the input field we have already added.
|
||||
*/
|
||||
|
||||
/* stylelint-disable property-no-vendor-prefix */
|
||||
input[type='search'] {
|
||||
-webkit-appearance: textfield;
|
||||
-moz-appearance: textfield;
|
||||
appearance: textfield;
|
||||
|
||||
&::-webkit-search-cancel-button,
|
||||
&::-webkit-search-results-button {
|
||||
@include gl-display-none;
|
||||
}
|
||||
}
|
||||
/* stylelint-enable property-no-vendor-prefix */
|
||||
|
||||
.datetime-controls {
|
||||
select {
|
||||
width: 100px;
|
||||
|
|
|
|||
|
|
@ -324,13 +324,20 @@ $border-radius-medium: 3px;
|
|||
}
|
||||
}
|
||||
|
||||
// Disable Webkit's search input styles
|
||||
/**
|
||||
* When form input type is search, browsers add a clear input button inside
|
||||
* the input field. This overlaps with the input field we have already added.
|
||||
*/
|
||||
|
||||
/* stylelint-disable property-no-vendor-prefix */
|
||||
input[type='search'] {
|
||||
/* stylelint-disable-next-line property-no-vendor-prefix */
|
||||
-webkit-appearance: textfield;
|
||||
-moz-appearance: textfield;
|
||||
appearance: textfield;
|
||||
|
||||
&::-webkit-search-cancel-button,
|
||||
&::-webkit-search-results-button {
|
||||
@include gl-display-none;
|
||||
}
|
||||
}
|
||||
/* stylelint-enable property-no-vendor-prefix */
|
||||
|
|
|
|||
|
|
@ -761,6 +761,9 @@ input {
|
|||
color: #ececef;
|
||||
background-color: #333238;
|
||||
}
|
||||
input[type="search"] {
|
||||
appearance: textfield;
|
||||
}
|
||||
.form-control {
|
||||
border-radius: 4px;
|
||||
padding: 6px 10px;
|
||||
|
|
|
|||
|
|
@ -761,6 +761,9 @@ input {
|
|||
color: #333238;
|
||||
background-color: #fff;
|
||||
}
|
||||
input[type="search"] {
|
||||
appearance: textfield;
|
||||
}
|
||||
.form-control {
|
||||
border-radius: 4px;
|
||||
padding: 6px 10px;
|
||||
|
|
|
|||
|
|
@ -7,9 +7,11 @@ module Analytics
|
|||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
extend ::Gitlab::Utils::Override
|
||||
include CycleAnalyticsParams
|
||||
|
||||
before_action :validate_params, only: %i[median]
|
||||
before_action :validate_params, except: %i[index]
|
||||
before_action :authorize_stage, except: %i[index]
|
||||
end
|
||||
|
||||
def index
|
||||
|
|
@ -44,11 +46,11 @@ module Analytics
|
|||
|
||||
private
|
||||
|
||||
def parent
|
||||
def namespace
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def value_stream_class
|
||||
def authorize_stage
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
|
|
@ -64,7 +66,7 @@ module Analytics
|
|||
end
|
||||
|
||||
def stage
|
||||
@stage ||= ::Analytics::CycleAnalytics::StageFinder.new(parent: parent, stage_id: params[:id]).execute
|
||||
@stage ||= ::Analytics::CycleAnalytics::StageFinder.new(parent: namespace, stage_id: params[:id]).execute
|
||||
end
|
||||
|
||||
def data_collector
|
||||
|
|
@ -75,7 +77,7 @@ module Analytics
|
|||
end
|
||||
|
||||
def value_stream
|
||||
@value_stream ||= value_stream_class.build_default_value_stream(parent)
|
||||
@value_stream ||= Analytics::CycleAnalytics::ValueStream.build_default_value_stream(namespace)
|
||||
end
|
||||
|
||||
def list_params
|
||||
|
|
@ -83,7 +85,7 @@ module Analytics
|
|||
end
|
||||
|
||||
def list_service
|
||||
Analytics::CycleAnalytics::Stages::ListService.new(parent: parent, current_user: current_user, params: list_params)
|
||||
Analytics::CycleAnalytics::Stages::ListService.new(parent: namespace, current_user: current_user, params: list_params)
|
||||
end
|
||||
|
||||
def cycle_analytics_configuration(stages)
|
||||
|
|
@ -94,3 +96,5 @@ module Analytics
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
Analytics::CycleAnalytics::StageActions.prepend_mod_with('Analytics::CycleAnalytics::StageActions')
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ module Groups
|
|||
class UsageQuotasController < Groups::ApplicationController
|
||||
before_action :authorize_read_usage_quotas!
|
||||
before_action :verify_usage_quotas_enabled!
|
||||
before_action :push_frontend_feature_flags
|
||||
|
||||
feature_category :subscription_cost_management
|
||||
urgency :low
|
||||
|
|
@ -15,6 +16,10 @@ module Groups
|
|||
|
||||
private
|
||||
|
||||
def push_frontend_feature_flags
|
||||
push_frontend_feature_flag(:usage_quotas_for_all_editions, @group)
|
||||
end
|
||||
|
||||
def verify_usage_quotas_enabled!
|
||||
render_404 unless group.usage_quotas_enabled?
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
class Projects::Analytics::CycleAnalytics::StagesController < Projects::ApplicationController
|
||||
include ::Analytics::CycleAnalytics::StageActions
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
extend ::Gitlab::Utils::Override
|
||||
|
||||
respond_to :json
|
||||
|
||||
|
|
@ -11,20 +10,14 @@ class Projects::Analytics::CycleAnalytics::StagesController < Projects::Applicat
|
|||
|
||||
before_action :authorize_read_cycle_analytics!
|
||||
before_action :only_default_value_stream_is_allowed!
|
||||
before_action :authorize_stage!, only: [:median, :count, :average, :records]
|
||||
|
||||
urgency :low
|
||||
|
||||
private
|
||||
|
||||
override :parent
|
||||
def parent
|
||||
@project
|
||||
end
|
||||
|
||||
override :value_stream_class
|
||||
def value_stream_class
|
||||
Analytics::CycleAnalytics::ProjectValueStream
|
||||
override :namespace
|
||||
def namespace
|
||||
@project.project_namespace
|
||||
end
|
||||
|
||||
override :cycle_analytics_configuration
|
||||
|
|
@ -33,7 +26,9 @@ class Projects::Analytics::CycleAnalytics::StagesController < Projects::Applicat
|
|||
end
|
||||
|
||||
def only_default_value_stream_is_allowed!
|
||||
render_404 if params[:value_stream_id] != Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME
|
||||
return if requests_default_value_stream?
|
||||
|
||||
render_403
|
||||
end
|
||||
|
||||
def permitted_stage?(stage)
|
||||
|
|
@ -42,11 +37,20 @@ class Projects::Analytics::CycleAnalytics::StagesController < Projects::Applicat
|
|||
|
||||
def permissions
|
||||
strong_memoize(:permissions) do
|
||||
Gitlab::CycleAnalytics::Permissions.new(user: current_user, project: parent).get
|
||||
Gitlab::CycleAnalytics::Permissions.new(user: current_user, project: @project).get
|
||||
end
|
||||
end
|
||||
|
||||
def authorize_stage!
|
||||
def authorize_stage
|
||||
render_403 unless permitted_stage?(stage)
|
||||
end
|
||||
|
||||
def requests_default_value_stream?
|
||||
default_name = Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME
|
||||
|
||||
params[:value_stream_id] == default_name
|
||||
end
|
||||
end
|
||||
|
||||
mod = 'Projects::Analytics::CycleAnalytics::StagesController'
|
||||
Projects::Analytics::CycleAnalytics::StagesController.prepend_mod_with(mod) # rubocop: disable Cop/InjectEnterpriseEditionModule
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@
|
|||
# search: string
|
||||
# include_subgroups: boolean
|
||||
# ids: int[]
|
||||
# with_issues_enabled: boolean
|
||||
# with_merge_requests_enabled: boolean
|
||||
#
|
||||
module Namespaces
|
||||
class ProjectsFinder
|
||||
|
|
@ -30,7 +32,9 @@ module Namespaces
|
|||
namespace.projects.with_route
|
||||
end
|
||||
|
||||
filter_projects(collection)
|
||||
collection = filter_projects(collection)
|
||||
|
||||
sort(collection)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
@ -39,7 +43,8 @@ module Namespaces
|
|||
|
||||
def filter_projects(collection)
|
||||
collection = by_ids(collection)
|
||||
by_similarity(collection)
|
||||
collection = by_similarity(collection)
|
||||
by_feature_availability(collection)
|
||||
end
|
||||
|
||||
def by_ids(items)
|
||||
|
|
@ -51,11 +56,26 @@ module Namespaces
|
|||
def by_similarity(items)
|
||||
return items unless params[:search].present?
|
||||
|
||||
if params[:sort] == :similarity
|
||||
items = items.sorted_by_similarity_desc(params[:search], include_in_select: true)
|
||||
items.merge(Project.search(params[:search]))
|
||||
end
|
||||
|
||||
def by_feature_availability(items)
|
||||
items = items.with_issues_available_for_user(current_user) if params[:with_issues_enabled].present?
|
||||
if params[:with_merge_requests_enabled].present?
|
||||
items = items.with_merge_requests_available_for_user(current_user)
|
||||
end
|
||||
|
||||
items.merge(Project.search(params[:search]))
|
||||
items
|
||||
end
|
||||
|
||||
def sort(items)
|
||||
return items.projects_order_id_desc unless params[:sort]
|
||||
|
||||
if params[:sort] == :similarity && params[:search].present?
|
||||
return items.sorted_by_similarity_desc(params[:search], include_in_select: true)
|
||||
end
|
||||
|
||||
items.sort_by_attribute(params[:sort])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -56,11 +56,7 @@ class ProjectsFinder < UnionFinder
|
|||
collection = Project.wrap_with_cte(collection) if use_cte
|
||||
collection = filter_projects(collection)
|
||||
|
||||
if params[:sort] == 'similarity' && params[:search]
|
||||
collection.sorted_by_similarity_desc(params[:search])
|
||||
else
|
||||
sort(collection)
|
||||
end
|
||||
sort(collection)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
@ -90,6 +86,7 @@ class ProjectsFinder < UnionFinder
|
|||
collection = by_last_activity_after(collection)
|
||||
collection = by_last_activity_before(collection)
|
||||
collection = by_language(collection)
|
||||
collection = by_feature_availability(collection)
|
||||
by_repository_storage(collection)
|
||||
end
|
||||
|
||||
|
|
@ -247,11 +244,13 @@ class ProjectsFinder < UnionFinder
|
|||
end
|
||||
|
||||
def sort(items)
|
||||
if params[:sort].present?
|
||||
items.sort_by_attribute(params[:sort])
|
||||
else
|
||||
items.projects_order_id_desc
|
||||
return items.projects_order_id_desc unless params[:sort]
|
||||
|
||||
if params[:sort] == 'similarity' && params[:search].present?
|
||||
return items.sorted_by_similarity_desc(params[:search], include_in_select: true)
|
||||
end
|
||||
|
||||
items.sort_by_attribute(params[:sort])
|
||||
end
|
||||
|
||||
def by_archived(projects)
|
||||
|
|
@ -270,6 +269,12 @@ class ProjectsFinder < UnionFinder
|
|||
end
|
||||
end
|
||||
|
||||
def by_feature_availability(items)
|
||||
items = items.with_issues_available_for_user(current_user) if params[:with_issues_enabled]
|
||||
items = items.with_merge_requests_available_for_user(current_user) if params[:with_merge_requests_enabled]
|
||||
items
|
||||
end
|
||||
|
||||
def finder_params
|
||||
return {} unless min_access_level?
|
||||
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ module Resolvers
|
|||
description: 'Include also subgroup projects.'
|
||||
|
||||
argument :search, GraphQL::Types::String,
|
||||
required: false,
|
||||
default_value: nil,
|
||||
description: 'Search project with most similar names or paths.'
|
||||
required: false,
|
||||
default_value: nil,
|
||||
description: 'Search project with most similar names or paths.'
|
||||
|
||||
argument :sort, Types::Projects::NamespaceProjectSortEnum,
|
||||
required: false,
|
||||
|
|
@ -22,6 +22,14 @@ module Resolvers
|
|||
default_value: nil,
|
||||
description: 'Filter projects by IDs.'
|
||||
|
||||
argument :with_issues_enabled, GraphQL::Types::Boolean,
|
||||
required: false,
|
||||
description: "Return only projects with issues enabled."
|
||||
|
||||
argument :with_merge_requests_enabled, GraphQL::Types::Boolean,
|
||||
required: false,
|
||||
description: "Return only projects with merge requests enabled."
|
||||
|
||||
type Types::ProjectType, null: true
|
||||
|
||||
def resolve(args)
|
||||
|
|
@ -54,7 +62,9 @@ module Resolvers
|
|||
include_subgroups: args.dig(:include_subgroups),
|
||||
sort: args.dig(:sort),
|
||||
search: args.dig(:search),
|
||||
ids: parse_gids(args.dig(:ids))
|
||||
ids: parse_gids(args.dig(:ids)),
|
||||
with_issues_enabled: args[:with_issues_enabled],
|
||||
with_merge_requests_enabled: args[:with_merge_requests_enabled]
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -15,14 +15,30 @@ module Resolvers
|
|||
description: "Sort order of results. Format: `<field_name>_<sort_direction>`, " \
|
||||
"for example: `id_desc` or `name_asc`"
|
||||
|
||||
argument :with_issues_enabled, GraphQL::Types::Boolean,
|
||||
required: false,
|
||||
description: "Return only projects with issues enabled."
|
||||
|
||||
argument :with_merge_requests_enabled, GraphQL::Types::Boolean,
|
||||
required: false,
|
||||
description: "Return only projects with merge requests enabled."
|
||||
|
||||
def resolve(**args)
|
||||
ProjectsFinder
|
||||
.new(current_user: current_user, params: project_finder_params(args), project_ids_relation: parse_gids(args[:ids]))
|
||||
.new(current_user: current_user, params: finder_params(args), project_ids_relation: parse_gids(args[:ids]))
|
||||
.execute
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def finder_params(args)
|
||||
{
|
||||
**project_finder_params(args),
|
||||
with_issues_enabled: args[:with_issues_enabled],
|
||||
with_merge_requests_enabled: args[:with_merge_requests_enabled]
|
||||
}
|
||||
end
|
||||
|
||||
def parse_gids(gids)
|
||||
gids&.map { |gid| GitlabSchema.parse_gid(gid, expected_type: ::Project).model_id }
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ module Types
|
|||
|
||||
value 'SIMILARITY', 'Most similar to the search query.', value: :similarity
|
||||
value 'STORAGE', 'Sort by storage size.', value: :storage
|
||||
value 'ACTIVITY_DESC', 'Sort by latest activity, in descending order.', value: :latest_activity_desc
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -17,24 +17,30 @@ module Emails
|
|||
email_sender = sender(
|
||||
@support_bot.id,
|
||||
send_from_user_email: false,
|
||||
sender_name: @project.service_desk_setting&.outgoing_name
|
||||
sender_name: @service_desk_setting&.outgoing_name,
|
||||
sender_email: service_desk_sender_email_address
|
||||
)
|
||||
options = service_desk_options(email_sender, 'thank_you', @issue.external_author)
|
||||
.merge(subject: "Re: #{subject_base}")
|
||||
|
||||
mail_new_thread(@issue, options)
|
||||
inject_service_desk_custom_email(mail_new_thread(@issue, options))
|
||||
end
|
||||
|
||||
def service_desk_new_note_email(issue_id, note_id, recipient)
|
||||
@note = Note.find(note_id)
|
||||
setup_service_desk_mail(issue_id)
|
||||
|
||||
email_sender = sender(@note.author_id)
|
||||
email_sender = sender(
|
||||
@note.author_id,
|
||||
send_from_user_email: false,
|
||||
sender_email: service_desk_sender_email_address
|
||||
)
|
||||
|
||||
add_uploads_as_attachments if Feature.enabled?(:service_desk_new_note_email_native_attachments, @note.project)
|
||||
options = service_desk_options(email_sender, 'new_note', recipient)
|
||||
.merge(subject: subject_base)
|
||||
|
||||
mail_answer_thread(@issue, options)
|
||||
inject_service_desk_custom_email(mail_answer_thread(@issue, options))
|
||||
end
|
||||
|
||||
private
|
||||
|
|
@ -44,6 +50,8 @@ module Emails
|
|||
@project = @issue.project
|
||||
@support_bot = User.support_bot
|
||||
|
||||
@service_desk_setting = @project.service_desk_setting
|
||||
|
||||
@sent_notification = SentNotification.record(@issue, @support_bot.id, reply_key)
|
||||
end
|
||||
|
||||
|
|
@ -59,6 +67,22 @@ module Emails
|
|||
end
|
||||
end
|
||||
|
||||
def inject_service_desk_custom_email(mail)
|
||||
return mail unless service_desk_custom_email_enabled?
|
||||
|
||||
mail.delivery_method(::Mail::SMTP, @service_desk_setting.custom_email_delivery_options)
|
||||
end
|
||||
|
||||
def service_desk_custom_email_enabled?
|
||||
Feature.enabled?(:service_desk_custom_email, @project) && @service_desk_setting&.custom_email_enabled?
|
||||
end
|
||||
|
||||
def service_desk_sender_email_address
|
||||
return unless service_desk_custom_email_enabled?
|
||||
|
||||
@service_desk_setting.custom_email
|
||||
end
|
||||
|
||||
def template_content(email_type)
|
||||
template = Gitlab::Template::ServiceDeskTemplate.find(email_type, @project)
|
||||
text = substitute_template_replacements(template.content)
|
||||
|
|
|
|||
|
|
@ -68,14 +68,16 @@ class Notify < ApplicationMailer
|
|||
private
|
||||
|
||||
# Return an email address that displays the name of the sender.
|
||||
# Only the displayed name changes; the actual email address is always the same.
|
||||
def sender(sender_id, send_from_user_email: false, sender_name: nil)
|
||||
# Override sender_email if you want to hard replace the sender address (e.g. custom email for Service Desk)
|
||||
def sender(sender_id, send_from_user_email: false, sender_name: nil, sender_email: nil)
|
||||
return unless sender = User.find(sender_id)
|
||||
|
||||
address = default_sender_address
|
||||
address.display_name = sender_name.presence || "#{sender.name} (#{sender.to_reference})"
|
||||
|
||||
if send_from_user_email && can_send_from_user_email?(sender)
|
||||
if sender_email
|
||||
address.address = sender_email
|
||||
elsif send_from_user_email && can_send_from_user_email?(sender)
|
||||
address.address = sender.email
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Analytics
|
||||
module CycleAnalytics
|
||||
class Stage < ApplicationRecord
|
||||
self.table_name = :analytics_cycle_analytics_group_stages
|
||||
|
||||
include DatabaseEventTracking
|
||||
include Analytics::CycleAnalytics::Stageable
|
||||
include Analytics::CycleAnalytics::Parentable
|
||||
|
||||
validates :name, uniqueness: { scope: [:group_id, :group_value_stream_id] }
|
||||
belongs_to :value_stream, class_name: 'Analytics::CycleAnalytics::ValueStream',
|
||||
foreign_key: :group_value_stream_id, inverse_of: :stages
|
||||
|
||||
alias_attribute :parent, :namespace
|
||||
alias_attribute :parent_id, :group_id
|
||||
alias_attribute :value_stream_id, :group_value_stream_id
|
||||
|
||||
def self.distinct_stages_within_hierarchy(namespace)
|
||||
with_preloaded_labels
|
||||
.where(group_id: namespace.self_and_descendants.select(:id))
|
||||
.select("DISTINCT ON(stage_event_hash_id) #{quoted_table_name}.*")
|
||||
end
|
||||
|
||||
SNOWPLOW_ATTRIBUTES = %i[
|
||||
id
|
||||
created_at
|
||||
updated_at
|
||||
relative_position
|
||||
start_event_identifier
|
||||
end_event_identifier
|
||||
group_id
|
||||
start_event_label_id
|
||||
end_event_label_id
|
||||
hidden
|
||||
custom
|
||||
name
|
||||
group_value_stream_id
|
||||
].freeze
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -4,6 +4,7 @@ module Analytics
|
|||
module CycleAnalytics
|
||||
class StageEventHash < ApplicationRecord
|
||||
has_many :cycle_analytics_project_stages, class_name: 'Analytics::CycleAnalytics::ProjectStage', inverse_of: :stage_event_hash
|
||||
has_many :cycle_analytics_stages, class_name: 'Analytics::CycleAnalytics::Stage', inverse_of: :stage_event_hash
|
||||
|
||||
validates :hash_sha256, presence: true
|
||||
|
||||
|
|
@ -33,10 +34,14 @@ module Analytics
|
|||
end
|
||||
|
||||
def self.unused_hashes_for(id)
|
||||
exists_query = Analytics::CycleAnalytics::ProjectStage.where(stage_event_hash_id: id).select('1').limit(1)
|
||||
where.not('EXISTS (?)', exists_query)
|
||||
project_stage_exists_query = Analytics::CycleAnalytics::ProjectStage.where(stage_event_hash_id: id).select('1').limit(1)
|
||||
stage_exists_query = ::Analytics::CycleAnalytics::Stage.where(stage_event_hash_id: id).select('1').limit(1)
|
||||
|
||||
where
|
||||
.not('EXISTS (?)', project_stage_exists_query)
|
||||
.where
|
||||
.not('EXISTS (?)', stage_exists_query)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Analytics::CycleAnalytics::StageEventHash.prepend_mod_with('Analytics::CycleAnalytics::StageEventHash')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Analytics
|
||||
module CycleAnalytics
|
||||
class ValueStream < ApplicationRecord
|
||||
self.table_name = :analytics_cycle_analytics_group_value_streams
|
||||
|
||||
include Analytics::CycleAnalytics::Parentable
|
||||
|
||||
has_many :stages, -> { ordered },
|
||||
class_name: 'Analytics::CycleAnalytics::Stage',
|
||||
foreign_key: :group_value_stream_id,
|
||||
index_errors: true,
|
||||
inverse_of: :value_stream
|
||||
|
||||
validates :name, presence: true
|
||||
validates :name, length: { minimum: 3, maximum: 100, allow_nil: false }, uniqueness: { scope: :group_id }
|
||||
|
||||
accepts_nested_attributes_for :stages, allow_destroy: true
|
||||
|
||||
scope :preload_associated_models, -> {
|
||||
includes(:namespace,
|
||||
stages: [
|
||||
:namespace,
|
||||
:end_event_label,
|
||||
:start_event_label
|
||||
])
|
||||
}
|
||||
|
||||
after_save :ensure_aggregation_record_presence
|
||||
|
||||
def custom?
|
||||
persisted? || name != Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME
|
||||
end
|
||||
|
||||
def self.build_default_value_stream(namespace)
|
||||
new(name: Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME, namespace: namespace)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_aggregation_record_presence
|
||||
Analytics::CycleAnalytics::Aggregation.safe_create_for_namespace(namespace)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -85,6 +85,8 @@ class Namespace < ApplicationRecord
|
|||
has_many :timelog_categories, class_name: 'TimeTracking::TimelogCategory'
|
||||
has_many :achievements, class_name: 'Achievements::Achievement'
|
||||
has_many :namespace_commit_emails, class_name: 'Users::NamespaceCommitEmail'
|
||||
has_many :cycle_analytics_stages, class_name: 'Analytics::CycleAnalytics::Stage', foreign_key: :group_id, inverse_of: :namespace
|
||||
has_many :value_streams, class_name: 'Analytics::CycleAnalytics::ValueStream', foreign_key: :group_id, inverse_of: :namespace
|
||||
|
||||
validates :owner, presence: true, if: ->(n) { n.owner_required? }
|
||||
validates :name,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,14 @@
|
|||
class ServiceDeskSetting < ApplicationRecord
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
attribute :custom_email_enabled, default: false
|
||||
attr_encrypted :custom_email_smtp_password,
|
||||
mode: :per_attribute_iv,
|
||||
algorithm: 'aes-256-gcm',
|
||||
key: Settings.attr_encrypted_db_key_base_32,
|
||||
encode: false,
|
||||
encode_iv: false
|
||||
|
||||
belongs_to :project
|
||||
validates :project_id, presence: true
|
||||
validate :valid_issue_template
|
||||
|
|
@ -13,8 +21,42 @@ class ServiceDeskSetting < ApplicationRecord
|
|||
allow_blank: true,
|
||||
format: { with: /\A[a-z0-9_]+\z/, message: -> (setting, data) { _("can contain only lowercase letters, digits, and '_'.") } }
|
||||
|
||||
validates :custom_email,
|
||||
length: { maximum: 255 },
|
||||
uniqueness: true,
|
||||
allow_nil: true,
|
||||
format: /\A[\w\-._]+@[\w\-.]+\.{1}[a-zA-Z]{2,}\z/
|
||||
validates :custom_email_smtp_address, length: { maximum: 255 }
|
||||
validates :custom_email_smtp_username, length: { maximum: 255 }
|
||||
|
||||
validates :custom_email,
|
||||
presence: true,
|
||||
devise_email: true,
|
||||
if: :custom_email_enabled?
|
||||
validates :custom_email_smtp_address,
|
||||
presence: true,
|
||||
hostname: { allow_numeric_hostname: true, require_valid_tld: true },
|
||||
if: :custom_email_enabled?
|
||||
validates :custom_email_smtp_username,
|
||||
presence: true,
|
||||
if: :custom_email_enabled?
|
||||
validates :custom_email_smtp_port,
|
||||
presence: true,
|
||||
numericality: { only_integer: true, greater_than: 0 },
|
||||
if: :custom_email_enabled?
|
||||
|
||||
scope :with_project_key, ->(key) { where(project_key: key) }
|
||||
|
||||
def custom_email_delivery_options
|
||||
{
|
||||
user_name: custom_email_smtp_username,
|
||||
password: custom_email_smtp_password,
|
||||
address: custom_email_smtp_address,
|
||||
domain: Mail::Address.new(custom_email).domain,
|
||||
port: custom_email_smtp_port || 587
|
||||
}
|
||||
end
|
||||
|
||||
def issue_template_content
|
||||
strong_memoize(:issue_template_content) do
|
||||
next unless issue_template_key.present?
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ class Todo < ApplicationRecord
|
|||
end
|
||||
|
||||
def resource_parent
|
||||
project
|
||||
project || group
|
||||
end
|
||||
|
||||
def unmergeable?
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ module Analytics
|
|||
end
|
||||
|
||||
def value_stream
|
||||
@value_stream ||= params[:value_stream]
|
||||
@value_stream ||= params.fetch(:value_stream)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ module Analytics
|
|||
private
|
||||
|
||||
def allowed?
|
||||
can?(current_user, :read_cycle_analytics, parent)
|
||||
can?(current_user, :read_cycle_analytics, parent.project)
|
||||
end
|
||||
|
||||
def success(stages)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
%fieldset
|
||||
.gl-form-group
|
||||
%span.form-text.gl-mb-3.gl-mt-0
|
||||
= _('If no options are selected, only administrators can register runners.')
|
||||
= s_('Runners|If both settings are disabled, new runners cannot be registered.')
|
||||
= link_to _('Learn more.'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'restrict-runner-registration-by-all-users-in-an-instance'), target: '_blank', rel: 'noopener noreferrer'
|
||||
= hidden_field_tag "application_setting[valid_runner_registrars][]", nil
|
||||
- ApplicationSetting::VALID_RUNNER_REGISTRAR_TYPES.each do |type|
|
||||
|
|
|
|||
|
|
@ -9,13 +9,18 @@
|
|||
- if current_user.two_factor_otp_enabled?
|
||||
.row.gl-mb-3
|
||||
.col-md-5
|
||||
%button#js-setup-token-2fa-device.gl-button.btn.btn-confirm= _("Set up new device")
|
||||
= render Pajamas::ButtonComponent.new(variant: :confirm,
|
||||
button_options: { id: 'js-setup-token-2fa-device' }) do
|
||||
= _("Set up new device")
|
||||
.col-md-7
|
||||
%p= _("Your device needs to be set up. Plug it in (if needed) and click the button on the left.")
|
||||
- else
|
||||
.row.gl-mb-3
|
||||
.col-md-4
|
||||
%button#js-setup-token-2fa-device.gl-button.btn.btn-confirm.btn-block{ disabled: true }= _("Set up new device")
|
||||
= render Pajamas::ButtonComponent.new(variant: :confirm,
|
||||
disabled: true,
|
||||
button_options: { id: 'js-setup-token-2fa-device' }) do
|
||||
= _("Set up new device")
|
||||
.col-md-8
|
||||
%p= _("You need to register a two-factor authentication app before you can set up a device.")
|
||||
|
||||
|
|
@ -24,7 +29,8 @@
|
|||
%div
|
||||
%p
|
||||
%span <%= error_message %> (<%= error_name %>)
|
||||
%a.btn.btn-default.gl-button#js-token-2fa-try-again= _("Try again?")
|
||||
= render Pajamas::ButtonComponent.new(button_options: { id: 'js-token-2fa-try-again' }) do
|
||||
= _("Try again?")
|
||||
|
||||
-# haml-lint:disable InlineJavaScript
|
||||
%script#js-register-token-2fa-registered{ type: "text/template" }
|
||||
|
|
@ -37,4 +43,5 @@
|
|||
= text_field_tag 'device_registration[name]', nil, class: 'form-control', placeholder: _("Pick a name")
|
||||
.col-md-3
|
||||
= hidden_field_tag 'device_registration[device_response]', nil, class: 'form-control', required: true, id: "js-device-response"
|
||||
= submit_tag _("Register device"), class: "gl-button btn btn-confirm"
|
||||
= render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm) do
|
||||
= _("Register device")
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
- page_title s_("UsageQuota|Usage")
|
||||
|
||||
.gl-alert.gl-alert-no-icon.gl-alert-info.gl-mt-6
|
||||
%h2.gl-alert-title
|
||||
Development
|
||||
.gl-alert-content
|
||||
Placeholder for usage quotas Vue app
|
||||
#js-usage-quotas-view{ data: { namespace_name: @group.name } }
|
||||
|
|
|
|||
|
|
@ -125,7 +125,12 @@
|
|||
%span.gl-text-gray-500
|
||||
= _("no name set")
|
||||
%td= registration[:created_at].to_date.to_s(:medium)
|
||||
%td= link_to _('Delete'), registration[:delete_path], method: :delete, class: "gl-button btn btn-danger float-right", data: { confirm: _('Are you sure you want to delete this device? This action cannot be undone.'), confirm_btn_variant: "danger" }, aria: { label: _('Delete') }
|
||||
%td
|
||||
= render Pajamas::ButtonComponent.new(variant: :danger,
|
||||
href: registration[:delete_path],
|
||||
method: :delete,
|
||||
button_options: { class: 'float-right', data: { confirm: _('Are you sure you want to delete this device? This action cannot be undone.'), confirm_btn_variant: "danger" }, aria: { label: _('Delete') }}) do
|
||||
= _('Delete')
|
||||
|
||||
- else
|
||||
.settings-message.text-center
|
||||
|
|
|
|||
|
|
@ -6,9 +6,4 @@
|
|||
- graph_path = project_graph_path(@project, current_ref, format: :json)
|
||||
- commits_path = project_commits_path(@project, current_ref)
|
||||
|
||||
.sub-header-block.gl-bg-gray-10.gl-p-5
|
||||
.tree-ref-holder.gl-display-inline-block.gl-vertical-align-middle.gl-mr-3>
|
||||
= render 'shared/ref_switcher', destination: 'graphs'
|
||||
= link_to s_('Commits|History'), commits_path, class: 'btn gl-button btn-default'
|
||||
|
||||
.js-contributors-graph{ class: container_class, data: { project_graph_path: graph_path, project_branch: current_ref, default_branch: @project.default_branch } }
|
||||
.js-contributors-graph{ class: container_class, data: { project_graph_path: graph_path, project_branch: current_ref, default_branch: @project.default_branch, project_id: @project.id, commits_path: commits_path } }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
name: service_desk_custom_email
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/108017
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/329990
|
||||
milestone: '15.9'
|
||||
type: development
|
||||
group: group::incubation
|
||||
default_enabled: false
|
||||
log_state_changes: true
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddSmtpCredentialsToServiceDeskSettings < Gitlab::Database::Migration[2.1]
|
||||
def up
|
||||
# rubocop:disable Migration/AddLimitToTextColumns
|
||||
# limit is added in 20230102131100_add_text_limits_to_smtp_credentials_on_service_desk_settings.rb
|
||||
add_column :service_desk_settings, :custom_email_enabled, :boolean, default: false, null: false
|
||||
# Unique constraint/index is added in 20230102131050_add_unique_constraint_for_custom_email_to_...
|
||||
add_column :service_desk_settings, :custom_email, :text
|
||||
add_column :service_desk_settings, :custom_email_smtp_address, :text
|
||||
add_column :service_desk_settings, :custom_email_smtp_port, :integer
|
||||
add_column :service_desk_settings, :custom_email_smtp_username, :text
|
||||
# Encrypted attribute via attr_encrypted needs these two columns
|
||||
add_column :service_desk_settings, :encrypted_custom_email_smtp_password, :binary
|
||||
add_column :service_desk_settings, :encrypted_custom_email_smtp_password_iv, :binary
|
||||
# rubocop:enable Migration/AddLimitToTextColumns
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column :service_desk_settings, :custom_email_enabled
|
||||
remove_column :service_desk_settings, :custom_email
|
||||
remove_column :service_desk_settings, :custom_email_smtp_address
|
||||
remove_column :service_desk_settings, :custom_email_smtp_port
|
||||
remove_column :service_desk_settings, :custom_email_smtp_username
|
||||
remove_column :service_desk_settings, :encrypted_custom_email_smtp_password
|
||||
remove_column :service_desk_settings, :encrypted_custom_email_smtp_password_iv
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddUniqueConstraintForCustomEmailToServiceDeskSettings < Gitlab::Database::Migration[2.1]
|
||||
disable_ddl_transaction!
|
||||
|
||||
INDEX_NAME = 'custom_email_unique_constraint'
|
||||
|
||||
def up
|
||||
# Force custom_email to be unique instance-wide. This is neccessary because we will match
|
||||
# incoming service desk emails with a custom email by the custom_email field.
|
||||
# This also adds the corresponding index
|
||||
add_concurrent_index(:service_desk_settings, :custom_email, unique: true, name: INDEX_NAME)
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name(:service_desk_settings, INDEX_NAME)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddTextLimitsToSmtpCredentialsOnServiceDeskSettings < Gitlab::Database::Migration[2.1]
|
||||
MAXIMUM_LIMIT = 255
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_text_limit :service_desk_settings, :custom_email, MAXIMUM_LIMIT
|
||||
add_text_limit :service_desk_settings, :custom_email_smtp_address, MAXIMUM_LIMIT
|
||||
add_text_limit :service_desk_settings, :custom_email_smtp_username, MAXIMUM_LIMIT
|
||||
end
|
||||
|
||||
def down
|
||||
remove_text_limit :service_desk_settings, :custom_email
|
||||
remove_text_limit :service_desk_settings, :custom_email_smtp_address
|
||||
remove_text_limit :service_desk_settings, :custom_email_smtp_username
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# This is workaround for
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/388253. During a
|
||||
# zero-downtime upgrade, duplicate jobs cookies can fail to get deleted.
|
||||
# This post-deployment migration deletes all such cookies. This can
|
||||
# cause some jobs that normally would have been deduplicated to twice
|
||||
# instead of once.
|
||||
class ClearDuplicateJobsCookies < Gitlab::Database::Migration[2.1]
|
||||
disable_ddl_transaction!
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
|
||||
def up
|
||||
Gitlab::Redis::Queues.with do |redis| # rubocop:disable Cop/RedisQueueUsage
|
||||
redis.scan_each(match: "resque:gitlab:duplicate:*:cookie:v2").each_slice(100) do |keys|
|
||||
redis.del(keys)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def down; end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
13b992cf6f30efc7a82062c5184f3e8398704c01e73618c6dd38071ee67595e1
|
||||
|
|
@ -0,0 +1 @@
|
|||
4933fd938c23b99963542c2f7e1f50e0270f6817ce49b0864fc7bdad63ea98b3
|
||||
|
|
@ -0,0 +1 @@
|
|||
889e814bc9633481afeae8e63bfe080bfc956839fd5f97c0d39725f3acdff100
|
||||
|
|
@ -0,0 +1 @@
|
|||
f4ba0d1de73da2b7a912c06ca458898f3404235025089efc74aee9fc4caa511a
|
||||
|
|
@ -21807,7 +21807,17 @@ CREATE TABLE service_desk_settings (
|
|||
issue_template_key character varying(255),
|
||||
outgoing_name character varying(255),
|
||||
project_key character varying(255),
|
||||
file_template_project_id bigint
|
||||
file_template_project_id bigint,
|
||||
custom_email_enabled boolean DEFAULT false NOT NULL,
|
||||
custom_email text,
|
||||
custom_email_smtp_address text,
|
||||
custom_email_smtp_port integer,
|
||||
custom_email_smtp_username text,
|
||||
encrypted_custom_email_smtp_password bytea,
|
||||
encrypted_custom_email_smtp_password_iv bytea,
|
||||
CONSTRAINT check_57a79552e1 CHECK ((char_length(custom_email) <= 255)),
|
||||
CONSTRAINT check_b283637a9e CHECK ((char_length(custom_email_smtp_address) <= 255)),
|
||||
CONSTRAINT check_e3535d46ee CHECK ((char_length(custom_email_smtp_username) <= 255))
|
||||
);
|
||||
|
||||
CREATE TABLE shards (
|
||||
|
|
@ -28334,6 +28344,8 @@ CREATE UNIQUE INDEX commit_user_mentions_on_commit_id_and_note_id_unique_index O
|
|||
|
||||
CREATE INDEX composer_cache_files_index_on_deleted_at ON packages_composer_cache_files USING btree (delete_at, id);
|
||||
|
||||
CREATE UNIQUE INDEX custom_email_unique_constraint ON service_desk_settings USING btree (custom_email);
|
||||
|
||||
CREATE UNIQUE INDEX dast_scanner_profiles_builds_on_ci_build_id ON dast_scanner_profiles_builds USING btree (ci_build_id);
|
||||
|
||||
CREATE UNIQUE INDEX dast_site_profiles_builds_on_ci_build_id ON dast_site_profiles_builds USING btree (ci_build_id);
|
||||
|
|
|
|||
|
|
@ -798,20 +798,19 @@ incoming_email:
|
|||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214900) in GitLab 13.11.
|
||||
|
||||
GitLab can read incoming email using the Microsoft Graph API instead of
|
||||
IMAP. Because [Microsoft is deprecating IMAP usage with Basic Authentication](https://techcommunity.microsoft.com/t5/exchange-team-blog/announcing-oauth-2-0-support-for-imap-and-smtp-auth-protocols-in/ba-p/1330432), the Microsoft Graph API will soon be required for new Microsoft Exchange Online
|
||||
mailboxes.
|
||||
IMAP. Because [Microsoft is deprecating IMAP usage with Basic Authentication](https://techcommunity.microsoft.com/t5/exchange-team-blog/announcing-oauth-2-0-support-for-imap-and-smtp-auth-protocols-in/ba-p/1330432), the Microsoft Graph API is be required for new Microsoft Exchange Online mailboxes.
|
||||
|
||||
To configure GitLab for Microsoft Graph, you will need to register an
|
||||
OAuth2 application in your Azure Active Directory that has the
|
||||
To configure GitLab for Microsoft Graph, you need to register an
|
||||
OAuth 2.0 application in your Azure Active Directory that has the
|
||||
`Mail.ReadWrite` permission for all mailboxes. See the [MailRoom step-by-step guide](https://github.com/tpitale/mail_room/#microsoft-graph-configuration)
|
||||
and [Microsoft instructions](https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app)
|
||||
for more details.
|
||||
|
||||
Record the following when you configure your OAuth2 application:
|
||||
Record the following when you configure your OAuth 2.0 application:
|
||||
|
||||
- Tenant ID for your Azure Active Directory
|
||||
- Client ID for your OAuth2 application
|
||||
- Client secret your OAuth2 application
|
||||
- Client ID for your OAuth 2.0 application
|
||||
- Client secret your OAuth 2.0 application
|
||||
|
||||
##### Restrict mailbox access
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ replication failover requires:
|
|||
- A minimum of three PgBouncer nodes that track and handle primary database reads and writes.
|
||||
- An internal load balancer (TCP) to balance requests between the PgBouncer nodes.
|
||||
- [Database Load Balancing](database_load_balancing.md) enabled.
|
||||
- A local PgBouncer service configured on each PostgreSQL node. Note that this is separate from the main PgBouncer cluster that tracks the primary.
|
||||
- A local PgBouncer service configured on each PostgreSQL node. This is separate from the main PgBouncer cluster that tracks the primary.
|
||||
|
||||
```plantuml
|
||||
@startuml
|
||||
|
|
@ -356,7 +356,7 @@ If you enable Monitoring, it must be enabled on **all** database servers.
|
|||
|
||||
#### Enable TLS support for the Patroni API
|
||||
|
||||
By default, Patroni's [REST API](https://patroni.readthedocs.io/en/latest/rest_api.html#rest-api) is served over HTTP.
|
||||
By default, the Patroni [REST API](https://patroni.readthedocs.io/en/latest/rest_api.html#rest-api) is served over HTTP.
|
||||
You have the option to enable TLS and use HTTPS over the same [port](../package_information/defaults.md).
|
||||
|
||||
To enable TLS, you need PEM-formatted certificate and private key files. Both files must be readable by the PostgreSQL user (`gitlab-psql` by default, or the one set by `postgresql['username']`):
|
||||
|
|
@ -1009,7 +1009,7 @@ Here are a few key facts that you must consider before upgrading PostgreSQL:
|
|||
GitLab deployment is down for the duration of database upgrade or, at least, as long as your leader
|
||||
node is upgraded. This can be **a significant downtime depending on the size of your database**.
|
||||
|
||||
- Upgrading PostgreSQL creates a new data directory with a new control data. From Patroni's perspective this is a new cluster that needs to be bootstrapped again. Therefore, as part of the upgrade procedure, the cluster state (stored in Consul) is wiped out. After the upgrade is complete, Patroni bootstraps a new cluster. **This changes your _cluster ID_**.
|
||||
- Upgrading PostgreSQL creates a new data directory with a new control data. From the perspective of Petroni, this is a new cluster that needs to be bootstrapped again. Therefore, as part of the upgrade procedure, the cluster state (stored in Consul) is wiped out. After the upgrade is complete, Patroni bootstraps a new cluster. **This changes your _cluster ID_**.
|
||||
|
||||
- The procedures for upgrading leader and replicas are not the same. That is why it is important to use the right procedure on each node.
|
||||
|
||||
|
|
@ -1017,7 +1017,7 @@ Here are a few key facts that you must consider before upgrading PostgreSQL:
|
|||
configured replication method (`pg_basebackup` is the only available option). It might take some
|
||||
time for replica to catch up with the leader, depending on the size of your database.
|
||||
|
||||
- An overview of the upgrade procedure is outlined in [Patroni's documentation](https://patroni.readthedocs.io/en/latest/existing_data.html#major-upgrade-of-postgresql-version).
|
||||
- An overview of the upgrade procedure is outlined in [the Patroni documentation](https://patroni.readthedocs.io/en/latest/existing_data.html#major-upgrade-of-postgresql-version).
|
||||
You can still use `gitlab-ctl pg-upgrade` which implements this procedure with a few adjustments.
|
||||
|
||||
Considering these, you should carefully plan your PostgreSQL upgrade:
|
||||
|
|
@ -1322,7 +1322,7 @@ If a replica cannot start or rejoin the cluster, or when it lags behind and cann
|
|||
+-------------------------------------+--------------+---------+--------------+----+-----------+
|
||||
```
|
||||
|
||||
1. Sign in to the broken server and reinitialize the database and replication. Patroni will shut
|
||||
1. Sign in to the broken server and reinitialize the database and replication. Patroni shuts
|
||||
down PostgreSQL on that server, remove the data directory, and reinitialize it from scratch:
|
||||
|
||||
```shell
|
||||
|
|
@ -1330,7 +1330,7 @@ If a replica cannot start or rejoin the cluster, or when it lags behind and cann
|
|||
```
|
||||
|
||||
This can be run on any Patroni node, but be aware that `sudo gitlab-ctl patroni
|
||||
reinitialize-replica` without `--member` will reinitialize the server it is run on.
|
||||
reinitialize-replica` without `--member` restarts the server it is run on.
|
||||
It is recommended to run it locally on the broken server to reduce the risk of
|
||||
unintended data loss.
|
||||
1. Monitor the logs:
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ X-Gitlab-Event: System Hook
|
|||
}
|
||||
```
|
||||
|
||||
Note that `project_rename` is not triggered if the namespace changes.
|
||||
`project_rename` is not triggered if the namespace changes.
|
||||
Refer to `group_rename` and `user_rename` for that case.
|
||||
|
||||
**Project transferred:**
|
||||
|
|
|
|||
|
|
@ -15,12 +15,12 @@ Get all environments for a given project.
|
|||
GET /projects/:id/environments
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ------- | -------- |-------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `name` | string | no | Return the environment with this name. Mutually exclusive with `search` |
|
||||
| `search` | string | no | Return list of environments matching the search criteria. Mutually exclusive with `name`. Must be at least 3 characters long. |
|
||||
| `states` | string | no | List all environments that match a specific state. Accepted values: `available`, `stopping`, or `stopped`. If no state value given, returns all environments. |
|
||||
| Attribute | Type | Required | Description |
|
||||
|-----------|----------------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded](rest/index.md#namespaced-path-encoding) path of the project. |
|
||||
| `name` | string | no | Return the environment with this name. Mutually exclusive with `search`. |
|
||||
| `search` | string | no | Return list of environments matching the search criteria. Mutually exclusive with `name`. Must be at least 3 characters long. |
|
||||
| `states` | string | no | List all environments that match a specific state. Accepted values: `available`, `stopping`, or `stopped`. If no state value given, returns all environments. |
|
||||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/environments?name=review%2Ffix-foo"
|
||||
|
|
@ -133,10 +133,10 @@ Example response:
|
|||
GET /projects/:id/environments/:environment_id
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|-----------|---------|----------|---------------------|
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `environment_id` | integer | yes | The ID of the environment |
|
||||
| Attribute | Type | Required | Description |
|
||||
|------------------|----------------|----------|--------------------------------------------------------------------------------------|
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path](rest/index.md#namespaced-path-encoding) of the project. |
|
||||
| `environment_id` | integer | yes | The ID of the environment. |
|
||||
|
||||
```shell
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/environments/1"
|
||||
|
|
@ -252,12 +252,12 @@ It returns `201` if the environment was successfully created, `400` for wrong pa
|
|||
POST /projects/:id/environments
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| ------------- | ------- | -------- | ---------------------------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `name` | string | yes | The name of the environment |
|
||||
| `external_url` | string | no | Place to link to for this environment |
|
||||
| `tier` | string | no | The tier of the new environment. Allowed values are `production`, `staging`, `testing`, `development`, and `other` |
|
||||
| Attribute | Type | Required | Description |
|
||||
|----------------|----------------|----------|---------------------------------------------------------------------------------------------------------------------|
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path](rest/index.md#namespaced-path-encoding) of the project. |
|
||||
| `name` | string | yes | The name of the environment. |
|
||||
| `external_url` | string | no | Place to link to for this environment. |
|
||||
| `tier` | string | no | The tier of the new environment. Allowed values are `production`, `staging`, `testing`, `development`, and `other`. |
|
||||
|
||||
```shell
|
||||
curl --data "name=deploy&external_url=https://deploy.gitlab.example.com" \
|
||||
|
|
@ -289,13 +289,13 @@ It returns `200` if the environment was successfully updated. In case of an erro
|
|||
PUT /projects/:id/environments/:environments_id
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------------- | ------- | --------------------------------- | ------------------------------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `environment_id` | integer | yes | The ID of the environment |
|
||||
| `name` | string | no | [Deprecated and will be removed in GitLab 15.0](https://gitlab.com/gitlab-org/gitlab/-/issues/338897) |
|
||||
| `external_url` | string | no | The new `external_url` |
|
||||
| `tier` | string | no | The tier of the new environment. Allowed values are `production`, `staging`, `testing`, `development`, and `other` |
|
||||
| Attribute | Type | Required | Description |
|
||||
|------------------|----------------|----------|---------------------------------------------------------------------------------------------------------------------|
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). |
|
||||
| `environment_id` | integer | yes | The ID of the environment. |
|
||||
| `name` | string | no | [Deprecated and will be removed in GitLab 15.0](https://gitlab.com/gitlab-org/gitlab/-/issues/338897). |
|
||||
| `external_url` | string | no | The new `external_url`. |
|
||||
| `tier` | string | no | The tier of the new environment. Allowed values are `production`, `staging`, `testing`, `development`, and `other`. |
|
||||
|
||||
```shell
|
||||
curl --request PUT --data "name=staging&external_url=https://staging.gitlab.example.com" \
|
||||
|
|
@ -325,10 +325,10 @@ It returns `204` if the environment was successfully deleted, and `404` if the e
|
|||
DELETE /projects/:id/environments/:environment_id
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ------- | -------- | --------------------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `environment_id` | integer | yes | The ID of the environment |
|
||||
| Attribute | Type | Required | Description |
|
||||
|------------------|----------------|----------|--------------------------------------------------------------------------------------|
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path](rest/index.md#namespaced-path-encoding) of the project. |
|
||||
| `environment_id` | integer | yes | The ID of the environment. |
|
||||
|
||||
```shell
|
||||
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/environments/1"
|
||||
|
|
@ -348,12 +348,12 @@ By default, it only deletes environments 30 days or older. You can change this d
|
|||
DELETE /projects/:id/environments/review_apps
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ------- | -------- | --------------------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user. |
|
||||
| `before` | datetime | no | The date before which environments can be deleted. Defaults to 30 days ago. Expected in ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`). |
|
||||
| `limit` | integer | no | Maximum number of environments to delete. Defaults to 100. |
|
||||
| `dry_run` | boolean | no | Defaults to `true` for safety reasons. It performs a dry run where no actual deletion will be performed. Set to `false` to actually delete the environment. |
|
||||
| Attribute | Type | Required | Description |
|
||||
|-----------|----------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path](rest/index.md#namespaced-path-encoding) of the project. |
|
||||
| `before` | datetime | no | The date before which environments can be deleted. Defaults to 30 days ago. Expected in ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`). |
|
||||
| `limit` | integer | no | Maximum number of environments to delete. Defaults to 100. |
|
||||
| `dry_run` | boolean | no | Defaults to `true` for safety reasons. It performs a dry run where no actual deletion is performed. Set to `false` to actually delete the environment. |
|
||||
|
||||
```shell
|
||||
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/environments/review_apps"
|
||||
|
|
@ -389,11 +389,11 @@ It returns `200` if the environment was successfully stopped, and `404` if the e
|
|||
POST /projects/:id/environments/:environment_id/stop
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|------------------|----------------|----------|----------------------------------------------------------------------------------------------------------------|
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `environment_id` | integer | yes | The ID of the environment |
|
||||
| `force` | boolean | no | Force environment to stop without executing `on_stop` actions |
|
||||
| Attribute | Type | Required | Description |
|
||||
|------------------|----------------|----------|--------------------------------------------------------------------------------------|
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path](rest/index.md#namespaced-path-encoding) of the project. |
|
||||
| `environment_id` | integer | yes | The ID of the environment. |
|
||||
| `force` | boolean | no | Force environment to stop without executing `on_stop` actions. |
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/environments/1/stop"
|
||||
|
|
@ -423,7 +423,7 @@ POST /projects/:id/environments/stop_stale
|
|||
|
||||
| Attribute | Type | Required | Description |
|
||||
|-----------|----------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path](rest/index.md#namespaced-path-encoding) of the project. |
|
||||
| `before` | date | yes | Stop environments that have been modified or deployed to before the specified date. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). Valid inputs are between 10 years ago and 1 week ago |
|
||||
|
||||
```shell
|
||||
|
|
|
|||
|
|
@ -389,6 +389,8 @@ four standard [pagination arguments](#connection-pagination-arguments):
|
|||
| <a id="queryprojectssearchnamespaces"></a>`searchNamespaces` | [`Boolean`](#boolean) | Include namespace in project search. |
|
||||
| <a id="queryprojectssort"></a>`sort` | [`String`](#string) | Sort order of results. Format: `<field_name>_<sort_direction>`, for example: `id_desc` or `name_asc`. |
|
||||
| <a id="queryprojectstopics"></a>`topics` | [`[String!]`](#string) | Filter projects by topics. |
|
||||
| <a id="queryprojectswithissuesenabled"></a>`withIssuesEnabled` | [`Boolean`](#boolean) | Return only projects with issues enabled. |
|
||||
| <a id="queryprojectswithmergerequestsenabled"></a>`withMergeRequestsEnabled` | [`Boolean`](#boolean) | Return only projects with merge requests enabled. |
|
||||
|
||||
### `Query.queryComplexity`
|
||||
|
||||
|
|
@ -14343,6 +14345,8 @@ four standard [pagination arguments](#connection-pagination-arguments):
|
|||
| <a id="groupprojectsincludesubgroups"></a>`includeSubgroups` | [`Boolean`](#boolean) | Include also subgroup projects. |
|
||||
| <a id="groupprojectssearch"></a>`search` | [`String`](#string) | Search project with most similar names or paths. |
|
||||
| <a id="groupprojectssort"></a>`sort` | [`NamespaceProjectSort`](#namespaceprojectsort) | Sort projects by this criteria. |
|
||||
| <a id="groupprojectswithissuesenabled"></a>`withIssuesEnabled` | [`Boolean`](#boolean) | Return only projects with issues enabled. |
|
||||
| <a id="groupprojectswithmergerequestsenabled"></a>`withMergeRequestsEnabled` | [`Boolean`](#boolean) | Return only projects with merge requests enabled. |
|
||||
|
||||
##### `Group.runners`
|
||||
|
||||
|
|
@ -16548,6 +16552,8 @@ four standard [pagination arguments](#connection-pagination-arguments):
|
|||
| <a id="namespaceprojectsincludesubgroups"></a>`includeSubgroups` | [`Boolean`](#boolean) | Include also subgroup projects. |
|
||||
| <a id="namespaceprojectssearch"></a>`search` | [`String`](#string) | Search project with most similar names or paths. |
|
||||
| <a id="namespaceprojectssort"></a>`sort` | [`NamespaceProjectSort`](#namespaceprojectsort) | Sort projects by this criteria. |
|
||||
| <a id="namespaceprojectswithissuesenabled"></a>`withIssuesEnabled` | [`Boolean`](#boolean) | Return only projects with issues enabled. |
|
||||
| <a id="namespaceprojectswithmergerequestsenabled"></a>`withMergeRequestsEnabled` | [`Boolean`](#boolean) | Return only projects with merge requests enabled. |
|
||||
|
||||
##### `Namespace.scanExecutionPolicies`
|
||||
|
||||
|
|
@ -18718,6 +18724,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
|
|||
| <a id="projectworkitemsiid"></a>`iid` | [`String`](#string) | IID of the issue. For example, "1". |
|
||||
| <a id="projectworkitemsiids"></a>`iids` | [`[String!]`](#string) | List of IIDs of work items. For example, `["1", "2"]`. |
|
||||
| <a id="projectworkitemsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
|
||||
| <a id="projectworkitemsrequirementlegacywidget"></a>`requirementLegacyWidget` **{warning-solid}** | [`RequirementLegacyFilterInput`](#requirementlegacyfilterinput) | **Deprecated** in 15.9. Use work item IID filter instead. |
|
||||
| <a id="projectworkitemssearch"></a>`search` | [`String`](#string) | Search query for title or description. |
|
||||
| <a id="projectworkitemssort"></a>`sort` | [`WorkItemSort`](#workitemsort) | Sort work items by this criteria. |
|
||||
| <a id="projectworkitemsstate"></a>`state` | [`IssuableState`](#issuablestate) | Current state of this work item. |
|
||||
|
|
@ -22693,6 +22700,7 @@ Values for sorting projects.
|
|||
|
||||
| Value | Description |
|
||||
| ----- | ----------- |
|
||||
| <a id="namespaceprojectsortactivity_desc"></a>`ACTIVITY_DESC` | Sort by latest activity, in descending order. |
|
||||
| <a id="namespaceprojectsortsimilarity"></a>`SIMILARITY` | Most similar to the search query. |
|
||||
| <a id="namespaceprojectsortstorage"></a>`STORAGE` | Sort by storage size. |
|
||||
|
||||
|
|
@ -25277,6 +25285,14 @@ Fields that are available when modifying release assets.
|
|||
| ---- | ---- | ----------- |
|
||||
| <a id="releaseassetsinputlinks"></a>`links` | [`[ReleaseAssetLinkInput!]`](#releaseassetlinkinput) | List of asset links to associate to the release. |
|
||||
|
||||
### `RequirementLegacyFilterInput`
|
||||
|
||||
#### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="requirementlegacyfilterinputlegacyiids"></a>`legacyIids` | [`[String!]!`](#string) | List of legacy requirement IIDs of work items. or example `["1", "2"]`. |
|
||||
|
||||
### `SastCiConfigurationAnalyzersEntityInput`
|
||||
|
||||
Represents the analyzers entity in SAST CI configuration.
|
||||
|
|
|
|||
|
|
@ -79,3 +79,15 @@ GET /projects/:id/product_analytics/request/meta
|
|||
| Attribute | Type | Required | Description |
|
||||
| --------- |------------------| -------- |---------------------------------------------------------------|
|
||||
| `id` | integer | yes | The ID of a project that the current user has read access to. |
|
||||
|
||||
## List a project's funnels
|
||||
|
||||
List all funnels for a project. For example:
|
||||
|
||||
```plaintext
|
||||
GET /projects/:id/product_analytics/funnels
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- |------------------| -------- |--------------------------------------------------------------------|
|
||||
| `id` | integer | yes | The ID of a project that the current user has the Developer role for. |
|
||||
|
|
|
|||
|
|
@ -163,32 +163,32 @@ information such as what the variable is used for, and what the acceptable value
|
|||
Job-level variables cannot be pre-filled.
|
||||
|
||||
In manually-triggered pipelines, the **Run pipeline** page displays all pipeline-level variables
|
||||
with a `description` defined in the `.gitlab-ci.yml` file. The description displays
|
||||
that have a `description` defined in the `.gitlab-ci.yml` file. The description displays
|
||||
below the variable.
|
||||
|
||||
You can change the prefilled value, which overrides the value for that single pipeline run.
|
||||
If you do not define a `value` for the variable in the configuration file, the variable still displays,
|
||||
If you do not define a `value` for the variable in the configuration file, the variable name is still listed,
|
||||
but the value field is blank.
|
||||
|
||||
For example:
|
||||
|
||||
```yaml
|
||||
variables:
|
||||
TEST_SUITE:
|
||||
description: "The test suite that will run. Valid options are: 'default', 'short', 'full'."
|
||||
value: "default"
|
||||
DEPLOY_CREDENTIALS:
|
||||
description: "The deployment credentials."
|
||||
DEPLOY_ENVIRONMENT:
|
||||
description: "Select the deployment target. Valid options are: 'canary', 'staging', 'production', or a stable branch of your choice."
|
||||
value: "canary"
|
||||
```
|
||||
|
||||
In this example:
|
||||
|
||||
- `TEST_SUITE` is pre-filled in the **Run pipeline** page with `default`,
|
||||
and the message explains the other options.
|
||||
- `DEPLOY_ENVIRONMENT` is listed in the **Run pipeline** page, but with no value set.
|
||||
- `DEPLOY_CREDENTIALS` is listed in the **Run pipeline** page, but with no value set.
|
||||
The user is expected to define the value each time the pipeline is run manually.
|
||||
- `DEPLOY_ENVIRONMENT` is pre-filled in the **Run pipeline** page with `canary` as the default value,
|
||||
and the message explains the other options.
|
||||
|
||||
##### Configure a list of selectable values for a prefilled variable
|
||||
#### Configure a list of selectable prefilled variable values
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/363660) in GitLab 15.5 [with a flag](../../administration/feature_flags.md) named `run_pipeline_graphql`. Disabled by default.
|
||||
> - The `options` keyword was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105502) in GitLab 15.7.
|
||||
|
|
|
|||
|
|
@ -4398,35 +4398,89 @@ deploy_review_job:
|
|||
|
||||
#### `variables:description`
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30101) in GitLab 13.7.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/363660) in GitLab 15.5, `variables:value` can contain an array of values.
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30101) in GitLab 13.7.
|
||||
|
||||
Use the `description` keyword to define a [pipeline-level (global) variable that is prefilled](../pipelines/index.md#prefill-variables-in-manual-pipelines)
|
||||
when [running a pipeline manually](../pipelines/index.md#run-a-pipeline-manually).
|
||||
|
||||
If used with `value`, the variable value is also prefilled when running a pipeline manually.
|
||||
Use the `description` keyword to define a description for a pipeline-level (global) variable.
|
||||
The description displays with [the prefilled variable name when running a pipeline manually](../pipelines/index.md#prefill-variables-in-manual-pipelines).
|
||||
|
||||
**Keyword type**: Global keyword. You cannot use it for job-level variables.
|
||||
|
||||
**Possible inputs**:
|
||||
|
||||
- A string.
|
||||
- An array of strings.
|
||||
|
||||
**Example of `variables:description`**:
|
||||
|
||||
```yaml
|
||||
variables:
|
||||
DEPLOY_ENVIRONMENT:
|
||||
description: "The deployment target. Change this variable to 'canary' or 'production' if needed."
|
||||
value: "staging"
|
||||
DEPLOY_NOTE:
|
||||
description: "The deployment note. Explain the reason for this deployment."
|
||||
```
|
||||
|
||||
**Additional details**:
|
||||
|
||||
- A global variable defined with `value` but no `description` behaves the same as
|
||||
[`variables`](#variables).
|
||||
- `variables:value` can [contain an array of selectable values](../pipelines/index.md#configure-a-list-of-selectable-values-for-a-prefilled-variable).
|
||||
- When used without `value`, the variable exists in pipelines that were not triggered manually,
|
||||
and the default value is an empty string (`''`).
|
||||
|
||||
#### `variables:value`
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30101) in GitLab 13.7.
|
||||
|
||||
Use the `value` keyword to define a pipeline-level (global) variable's value. When used with
|
||||
[`variables: description`](#variablesdescription), the variable value is [prefilled when running a pipeline manually](../pipelines/index.md#prefill-variables-in-manual-pipelines).
|
||||
|
||||
**Keyword type**: Global keyword. You cannot use it for job-level variables.
|
||||
|
||||
**Possible inputs**:
|
||||
|
||||
- A string.
|
||||
|
||||
**Example of `variables:value`**:
|
||||
|
||||
```yaml
|
||||
variables:
|
||||
DEPLOY_ENVIRONMENT:
|
||||
value: "staging"
|
||||
description: "The deployment target. Change this variable to 'canary' or 'production' if needed."
|
||||
```
|
||||
|
||||
**Additional details**:
|
||||
|
||||
- If used without [`variables: description`](#variablesdescription), the behavior is
|
||||
the same as [`variables`](#variables).
|
||||
|
||||
#### `variables:options`
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105502) in GitLab 15.7.
|
||||
|
||||
Use `variables:options` to define an array of values that are [selectable in the UI when running a pipeline manually](../pipelines/index.md#configure-a-list-of-selectable-prefilled-variable-values).
|
||||
|
||||
Must be used with `variables: value`, and the string defined for `value`:
|
||||
|
||||
- Must also be one of the strings in the `options` array.
|
||||
- Is the default selection.
|
||||
|
||||
If there is no [`description`](#variablesdescription),
|
||||
this keyword has no effect.
|
||||
|
||||
**Keyword type**: Global keyword. You cannot use it for job-level variables.
|
||||
|
||||
**Possible inputs**:
|
||||
|
||||
- An array of strings.
|
||||
|
||||
**Example of `variables:options`**:
|
||||
|
||||
```yaml
|
||||
variables:
|
||||
DEPLOY_ENVIRONMENT:
|
||||
value: "staging"
|
||||
options:
|
||||
- "production"
|
||||
- "staging"
|
||||
- "canary"
|
||||
description: "The deployment target. Set to 'staging' by default."
|
||||
```
|
||||
|
||||
#### `variables:expand`
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ By default, this task does not delete anything but shows how many file reference
|
|||
delete. Run the command with `DRY_RUN=false` if you actually want to
|
||||
delete the references. You can also use `LIMIT={number}` parameter to limit the number of deleted references.
|
||||
|
||||
Note that this Rake task only removes the references to LFS files. Unreferenced LFS files are garbage-collected
|
||||
This Rake task only removes the references to LFS files. Unreferenced LFS files are garbage-collected
|
||||
later (once a day). If you need to garbage collect them immediately, run
|
||||
`rake gitlab:cleanup:orphan_lfs_files` described below.
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ migrate projects using either:
|
|||
- [Direct transfer](../user/group/import/index.md#migrate-groups-by-direct-transfer-recommended).
|
||||
- [An export file](../user/project/settings/import_export.md).
|
||||
|
||||
Note that:
|
||||
When you import a repository:
|
||||
|
||||
- The owner of the project is the first administrator.
|
||||
- The groups are created as needed, including subgroups.
|
||||
|
|
@ -159,7 +159,7 @@ project.set_full_path
|
|||
```
|
||||
|
||||
In a Rails console session, run the following to migrate all of a namespace's
|
||||
projects (this may take a while if there are 1000s of projects in a namespace):
|
||||
projects (this may take a while if there are thousands of projects in a namespace):
|
||||
|
||||
```ruby
|
||||
namespace = Namespace.find_by_full_path('gitlab-org')
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ arguments until the status query returns no rows.
|
|||
##### For a no-downtime deployment
|
||||
|
||||
As the failing migrations are post-deployment migrations, you can remain on a running instance of the upgraded
|
||||
version and wait for the batched background migrations to finish normally.
|
||||
version and wait for the batched background migrations to finish.
|
||||
|
||||
1. [Check the status](#check-the-status-of-batched-background-migrations) of the batched background migration from
|
||||
the error message, and make sure it is listed as finished. If it is still active, either wait until it is done,
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ When denying access, a `reason` can be optionally specified in the JSON body:
|
|||
Any other status code than 200, 401 or 403 also deny access to the user, but the
|
||||
response isn't cached.
|
||||
|
||||
If the service times out (after 500ms), a message "External Policy Server did
|
||||
If the service times out (after 500 ms), a message "External Policy Server did
|
||||
not respond" is displayed.
|
||||
|
||||
## Classification labels
|
||||
|
|
|
|||
|
|
@ -8,10 +8,14 @@ type: reference, howto
|
|||
# DAST authentication **(ULTIMATE)**
|
||||
|
||||
WARNING:
|
||||
**Never** run an authenticated scan against a production server.
|
||||
Authenticated scans may perform *any* function that the authenticated user can,
|
||||
**DO NOT** use credentials that are valid for production systems, production servers, or any that
|
||||
contain production data.
|
||||
|
||||
WARNING:
|
||||
**DO NOT** run an authenticated scan against a production server.
|
||||
Authenticated scans may perform **any** function that the authenticated user can,
|
||||
including modifying or deleting data, submitting forms, and following links.
|
||||
Only run an authenticated scan against a test server.
|
||||
Only run an authenticated scan against non-production systems or servers.
|
||||
|
||||
Authentication logs a user in before a DAST scan so that the analyzer can test
|
||||
as much of the application as possible when searching for vulnerabilities.
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ IaC Scanning supports configuration files for Terraform, Ansible, AWS CloudForma
|
|||
|
||||
IaC Scanning runs in the `test` stage, which is available by default. If you redefine the stages in the `.gitlab-ci.yml` file, the `test` stage is required.
|
||||
|
||||
We recommend a minimum of 4GB RAM to ensure consistent performance.
|
||||
We recommend a minimum of 4 GB RAM to ensure consistent performance.
|
||||
|
||||
To run IaC Scanning jobs, by default, you need GitLab Runner with the
|
||||
[`docker`](https://docs.gitlab.com/runner/executors/docker.html) or
|
||||
|
|
@ -222,13 +222,13 @@ To override the automatic update behavior, set the `SAST_ANALYZER_IMAGE_TAG` CI/
|
|||
in your CI/CD configuration file after you include the [`SAST-IaC.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Jobs/SAST-IaC.gitlab-ci.yml).
|
||||
|
||||
Only set this variable in a specific job.
|
||||
If you set it [at the top level](../../../ci/variables/index.md#define-a-cicd-variable-in-the-gitlab-ciyml-file), the version you set will be used for other SAST analyzers.
|
||||
If you set it [at the top level](../../../ci/variables/index.md#define-a-cicd-variable-in-the-gitlab-ciyml-file), the version you set is used for other SAST analyzers.
|
||||
|
||||
You can set the tag to:
|
||||
|
||||
- A major version, like `3`. Your pipelines will use any minor or patch updates that are released within this major version.
|
||||
- A minor version, like `3.7`. Your pipelines will use any patch updates that are released within this minor version.
|
||||
- A patch version, like `3.7.0`. Your pipelines won't receive any updates.
|
||||
- A major version, like `3`. Your pipelines use any minor or patch updates that are released within this major version.
|
||||
- A minor version, like `3.7`. Your pipelines use any patch updates that are released within this minor version.
|
||||
- A patch version, like `3.7.0`. Your pipelines don't receive any updates.
|
||||
|
||||
This example uses a specific minor version of the `KICS` analyzer:
|
||||
|
||||
|
|
|
|||
|
|
@ -117,3 +117,77 @@ The example below includes three dashboards and one visualization that applies t
|
|||
├── visualizations
|
||||
│ └── example_line_chart.yaml
|
||||
```
|
||||
|
||||
## Funnel analysis
|
||||
|
||||
Funnel analysis can be used to understand the flow of users through your application and where
|
||||
users drop out of a predefined flow (for example, a checkout process or ticket purchase).
|
||||
|
||||
Each product can also define an unlimited number of funnels.
|
||||
These funnels are defined using our YAML schema and stored in the `.gitlab/product_analytics/funnels/` directory of a project repository.
|
||||
|
||||
Funnel definitions must include the keys `name`, `seconds_to_convert`, and an array of `steps`.
|
||||
|
||||
| Key | Description |
|
||||
|----------------------|----------------------------------------------------------|
|
||||
| `name` | The name of the funnel. |
|
||||
| `seconds_to_convert` | The number of seconds a user has to complete the funnel. |
|
||||
| `steps` | An array of funnel steps. |
|
||||
|
||||
Each step must include the keys `name`, `target`, and `action`.
|
||||
|
||||
| Key | Description |
|
||||
|----------|------------------------------------------------------------------------------------------|
|
||||
| `name` | The name of the step. This should be a unique slug. |
|
||||
| `action` | The action performed. (Only `pageview` is supported.) |
|
||||
| `target` | The target of the step. (Because only `pageview` is supported, this should be a path.) |
|
||||
|
||||
### Example funnel definition
|
||||
|
||||
```yaml
|
||||
name: completed_purchase
|
||||
seconds_to_convert: 3600
|
||||
steps:
|
||||
- name: view_page_1
|
||||
target: '/page1.html'
|
||||
action: 'pageview'
|
||||
- name: view_page_2
|
||||
target: '/page2.html'
|
||||
action: 'pageview'
|
||||
- name: view_page_3
|
||||
target: '/page3.html'
|
||||
action: 'pageview'
|
||||
```
|
||||
|
||||
### Query a funnel
|
||||
|
||||
You can [query the funnel data with the REST API](../../api/product_analytics.md#send-query-request-to-cube).
|
||||
To do this, you can use the example query body below, where you need to replace `FUNNEL_NAME` with your funnel's name.
|
||||
|
||||
NOTE:
|
||||
The `afterDate` filter is not supported. Please use `beforeDate` or `inDateRange`.
|
||||
|
||||
```json
|
||||
{
|
||||
"query": {
|
||||
"measures": [
|
||||
"FUNNEL_NAME.count"
|
||||
],
|
||||
"order": {
|
||||
"completed_purchase.count": "desc"
|
||||
},
|
||||
"filters": [
|
||||
{
|
||||
"member": "FUNNEL_NAME.date",
|
||||
"operator": "beforeDate",
|
||||
"values": [
|
||||
"2023-02-01"
|
||||
]
|
||||
}
|
||||
],
|
||||
"dimensions": [
|
||||
"FUNNEL_NAME.step"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -19,21 +19,24 @@ project maintainers.
|
|||
Badges can be added to a project by Maintainers or Owners, and are visible on the project's overview page.
|
||||
If you find that you have to add the same badges to several projects, you may want to add them at the [group level](#group-badges).
|
||||
|
||||
### Add a badge to a project
|
||||
|
||||
To add a new badge to a project:
|
||||
|
||||
1. On the top bar, select **Main menu > Projects** and find your project.
|
||||
1. On the left sidebar, select **Settings > General**.
|
||||
1. Expand **Badges**.
|
||||
1. Under "Link", enter the URL that the badges should point to and under
|
||||
"Badge image URL" the URL of the image that should be displayed.
|
||||
1. Under **Link**, enter the URL that the badges should point to.
|
||||
1. Under **Badge image URL**, enter the URL of the image that should be displayed.
|
||||
1. Select **Add badge**.
|
||||
|
||||
After adding a badge to a project, you can see it in the list below the form.
|
||||
You can edit the badge by selecting **Edit** (**{pencil}**) next to it or delete it by
|
||||
selecting **Delete** (**{remove}**).
|
||||
After adding a badge to a project, you can see the badge in the list below the form.
|
||||
|
||||
Badges associated with a group can only be edited or deleted on the
|
||||
[group level](#group-badges).
|
||||
### Edit or delete a project badge
|
||||
|
||||
To edit a badge, select **Edit** (**{pencil}**).
|
||||
|
||||
To delete a badge, select **Delete** (**{remove}**).
|
||||
|
||||
### Example project badge: Pipeline Status
|
||||
|
||||
|
|
@ -66,6 +69,8 @@ If you need individual badges for each project, either:
|
|||
- Add the badge at the [project level](#project-badges).
|
||||
- Use [placeholders](#placeholders).
|
||||
|
||||
### Add a badge to a group
|
||||
|
||||
To add a new badge to a group:
|
||||
|
||||
1. On the top bar, select **Main menu > Groups** and find your group.
|
||||
|
|
@ -76,17 +81,20 @@ To add a new badge to a group:
|
|||
1. Select **Add badge**.
|
||||
|
||||
After adding a badge to a group, you can see it in the list below the form.
|
||||
You can edit the badge by selecting **Edit** (**{pencil}**) next to it or delete it by
|
||||
selecting **Delete** (**{remove}**).
|
||||
|
||||
Badges directly associated with a project can be configured on the
|
||||
[project level](#project-badges).
|
||||
### Edit or delete a group badge
|
||||
|
||||
To edit a badge, select **Edit** (**{pencil}**).
|
||||
|
||||
To delete a badge, select **Delete** (**{remove}**).
|
||||
|
||||
Badges associated with a group can be edited or deleted only at the [group level](#group-badges).
|
||||
|
||||
## Placeholders
|
||||
|
||||
Both the URL a badge points to and the image URL can contain placeholders
|
||||
which are evaluated when displaying the badge. The following placeholders
|
||||
are available:
|
||||
Both the URL a badge points to and the image URL can contain placeholders,
|
||||
which are evaluated when displaying the badge.
|
||||
The following placeholders are available:
|
||||
|
||||
- `%{project_path}`: Path of a project including the parent groups
|
||||
- `%{project_title}`: Title of a project
|
||||
|
|
@ -99,7 +107,7 @@ are available:
|
|||
NOTE:
|
||||
Placeholders allow badges to expose otherwise-private information, such as the
|
||||
default branch or commit SHA when the project is configured to have a private
|
||||
repository. This is by design, as badges are intended to be used publicly. Avoid
|
||||
repository. This behavior is intentional, as badges are intended to be used publicly. Avoid
|
||||
using these placeholders if the information is sensitive.
|
||||
|
||||
## Use custom badge images
|
||||
|
|
@ -118,7 +126,7 @@ Using placeholders, here is an example badge image URL referring to a raw image
|
|||
https://gitlab.example.com/<project_path>/-/raw/<default_branch>/my-image.svg
|
||||
```
|
||||
|
||||
To add a new badge to a group or project with a custom image:
|
||||
To add a new badge with a custom image to a group or project:
|
||||
|
||||
1. On the top bar, select **Main menu** and find your group or project.
|
||||
1. On the left sidebar, select **Settings > General**.
|
||||
|
|
@ -129,11 +137,11 @@ To add a new badge to a group or project with a custom image:
|
|||
displayed.
|
||||
1. Select **Add badge**.
|
||||
|
||||
To learn how to use custom images generated via a pipeline, see our documentation on
|
||||
To learn how to use custom images generated through a pipeline, see the documentation on
|
||||
[accessing the latest job artifacts by URL](../../ci/pipelines/job_artifacts.md#access-the-latest-job-artifacts).
|
||||
|
||||
## API
|
||||
## Configure badges through the API
|
||||
|
||||
You can also configure badges via the GitLab API. As in the settings, there is
|
||||
a distinction between endpoints for badges on the
|
||||
a distinction between endpoints for badges at the
|
||||
[project level](../../api/project_badges.md) and [group level](../../api/group_badges.md).
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ GitLab retrieves performance data from the configured Prometheus server, and
|
|||
attempts to identifying the presence of known metrics. Once identified, GitLab
|
||||
then needs to be able to map the data to a particular environment.
|
||||
|
||||
In order to isolate and only display relevant metrics for a given environment,
|
||||
To isolate and only display relevant metrics for a given environment,
|
||||
GitLab needs a method to detect which labels are associated. To do that,
|
||||
GitLab uses the defined queries and fills in the environment specific variables.
|
||||
Typically this involves looking for the
|
||||
|
|
|
|||
|
|
@ -42,6 +42,6 @@ Managing these settings depends on how NGINX Ingress has been deployed. If you h
|
|||
|
||||
## Specifying the Environment label
|
||||
|
||||
In order to isolate and only display relevant metrics for a given environment, GitLab needs a method to detect which labels are associated. To do this, GitLab searches for metrics with appropriate labels. In this case, the `upstream` label must be of the form `<KUBE_NAMESPACE>-<CI_ENVIRONMENT_SLUG>-*`.
|
||||
To isolate and only display relevant metrics for a given environment, GitLab needs a method to detect which labels are associated. To do this, GitLab searches for metrics with appropriate labels. In this case, the `upstream` label must be of the form `<KUBE_NAMESPACE>-<CI_ENVIRONMENT_SLUG>-*`.
|
||||
|
||||
If you have used [Auto Deploy](../../../../topics/autodevops/stages.md#auto-deploy) to deploy your app, this format is used automatically and metrics are detected with no action on your part.
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ To report abuse from a user's profile page:
|
|||
|
||||
1. Anywhere in GitLab, select the name of the user.
|
||||
1. In the top right corner of the user's profile, select **Report abuse to administrator** (**{information-o}**).
|
||||
1. Select a reason for reporting the user.
|
||||
1. Complete an abuse report.
|
||||
1. Select **Send report**.
|
||||
|
||||
|
|
@ -37,6 +38,7 @@ To report abuse from a user's comment:
|
|||
|
||||
1. In the comment, in the top right corner, select **More actions** (**{ellipsis_v}**).
|
||||
1. Select **Report abuse to administrator**.
|
||||
1. Select a reason for reporting the user.
|
||||
1. Complete an abuse report.
|
||||
1. Select **Send report**.
|
||||
|
||||
|
|
@ -48,14 +50,16 @@ A URL to the reported user's comment is pre-filled in the abuse report's
|
|||
|
||||
1. On the issue, in the top right corner, select the vertical ellipsis (**{ellipsis_v}**).
|
||||
1. Select **Report abuse to administrator**.
|
||||
1. Submit an abuse report.
|
||||
1. Select a reason for reporting the user.
|
||||
1. Complete an abuse report.
|
||||
1. Select **Send report**.
|
||||
|
||||
## Report abuse from a merge request
|
||||
|
||||
1. On the merge request, in the top right corner, select the vertical ellipsis (**{ellipsis_v}**).
|
||||
1. Select **Report abuse to administrator**.
|
||||
1. Submit an abuse report.
|
||||
1. Select a reason for reporting this user.
|
||||
1. Complete an abuse report.
|
||||
1. Select **Send report**.
|
||||
|
||||
## Related topics
|
||||
|
|
|
|||
|
|
@ -57,9 +57,14 @@ module Gitlab
|
|||
end
|
||||
|
||||
def add_parent_model_params!(finder_params)
|
||||
raise(ArgumentError, "unknown parent_class: #{parent_class}") unless parent_class.eql?(Project)
|
||||
|
||||
finder_params[:project_id] = stage.parent_id
|
||||
case stage.parent
|
||||
when Namespaces::ProjectNamespace
|
||||
finder_params[:project_id] = stage.parent.project.id
|
||||
when Project
|
||||
finder_params[:project_id] = stage.parent_id
|
||||
else
|
||||
raise(ArgumentError, "unknown parent_class: #{parent_class}")
|
||||
end
|
||||
end
|
||||
|
||||
def add_time_range_params!(finder_params, from, to)
|
||||
|
|
|
|||
|
|
@ -105,11 +105,9 @@ module Gitlab
|
|||
def read_write
|
||||
connection = nil
|
||||
transaction_open = nil
|
||||
attempts = 3
|
||||
|
||||
if prevent_load_balancer_retries_in_transaction?
|
||||
attempts = 1 if pool.connection.transaction_open?
|
||||
end
|
||||
# Retry only once when in a transaction (see https://gitlab.com/gitlab-org/gitlab/-/issues/220242)
|
||||
attempts = pool.connection.transaction_open? ? 1 : 3
|
||||
|
||||
# In the event of a failover the primary may be briefly unavailable.
|
||||
# Instead of immediately grinding to a halt we'll retry the operation
|
||||
|
|
@ -348,10 +346,6 @@ module Gitlab
|
|||
row = ar_connection.select_all(sql).first
|
||||
row['location'] if row
|
||||
end
|
||||
|
||||
def prevent_load_balancer_retries_in_transaction?
|
||||
Gitlab::Utils.to_boolean(ENV['PREVENT_LOAD_BALANCER_RETRIES_IN_TRANSACTION'], default: false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ namespace :tw do
|
|||
CodeOwnerRule.new('Certify', '@msedlakjakubowski'),
|
||||
CodeOwnerRule.new('Code Review', '@aqualls'),
|
||||
CodeOwnerRule.new('Compliance', '@eread'),
|
||||
CodeOwnerRule.new('Commerce Integrations', '@drcatherinepope'),
|
||||
CodeOwnerRule.new('Composition Analysis', '@rdickenson'),
|
||||
CodeOwnerRule.new('Configure', '@phillipwells'),
|
||||
CodeOwnerRule.new('Container Registry', '@claytoncornell'),
|
||||
|
|
@ -50,11 +51,11 @@ namespace :tw do
|
|||
CodeOwnerRule.new('Knowledge', '@aqualls'),
|
||||
CodeOwnerRule.new('Application Performance', '@jglassman1'),
|
||||
CodeOwnerRule.new('Monitor', '@msedlakjakubowski'),
|
||||
CodeOwnerRule.new('Observability', '@msedlakjakubowski'),
|
||||
CodeOwnerRule.new('Observability', '@drcatherinepope'),
|
||||
CodeOwnerRule.new('Optimize', '@lciutacu'),
|
||||
CodeOwnerRule.new('Package Registry', '@claytoncornell'),
|
||||
CodeOwnerRule.new('Pipeline Authoring', '@marcel.amirault'),
|
||||
CodeOwnerRule.new('Pipeline Execution', '@marcel.amirault'),
|
||||
CodeOwnerRule.new('Pipeline Execution', '@drcatherinepope'),
|
||||
CodeOwnerRule.new('Pipeline Insights', '@marcel.amirault'),
|
||||
CodeOwnerRule.new('Portfolio Management', '@msedlakjakubowski'),
|
||||
CodeOwnerRule.new('Product Analytics', '@lciutacu'),
|
||||
|
|
|
|||
|
|
@ -10145,9 +10145,6 @@ msgstr ""
|
|||
msgid "Commits|An error occurred while fetching merge requests data."
|
||||
msgstr ""
|
||||
|
||||
msgid "Commits|History"
|
||||
msgstr ""
|
||||
|
||||
msgid "Commits|No related merge requests found"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -14289,9 +14286,6 @@ msgstr ""
|
|||
msgid "Developer"
|
||||
msgstr ""
|
||||
|
||||
msgid "Development"
|
||||
msgstr ""
|
||||
|
||||
msgid "Devices (optional)"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -21074,9 +21068,6 @@ msgstr ""
|
|||
msgid "If enabled, only protected branches will be mirrored."
|
||||
msgstr ""
|
||||
|
||||
msgid "If no options are selected, only administrators can register runners."
|
||||
msgstr ""
|
||||
|
||||
msgid "If the number of active users exceeds the user limit, you will be charged for the number of %{users_over_license_link} at your next license reconciliation."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -36477,6 +36468,9 @@ msgstr ""
|
|||
msgid "Runners|Idle"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|If both settings are disabled, new runners cannot be registered."
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Install a runner"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -44856,6 +44850,12 @@ msgstr ""
|
|||
msgid "Unable to fetch groups. Reload the page to try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Unable to fetch project. Reload the page to try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Unable to fetch projects. Reload the page to try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Unable to fetch upstream and downstream pipelines."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -45462,6 +45462,9 @@ msgstr ""
|
|||
msgid "UsageQuota|Usage by project"
|
||||
msgstr ""
|
||||
|
||||
msgid "UsageQuota|Usage of group resources across the projects in the %{namespaceName} group"
|
||||
msgstr ""
|
||||
|
||||
msgid "UsageQuota|Usage of group resources across the projects in the %{strong_start}%{group_name}%{strong_end} group"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -22,16 +22,19 @@ fi
|
|||
qa_cache_key="qa-e2e-ruby-${RUBY_VERSION}-$(md5sum qa/Gemfile.lock | awk '{ print $1 }')"
|
||||
variables=$(cat <<YML
|
||||
variables:
|
||||
COLORIZED_LOGS: "true"
|
||||
GIT_DEPTH: "20"
|
||||
GIT_STRATEGY: "clone" # 'GIT_STRATEGY: clone' optimizes the pack-objects cache hit ratio
|
||||
GIT_SUBMODULE_STRATEGY: "none"
|
||||
GITLAB_QA_CACHE_KEY: "$qa_cache_key"
|
||||
GITLAB_VERSION: "$(cat VERSION)"
|
||||
COLORIZED_LOGS: "true"
|
||||
QA_EXPORT_TEST_METRICS: "${QA_EXPORT_TEST_METRICS:-true}"
|
||||
QA_SAVE_TEST_METRICS: "${QA_SAVE_TEST_METRICS:-false}"
|
||||
QA_RUN_ALL_TESTS: "${QA_RUN_ALL_TESTS:-false}"
|
||||
QA_FRAMEWORK_CHANGES: "${QA_FRAMEWORK_CHANGES:-false}"
|
||||
QA_FEATURE_FLAGS: "${QA_FEATURE_FLAGS}"
|
||||
QA_TESTS: "$QA_TESTS"
|
||||
QA_FRAMEWORK_CHANGES: "${QA_FRAMEWORK_CHANGES:-false}"
|
||||
QA_RUN_ALL_TESTS: "${QA_RUN_ALL_TESTS:-false}"
|
||||
QA_SAVE_TEST_METRICS: "${QA_SAVE_TEST_METRICS:-false}"
|
||||
QA_SUITES: "$QA_SUITES"
|
||||
QA_TESTS: "$QA_TESTS"
|
||||
YML
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -294,10 +294,6 @@ if $PROGRAM_NAME == __FILE__
|
|||
|
||||
automated_cleanup = ReviewApps::AutomatedCleanup.new(options: options)
|
||||
|
||||
timed('Review Apps cleanup') do
|
||||
automated_cleanup.perform_gitlab_environment_cleanup!(days_for_stop: 5, days_for_delete: 6)
|
||||
end
|
||||
|
||||
timed('Docs Review Apps cleanup') do
|
||||
automated_cleanup.perform_gitlab_docs_environment_cleanup!(days_for_stop: 20, days_for_delete: 30)
|
||||
end
|
||||
|
|
@ -305,11 +301,11 @@ if $PROGRAM_NAME == __FILE__
|
|||
puts
|
||||
|
||||
timed('Helm releases cleanup') do
|
||||
automated_cleanup.perform_helm_releases_cleanup!(days: 7)
|
||||
automated_cleanup.perform_helm_releases_cleanup!(days: 2)
|
||||
end
|
||||
|
||||
timed('Stale Namespace cleanup') do
|
||||
automated_cleanup.perform_stale_namespace_cleanup!(days: 14)
|
||||
automated_cleanup.perform_stale_namespace_cleanup!(days: 3)
|
||||
end
|
||||
|
||||
timed('Stale PVC cleanup') do
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ gitlab:
|
|||
memory: 1927Mi
|
||||
limits:
|
||||
cpu: 450m
|
||||
memory: 2890Mi
|
||||
memory: 3500Mi
|
||||
|
||||
webservice:
|
||||
resources:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :cycle_analytics_stage, class: 'Analytics::CycleAnalytics::Stage' do
|
||||
sequence(:name) { |n| "Stage ##{n}" }
|
||||
start_event_identifier { Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestCreated.identifier }
|
||||
end_event_identifier { Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestMerged.identifier }
|
||||
|
||||
namespace { association(:group) }
|
||||
value_stream { association(:cycle_analytics_value_stream, namespace: namespace) }
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :cycle_analytics_value_stream, class: 'Analytics::CycleAnalytics::ValueStream' do
|
||||
sequence(:name) { |n| "Value Stream ##{n}" }
|
||||
|
||||
namespace { association(:group) }
|
||||
end
|
||||
end
|
||||
|
|
@ -3,7 +3,6 @@
|
|||
FactoryBot.define do
|
||||
factory :ci_processable, class: 'Ci::Processable' do
|
||||
name { 'processable' }
|
||||
stage { 'test' }
|
||||
stage_idx { ci_stage.try(:position) || 0 }
|
||||
ref { 'master' }
|
||||
tag { false }
|
||||
|
|
@ -12,6 +11,30 @@ FactoryBot.define do
|
|||
scheduling_type { 'stage' }
|
||||
partition_id { pipeline.partition_id }
|
||||
|
||||
# This factory was updated to help with the efforts of the removal of `ci_builds.stage`:
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/364377
|
||||
# These additions can be removed once the specs that use the stage attribute have been updated
|
||||
|
||||
transient do
|
||||
stage { 'test' }
|
||||
end
|
||||
|
||||
after(:build) do |processable, evaluator|
|
||||
processable.stage = evaluator.stage
|
||||
end
|
||||
|
||||
before(:create) do |processable, evaluator|
|
||||
next if processable.ci_stage
|
||||
|
||||
if ci_stage = processable.pipeline.stages.find_by(name: evaluator.stage)
|
||||
processable.ci_stage = ci_stage
|
||||
else
|
||||
processable.ci_stage = create(:ci_stage, pipeline: processable.pipeline,
|
||||
project: processable.project || evaluator.project,
|
||||
name: evaluator.stage, position: evaluator.stage_idx, status: 'created')
|
||||
end
|
||||
end
|
||||
|
||||
trait :waiting_for_resource do
|
||||
status { 'waiting_for_resource' }
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ RSpec.describe Namespaces::ProjectsFinder do
|
|||
let_it_be(:subgroup) { create(:group, parent: namespace) }
|
||||
let_it_be(:project_1) { create(:project, :public, group: namespace, path: 'project', name: 'Project') }
|
||||
let_it_be(:project_2) { create(:project, :public, group: namespace, path: 'test-project', name: 'Test Project') }
|
||||
let_it_be(:project_3) { create(:project, :public, path: 'sub-test-project', group: subgroup, name: 'Sub Test Project') }
|
||||
let_it_be(:project_4) { create(:project, :public, path: 'test-project-2', group: namespace, name: 'Test Project 2') }
|
||||
let_it_be(:project_3) { create(:project, :public, :issues_disabled, path: 'sub-test-project', group: subgroup, name: 'Sub Test Project') }
|
||||
let_it_be(:project_4) { create(:project, :public, :merge_requests_disabled, path: 'test-project-2', group: namespace, name: 'Test Project 2') }
|
||||
|
||||
let(:params) { {} }
|
||||
|
||||
|
|
@ -55,6 +55,22 @@ RSpec.describe Namespaces::ProjectsFinder do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when with_issues_enabled is true' do
|
||||
let(:params) { { with_issues_enabled: true, include_subgroups: true } }
|
||||
|
||||
it 'returns the projects that have issues enabled' do
|
||||
expect(projects).to contain_exactly(project_1, project_2, project_4)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when with_merge_requests_enabled is true' do
|
||||
let(:params) { { with_merge_requests_enabled: true } }
|
||||
|
||||
it 'returns the projects that have merge requests enabled' do
|
||||
expect(projects).to contain_exactly(project_1, project_2)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sort is similarity' do
|
||||
let(:params) { { sort: :similarity, search: 'test' } }
|
||||
|
||||
|
|
@ -78,6 +94,20 @@ RSpec.describe Namespaces::ProjectsFinder do
|
|||
expect(projects).to contain_exactly(project_2, project_4)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sort parameter is ACTIVITY_DESC' do
|
||||
let(:params) { { sort: :latest_activity_desc } }
|
||||
|
||||
before do
|
||||
project_2.update!(last_activity_at: 10.minutes.ago)
|
||||
project_1.update!(last_activity_at: 5.minutes.ago)
|
||||
project_4.update!(last_activity_at: 1.minute.ago)
|
||||
end
|
||||
|
||||
it 'returns projects sorted by latest activity' do
|
||||
expect(projects).to eq([project_4, project_1, project_2])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@ RSpec.describe ProjectsFinder do
|
|||
end
|
||||
|
||||
let_it_be(:internal_project) do
|
||||
create(:project, :internal, group: group, name: 'B', path: 'B')
|
||||
create(:project, :internal, :merge_requests_disabled, group: group, name: 'B', path: 'B')
|
||||
end
|
||||
|
||||
let_it_be(:public_project) do
|
||||
create(:project, :public, group: group, name: 'C', path: 'C')
|
||||
create(:project, :public, :merge_requests_enabled, :issues_disabled, group: group, name: 'C', path: 'C')
|
||||
end
|
||||
|
||||
let_it_be(:shared_project) do
|
||||
|
|
@ -409,6 +409,18 @@ RSpec.describe ProjectsFinder do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'when with_issues_enabled is true' do
|
||||
let(:params) { { with_issues_enabled: true } }
|
||||
|
||||
it { is_expected.to match_array([internal_project]) }
|
||||
end
|
||||
|
||||
describe 'when with_merge_requests_enabled is true' do
|
||||
let(:params) { { with_merge_requests_enabled: true } }
|
||||
|
||||
it { is_expected.to match_array([public_project]) }
|
||||
end
|
||||
|
||||
describe 'sorting' do
|
||||
let_it_be(:more_projects) do
|
||||
[
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import $ from 'jquery';
|
||||
import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
|
||||
import { trimText } from 'helpers/text_helper';
|
||||
import U2FRegister from '~/authentication/u2f/register';
|
||||
import 'vendor/u2f';
|
||||
import MockU2FDevice from './mock_u2f_device';
|
||||
|
|
@ -24,7 +25,7 @@ describe('U2FRegister', () => {
|
|||
it('allows registering a U2F device', () => {
|
||||
const setupButton = container.find('#js-setup-token-2fa-device');
|
||||
|
||||
expect(setupButton.text()).toBe('Set up new device');
|
||||
expect(trimText(setupButton.text())).toBe('Set up new device');
|
||||
setupButton.trigger('click');
|
||||
const inProgressMessage = container.children('p');
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import $ from 'jquery';
|
||||
import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
|
||||
import { trimText } from 'helpers/text_helper';
|
||||
import setWindowLocation from 'helpers/set_window_location_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import WebAuthnRegister from '~/authentication/webauthn/register';
|
||||
|
|
@ -52,7 +53,7 @@ describe('WebAuthnRegister', () => {
|
|||
const findRetryButton = () => container.find('#js-token-2fa-try-again');
|
||||
|
||||
it('shows setup button', () => {
|
||||
expect(findSetupButton().text()).toBe('Set up new device');
|
||||
expect(trimText(findSetupButton().text())).toBe('Set up new device');
|
||||
});
|
||||
|
||||
describe('when unsupported', () => {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,48 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Contributors charts should render charts when loading completed and there is chart data 1`] = `
|
||||
exports[`Contributors charts should render charts and a RefSelector when loading completed and there is chart data 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="contributors-charts"
|
||||
class="gl-border-b gl-border-gray-100 gl-mb-6 gl-bg-gray-10 gl-p-5"
|
||||
>
|
||||
<div
|
||||
class="gl-display-flex"
|
||||
>
|
||||
<div
|
||||
class="gl-mr-3"
|
||||
>
|
||||
<refselector-stub
|
||||
enabledreftypes="REF_TYPE_BRANCHES,REF_TYPE_TAGS"
|
||||
name=""
|
||||
projectid="23"
|
||||
state="true"
|
||||
togglebuttonclass="gl-max-w-26"
|
||||
translations="[object Object]"
|
||||
value="main"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<a
|
||||
class="btn btn-default btn-md gl-button"
|
||||
data-testid="history-button"
|
||||
href="some/path"
|
||||
>
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
|
||||
<span
|
||||
class="gl-button-text"
|
||||
>
|
||||
History
|
||||
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-testid="contributors-charts"
|
||||
>
|
||||
<h4
|
||||
class="gl-mb-2 gl-mt-5"
|
||||
|
|
@ -49,8 +88,8 @@ exports[`Contributors charts should render charts when loading completed and the
|
|||
class="gl-mb-3"
|
||||
>
|
||||
|
||||
2 commits (jawnnypoo@gmail.com)
|
||||
|
||||
2 commits (jawnnypoo@gmail.com)
|
||||
|
||||
</p>
|
||||
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -1,21 +1,29 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import ContributorsCharts from '~/contributors/components/contributors.vue';
|
||||
import { createStore } from '~/contributors/stores';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { visitUrl } from '~/lib/utils/url_utility';
|
||||
import RefSelector from '~/ref/components/ref_selector.vue';
|
||||
import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants';
|
||||
|
||||
jest.mock('~/lib/utils/url_utility', () => ({
|
||||
visitUrl: jest.fn(),
|
||||
}));
|
||||
|
||||
let wrapper;
|
||||
let mock;
|
||||
let store;
|
||||
const Component = Vue.extend(ContributorsCharts);
|
||||
const endpoint = 'contributors';
|
||||
const endpoint = 'contributors/-/graphs';
|
||||
const branch = 'main';
|
||||
const chartData = [
|
||||
{ author_name: 'John', author_email: 'jawnnypoo@gmail.com', date: '2019-05-05' },
|
||||
{ author_name: 'John', author_email: 'jawnnypoo@gmail.com', date: '2019-03-03' },
|
||||
];
|
||||
const projectId = '23';
|
||||
const commitsPath = 'some/path';
|
||||
|
||||
function factory() {
|
||||
mock = new MockAdapter(axios);
|
||||
|
|
@ -23,19 +31,27 @@ function factory() {
|
|||
mock.onGet().reply(200, chartData);
|
||||
store = createStore();
|
||||
|
||||
wrapper = mount(Component, {
|
||||
wrapper = mountExtended(Component, {
|
||||
propsData: {
|
||||
endpoint,
|
||||
branch,
|
||||
projectId,
|
||||
commitsPath,
|
||||
},
|
||||
stubs: {
|
||||
GlLoadingIcon: true,
|
||||
GlAreaChart: true,
|
||||
RefSelector: true,
|
||||
},
|
||||
store,
|
||||
});
|
||||
}
|
||||
|
||||
const findLoadingIcon = () => wrapper.findByTestId('loading-app-icon');
|
||||
const findRefSelector = () => wrapper.findComponent(RefSelector);
|
||||
const findHistoryButton = () => wrapper.findByTestId('history-button');
|
||||
const findContributorsCharts = () => wrapper.findByTestId('contributors-charts');
|
||||
|
||||
describe('Contributors charts', () => {
|
||||
beforeEach(() => {
|
||||
factory();
|
||||
|
|
@ -53,15 +69,46 @@ describe('Contributors charts', () => {
|
|||
it('should display loader whiled loading data', async () => {
|
||||
wrapper.vm.$store.state.loading = true;
|
||||
await nextTick();
|
||||
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
|
||||
expect(findLoadingIcon().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should render charts when loading completed and there is chart data', async () => {
|
||||
it('should render charts and a RefSelector when loading completed and there is chart data', async () => {
|
||||
wrapper.vm.$store.state.loading = false;
|
||||
wrapper.vm.$store.state.chartData = chartData;
|
||||
await nextTick();
|
||||
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
|
||||
expect(wrapper.find('.contributors-charts').exists()).toBe(true);
|
||||
|
||||
expect(findLoadingIcon().exists()).toBe(false);
|
||||
expect(findRefSelector().exists()).toBe(true);
|
||||
expect(findRefSelector().props()).toMatchObject({
|
||||
enabledRefTypes: [REF_TYPE_BRANCHES, REF_TYPE_TAGS],
|
||||
value: branch,
|
||||
projectId,
|
||||
translations: { dropdownHeader: 'Switch branch/tag' },
|
||||
useSymbolicRefNames: false,
|
||||
state: true,
|
||||
name: '',
|
||||
});
|
||||
expect(findContributorsCharts().exists()).toBe(true);
|
||||
expect(wrapper.element).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should have a history button with a set href attribute', async () => {
|
||||
wrapper.vm.$store.state.loading = false;
|
||||
wrapper.vm.$store.state.chartData = chartData;
|
||||
await nextTick();
|
||||
|
||||
const historyButton = findHistoryButton();
|
||||
expect(historyButton.exists()).toBe(true);
|
||||
expect(historyButton.attributes('href')).toBe(commitsPath);
|
||||
});
|
||||
|
||||
it('visits a URL when clicking on a branch/tag', async () => {
|
||||
wrapper.vm.$store.state.loading = false;
|
||||
wrapper.vm.$store.state.chartData = chartData;
|
||||
await nextTick();
|
||||
|
||||
findRefSelector().vm.$emit('input', branch);
|
||||
|
||||
expect(visitUrl).toHaveBeenCalledWith(`${endpoint}/${branch}`);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ describe('projects/settings/components/default_branch_selector', () => {
|
|||
projectId,
|
||||
refType: null,
|
||||
state: true,
|
||||
toggleButtonClass: null,
|
||||
translations: {
|
||||
dropdownHeader: expect.any(String),
|
||||
searchPlaceholder: expect.any(String),
|
||||
|
|
|
|||
|
|
@ -715,7 +715,7 @@ describe('Ref selector component', () => {
|
|||
describe('validation state', () => {
|
||||
const invalidClass = 'gl-inset-border-1-red-500!';
|
||||
const isInvalidClassApplied = () =>
|
||||
wrapper.findComponent(GlDropdown).props('toggleClass')[invalidClass];
|
||||
wrapper.findComponent(GlDropdown).props('toggleClass')[0][invalidClass];
|
||||
|
||||
describe('valid state', () => {
|
||||
describe('when the state prop is not provided', () => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
import { GlSprintf } from '@gitlab/ui';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import UsageQuotasApp from '~/usage_quotas/components/usage_quotas_app.vue';
|
||||
import { USAGE_QUOTAS_TITLE } from '~/usage_quotas/constants';
|
||||
import { defaultProvide } from '../mock_data';
|
||||
|
||||
describe('UsageQuotasApp', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = ({ provide = {} } = {}) => {
|
||||
wrapper = shallowMountExtended(UsageQuotasApp, {
|
||||
provide: {
|
||||
...defaultProvide,
|
||||
...provide,
|
||||
},
|
||||
stubs: {
|
||||
GlSprintf,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
const findSubTitle = () => wrapper.findByTestId('usage-quotas-page-subtitle');
|
||||
|
||||
it('renders the view title', () => {
|
||||
expect(wrapper.text()).toContain(USAGE_QUOTAS_TITLE);
|
||||
});
|
||||
|
||||
it('renders the view subtitle', () => {
|
||||
expect(findSubTitle().text()).toContain(defaultProvide.namespaceName);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export const defaultProvide = {
|
||||
namespaceName: 'Group 1',
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue