Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
8ebab6079e
commit
c31a6781a3
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
259fa00544b9f63d512738dcbd4fb1ffcdbbfae58e15f7fbeb4fe34e5e7fe1f3
|
||||
|
|
@ -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'
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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` |
|
||||
|
|
|
|||
|
|
@ -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|
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
|||
|
||||

|
||||
|
||||
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' \
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
`;
|
||||
|
|
@ -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');
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -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', {});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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',
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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([[]]);
|
||||
});
|
||||
});
|
||||
|
|
@ -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');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue