Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-01-24 18:11:44 +00:00
parent df9890e9a7
commit fd247970cf
119 changed files with 2026 additions and 407 deletions

View File

@ -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

View File

@ -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

View File

@ -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: -->

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -1 +1 @@
5b5abda2e69a93c5898609cd9c9aa02954c10556
69e486270838efbbb78e6736ac6aecde5ccd8caa

View File

@ -1 +1 @@
65bbfa0b62518691961de096e4a27d7c76307b7c
27d39b816071e9630436f8bf19740c173594631d

View File

@ -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>

View File

@ -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,
},
});
},

View File

@ -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",

View File

@ -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();

View File

@ -0,0 +1,3 @@
import initUsageQuotas from '~/usage_quotas';
initUsageQuotas();

View File

@ -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"

View File

@ -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>

View File

@ -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',
);

View File

@ -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);
},
});
};

View File

@ -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.');

View File

@ -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>

View File

@ -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,
},
});
},
});
});
};

View File

@ -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>

View File

@ -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;

View File

@ -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 */

View File

@ -761,6 +761,9 @@ input {
color: #ececef;
background-color: #333238;
}
input[type="search"] {
appearance: textfield;
}
.form-control {
border-radius: 4px;
padding: 6px 10px;

View File

@ -761,6 +761,9 @@ input {
color: #333238;
background-color: #fff;
}
input[type="search"] {
appearance: textfield;
}
.form-control {
border-radius: 4px;
padding: 6px 10px;

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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?

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -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,

View File

@ -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?

View File

@ -177,7 +177,7 @@ class Todo < ApplicationRecord
end
def resource_parent
project
project || group
end
def unmergeable?

View File

@ -37,7 +37,7 @@ module Analytics
end
def value_stream
@value_stream ||= params[:value_stream]
@value_stream ||= params.fetch(:value_stream)
end
end
end

View File

@ -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)

View File

@ -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|

View File

@ -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")

View File

@ -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 } }

View File

@ -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

View File

@ -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 } }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
13b992cf6f30efc7a82062c5184f3e8398704c01e73618c6dd38071ee67595e1

View File

@ -0,0 +1 @@
4933fd938c23b99963542c2f7e1f50e0270f6817ce49b0864fc7bdad63ea98b3

View File

@ -0,0 +1 @@
889e814bc9633481afeae8e63bfe080bfc956839fd5f97c0d39725f3acdff100

View File

@ -0,0 +1 @@
f4ba0d1de73da2b7a912c06ca458898f3404235025089efc74aee9fc4caa511a

View File

@ -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);

View File

@ -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

View File

@ -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:

View File

@ -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:**

View File

@ -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

View File

@ -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.

View File

@ -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. |

View File

@ -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.

View File

@ -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`

View File

@ -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.

View File

@ -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')

View File

@ -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,

View File

@ -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

View File

@ -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.

View File

@ -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:

View File

@ -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"
]
}
}
```

View File

@ -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).

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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'),

View File

@ -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 ""

View File

@ -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
)

View File

@ -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

View File

@ -91,7 +91,7 @@ gitlab:
memory: 1927Mi
limits:
cpu: 450m
memory: 2890Mi
memory: 3500Mi
webservice:
resources:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
[

View File

@ -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');

View File

@ -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', () => {

View File

@ -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>

View File

@ -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}`);
});
});

View File

@ -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),

View File

@ -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', () => {

View File

@ -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);
});
});

View File

@ -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