Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-01-27 09:08:49 +00:00
parent 8ebab6079e
commit c31a6781a3
44 changed files with 1720 additions and 436 deletions

View File

@ -26,6 +26,7 @@ memory-on-boot:
NODE_ENV: "production"
RAILS_ENV: "production"
SETUP_DB: "true"
GITLAB_ALLOW_SEPARATE_CI_DATABASE: "true"
MEMORY_ON_BOOT_FILE_PREFIX: "tmp/memory_on_boot_"
TEST_COUNT: 5
script:

View File

@ -0,0 +1,54 @@
<script>
import { GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: {
GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
GlModal: GlModalDirective,
},
props: {
secureFile: {
type: Object,
required: true,
},
admin: {
type: Boolean,
required: true,
},
modalId: {
type: String,
required: true,
},
},
i18n: {
metadataLabel: __('View File Metadata'),
},
metadataModalId: 'metadataModalId',
methods: {
selectSecureFile() {
this.$emit('selectSecureFile', this.secureFile);
},
hasMetadata() {
return this.secureFile.metadata !== null;
},
},
};
</script>
<template>
<gl-button
v-if="admin && hasMetadata()"
v-gl-modal="modalId"
v-gl-tooltip.hover.top="$options.i18n.metadataLabel"
category="secondary"
variant="info"
icon="doc-text"
:aria-label="$options.i18n.metadataLabel"
data-testid="metadata-button"
@click="selectSecureFile()"
/>
</template>

View File

@ -0,0 +1,129 @@
<script>
import { GlModal, GlSprintf, GlModalDirective } from '@gitlab/ui';
import { __, s__, createDateTimeFormat } from '~/locale';
import Tracking from '~/tracking';
import MetadataTable from './table.vue';
const dateFormat = createDateTimeFormat({
dateStyle: 'long',
timeStyle: 'long',
});
export default {
components: {
GlModal,
GlSprintf,
MetadataTable,
},
directives: {
GlModal: GlModalDirective,
},
mixins: [Tracking.mixin()],
props: {
name: {
type: String,
required: false,
default: '',
},
fileExtension: {
type: String,
required: false,
default: '',
},
metadata: {
type: Object,
required: false,
default: Object.new,
},
modalId: {
type: String,
required: true,
},
},
i18n: {
metadataLabel: __('View File Metadata'),
metadataModalTitle: s__('SecureFiles|%{name} Metadata'),
},
metadataModalId: 'metadataModalId',
methods: {
teamName() {
return `${this.metadata.subject.O} (${this.metadata.subject.OU})`;
},
issuerName() {
return [this.metadata.issuer.CN, '-', this.metadata.issuer.OU].join(' ');
},
expiresAt() {
return dateFormat.format(new Date(this.metadata.expires_at));
},
mobileprovisionTeamName() {
return `${this.metadata.team_name} (${this.metadata.team_id.join(', ')})`;
},
platformNames() {
return this.metadata.platforms.join(', ');
},
appName() {
return [this.metadata.app_name, '-', this.metadata.app_id].join(' ');
},
certificates() {
return this.metadata.certificate_ids.join(', ');
},
cerItems() {
return [
{ name: s__('SecureFiles|Name'), data: this.metadata.subject.CN },
{ name: s__('SecureFiles|Serial'), data: this.metadata.id },
{ name: s__('SecureFiles|Team'), data: this.teamName() },
{ name: s__('SecureFiles|Issuer'), data: this.issuerName() },
{ name: s__('SecureFiles|Expires at'), data: this.expiresAt() },
];
},
p12Items() {
return [
{ name: s__('SecureFiles|Name'), data: this.metadata.subject.CN },
{ name: s__('SecureFiles|Serial'), data: this.metadata.id },
{ name: s__('SecureFiles|Team'), data: this.teamName() },
{ name: s__('SecureFiles|Issuer'), data: this.issuerName() },
{ name: s__('SecureFiles|Expires at'), data: this.expiresAt() },
];
},
mobileprovisionItems() {
return [
{ name: s__('SecureFiles|UUID'), data: this.metadata.id },
{ name: s__('SecureFiles|Platforms'), data: this.platformNames() },
{ name: s__('SecureFiles|Team'), data: this.mobileprovisionTeamName() },
{ name: s__('SecureFiles|App'), data: this.appName() },
{ name: s__('SecureFiles|Certificates'), data: this.certificates() },
{ name: s__('SecureFiles|Expires at'), data: this.expiresAt() },
];
},
items() {
if (this.metadata) {
if (this.fileExtension === 'cer') {
this.track('load_secure_file_metadata_cer');
return this.cerItems();
} else if (this.fileExtension === 'p12') {
this.track('load_secure_file_metadata_p12');
return this.p12Items();
} else if (this.fileExtension === 'mobileprovision') {
this.track('load_secure_file_metadata_mobileprovision');
return this.mobileprovisionItems(this.metadata);
}
}
return [];
},
},
};
</script>
``
<template>
<gl-modal :ref="modalId" :modal-id="modalId" title-tag="h4" category="primary" hide-footer>
<template #modal-title>
<gl-sprintf :message="$options.i18n.metadataModalTitle">
<template #name>{{ name }}</template>
</gl-sprintf>
</template>
<metadata-table :items="items()" />
</gl-modal>
</template>

View File

@ -0,0 +1,36 @@
<script>
import { GlTableLite } from '@gitlab/ui';
export default {
components: {
GlTableLite,
},
props: {
items: {
required: true,
type: Array,
},
},
fields: [
{
key: 'item_name',
thClass: 'hidden',
},
{
key: 'item_data',
thClass: 'hidden',
},
],
};
</script>
<template>
<gl-table-lite :items="items" :fields="$options.fields">
<template #cell(item_name)="{ item }">
<strong>{{ item.name }}</strong>
</template>
<template #cell(item_data)="{ item }">
{{ item.data }}
</template>
</gl-table-lite>
</template>

View File

@ -17,6 +17,8 @@ import { HTTP_STATUS_PAYLOAD_TOO_LARGE } from '~/lib/utils/http_status';
import { __, s__, sprintf } from '~/locale';
import Tracking from '~/tracking';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import MetadataButton from './metadata/button.vue';
import MetadataModal from './metadata/modal.vue';
export default {
components: {
@ -29,6 +31,8 @@ export default {
GlSprintf,
GlTable,
TimeagoTooltip,
MetadataButton,
MetadataModal,
},
directives: {
GlTooltip: GlTooltipDirective,
@ -57,6 +61,7 @@ export default {
deleteModalButton: s__('SecureFiles|Delete secure file'),
},
deleteModalId: 'deleteModalId',
metadataModalId: 'metadataModalId',
data() {
return {
page: 1,
@ -68,6 +73,7 @@ export default {
projectSecureFiles: [],
deleteModalFileId: null,
deleteModalFileName: null,
metadataSecureFile: {},
};
},
fields: [
@ -162,6 +168,9 @@ export default {
this.deleteModalFileId = secureFile.id;
this.deleteModalFileName = secureFile.name;
},
updateMetadataSecureFile(secureFile) {
this.metadataSecureFile = secureFile;
},
uploadFormData(file) {
const formData = new FormData();
formData.append('name', file.name);
@ -208,6 +217,12 @@ export default {
</template>
<template #cell(actions)="{ item }">
<metadata-button
:secure-file="item"
:admin="admin"
modal-id="$options.metadataModalId"
@selectSecureFile="updateMetadataSecureFile"
/>
<gl-button
v-if="admin"
v-gl-modal="$options.deleteModalId"
@ -272,5 +287,12 @@ export default {
<template #name>{{ deleteModalFileName }}</template>
</gl-sprintf>
</gl-modal>
<metadata-modal
:name="metadataSecureFile.name"
:file-extension="metadataSecureFile.file_extension"
:metadata="metadataSecureFile.metadata"
modal-id="$options.metadataModalId"
/>
</div>
</template>

View File

@ -0,0 +1,119 @@
<script>
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import SafeHtml from '~/vue_shared/directives/safe_html';
import axios from '~/lib/utils/axios_utils';
import CompareDropdown from '~/merge_requests/components/compare_dropdown.vue';
export default {
components: {
GlIcon,
GlLoadingIcon,
CompareDropdown,
},
directives: {
SafeHtml,
},
inject: {
projectsPath: {
default: '',
},
branchCommitPath: {
default: '',
},
currentProject: {
default: '',
},
currentBranch: {
default: () => ({}),
},
inputs: {
default: () => ({}),
},
i18n: {
default: () => ({}),
},
toggleClass: {
default: () => ({}),
},
},
data() {
return {
selectedProject: this.currentProject,
selectedBranch: this.currentBranch,
loading: false,
commitHtml: null,
};
},
computed: {
staticProjectData() {
if (this.projectsPath) return undefined;
return [this.currentProject];
},
},
mounted() {
this.fetchCommit();
},
methods: {
selectProject(p) {
this.selectedProject = p;
},
selectBranch(branch) {
this.selectedBranch = branch;
this.fetchCommit();
},
async fetchCommit() {
this.loading = true;
const { data } = await axios.get(this.branchCommitPath, {
params: { target_project_id: this.selectedProject.value, ref: this.selectedBranch.value },
});
this.loading = false;
this.commitHtml = data;
},
},
};
</script>
<template>
<div>
<div class="clearfix">
<div class="merge-request-select gl-pl-0">
<compare-dropdown
:static-data="staticProjectData"
:endpoint="projectsPath"
:default="currentProject"
:dropdown-header="i18n.projectHeaderText"
:input-id="inputs.project.id"
:input-name="inputs.project.name"
:toggle-class="toggleClass.project"
is-project
@selected="selectProject"
/>
</div>
<div class="merge-request-select merge-request-branch-select gl-pr-0">
<compare-dropdown
:endpoint="selectedProject.refsUrl"
:dropdown-header="i18n.branchHeaderText"
:input-id="inputs.branch.id"
:input-name="inputs.branch.name"
:default="currentBranch"
:toggle-class="toggleClass.branch"
@selected="selectBranch"
/>
</div>
</div>
<div v-if="commitHtml || loading" class="gl-bg-gray-50 gl-rounded-base gl-my-4">
<gl-loading-icon v-if="loading" class="gl-py-3" />
<div
v-if="!selectedBranch.value"
class="compare-commit-empty gl-display-flex gl-align-items-center gl-p-5"
>
<gl-icon name="branch" class="gl-mr-3" />
{{ __('Select a branch to compare') }}
</div>
<ul v-safe-html="commitHtml" class="list-unstyled mr_source_commit"></ul>
</div>
</div>
</template>

View File

@ -0,0 +1,139 @@
<script>
import { GlListbox } from '@gitlab/ui';
import { debounce } from 'lodash';
import { createAlert } from '~/flash';
import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
export default {
components: {
GlListbox,
},
props: {
staticData: {
type: Array,
required: false,
default: () => [],
},
endpoint: {
type: String,
required: false,
default: '',
},
default: {
type: Object,
required: true,
},
dropdownHeader: {
type: String,
required: true,
},
isProject: {
type: Boolean,
required: false,
default: false,
},
inputId: {
type: String,
required: true,
},
inputName: {
type: String,
required: true,
},
toggleClass: {
type: String,
required: false,
default: '',
},
},
data() {
return {
current: this.default,
selected: this.default.value,
isLoading: false,
data: this.staticData,
searchStr: '',
};
},
computed: {
filteredData() {
if (this.endpoint) return this.data;
return this.data.filter(
(d) => d.text.toLowerCase().indexOf(this.searchStr.toLowerCase()) >= 0,
);
},
},
methods: {
async fetchData() {
if (!this.endpoint) return;
this.isLoading = true;
try {
const { data } = await axios.get(this.endpoint, {
params: { search: this.searchStr },
});
if (this.isProject) {
this.data = data.map((p) => ({
value: `${p.id}`,
text: p.full_path.replace(/^\//, ''),
refsUrl: p.refs_url,
}));
} else {
this.data = data.Branches.map((d) => ({
value: d,
text: d,
}));
}
this.isLoading = false;
} catch {
createAlert({
message: __('Error fetching data. Please try again.'),
primaryButton: { text: __('Try again'), clickHandler: () => this.fetchData() },
});
}
},
searchData: debounce(function searchData(search) {
this.searchStr = search;
this.fetchData();
}, 500),
selectItem(id) {
this.current = this.data.find((d) => d.value === id);
this.$emit('selected', this.current);
},
},
};
</script>
<template>
<div>
<input
:id="inputId"
type="hidden"
:value="current.value"
:name="inputName"
data-testid="target-project-input"
/>
<gl-listbox
v-model="selected"
:items="filteredData"
:toggle-text="current.text || dropdownHeader"
:header-text="dropdownHeader"
:searching="isLoading"
searchable
class="gl-w-full dropdown-target-project"
:toggle-class="[
'gl-align-items-flex-start! gl-justify-content-start! mr-compare-dropdown',
toggleClass,
]"
@shown="fetchData"
@search="searchData"
@select="selectItem"
/>
</div>
</template>

View File

@ -1,87 +0,0 @@
<script>
import { GlListbox } from '@gitlab/ui';
import { debounce } from 'lodash';
import { createAlert } from '~/flash';
import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
export default {
components: {
GlListbox,
},
inject: {
targetProjectsPath: {
type: String,
required: true,
},
currentProject: {
type: Object,
required: true,
},
},
data() {
return {
currentProject: this.currentProject,
selected: this.currentProject.value,
isLoading: false,
projects: [],
};
},
methods: {
async fetchProjects(search = '') {
this.isLoading = true;
try {
const { data } = await axios.get(this.targetProjectsPath, {
params: { search },
});
this.projects = data.map((p) => ({
value: `${p.id}`,
text: p.full_path.replace(/^\//, ''),
refsUrl: p.refs_url,
}));
this.isLoading = false;
} catch {
createAlert({
message: __('Error fetching target projects. Please try again.'),
primaryButton: { text: __('Try again'), clickHandler: () => this.fetchProjects(search) },
});
}
},
searchProjects: debounce(function searchProjects(search) {
this.fetchProjects(search);
}, 500),
selectProject(projectId) {
this.currentProject = this.projects.find((p) => p.value === projectId);
this.$emit('project-selected', this.currentProject.refsUrl);
},
},
};
</script>
<template>
<div>
<input
id="merge_request_target_project_id"
type="hidden"
:value="currentProject.value"
name="merge_request[target_project_id]"
data-testid="target-project-input"
/>
<gl-listbox
v-model="selected"
:items="projects"
:toggle-text="currentProject.text"
:header-text="__('Select target project')"
:searching="isLoading"
searchable
class="gl-w-full dropdown-target-project"
toggle-class="gl-align-items-flex-start! gl-justify-content-start! mr-compare-dropdown js-target-project"
@shown="fetchProjects"
@search="searchProjects"
@select="selectProject"
/>
</div>
</template>

View File

@ -1,37 +1,84 @@
import $ from 'jquery';
import Vue from 'vue';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
import MergeRequest from '~/merge_request';
import TargetProjectDropdown from '~/merge_requests/components/target_project_dropdown.vue';
import CompareApp from '~/merge_requests/components/compare_app.vue';
import { __ } from '~/locale';
import initCompare from './compare';
const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare');
if (mrNewCompareNode) {
initCompare(mrNewCompareNode);
const targetCompareEl = document.getElementById('js-target-project-dropdown');
const sourceCompareEl = document.getElementById('js-source-project-dropdown');
const compareEl = document.querySelector('.js-merge-request-new-compare');
const el = document.getElementById('js-target-project-dropdown');
const { targetProjectsPath, currentProject } = el.dataset;
// eslint-disable-next-line no-new
new Vue({
el,
name: 'TargetProjectDropdown',
provide: {
targetProjectsPath,
currentProject: JSON.parse(currentProject),
},
render(h) {
return h(TargetProjectDropdown, {
on: {
'project-selected': function projectSelectedFunction(refsUrl) {
const $targetBranchDropdown = $('.js-target-branch');
$targetBranchDropdown.data('refsUrl', refsUrl);
$targetBranchDropdown.data('deprecatedJQueryDropdown').clearMenu();
if (window.gon?.features?.mrCompareDropdowns) {
// eslint-disable-next-line no-new
new Vue({
el: sourceCompareEl,
name: 'SourceCompareApp',
provide: {
currentProject: JSON.parse(sourceCompareEl.dataset.currentProject),
currentBranch: JSON.parse(sourceCompareEl.dataset.currentBranch),
branchCommitPath: compareEl.dataset.sourceBranchUrl,
inputs: {
project: {
id: 'merge_request_source_project_id',
name: 'merge_request[source_project_id]',
},
branch: {
id: 'merge_request_source_branch',
name: 'merge_request[source_branch]',
},
},
});
},
});
i18n: {
projectHeaderText: __('Select source project'),
branchHeaderText: __('Select source branch'),
},
toggleClass: {
project: 'js-source-project',
branch: 'js-source-branch',
},
},
render(h) {
return h(CompareApp);
},
});
// eslint-disable-next-line no-new
new Vue({
el: targetCompareEl,
name: 'TargetCompareApp',
provide: {
currentProject: JSON.parse(targetCompareEl.dataset.currentProject),
currentBranch: JSON.parse(targetCompareEl.dataset.currentBranch),
projectsPath: targetCompareEl.dataset.targetProjectsPath,
branchCommitPath: compareEl.dataset.targetBranchUrl,
inputs: {
project: {
id: 'merge_request_target_project_id',
name: 'merge_request[target_project_id]',
},
branch: {
id: 'merge_request_target_branch',
name: 'merge_request[target_branch]',
},
},
i18n: {
projectHeaderText: __('Select target project'),
branchHeaderText: __('Select target branch'),
},
toggleClass: {
project: 'js-target-project',
branch: 'js-target-branch',
},
},
render(h) {
return h(CompareApp);
},
});
} else {
initCompare(mrNewCompareNode);
}
} else {
const mrNewSubmitNode = document.querySelector('.js-merge-request-new-submit');
// eslint-disable-next-line no-new

View File

@ -0,0 +1,80 @@
<script>
import { GlAlert, GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: {
GlAlert,
GlButton,
},
props: {
error: {
type: String,
required: false,
default: '',
},
},
data() {
return {
isOpen: true,
};
},
computed: {
toggleIcon() {
return this.isOpen ? 'chevron-lg-up' : 'chevron-lg-down';
},
toggleLabel() {
return this.isOpen ? __('Collapse') : __('Expand');
},
},
methods: {
hide() {
this.isOpen = false;
},
show() {
this.isOpen = true;
},
toggle() {
this.isOpen = !this.isOpen;
},
},
};
</script>
<template>
<div class="gl-rounded-base gl-border-1 gl-border-solid gl-border-gray-100 gl-bg-gray-10 gl-mt-4">
<div
class="gl-px-5 gl-py-3 gl-display-flex gl-justify-content-space-between"
:class="{ 'gl-border-b-1 gl-border-b-solid gl-border-b-gray-100': isOpen }"
>
<div class="gl-display-flex gl-flex-grow-1">
<h5 class="gl-m-0 gl-line-height-24">
<slot name="header"></slot>
</h5>
<slot name="header-suffix"></slot>
</div>
<slot name="header-right"></slot>
<div class="gl-border-l-1 gl-border-l-solid gl-border-l-gray-100 gl-pl-3 gl-ml-3">
<gl-button
category="tertiary"
size="small"
:icon="toggleIcon"
:aria-label="toggleLabel"
data-testid="widget-toggle"
@click="toggle"
/>
</div>
</div>
<gl-alert v-if="error" variant="danger" @dismiss="$emit('dismissAlert')">
{{ error }}
</gl-alert>
<div
v-if="isOpen"
class="gl-bg-gray-10 gl-rounded-bottom-left-base gl-rounded-bottom-right-base"
:class="{ 'gl-p-5 gl-pb-3': !error }"
data-testid="widget-body"
>
<slot name="body"></slot>
</div>
</div>
</template>

View File

@ -1,13 +1,5 @@
<script>
import {
GlButton,
GlDropdown,
GlDropdownItem,
GlIcon,
GlAlert,
GlLoadingIcon,
GlTooltipDirective,
} from '@gitlab/ui';
import { GlDropdown, GlDropdownItem, GlIcon, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import { produce } from 'immer';
import { isEmpty } from 'lodash';
import { s__ } from '~/locale';
@ -17,30 +9,30 @@ import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import getIssueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
import { isMetaKey, parseBoolean } from '~/lib/utils/common_utils';
import { setUrlParams, updateHistory, getParameterByName } from '~/lib/utils/url_utility';
import { getParameterByName, setUrlParams, updateHistory } from '~/lib/utils/url_utility';
import {
FORM_TYPES,
WIDGET_ICONS,
WORK_ITEM_STATUS_TEXT,
WIDGET_TYPE_HIERARCHY,
WORK_ITEM_STATUS_TEXT,
} from '../../constants';
import getWorkItemLinksQuery from '../../graphql/work_item_links.query.graphql';
import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql';
import workItemQuery from '../../graphql/work_item.query.graphql';
import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
import WidgetWrapper from '../widget_wrapper.vue';
import WorkItemDetailModal from '../work_item_detail_modal.vue';
import WorkItemLinkChild from './work_item_link_child.vue';
import WorkItemLinksForm from './work_item_links_form.vue';
export default {
components: {
GlButton,
GlDropdown,
GlDropdownItem,
GlIcon,
GlAlert,
GlLoadingIcon,
WidgetWrapper,
WorkItemLinkChild,
WorkItemLinksForm,
WorkItemDetailModal,
@ -105,13 +97,13 @@ export default {
data() {
return {
isShownAddForm: false,
isOpen: true,
activeChild: {},
activeToast: null,
prefetchedWorkItem: null,
error: undefined,
parentIssue: null,
formType: null,
workItem: null,
};
},
computed: {
@ -137,12 +129,6 @@ export default {
isChildrenEmpty() {
return this.children?.length === 0;
},
toggleIcon() {
return this.isOpen ? 'chevron-lg-up' : 'chevron-lg-down';
},
toggleLabel() {
return this.isOpen ? s__('WorkItem|Collapse tasks') : s__('WorkItem|Expand tasks');
},
issuableGid() {
return this.issuableId ? convertToGraphQLId(TYPE_WORK_ITEM, this.issuableId) : null;
},
@ -180,11 +166,8 @@ export default {
}
},
methods: {
toggle() {
this.isOpen = !this.isOpen;
},
showAddForm(formType) {
this.isOpen = true;
this.$refs.wrapper.show();
this.isShownAddForm = true;
this.formType = formType;
this.$nextTick(() => {
@ -323,24 +306,23 @@ export default {
</script>
<template>
<div
class="gl-rounded-base gl-border-1 gl-border-solid gl-border-gray-100 gl-bg-gray-10 gl-mt-4"
<widget-wrapper
ref="wrapper"
:error="error"
data-testid="work-item-links"
@dismissAlert="error = undefined"
>
<div
class="gl-px-5 gl-py-3 gl-display-flex gl-justify-content-space-between"
:class="{ 'gl-border-b-1 gl-border-b-solid gl-border-b-gray-100': isOpen }"
>
<div class="gl-display-flex gl-flex-grow-1">
<h5 class="gl-m-0 gl-line-height-24">{{ $options.i18n.title }}</h5>
<span
class="gl-display-inline-flex gl-align-items-center gl-line-height-24 gl-ml-3"
data-testid="children-count"
>
<gl-icon :name="$options.WIDGET_TYPE_TASK_ICON" class="gl-mr-2 gl-text-secondary" />
{{ childrenCountLabel }}
</span>
</div>
<template #header>{{ $options.i18n.title }}</template>
<template #header-suffix>
<span
class="gl-display-inline-flex gl-align-items-center gl-line-height-24 gl-ml-3"
data-testid="children-count"
>
<gl-icon :name="$options.WIDGET_TYPE_TASK_ICON" class="gl-mr-2 gl-text-secondary" />
{{ childrenCountLabel }}
</span>
</template>
<template #header-right>
<gl-dropdown
v-if="canUpdate"
right
@ -361,26 +343,8 @@ export default {
{{ $options.i18n.addChildOptionLabel }}
</gl-dropdown-item>
</gl-dropdown>
<div class="gl-border-l-1 gl-border-l-solid gl-border-l-gray-100 gl-pl-3 gl-ml-3">
<gl-button
category="tertiary"
size="small"
:icon="toggleIcon"
:aria-label="toggleLabel"
data-testid="toggle-links"
@click="toggle"
/>
</div>
</div>
<gl-alert v-if="error && !isLoading" variant="danger" @dismiss="error = undefined">
{{ error }}
</gl-alert>
<div
v-if="isOpen"
class="gl-bg-gray-10 gl-rounded-bottom-left-base gl-rounded-bottom-right-base"
:class="{ 'gl-p-5 gl-pb-3': !error }"
data-testid="links-body"
>
</template>
<template #body>
<gl-loading-icon v-if="isLoading" color="dark" class="gl-my-3" />
<template v-else>
@ -423,6 +387,6 @@ export default {
@workItemDeleted="handleWorkItemDeleted(activeChild.id)"
/>
</template>
</div>
</div>
</template>
</widget-wrapper>
</template>

View File

@ -1,7 +1,5 @@
<script>
import { GlButton } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { __ } from '~/locale';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
@ -19,6 +17,7 @@ import {
} from '../../constants';
import workItemQuery from '../../graphql/work_item.query.graphql';
import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
import WidgetWrapper from '../widget_wrapper.vue';
import OkrActionsSplitButton from './okr_actions_split_button.vue';
import WorkItemLinksForm from './work_item_links_form.vue';
import WorkItemLinkChild from './work_item_link_child.vue';
@ -29,8 +28,8 @@ export default {
WORK_ITEM_TYPE_ENUM_OBJECTIVE,
WORK_ITEM_TYPE_ENUM_KEY_RESULT,
components: {
GlButton,
OkrActionsSplitButton,
WidgetWrapper,
WorkItemLinksForm,
WorkItemLinkChild,
},
@ -72,20 +71,12 @@ export default {
data() {
return {
isShownAddForm: false,
isOpen: true,
error: null,
formType: null,
childType: null,
prefetchedWorkItem: null,
};
},
computed: {
toggleIcon() {
return this.isOpen ? 'chevron-lg-up' : 'chevron-lg-down';
},
toggleLabel() {
return this.isOpen ? __('Collapse') : __('Expand');
},
fetchByIid() {
return this.glFeatures.useIidInWorkItemsPath && parseBoolean(getParameterByName('iid_path'));
},
@ -121,11 +112,8 @@ export default {
}
},
methods: {
toggle() {
this.isOpen = !this.isOpen;
},
showAddForm(formType, childType) {
this.isOpen = true;
this.$refs.wrapper.show();
this.isShownAddForm = true;
this.formType = formType;
this.childType = childType;
@ -176,19 +164,11 @@ export default {
</script>
<template>
<div
class="gl-rounded-base gl-border-1 gl-border-solid gl-border-gray-100 gl-bg-gray-10 gl-mt-4"
data-testid="work-item-tree"
>
<div
class="gl-px-5 gl-py-3 gl-display-flex gl-justify-content-space-between"
:class="{ 'gl-border-b-1 gl-border-b-solid gl-border-b-gray-100': isOpen }"
>
<div class="gl-display-flex gl-flex-grow-1">
<h5 class="gl-m-0 gl-line-height-24">
{{ $options.WORK_ITEMS_TREE_TEXT_MAP[workItemType].title }}
</h5>
</div>
<widget-wrapper ref="wrapper" data-testid="work-item-tree">
<template #header>
{{ $options.WORK_ITEMS_TREE_TEXT_MAP[workItemType].title }}
</template>
<template #header-right>
<okr-actions-split-button
@showCreateObjectiveForm="
showAddForm($options.FORM_TYPES.create, $options.WORK_ITEM_TYPE_ENUM_OBJECTIVE)
@ -203,24 +183,9 @@ export default {
showAddForm($options.FORM_TYPES.add, $options.WORK_ITEM_TYPE_ENUM_KEY_RESULT)
"
/>
<div class="gl-border-l-1 gl-border-l-solid gl-border-l-gray-100 gl-pl-3 gl-ml-3">
<gl-button
category="tertiary"
size="small"
:icon="toggleIcon"
:aria-label="toggleLabel"
data-testid="toggle-tree"
@click="toggle"
/>
</div>
</div>
<div
v-if="isOpen"
class="gl-bg-gray-10 gl-rounded-bottom-left-base gl-rounded-bottom-right-base"
:class="{ 'gl-p-5 gl-pb-3': !error }"
data-testid="tree-body"
>
<div v-if="!isShownAddForm && !error && children.length === 0" data-testid="tree-empty">
</template>
<template #body>
<div v-if="!isShownAddForm && children.length === 0" data-testid="tree-empty">
<p class="gl-mb-3">
{{ $options.WORK_ITEMS_TREE_TEXT_MAP[workItemType].empty }}
</p>
@ -253,6 +218,6 @@ export default {
@removeChild="$emit('removeChild', $event)"
@click="$emit('show-modal', $event, $event.childItem || child)"
/>
</div>
</div>
</template>
</widget-wrapper>
</template>

View File

@ -384,7 +384,15 @@ $comparison-empty-state-height: 62px;
}
.mr-compare-dropdown {
@include gl-w-full;
.gl-button-text {
@include gl-w-full;
}
}
.merge-request-branch-select {
.gl-button-text {
@include gl-font-monospace;
}
}

View File

@ -35,7 +35,7 @@ module Ci
end
def file_extension
File.extname(name).delete_prefix('.')
File.extname(name).delete_prefix('.').presence
end
def metadata_parsable?

View File

@ -9,41 +9,44 @@
.card-new-merge-request
%h2.gl-font-size-h2
= _('Source branch')
.clearfix
.merge-request-select.dropdown
= f.hidden_field :source_project_id
= dropdown_toggle @merge_request.source_project_path, { toggle: "dropdown", 'field-name': "#{f.object_name}[source_project_id]", disabled: @merge_request.persisted?, default_text: _("Select source project") }, { toggle_class: "js-compare-dropdown js-source-project" }
.dropdown-menu.dropdown-menu-selectable.dropdown-source-project
= dropdown_title(_("Select source project"))
= dropdown_filter(_("Search projects"))
= dropdown_content do
= render 'projects/merge_requests/dropdowns/project',
projects: [@merge_request.source_project],
selected: f.object.source_project_id
.merge-request-select.dropdown
= f.hidden_field :source_branch
= dropdown_toggle f.object.source_branch.presence || _("Select source branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[source_branch]", 'refs-url': refs_project_path(@source_project), selected: f.object.source_branch, default_text: _("Select target branch"), qa_selector: "source_branch_dropdown" }, { toggle_class: "js-compare-dropdown js-source-branch monospace" }
.dropdown-menu.dropdown-menu-selectable.js-source-branch-dropdown.git-revision-dropdown
= dropdown_title(_("Select source branch"))
= dropdown_filter(_("Search branches"))
= dropdown_content
= dropdown_loading
.gl-bg-gray-50.gl-rounded-base.gl-mx-2.gl-my-4
.compare-commit-empty.js-source-commit-empty.gl-display-flex.gl-align-items-center.gl-p-5{ style: 'display: none;' }
= sprite_icon('branch', size: 16, css_class: 'gl-mr-3')
= _('Select a branch to compare')
= gl_loading_icon(css_class: 'js-source-loading gl-py-3')
%ul.list-unstyled.mr_source_commit
- if Feature.enabled?(:mr_compare_dropdowns, @project)
#js-source-project-dropdown{ data: { current_project: { value: f.object.source_project_id.to_s, text: f.object.source_project.full_path, refsUrl: refs_project_path(f.object.source_project) }.to_json, current_branch: { value: f.object.source_branch.presence, text: f.object.source_branch.presence }.to_json } }
- else
.clearfix
.merge-request-select.dropdown
= f.hidden_field :source_project_id
= dropdown_toggle @merge_request.source_project_path, { toggle: "dropdown", 'field-name': "#{f.object_name}[source_project_id]", disabled: @merge_request.persisted?, default_text: _("Select source project") }, { toggle_class: "js-compare-dropdown js-source-project" }
.dropdown-menu.dropdown-menu-selectable.dropdown-source-project
= dropdown_title(_("Select source project"))
= dropdown_filter(_("Search projects"))
= dropdown_content do
= render 'projects/merge_requests/dropdowns/project',
projects: [@merge_request.source_project],
selected: f.object.source_project_id
.merge-request-select.dropdown
= f.hidden_field :source_branch
= dropdown_toggle f.object.source_branch.presence || _("Select source branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[source_branch]", 'refs-url': refs_project_path(@source_project), selected: f.object.source_branch, default_text: _("Select target branch"), qa_selector: "source_branch_dropdown" }, { toggle_class: "js-compare-dropdown js-source-branch monospace" }
.dropdown-menu.dropdown-menu-selectable.js-source-branch-dropdown.git-revision-dropdown
= dropdown_title(_("Select source branch"))
= dropdown_filter(_("Search branches"))
= dropdown_content
= dropdown_loading
.gl-bg-gray-50.gl-rounded-base.gl-mx-2.gl-my-4
.compare-commit-empty.js-source-commit-empty.gl-display-flex.gl-align-items-center.gl-p-5{ style: 'display: none;' }
= sprite_icon('branch', size: 16, css_class: 'gl-mr-3')
= _('Select a branch to compare')
= gl_loading_icon(css_class: 'js-source-loading gl-py-3')
%ul.list-unstyled.mr_source_commit
.col-lg-6
.card-new-merge-request
%h2.gl-font-size-h2
= _('Target branch')
.clearfix
.merge-request-select.dropdown
- if Feature.enabled?(:mr_compare_dropdowns, @project)
#js-target-project-dropdown{ data: { target_projects_path: project_new_merge_request_json_target_projects_path(@project), current_project: { value: f.object.target_project_id.to_s, text: f.object.target_project.full_path }.to_json } }
- else
- if Feature.enabled?(:mr_compare_dropdowns, @project)
#js-target-project-dropdown{ data: { target_projects_path: project_new_merge_request_json_target_projects_path(@project), current_project: { value: f.object.target_project_id.to_s, text: f.object.target_project.full_path, refsUrl: refs_project_path(f.object.target_project) }.to_json, current_branch: { value: f.object.target_branch.presence, text: f.object.target_branch.presence }.to_json } }
- else
.clearfix
.merge-request-select.dropdown
- projects = target_projects(@project)
= f.hidden_field :target_project_id
= dropdown_toggle f.object.target_project.full_path, { toggle: "dropdown", 'field-name': "#{f.object_name}[target_project_id]", disabled: @merge_request.persisted?, default_text: _("Select target project") }, { toggle_class: "js-compare-dropdown js-target-project" }
@ -54,21 +57,23 @@
= render 'projects/merge_requests/dropdowns/project',
projects: projects,
selected: f.object.target_project_id
.merge-request-select.dropdown
= f.hidden_field :target_branch
= dropdown_toggle f.object.target_branch.presence || _("Select target branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[target_branch]", 'refs-url': refs_project_path(f.object.target_project), selected: f.object.target_branch, default_text: _("Select target branch") }, { toggle_class: "js-compare-dropdown js-target-branch monospace" }
.dropdown-menu.dropdown-menu-selectable.js-target-branch-dropdown.git-revision-dropdown
= dropdown_title(_("Select target branch"))
= dropdown_filter(_("Search branches"))
= dropdown_content
= dropdown_loading
.gl-bg-gray-50.gl-rounded-base.gl-mx-2.gl-my-4
.compare-commit-empty.js-target-commit-empty.gl-display-flex.gl-align-items-center.gl-p-5{ style: 'display: none;' }
= sprite_icon('branch', size: 16, css_class: 'gl-mr-3')
= _('Select a branch to compare')
= gl_loading_icon(css_class: 'js-target-loading gl-py-3')
%ul.list-unstyled.mr_target_commit
.merge-request-select.dropdown
= f.hidden_field :target_branch
= dropdown_toggle f.object.target_branch.presence || _("Select target branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[target_branch]", 'refs-url': refs_project_path(f.object.target_project), selected: f.object.target_branch, default_text: _("Select target branch") }, { toggle_class: "js-compare-dropdown js-target-branch monospace" }
.dropdown-menu.dropdown-menu-selectable.js-target-branch-dropdown.git-revision-dropdown
= dropdown_title(_("Select target branch"))
= dropdown_filter(_("Search branches"))
= dropdown_content
= dropdown_loading
.gl-bg-gray-50.gl-rounded-base.gl-mx-2.gl-my-4
.compare-commit-empty.js-target-commit-empty.gl-display-flex.gl-align-items-center.gl-p-5{ style: 'display: none;' }
= sprite_icon('branch', size: 16, css_class: 'gl-mr-3')
= _('Select a branch to compare')
= gl_loading_icon(css_class: 'js-target-loading gl-py-3')
%ul.list-unstyled.mr_target_commit
- if @merge_request.errors.any?
= form_errors(@merge_request)
= f.submit _('Compare branches and continue'), data: { qa_selector: 'compare_branches_button' }, pajamas_button: true
.row
.col-12
= f.submit _('Compare branches and continue'), data: { qa_selector: 'compare_branches_button' }, pajamas_button: true

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
return if Gitlab::Utils.to_boolean(ENV.fetch('GITLAB_ALLOW_SEPARATE_CI_DATABASE', false))
# GitLab.com is already decomposed
return if Gitlab.com?
# It is relatively safe for development, and GDK defaults to decomposed already
return if Gitlab.dev_or_test_env?
ci_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, name: 'ci')
main_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, name: 'main')
return unless ci_config
# If the ci `database` is the same as main `database`, it is likely the same
return if ci_config.database == main_config.database &&
ci_config.host == main_config.host
raise "Separate CI database is not ready for production use!\n\n" \
"Did you mean to use `database: #{main_config.database}` for the `ci:` database connection?\n" \
"Or, use `export GITLAB_ALLOW_SEPARATE_CI_DATABASE=1` to ignore this check."

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
class RemoveNotNullConstraintsForTablesWithSchemaDifferencesV3 < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
def up
return unless Gitlab.com?
change_column_null :integrations, :updated_at, true
change_column_null :integrations, :created_at, true
change_column_null :project_settings, :show_default_award_emojis, true
end
def down
# no-op
end
end

View File

@ -0,0 +1 @@
259fa00544b9f63d512738dcbd4fb1ffcdbbfae58e15f7fbeb4fe34e5e7fe1f3

View File

@ -67,6 +67,9 @@ the other way around.
1. Save the `config/database.yml` file.
1. Update the service files to set the `GITLAB_ALLOW_SEPARATE_CI_DATABASE`
environment variable to `true`.
1. Create the `gitlabhq_production_ci` database:
```shell
@ -100,6 +103,7 @@ the other way around.
1. Edit `/etc/gitlab/gitlab.rb` and add the following lines:
```ruby
gitlab_rails['env'] = { 'GITLAB_ALLOW_SEPARATE_CI_DATABASE' => 'true' }
gitlab_rails['databases']['ci']['enable'] = true
gitlab_rails['databases']['ci']['db_database'] = 'gitlabhq_production_ci'
```

View File

@ -1229,8 +1229,8 @@ curl --request POST --header "PRIVATE-TOKEN: <your-token>" \
| Attribute | Type | Required | Description |
|-------------------------------------------------------------|---------|------------------------|-------------|
| `name` | string | **{check-circle}** Yes (if path isn't provided) | The name of the new project. Equals path if not provided. |
| `path` | string | **{check-circle}** Yes (if name isn't provided) | Repository name for new project. Generated based on name if not provided (generated as lowercase with dashes). Starting with GitLab 14.9, path must not start or end with a special character and must not contain consecutive special characters. |
| `name` | string | **{check-circle}** Yes (if `path` isn't provided) | The name of the new project. Equals path if not provided. |
| `path` | string | **{check-circle}** Yes (if `name` isn't provided) | Repository name for new project. Generated based on name if not provided (generated as lowercase with dashes). Starting with GitLab 14.9, path must not start or end with a special character and must not contain consecutive special characters. |
| `allow_merge_on_skipped_pipeline` | boolean | **{dotted-circle}** No | Set whether or not merge requests can be merged with skipped jobs. |
| `only_allow_merge_if_all_status_checks_passed` **(ULTIMATE)** | boolean | **{dotted-circle}** No | Indicates that merges of merge requests should be blocked unless all status checks have passed. Defaults to false. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/369859) in GitLab 15.5 with feature flag `only_allow_merge_if_all_status_checks_passed` disabled by default. |
| `analytics_access_level` | string | **{dotted-circle}** No | One of `disabled`, `private` or `enabled` |

View File

@ -31,7 +31,7 @@ for long-term growth.
Our main database is not prepared for analytical workloads. Executing long-running queries can
affect the reliability of the application. For large groups, the current
implementation (old backend) is slow and, in some cases, doesn't even load due to the configured
statement timeout (15s).
statement timeout (15 s).
The database queries in the old backend use the core domain models directly through
`IssuableFinders` classes: ([MergeRequestsFinder](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/finders/merge_requests_finder.rb) and [IssuesFinder](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/finders/issues_finder.rb)).
@ -62,7 +62,7 @@ different development workflows within the `Test Group` (top-level namespace).
The first value stream uses standard timestamp-based events for defining the stages. The second
value stream uses label events.
Each value stream and stage item from the example will be persisted in the database. Notice that
Each value stream and stage item from the example is persisted in the database. Notice that
the `Deployment` stage is identical for both value streams; that means that the underlying
`stage_event_hash_id` is the same for both stages. The `stage_event_hash_id` reduces
the amount of data the backend collects and plays a vital role in database partitioning.
@ -111,8 +111,8 @@ the service performs operations in batches and enforces strict application limit
later.
- Continue processing data from a specific point.
As of GitLab 14.7, the data loading is done manually. Once the feature is ready, the service will
be invoked periodically by the system via a cron job (this part is not implemented yet).
As of GitLab 14.7, the data loading is done manually. Once the feature is ready, the service is
invoked periodically by the system via a cron job (this part is not implemented yet).
#### Record iteration
@ -193,7 +193,7 @@ aggregated data separated, we use two additional database tables:
- `analytics_cycle_analytics_merge_request_stage_events`
Both tables are hash partitioned by the `stage_event_hash_id`. Each table uses 32 partitions. It's
an arbitrary number and it could be changed. Important is to keep the partitions under 100GB in
an arbitrary number and it could be changed. Important is to keep the partitions under 100 GB in
size (which gives the feature a lot of headroom).
|Column|Description|

View File

@ -54,12 +54,12 @@ Memory requirements are dependent on the number of users and expected workload.
The following is the recommended minimum Memory hardware guidance for a handful of example GitLab user base sizes.
- **4GB RAM** is the **required** minimum memory size and supports up to 500 users
- **4 GB RAM** is the **required** minimum memory size and supports up to 500 users
- Our [Memory Team](https://about.gitlab.com/handbook/engineering/development/enablement/data_stores/application_performance/) is working to reduce the memory requirement.
- 8GB RAM supports up to 1000 users
- 8 GB RAM supports up to 1000 users
- More users? Consult the [reference architectures page](../administration/reference_architectures/index.md)
In addition to the above, we generally recommend having at least 2GB of swap on your server,
In addition to the above, we generally recommend having at least 2 GB of swap on your server,
even if you currently have enough available RAM. Having swap helps to reduce the chance of errors occurring
if your available memory changes. We also recommend configuring the kernel's swappiness setting
to a low value like `10` to make the most of your RAM while still having the swap
@ -154,7 +154,7 @@ of GitLab Support or other GitLab engineers.
- GitLab may create new schemas as part of Rails database migrations. This happens when performing
a GitLab upgrade. The GitLab database account requires access to do this.
- GitLab creates and modifies tables during the upgrade process, and also as part of normal
- GitLab creates and modifies tables during the upgrade process, and also as part of standard
operations to manage partitioned tables.
- You should not modify the GitLab schema (for example, adding triggers or modifying tables).
@ -254,10 +254,10 @@ if you must increase the number of Puma workers.
## Redis and Sidekiq
Redis stores all user sessions and the background task queue.
The storage requirements for Redis are minimal, about 25kB per user.
The storage requirements for Redis are minimal, about 25 kB per user.
Sidekiq processes the background jobs with a multi-threaded process.
This process starts with the entire Rails stack (200MB+) but it can grow over time due to memory leaks.
On a very active server (10,000 billable users) the Sidekiq process can use 1GB+ of memory.
This process starts with the entire Rails stack (200 MB+) but it can grow over time due to memory leaks.
On a very active server (10,000 billable users) the Sidekiq process can use 1 GB+ of memory.
## Prometheus and its exporters

View File

@ -142,7 +142,7 @@ cd $indexer_path && sudo make install
The `gitlab-elasticsearch-indexer` is installed to `/usr/local/bin`.
You can change the installation path with the `PREFIX` environment variable.
Please remember to pass the `-E` flag to `sudo` if you do so.
Remember to pass the `-E` flag to `sudo` if you do so.
Example:
@ -165,7 +165,7 @@ These errors may occur when indexing Git repository data.
## Enable Advanced Search
For GitLab instances with more than 50GB repository data you can follow the instructions for [how to index large instances efficiently](#how-to-index-large-instances-efficiently) below.
For GitLab instances with more than 50 GB repository data you can follow the instructions for [how to index large instances efficiently](#how-to-index-large-instances-efficiently) below.
To enable Advanced Search, you must have administrator access to GitLab:
@ -217,7 +217,7 @@ The following Elasticsearch settings are available:
| `Number of Elasticsearch shards` | Elasticsearch indices are split into multiple shards for performance reasons. In general, you should use at least 5 shards, and indices with tens of millions of documents need to have more shards ([see below](#guidance-on-choosing-optimal-cluster-configuration)). Changes to this value do not take effect until the index is recreated. You can read more about tradeoffs in the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/scalability.html). |
| `Number of Elasticsearch replicas` | Each Elasticsearch shard can have a number of replicas. These are a complete copy of the shard, and can provide increased query performance or resilience against hardware failure. Increasing this value increases total disk space required by the index. |
| `Limit the number of namespaces and projects that can be indexed` | Enabling this allows you to select namespaces and projects to index. All other namespaces and projects use database search instead. If you enable this option but do not select any namespaces or projects, none are indexed. [Read more below](#limit-the-number-of-namespaces-and-projects-that-can-be-indexed).|
| `Using AWS OpenSearch Service with IAM credentials` | Sign your OpenSearch requests using [AWS IAM authorization](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html), [AWS EC2 Instance Profile Credentials](https://docs.aws.amazon.com/codedeploy/latest/userguide/getting-started-create-iam-instance-profile.html#getting-started-create-iam-instance-profile-cli), or [AWS ECS Tasks Credentials](https://docs.aws.amazon.com/AmazonECS/latest/userguide/task-iam-roles.html). Please refer to [Identity and Access Management in Amazon OpenSearch Service](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/ac.html) for details of AWS hosted OpenSearch domain access policy configuration. |
| `Using AWS OpenSearch Service with IAM credentials` | Sign your OpenSearch requests using [AWS IAM authorization](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html), [AWS EC2 Instance Profile Credentials](https://docs.aws.amazon.com/codedeploy/latest/userguide/getting-started-create-iam-instance-profile.html#getting-started-create-iam-instance-profile-cli), or [AWS ECS Tasks Credentials](https://docs.aws.amazon.com/AmazonECS/latest/userguide/task-iam-roles.html). Refer to [Identity and Access Management in Amazon OpenSearch Service](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/ac.html) for details of AWS hosted OpenSearch domain access policy configuration. |
| `AWS Region` | The AWS region in which your OpenSearch Service is located. |
| `AWS Access Key` | The AWS access key. |
| `AWS Secret Access Key` | The AWS secret access key. |
@ -319,7 +319,7 @@ under **Elasticsearch indexing restrictions** more options become available.
![limit namespaces and projects options](img/limit_namespaces_projects_options.png)
You can select namespaces and projects to index exclusively. Note that if the namespace is a group, it includes
You can select namespaces and projects to index exclusively. If the namespace is a group, it includes
any subgroups and projects belonging to those subgroups to be indexed as well.
Advanced Search only provides cross-group code/commit search (global) if all name-spaces are indexed. In this particular scenario where only a subset of namespaces are indexed, a global search does not provide a code or commit scope. This is possible only in the scope of an indexed namespace. There is no way to code/commit search in multiple indexed namespaces (when only a subset of namespaces has been indexed). For example if two groups are indexed, there is no way to run a single code search on both. You can only run a code search on the first group and then on the second.
@ -343,7 +343,7 @@ You can improve the language support for Chinese and Japanese languages by utili
To enable languages support:
1. Install the desired plugins, please refer to [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/plugins/7.9/installation.html) for plugins installation instructions. The plugins must be installed on every node in the cluster, and each node must be restarted after installation. For a list of plugins, see the table later in this section.
1. Install the desired plugins, refer to [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/plugins/7.9/installation.html) for plugins installation instructions. The plugins must be installed on every node in the cluster, and each node must be restarted after installation. For a list of plugins, see the table later in this section.
1. On the top bar, select **Main menu > Admin**.
1. On the left sidebar, select **Settings > Advanced Search**.
1. Locate **Custom analyzers: language support**.
@ -357,9 +357,9 @@ For guidance on what to install, see the following Elasticsearch language plugin
| Parameter | Description |
|-------------------------------------------------------|-------------|
| `Enable Chinese (smartcn) custom analyzer: Indexing` | Enables or disables Chinese language support using [`smartcn`](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-smartcn.html) custom analyzer for newly created indices.|
| `Enable Chinese (smartcn) custom analyzer: Search` | Enables or disables using [`smartcn`](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-smartcn.html) fields for Advanced Search. Please only enable this after [installing the plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-smartcn.html), enabling custom analyzer indexing and recreating the index.|
| `Enable Chinese (smartcn) custom analyzer: Search` | Enables or disables using [`smartcn`](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-smartcn.html) fields for Advanced Search. Only enable this after [installing the plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-smartcn.html), enabling custom analyzer indexing and recreating the index.|
| `Enable Japanese (kuromoji) custom analyzer: Indexing` | Enables or disables Japanese language support using [`kuromoji`](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html) custom analyzer for newly created indices.|
| `Enable Japanese (kuromoji) custom analyzer: Search` | Enables or disables using [`kuromoji`](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html) fields for Advanced Search. Please only enable this after [installing the plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html), enabling custom analyzer indexing and recreating the index.|
| `Enable Japanese (kuromoji) custom analyzer: Search` | Enables or disables using [`kuromoji`](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html) fields for Advanced Search. Only enable this after [installing the plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html), enabling custom analyzer indexing and recreating the index.|
## Disable Advanced Search
@ -393,7 +393,7 @@ and Elasticsearch index alias feature to perform the operation. We set up an ind
`primary` index which is used by GitLab for reads/writes. When reindexing process starts, we temporarily pause
the writes to the `primary` index. Then, we create another index and invoke the Reindex API which migrates the
index data onto the new index. After the reindexing job is complete, we switch to the new index by connecting the
index alias to it which becomes the new `primary` index. At the end, we resume the writes and normal operation resumes.
index alias to it which becomes the new `primary` index. At the end, we resume the writes and typical operation resumes.
### Trigger the reindex via the Advanced Search administration
@ -521,7 +521,7 @@ This should return something similar to:
}
```
In order to debug issues with the migrations you can check the [`elasticsearch.log` file](../../administration/logs/index.md#elasticsearchlog).
To debug issues with the migrations you can check the [`elasticsearch.log` file](../../administration/logs/index.md#elasticsearchlog).
### Retry a halted migration
@ -565,7 +565,7 @@ The following are some available Rake tasks:
| [`sudo gitlab-rake gitlab:elastic:resume_indexing`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Resumes Elasticsearch indexing. |
| [`sudo gitlab-rake gitlab:elastic:index_projects`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Iterates over all projects, and queues Sidekiq jobs to index them in the background. It can only be used after the index is created. |
| [`sudo gitlab-rake gitlab:elastic:index_projects_status`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Determines the overall status of the indexing. It is done by counting the total number of indexed projects, dividing by a count of the total number of projects, then multiplying by 100. |
| [`sudo gitlab-rake gitlab:elastic:clear_index_status`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Deletes all instances of IndexStatus for all projects. Note that this command results in a complete wipe of the index, and it should be used with caution. |
| [`sudo gitlab-rake gitlab:elastic:clear_index_status`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Deletes all instances of IndexStatus for all projects. This command results in a complete wipe of the index, and it should be used with caution. |
| [`sudo gitlab-rake gitlab:elastic:create_empty_index`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Generates empty indices (the default index and a separate issues index) and assigns an alias for each on the Elasticsearch side only if it doesn't already exist. |
| [`sudo gitlab-rake gitlab:elastic:delete_index`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Removes the GitLab indices and aliases (if they exist) on the Elasticsearch instance. |
| [`sudo gitlab-rake gitlab:elastic:recreate_index`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Wrapper task for `gitlab:elastic:delete_index` and `gitlab:elastic:create_empty_index`. |
@ -632,7 +632,7 @@ For basic guidance on choosing a cluster configuration you may refer to [Elastic
- You can use the [GitLab Performance Tool](https://gitlab.com/gitlab-org/quality/performance) to benchmark search performance with different search cluster sizes and configurations.
- `Heap size` should be set to no more than 50% of your physical RAM. Additionally, it shouldn't be set to more than the threshold for zero-based compressed oops. The exact threshold varies, but 26 GB is safe on most systems, but can also be as large as 30 GB on some systems. See [Heap size settings](https://www.elastic.co/guide/en/elasticsearch/reference/current/important-settings.html#heap-size-settings) and [Setting JVM options](https://www.elastic.co/guide/en/elasticsearch/reference/current/jvm-options.html) for more details.
- Number of CPUs (CPU cores) per node usually corresponds to the `Number of Elasticsearch shards` setting described below.
- A good guideline is to ensure you keep the number of shards per node below 20 per GB heap it has configured. A node with a 30GB heap should therefore have a maximum of 600 shards, but the further below this limit you can keep it the better. This generally helps the cluster stay in good health.
- A good guideline is to ensure you keep the number of shards per node below 20 per GB heap it has configured. A node with a 30 GB heap should therefore have a maximum of 600 shards, but the further below this limit you can keep it the better. This generally helps the cluster stay in good health.
- Number of Elasticsearch shards:
- Small shards result in small segments, which increases overhead. Aim to keep the average shard size between at least a few GB and a few tens of GB.
- Another consideration is the number of documents. To determine the number of shards to use, sum the numbers in the **Main menu > Admin > Dashboard > Statistics** pane (the number of documents to be indexed), divide by 5 million, and add 5. For example:
@ -685,7 +685,7 @@ Make sure to prepare for this task by having a
- You can temporarily disable [`refresh`](https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-refresh.html), the operation responsible for making changes to an index available to search.
- You can set the number of replicas to 0. This setting controls the number of copies each primary shard of an index will have. Thus, having 0 replicas effectively disables the replication of shards across nodes, which should increase the indexing performance. This is an important trade-off in terms of reliability and query performance. It is important to remember to set the replicas to a considered value after the initial indexing is complete.
- You can set the number of replicas to 0. This setting controls the number of copies each primary shard of an index has. Thus, having 0 replicas effectively disables the replication of shards across nodes, which should increase the indexing performance. This is an important trade-off in terms of reliability and query performance. It is important to remember to set the replicas to a considered value after the initial indexing is complete.
In our experience, you can expect a 20% decrease in indexing time. After completing indexing in a later step, you can return `refresh` and `number_of_replicas` to their desired settings.
@ -737,17 +737,17 @@ Make sure to prepare for this task by having a
```
Where `ID_FROM` and `ID_TO` are project IDs. Both parameters are optional.
The above example will index all projects from ID `1001` up to (and including) ID `2000`.
The above example indexes all projects from ID `1001` up to (and including) ID `2000`.
NOTE:
Sometimes the project indexing jobs queued by `gitlab:elastic:index_projects`
can get interrupted. This may happen for many reasons, but it's always safe
to run the indexing task again. It will skip repositories that have
to run the indexing task again. It skips repositories that have
already been indexed.
As the indexer stores the last commit SHA of every indexed repository in the
database, you can run the indexer with the special parameter `UPDATE_INDEX` and
it will check every project repository again to make sure that every commit in
it checks every project repository again to make sure that every commit in
a repository is indexed, which can be useful in case if your index is outdated:
```shell
@ -817,11 +817,11 @@ Make sure to prepare for this task by having a
Whenever a change or deletion is made to an indexed GitLab object (a merge request description is changed, a file is deleted from the default branch in a repository, a project is deleted, etc), a document in the index is deleted. However, since these are "soft" deletes, the overall number of "deleted documents", and therefore wasted space, increases. Elasticsearch does intelligent merging of segments to remove these deleted documents. However, depending on the amount and type of activity in your GitLab installation, it's possible to see as much as 50% wasted space in the index.
In general, we recommend letting Elasticsearch merge and reclaim space automatically, with the default settings. From [Lucene's Handling of Deleted Documents](https://www.elastic.co/blog/lucenes-handling-of-deleted-documents "Lucene's Handling of Deleted Documents"), _"Overall, besides perhaps decreasing the maximum segment size, it is best to leave Lucene's defaults as-is and not fret too much about when deletes are reclaimed."_
In general, we recommend letting Elasticsearch merge and reclaim space automatically, with the default settings. From [Lucene's Handling of Deleted Documents](https://www.elastic.co/blog/lucenes-handling-of-deleted-documents "Lucene's Handling of Deleted Documents"), _"Overall, besides perhaps decreasing the maximum segment size, it is best to leave Lucene defaults as-is and not fret too much about when deletes are reclaimed."_
However, some larger installations may wish to tune the merge policy settings:
- Consider reducing the `index.merge.policy.max_merged_segment` size from the default 5 GB to maybe 2 GB or 3 GB. Merging only happens when a segment has at least 50% deletions. Smaller segment sizes will allow merging to happen more frequently.
- Consider reducing the `index.merge.policy.max_merged_segment` size from the default 5 GB to maybe 2 GB or 3 GB. Merging only happens when a segment has at least 50% deletions. Smaller segment sizes allows merging to happen more frequently.
```shell
curl --request PUT localhost:9200/gitlab-production/_settings ---header 'Content-Type: application/json' \

View File

@ -296,7 +296,7 @@ To renew your subscription:
1. Log in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in) and beneath your existing subscription, select **Renew**.
The **Renew** button remains disabled (grayed-out) until 15 days before a subscription expires.
You can hover your mouse on the **Renew** button to see the date when it will become active.
You can hover your mouse on the **Renew** button to see the date when it becomes active.
1. Review your renewal details and complete the payment process.
1. Select **Confirm purchase**.
@ -362,7 +362,7 @@ for your personal or group namespace. CI/CD minutes are a **one-time purchase**,
## Add-on subscription for additional Storage and Transfer
NOTE:
Free namespaces are subject to a 5GB storage and 10GB transfer [soft limit](https://about.gitlab.com/pricing/). Once all storage is available to view in the usage quota workflow, GitLab will automatically enforce the namespace storage limit and the project limit will be removed. This change will be announced separately. The storage and transfer add-on can be purchased to increase the limits.
Free namespaces are subject to a 5 GB storage and 10 GB transfer [soft limit](https://about.gitlab.com/pricing/). Once all storage is available to view in the usage quota workflow, GitLab will automatically enforce the namespace storage limit and the project limit is removed. This change is announced separately. The storage and transfer add-on can be purchased to increase the limits.
Projects have a free storage quota of 10 GB. To exceed this quota you must first
[purchase one or more storage subscription units](#purchase-more-storage-and-transfer). Each unit provides 10 GB of additional
@ -449,7 +449,7 @@ and for communicating directly with the relevant GitLab team members.
If your credit card is declined when purchasing a GitLab subscription, possible reasons include:
- The credit card details provided are incorrect. The most common cause for this is an incomplete or dummy address.
- The credit card details provided are incorrect. The most common cause for this is an incomplete or fake address.
- The credit card account has insufficient funds.
- You are using a virtual credit card and it has insufficient funds, or has expired.
- The transaction exceeds the credit limit.

View File

@ -12,6 +12,7 @@ documentation: { type: 'string', example: '16630b189ab34b2e3504f4758e1054d2e478d
expose :created_at, documentation: { type: 'dateTime', example: '2022-02-22T22:22:22.222Z' }
expose :expires_at, documentation: { type: 'dateTime', example: '2022-09-21T14:56:00.000Z' }
expose :metadata, documentation: { type: 'Hash', example: { "id" => "75949910542696343243264405377658443914" } }
expose :file_extension, documentation: { type: 'string', example: 'jks' }
end
end
end

View File

@ -16067,6 +16067,9 @@ msgstr ""
msgid "Error fetching burnup chart data"
msgstr ""
msgid "Error fetching data. Please try again."
msgstr ""
msgid "Error fetching diverging counts for branches. Please try again."
msgstr ""
@ -16085,9 +16088,6 @@ msgstr ""
msgid "Error fetching refs"
msgstr ""
msgid "Error fetching target projects. Please try again."
msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
@ -37556,15 +37556,45 @@ msgstr ""
msgid "Secure token that identifies an external storage request."
msgstr ""
msgid "SecureFiles|%{name} Metadata"
msgstr ""
msgid "SecureFiles|App"
msgstr ""
msgid "SecureFiles|Certificates"
msgstr ""
msgid "SecureFiles|Delete %{name}?"
msgstr ""
msgid "SecureFiles|Delete secure file"
msgstr ""
msgid "SecureFiles|Expires at"
msgstr ""
msgid "SecureFiles|Issuer"
msgstr ""
msgid "SecureFiles|Name"
msgstr ""
msgid "SecureFiles|Platforms"
msgstr ""
msgid "SecureFiles|Secure File %{name} will be permanently deleted. Are you sure?"
msgstr ""
msgid "SecureFiles|Serial"
msgstr ""
msgid "SecureFiles|Team"
msgstr ""
msgid "SecureFiles|UUID"
msgstr ""
msgid "Security"
msgstr ""
@ -39720,7 +39750,7 @@ msgstr ""
msgid "Slack|%{asterisk}Slash commands%{asterisk}"
msgstr ""
msgid "Slack|%{emoji}Connected to GitLab account %{account}"
msgid "Slack|%{emoji}Connected to GitLab account %{account}."
msgstr ""
msgid "Slack|%{emoji}Welcome to GitLab for Slack!"
@ -39732,10 +39762,10 @@ msgstr ""
msgid "Slack|Control GitLab from Slack with %{startMarkup}slash commands%{endMarkup}. For a list of available commands, enter %{command}."
msgstr ""
msgid "Slack|GitLab for Slack now supports channel-based notifications.Let your team know when new issues are created or new CI/CD jobs are run.%{startMarkup}Learn more%{endMarkup}."
msgid "Slack|GitLab for Slack now supports channel-based notifications. Let your team know when new issues are created or new CI/CD jobs are run.%{startMarkup}Learn more%{endMarkup}."
msgstr ""
msgid "Slack|To start using notifications, %{startMarkup}enable the Slack integration%{endMarkup} in your project settings."
msgid "Slack|To start using notifications, %{startMarkup}enable the GitLab for Slack app integration%{endMarkup} in your project settings."
msgstr ""
msgid "Slack|To start using slash commands, connect your GitLab account."
@ -46427,6 +46457,9 @@ msgstr ""
msgid "VersionCheck|Your GitLab Version"
msgstr ""
msgid "View File Metadata"
msgstr ""
msgid "View Stage: %{title}"
msgstr ""
@ -47873,9 +47906,6 @@ msgstr ""
msgid "WorkItem|Closed"
msgstr ""
msgid "WorkItem|Collapse tasks"
msgstr ""
msgid "WorkItem|Create %{workItemType}"
msgstr ""
@ -47900,9 +47930,6 @@ msgstr ""
msgid "WorkItem|Existing task"
msgstr ""
msgid "WorkItem|Expand tasks"
msgstr ""
msgid "WorkItem|Health status"
msgstr ""

View File

@ -73,19 +73,9 @@ RSpec.describe 'User creates a merge request', :js, feature_category: :code_revi
expect(page).to have_content('You must select source and target branch')
first('.js-source-project').click
first('.dropdown-source-project a', text: forked_project.full_path)
first('.js-target-project').click
first('.dropdown-target-project li', text: project.full_path)
first('.js-source-branch').click
wait_for_requests
source_branch = 'fix'
first('.js-source-branch-dropdown .dropdown-content a', text: source_branch).click
select_project('.js-source-project', forked_project)
select_project('.js-target-project', project)
select_branch('.js-source-branch', 'fix')
click_button('Compare branches and continue')
@ -107,7 +97,7 @@ RSpec.describe 'User creates a merge request', :js, feature_category: :code_revi
click_button('Create merge request')
expect(page).to have_content(title).and have_content("requested to merge #{forked_project.full_path}:#{source_branch} into master")
expect(page).to have_content(title).and have_content("requested to merge #{forked_project.full_path}:fix into master")
end
end
end
@ -153,12 +143,27 @@ RSpec.describe 'User creates a merge request', :js, feature_category: :code_revi
private
def compare_source_and_target(source_branch, target_branch)
find('.js-source-branch').click
click_link(source_branch)
find('.js-target-branch').click
click_link(target_branch)
select_branch('.js-source-branch', source_branch)
select_branch('.js-target-branch', target_branch)
click_button('Compare branches')
end
def select_project(selector, project)
first(selector).click
wait_for_requests
find('.gl-listbox-search-input').set(project.full_path)
first('.gl-dropdown-item', text: project.full_path).click
end
def select_branch(selector, branch)
first(selector).click
wait_for_requests
find('.gl-listbox-search-input').set(branch)
first('.gl-dropdown-item', text: branch).click
end
end

View File

@ -59,9 +59,9 @@ RSpec.describe 'Merge request > User creates MR', feature_category: :code_review
it 'filters source project' do
find('.js-source-project').click
find('.dropdown-source-project input').set('source')
find('.gl-listbox-search-input').set('source')
expect(find('.dropdown-source-project .dropdown-content')).not_to have_content(source_project.name)
expect(first('.merge-request-select .dropdown-menu')).not_to have_content(source_project.name)
end
end
end

View File

@ -8,8 +8,8 @@ RSpec.describe 'Merge request > User selects branches for new MR', :js, feature_
def select_source_branch(branch_name)
find('.js-source-branch', match: :first).click
find('.js-source-branch-dropdown .dropdown-input-field').native.send_keys branch_name
find('.js-source-branch-dropdown .dropdown-content a', text: branch_name, match: :first).click
find('.gl-listbox-search-input').native.send_keys branch_name
find('.gl-dropdown-item', text: branch_name, match: :first).click
end
before do
@ -27,7 +27,7 @@ RSpec.describe 'Merge request > User selects branches for new MR', :js, feature_
expect(page).to have_content('Target branch')
first('.js-source-branch').click
find('.js-source-branch-dropdown .dropdown-content a', match: :first).click
find('.gl-dropdown-item', match: :first).click
expect(page).to have_content "b83d6e3"
end
@ -43,7 +43,8 @@ RSpec.describe 'Merge request > User selects branches for new MR', :js, feature_
expect(page).to have_content('Target branch')
first('.js-target-branch').click
find('.js-target-branch-dropdown .dropdown-content a', text: 'v1.1.0', match: :first).click
find('.gl-listbox-search-input').native.send_keys 'v1.1.0'
find('.gl-dropdown-item', text: 'v1.1.0', match: :first).click
expect(page).to have_content "b83d6e3"
end
@ -68,27 +69,6 @@ RSpec.describe 'Merge request > User selects branches for new MR', :js, feature_
expect(page).to have_content 'git checkout -b \'orphaned-branch\' \'origin/orphaned-branch\''
end
it 'allows filtering multiple dropdowns' do
visit project_new_merge_request_path(project)
first('.js-source-branch').click
page.within '.js-source-branch-dropdown' do
input = find('.dropdown-input-field')
input.click
input.send_keys('orphaned-branch')
expect(page).to have_css('.dropdown-content li', count: 1)
end
first('.js-target-branch').click
find('.js-target-branch-dropdown .dropdown-content li', match: :first)
target_items = all('.js-target-branch-dropdown .dropdown-content li')
expect(target_items.count).to be > 1
end
context 'when target project cannot be viewed by the current user' do
it 'does not leak the private project name & namespace' do
private_project = create(:project, :private, :repository)

View File

@ -31,15 +31,15 @@ RSpec.describe 'Work item children', :js, feature_category: :team_planning do
it 'toggles widget body', :aggregate_failures do
page.within('[data-testid="work-item-links"]') do
expect(page).to have_selector('[data-testid="links-body"]')
expect(page).to have_selector('[data-testid="widget-body"]')
click_button 'Collapse tasks'
click_button 'Collapse'
expect(page).not_to have_selector('[data-testid="links-body"]')
expect(page).not_to have_selector('[data-testid="widget-body"]')
click_button 'Expand tasks'
click_button 'Expand'
expect(page).to have_selector('[data-testid="links-body"]')
expect(page).to have_selector('[data-testid="widget-body"]')
end
end

View File

@ -0,0 +1,386 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Secure File Metadata Modal when a .cer file is supplied matches cer the snapshot 1`] = `
<div
category="primary"
hide-footer=""
>
<div
data-testid="slot-modal-title"
>
myfile.cer Metadata
</div>
<div
data-testid="slot-default"
>
<table
aria-busy="false"
aria-colcount="2"
class="table b-table gl-table"
role="table"
>
<!---->
<!---->
<thead
class=""
role="rowgroup"
>
<!---->
<tr
class=""
role="row"
>
<th
aria-colindex="1"
class="hidden"
role="columnheader"
scope="col"
>
<div>
Item Name
</div>
</th>
<th
aria-colindex="2"
class="hidden"
role="columnheader"
scope="col"
>
<div>
Item Data
</div>
</th>
</tr>
</thead>
<tbody
role="rowgroup"
>
<!---->
<tr
class=""
role="row"
>
<td
aria-colindex="1"
class=""
role="cell"
>
<strong>
Name
</strong>
</td>
<td
aria-colindex="2"
class=""
role="cell"
>
Apple Distribution: Team Name (ABC123XYZ)
</td>
</tr>
<tr
class=""
role="row"
>
<td
aria-colindex="1"
class=""
role="cell"
>
<strong>
Serial
</strong>
</td>
<td
aria-colindex="2"
class=""
role="cell"
>
33669367788748363528491290218354043267
</td>
</tr>
<tr
class=""
role="row"
>
<td
aria-colindex="1"
class=""
role="cell"
>
<strong>
Team
</strong>
</td>
<td
aria-colindex="2"
class=""
role="cell"
>
Team Name (ABC123XYZ)
</td>
</tr>
<tr
class=""
role="row"
>
<td
aria-colindex="1"
class=""
role="cell"
>
<strong>
Issuer
</strong>
</td>
<td
aria-colindex="2"
class=""
role="cell"
>
Apple Worldwide Developer Relations Certification Authority - G3
</td>
</tr>
<tr
class=""
role="row"
>
<td
aria-colindex="1"
class=""
role="cell"
>
<strong>
Expires at
</strong>
</td>
<td
aria-colindex="2"
class=""
role="cell"
>
April 26, 2022 at 7:20:40 PM GMT
</td>
</tr>
<!---->
<!---->
</tbody>
<!---->
</table>
</div>
</div>
`;
exports[`Secure File Metadata Modal when a .mobileprovision file is supplied matches the mobileprovision snapshot 1`] = `
<div
category="primary"
hide-footer=""
>
<div
data-testid="slot-modal-title"
>
sample.mobileprovision Metadata
</div>
<div
data-testid="slot-default"
>
<table
aria-busy="false"
aria-colcount="2"
class="table b-table gl-table"
role="table"
>
<!---->
<!---->
<thead
class=""
role="rowgroup"
>
<!---->
<tr
class=""
role="row"
>
<th
aria-colindex="1"
class="hidden"
role="columnheader"
scope="col"
>
<div>
Item Name
</div>
</th>
<th
aria-colindex="2"
class="hidden"
role="columnheader"
scope="col"
>
<div>
Item Data
</div>
</th>
</tr>
</thead>
<tbody
role="rowgroup"
>
<!---->
<tr
class=""
role="row"
>
<td
aria-colindex="1"
class=""
role="cell"
>
<strong>
UUID
</strong>
</td>
<td
aria-colindex="2"
class=""
role="cell"
>
6b9fcce1-b9a9-4b37-b2ce-ec4da2044abf
</td>
</tr>
<tr
class=""
role="row"
>
<td
aria-colindex="1"
class=""
role="cell"
>
<strong>
Platforms
</strong>
</td>
<td
aria-colindex="2"
class=""
role="cell"
>
iOS
</td>
</tr>
<tr
class=""
role="row"
>
<td
aria-colindex="1"
class=""
role="cell"
>
<strong>
Team
</strong>
</td>
<td
aria-colindex="2"
class=""
role="cell"
>
Team Name (ABC123XYZ)
</td>
</tr>
<tr
class=""
role="row"
>
<td
aria-colindex="1"
class=""
role="cell"
>
<strong>
App
</strong>
</td>
<td
aria-colindex="2"
class=""
role="cell"
>
iOS Demo - match Development com.gitlab.ios-demo
</td>
</tr>
<tr
class=""
role="row"
>
<td
aria-colindex="1"
class=""
role="cell"
>
<strong>
Certificates
</strong>
</td>
<td
aria-colindex="2"
class=""
role="cell"
>
33669367788748363528491290218354043267
</td>
</tr>
<tr
class=""
role="row"
>
<td
aria-colindex="1"
class=""
role="cell"
>
<strong>
Expires at
</strong>
</td>
<td
aria-colindex="2"
class=""
role="cell"
>
August 1, 2023 at 11:15:13 PM GMT
</td>
</tr>
<!---->
<!---->
</tbody>
<!---->
</table>
</div>
</div>
`;

View File

@ -0,0 +1,49 @@
import { GlButton } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import Button from '~/ci_secure_files/components/metadata/button.vue';
import { secureFiles } from '../../mock_data';
const secureFileWithoutMetadata = secureFiles[0];
const secureFileWithMetadata = secureFiles[2];
const modalId = 'metadataModalId';
describe('Secure File Metadata Button', () => {
let wrapper;
const findButton = () => wrapper.findComponent(GlButton);
afterEach(() => {
wrapper.destroy();
});
const createWrapper = (secureFile = {}, admin = false) => {
wrapper = mount(Button, {
propsData: {
admin,
modalId,
secureFile,
},
});
};
describe('metadata button visibility', () => {
it.each`
visibility | admin | fileName
${true} | ${true} | ${secureFileWithMetadata}
${false} | ${false} | ${secureFileWithMetadata}
${false} | ${false} | ${secureFileWithoutMetadata}
${false} | ${false} | ${secureFileWithoutMetadata}
`(
'button visibility is $visibility when admin equals $admin and $fileName.name is suppled',
({ visibility, admin, fileName }) => {
createWrapper(fileName, admin);
expect(findButton().exists()).toBe(visibility);
if (visibility) {
expect(findButton().isVisible()).toBe(true);
expect(findButton().attributes('aria-label')).toBe('View File Metadata');
}
},
);
});
});

View File

@ -0,0 +1,78 @@
import { GlModal } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { stubComponent, RENDER_ALL_SLOTS_TEMPLATE } from 'helpers/stub_component';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import Modal from '~/ci_secure_files/components/metadata/modal.vue';
import { secureFiles } from '../../mock_data';
const cerFile = secureFiles[2];
const mobileprovisionFile = secureFiles[3];
const modalId = 'metadataModalId';
describe('Secure File Metadata Modal', () => {
let wrapper;
let trackingSpy;
const createWrapper = (secureFile = {}) => {
wrapper = mount(Modal, {
stubs: {
GlModal: stubComponent(GlModal, {
template: RENDER_ALL_SLOTS_TEMPLATE,
}),
},
propsData: {
modalId,
name: secureFile.name,
metadata: secureFile.metadata,
fileExtension: secureFile.file_extension,
},
});
};
beforeEach(() => {
trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
});
afterEach(() => {
unmockTracking();
wrapper.destroy();
});
describe('when a .cer file is supplied', () => {
it('matches cer the snapshot', () => {
createWrapper(cerFile);
expect(wrapper.element).toMatchSnapshot();
});
});
describe('when a .mobileprovision file is supplied', () => {
it('matches the mobileprovision snapshot', () => {
createWrapper(mobileprovisionFile);
expect(wrapper.element).toMatchSnapshot();
});
});
describe('event tracking', () => {
it('sends cer tracking information when the modal is loaded', () => {
createWrapper(cerFile);
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'load_secure_file_metadata_cer', {});
expect(trackingSpy).not.toHaveBeenCalledWith(
undefined,
'load_secure_file_metadata_mobileprovision',
{},
);
});
it('sends mobileprovision tracking information when the modal is loaded', () => {
createWrapper(mobileprovisionFile);
expect(trackingSpy).toHaveBeenCalledWith(
undefined,
'load_secure_file_metadata_mobileprovision',
{},
);
expect(trackingSpy).not.toHaveBeenCalledWith(undefined, 'load_secure_file_metadata_cer', {});
});
});
});

View File

@ -4,15 +4,72 @@ export const secureFiles = [
name: 'myfile.jks',
checksum: '16630b189ab34b2e3504f4758e1054d2e478deda510b2b08cc0ef38d12e80aac',
checksum_algorithm: 'sha256',
permissions: 'read_only',
created_at: '2022-02-22T22:22:22.222Z',
file_extension: 'jks',
metadata: null,
},
{
id: 2,
name: 'myotherfile.jks',
checksum: '16630b189ab34b2e3504f4758e1054d2e478deda510b2b08cc0ef38d12e80aa2',
checksum_algorithm: 'sha256',
permissions: 'execute',
created_at: '2022-02-22T22:22:22.222Z',
file_extension: 'jks',
metadata: null,
},
{
id: 3,
name: 'myfile.cer',
checksum: '16630b189ab34b2e3504f4758e1054d2e478deda510b2b08cc0ef38d12e80aa2',
checksum_algorithm: 'sha256',
created_at: '2022-02-22T22:22:22.222Z',
file_extension: 'cer',
expires_at: '2022-04-26T19:20:40.000Z',
metadata: {
id: '33669367788748363528491290218354043267',
issuer: {
C: 'US',
O: 'Apple Inc.',
CN: 'Apple Worldwide Developer Relations Certification Authority',
OU: 'G3',
},
subject: {
C: 'US',
O: 'Team Name',
CN: 'Apple Distribution: Team Name (ABC123XYZ)',
OU: 'ABC123XYZ',
UID: 'ABC123XYZ',
},
expires_at: '2022-04-26T19:20:40.000Z',
},
},
{
id: 4,
name: 'sample.mobileprovision',
checksum: '9e194bbde00d57c64b6640ed2c9e166d76b4c79d9dbd49770f95be56678f2a62',
checksum_algorithm: 'sha256',
created_at: '2022-11-15T19:29:57.577Z',
expires_at: '2023-08-01T23:15:13.000Z',
metadata: {
id: '6b9fcce1-b9a9-4b37-b2ce-ec4da2044abf',
app_id: 'match Development com.gitlab.ios-demo',
devices: ['00008101-001454860C10001E'],
team_id: ['ABC123XYZ'],
app_name: 'iOS Demo',
platforms: ['iOS'],
team_name: 'Team Name',
expires_at: '2023-08-01T18:15:13.000-05:00',
entitlements: {
'get-task-allow': true,
'application-identifier': 'N7SYAN8PX8.com.gitlab.ios-demo',
'keychain-access-groups': ['ABC123XYZ.*', 'com.apple.token'],
'com.apple.developer.game-center': true,
'com.apple.developer.team-identifier': 'ABC123XYZ',
},
app_id_prefix: ['ABC123XYZ'],
xcode_managed: false,
certificate_ids: ['33669367788748363528491290218354043267'],
},
file_extension: 'mobileprovision',
},
];

View File

@ -3,23 +3,28 @@ import { GlCollapsibleListbox } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import TargetProjectDropdown from '~/merge_requests/components/target_project_dropdown.vue';
import CompareDropdown from '~/merge_requests/components/compare_dropdown.vue';
let wrapper;
let mock;
function factory() {
wrapper = mount(TargetProjectDropdown, {
provide: {
targetProjectsPath: '/gitlab-org/gitlab/target_projects',
currentProject: { value: 1, text: 'gitlab-org/gitlab' },
function factory(propsData = {}) {
wrapper = mount(CompareDropdown, {
propsData: {
endpoint: '/gitlab-org/gitlab/target_projects',
default: { value: 1, text: 'gitlab-org/gitlab' },
dropdownHeader: 'Select',
inputId: 'input_id',
inputName: 'input_name',
isProject: true,
...propsData,
},
});
}
const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
describe('Merge requests target project dropdown component', () => {
describe('Merge requests compare dropdown component', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet('/gitlab-org/gitlab/target_projects').reply(200, [
@ -77,4 +82,22 @@ describe('Merge requests target project dropdown component', () => {
expect(mock.history.get[1].params).toEqual({ search: 'test' });
});
it('renders static data', async () => {
factory({
endpoint: undefined,
staticData: [
{
value: '10',
text: 'GitLab Org',
},
],
});
wrapper.find('[data-testid="base-dropdown-toggle"]').trigger('click');
await waitForPromises();
expect(wrapper.findAll('li').length).toBe(1);
});
});

View File

@ -0,0 +1,46 @@
import { nextTick } from 'vue';
import { GlAlert, GlButton } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import WidgetWrapper from '~/work_items/components/widget_wrapper.vue';
describe('WidgetWrapper component', () => {
let wrapper;
const createComponent = ({ error } = {}) => {
wrapper = shallowMountExtended(WidgetWrapper, { propsData: { error } });
};
const findAlert = () => wrapper.findComponent(GlAlert);
const findToggleButton = () => wrapper.findComponent(GlButton);
const findWidgetBody = () => wrapper.findByTestId('widget-body');
it('is expanded by default', () => {
createComponent();
expect(findToggleButton().props('icon')).toBe('chevron-lg-up');
expect(findWidgetBody().exists()).toBe(true);
});
it('collapses on click toggle button', async () => {
createComponent();
findToggleButton().vm.$emit('click');
await nextTick();
expect(findToggleButton().props('icon')).toBe('chevron-lg-down');
expect(findWidgetBody().exists()).toBe(false);
});
it('shows alert when list loading fails', () => {
const error = 'Some error';
createComponent({ error });
expect(findAlert().text()).toBe(error);
});
it('emits event when dismissing the alert', () => {
createComponent({ error: 'error' });
findAlert().vm.$emit('dismiss');
expect(wrapper.emitted('dismissAlert')).toEqual([[]]);
});
});

View File

@ -1,5 +1,4 @@
import Vue, { nextTick } from 'vue';
import { GlAlert } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
@ -8,6 +7,7 @@ import setWindowLocation from 'helpers/set_window_location_helper';
import { stubComponent } from 'helpers/stub_component';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import issueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
import WidgetWrapper from '~/work_items/components/widget_wrapper.vue';
import WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue';
import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
@ -127,12 +127,12 @@ describe('WorkItemLinks', () => {
},
});
wrapper.vm.$refs.wrapper.show = jest.fn();
await waitForPromises();
};
const findAlert = () => wrapper.findComponent(GlAlert);
const findToggleButton = () => wrapper.findByTestId('toggle-links');
const findLinksBody = () => wrapper.findByTestId('links-body');
const findWidgetWrapper = () => wrapper.findComponent(WidgetWrapper);
const findEmptyState = () => wrapper.findByTestId('links-empty');
const findToggleFormDropdown = () => wrapper.findByTestId('toggle-form');
const findToggleAddFormButton = () => wrapper.findByTestId('toggle-add-form');
@ -142,31 +142,14 @@ describe('WorkItemLinks', () => {
const findAddLinksForm = () => wrapper.findByTestId('add-links-form');
const findChildrenCount = () => wrapper.findByTestId('children-count');
beforeEach(async () => {
await createComponent();
});
afterEach(() => {
wrapper.destroy();
mockApollo = null;
setWindowLocation('');
});
it('is expanded by default', () => {
expect(findToggleButton().props('icon')).toBe('chevron-lg-up');
expect(findLinksBody().exists()).toBe(true);
});
it('collapses on click toggle button', async () => {
findToggleButton().vm.$emit('click');
await nextTick();
expect(findToggleButton().props('icon')).toBe('chevron-lg-down');
expect(findLinksBody().exists()).toBe(false);
});
describe('add link form', () => {
it('displays add work item form on click add dropdown then add existing button and hides form on cancel', async () => {
await createComponent();
findToggleFormDropdown().vm.$emit('click');
findToggleAddFormButton().vm.$emit('click');
await nextTick();
@ -181,6 +164,7 @@ describe('WorkItemLinks', () => {
});
it('displays create work item form on click add dropdown then create button and hides form on cancel', async () => {
await createComponent();
findToggleFormDropdown().vm.$emit('click');
findToggleCreateFormButton().vm.$emit('click');
await nextTick();
@ -207,8 +191,8 @@ describe('WorkItemLinks', () => {
});
});
it('renders all hierarchy widget children', () => {
expect(findLinksBody().exists()).toBe(true);
it('renders all hierarchy widget children', async () => {
await createComponent();
expect(findWorkItemLinkChildItems()).toHaveLength(4);
});
@ -219,15 +203,13 @@ describe('WorkItemLinks', () => {
fetchHandler: jest.fn().mockRejectedValue(new Error(errorMessage)),
});
await nextTick();
expect(findAlert().exists()).toBe(true);
expect(findAlert().text()).toBe(errorMessage);
expect(findWidgetWrapper().props('error')).toBe(errorMessage);
});
it('displays number if children', () => {
expect(findChildrenCount().exists()).toBe(true);
it('displays number of children', async () => {
await createComponent();
expect(findChildrenCount().exists()).toBe(true);
expect(findChildrenCount().text()).toContain('4');
});

View File

@ -23,8 +23,6 @@ describe('WorkItemTree', () => {
let getWorkItemQueryHandler;
let wrapper;
const findToggleButton = () => wrapper.findByTestId('toggle-tree');
const findTreeBody = () => wrapper.findByTestId('tree-body');
const findEmptyState = () => wrapper.findByTestId('tree-empty');
const findToggleFormSplitButton = () => wrapper.findComponent(OkrActionsSplitButton);
const findForm = () => wrapper.findComponent(WorkItemLinksForm);
@ -64,36 +62,25 @@ describe('WorkItemTree', () => {
projectPath: 'test/project',
},
});
wrapper.vm.$refs.wrapper.show = jest.fn();
};
beforeEach(() => {
it('displays Add button', () => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('is expanded by default and displays Add button', () => {
expect(findToggleButton().props('icon')).toBe('chevron-lg-up');
expect(findTreeBody().exists()).toBe(true);
expect(findToggleFormSplitButton().exists()).toBe(true);
});
it('collapses on click toggle button', async () => {
findToggleButton().vm.$emit('click');
await nextTick();
expect(findToggleButton().props('icon')).toBe('chevron-lg-down');
expect(findTreeBody().exists()).toBe(false);
});
it('displays empty state if there are no children', () => {
createComponent({ children: [] });
expect(findEmptyState().exists()).toBe(true);
});
it('renders all hierarchy widget children', () => {
createComponent();
const workItemLinkChildren = findWorkItemLinkChildItems();
expect(workItemLinkChildren).toHaveLength(4);
expect(workItemLinkChildren.at(0).props().childItem.confidential).toBe(
@ -102,6 +89,8 @@ describe('WorkItemTree', () => {
});
it('does not display form by default', () => {
createComponent();
expect(findForm().exists()).toBe(false);
});
@ -114,6 +103,8 @@ describe('WorkItemTree', () => {
`(
'when selecting $option from split button, renders the form passing $formType and $childType',
async ({ event, formType, childType }) => {
createComponent();
findToggleFormSplitButton().vm.$emit(event);
await nextTick();
@ -128,13 +119,16 @@ describe('WorkItemTree', () => {
);
it('remove event on child triggers `removeChild` event', () => {
createComponent();
const firstChild = findWorkItemLinkChildItems().at(0);
firstChild.vm.$emit('removeChild', 'gid://gitlab/WorkItem/2');
expect(wrapper.emitted('removeChild')).toEqual([['gid://gitlab/WorkItem/2']]);
});
it('emits `show-modal` on `click` event', () => {
createComponent();
const firstChild = findWorkItemLinkChildItems().at(0);
const event = {
childItem: 'gid://gitlab/WorkItem/2',

View File

@ -0,0 +1,120 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'check_forced_decomposition initializer', feature_category: :pods do
subject(:check_forced_decomposition) do
load Rails.root.join('config/initializers/check_forced_decomposition.rb')
end
context 'for production env' do
before do
allow(Gitlab).to receive(:dev_or_test_env?).and_return(false)
end
context 'for single database' do
before do
skip_if_multiple_databases_are_setup
end
it { expect { check_forced_decomposition }.not_to raise_error }
end
context 'for multiple database' do
before do
skip_if_multiple_databases_not_setup
end
let(:main_database_config) do
Rails.application.config.load_database_yaml
.dig('test', 'main')
.slice('adapter', 'encoding', 'database', 'username', 'password', 'host')
.symbolize_keys
end
let(:additional_database_config) do
# Use built-in postgres database
main_database_config.merge(database: 'postgres')
end
around do |example|
with_reestablished_active_record_base(reconnect: true) do
with_db_configs(test: test_config) do
example.run
end
end
end
context 'when ci and main share the same database' do
let(:test_config) do
{
main: main_database_config,
ci: additional_database_config.merge(database: main_database_config[:database])
}
end
it { expect { check_forced_decomposition }.not_to raise_error }
context 'when host is not present' do
let(:test_config) do
{
main: main_database_config.except(:host),
ci: additional_database_config.merge(database: main_database_config[:database]).except(:host)
}
end
it { expect { check_forced_decomposition }.not_to raise_error }
end
end
context 'when ci and main share the same database but different host' do
let(:test_config) do
{
main: main_database_config,
ci: additional_database_config.merge(
database: main_database_config[:database],
host: 'otherhost.localhost'
)
}
end
it { expect { check_forced_decomposition }.to raise_error(/Separate CI database is not ready/) }
end
context 'when ci and main are different databases' do
let(:test_config) do
{
main: main_database_config,
ci: additional_database_config
}
end
it { expect { check_forced_decomposition }.to raise_error(/Separate CI database is not ready/) }
context 'for GitLab.com' do
before do
allow(::Gitlab).to receive(:com?).and_return(true)
end
it { expect { check_forced_decomposition }.not_to raise_error }
end
context 'when env var GITLAB_ALLOW_SEPARATE_CI_DATABASE is true' do
before do
stub_env('GITLAB_ALLOW_SEPARATE_CI_DATABASE', 'true')
end
it { expect { check_forced_decomposition }.not_to raise_error }
end
context 'when env var GITLAB_ALLOW_SEPARATE_CI_DATABASE is false' do
before do
stub_env('GITLAB_ALLOW_SEPARATE_CI_DATABASE', 'false')
end
it { expect { check_forced_decomposition }.to raise_error(/Separate CI database is not ready/) }
end
end
end
end
end

View File

@ -101,6 +101,11 @@ RSpec.describe Ci::SecureFile do
file = build(:ci_secure_file, name: 'file1.tar.gz')
expect(file.file_extension).to eq('gz')
end
it 'returns nil if there is no file extension' do
file = build(:ci_secure_file, name: 'file1')
expect(file.file_extension).to be nil
end
end
describe '#metadata_parsable?' do

View File

@ -5,7 +5,6 @@ require 'spec_helper'
RSpec.describe API::Ci::SecureFiles, feature_category: :pipeline_authoring do
before do
stub_ci_secure_file_object_storage
stub_feature_flags(ci_secure_files: true)
stub_feature_flags(ci_secure_files_read_only: false)
end
@ -128,6 +127,7 @@ RSpec.describe API::Ci::SecureFiles, feature_category: :pipeline_authoring do
expect(json_response['name']).to eq(secure_file.name)
expect(json_response['expires_at']).to be nil
expect(json_response['metadata']).to be nil
expect(json_response['file_extension']).to be nil
end
it 'returns project secure file details with metadata when supported' do
@ -138,6 +138,7 @@ RSpec.describe API::Ci::SecureFiles, feature_category: :pipeline_authoring do
expect(json_response['name']).to eq(secure_file_with_metadata.name)
expect(json_response['expires_at']).to eq('2022-04-26T19:20:40.000Z')
expect(json_response['metadata'].keys).to match_array(%w[id issuer subject expires_at])
expect(json_response['file_extension']).to eq('cer')
end
it 'responds with 404 Not Found if requesting non-existing secure file' do
@ -250,6 +251,7 @@ RSpec.describe API::Ci::SecureFiles, feature_category: :pipeline_authoring do
expect(json_response['name']).to eq('upload-keystore.jks')
expect(json_response['checksum']).to eq(secure_file.checksum)
expect(json_response['checksum_algorithm']).to eq('sha256')
expect(json_response['file_extension']).to eq('jks')
secure_file = Ci::SecureFile.find(json_response['id'])
expect(secure_file.checksum).to eq(

View File

@ -71,6 +71,14 @@ module Database
end
# rubocop:enable Database/MultipleDatabases
def with_db_configs(test: test_config)
current_configurations = ActiveRecord::Base.configurations # rubocop:disable Database/MultipleDatabases
ActiveRecord::Base.configurations = { test: test_config }
yield
ensure
ActiveRecord::Base.configurations = current_configurations
end
def with_added_ci_connection
if Gitlab::Database.has_config?(:ci)
# No need to add a ci: connection if we already have one

View File

@ -64,14 +64,16 @@ RSpec.shared_examples 'a creatable merge request' do
visit project_new_merge_request_path(source_project)
first('.js-target-project').click
find('.dropdown-target-project li', text: target_project.full_path).click
find('.gl-dropdown-item', text: target_project.full_path).click
wait_for_requests
first('.js-target-branch').click
within('.js-target-branch-dropdown .dropdown-content') do
expect(page).to have_content('a-brand-new-branch-to-test')
end
find('.gl-listbox-search-input').set('a-brand-new-branch-to-test')
wait_for_requests
expect(page).to have_selector('.gl-dropdown-item', text: 'a-brand-new-branch-to-test')
end
end

View File

@ -2,7 +2,7 @@
require 'rake_helper'
RSpec.describe 'gitlab:db:validate_config', :silence_stdout, :suppress_gitlab_schemas_validate_connection do
RSpec.describe 'gitlab:db:validate_config', :silence_stdout, :suppress_gitlab_schemas_validate_connection, feature_category: :pods do
# We don't need to delete this data since it only modifies `ar_internal_metadata`
# which would not be cleaned either by `DbCleaner`
self.use_transactional_tests = false
@ -235,12 +235,4 @@ RSpec.describe 'gitlab:db:validate_config', :silence_stdout, :suppress_gitlab_sc
end
end
end
def with_db_configs(test: test_config)
current_configurations = ActiveRecord::Base.configurations # rubocop:disable Database/MultipleDatabases
ActiveRecord::Base.configurations = { test: test_config }
yield
ensure
ActiveRecord::Base.configurations = current_configurations
end
end