Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
ede2fbdc87
commit
d7fd035dc3
101 changed files with 1691 additions and 719 deletions
|
@ -604,10 +604,6 @@ RSpec/EmptyLineAfterFinalLetItBe:
|
||||||
- ee/spec/services/incident_management/incidents/upload_metric_service_spec.rb
|
- ee/spec/services/incident_management/incidents/upload_metric_service_spec.rb
|
||||||
- ee/spec/services/incident_management/oncall_rotations/edit_service_spec.rb
|
- ee/spec/services/incident_management/oncall_rotations/edit_service_spec.rb
|
||||||
- ee/spec/services/merge_request_approval_settings/update_service_spec.rb
|
- ee/spec/services/merge_request_approval_settings/update_service_spec.rb
|
||||||
- ee/spec/services/merge_trains/check_status_service_spec.rb
|
|
||||||
- ee/spec/services/merge_trains/create_pipeline_service_spec.rb
|
|
||||||
- ee/spec/services/merge_trains/refresh_merge_request_service_spec.rb
|
|
||||||
- ee/spec/services/merge_trains/refresh_service_spec.rb
|
|
||||||
- ee/spec/services/personal_access_tokens/create_service_audit_log_spec.rb
|
- ee/spec/services/personal_access_tokens/create_service_audit_log_spec.rb
|
||||||
- ee/spec/services/personal_access_tokens/groups/update_lifetime_service_spec.rb
|
- ee/spec/services/personal_access_tokens/groups/update_lifetime_service_spec.rb
|
||||||
- ee/spec/services/projects/after_rename_service_spec.rb
|
- ee/spec/services/projects/after_rename_service_spec.rb
|
||||||
|
@ -779,9 +775,6 @@ RSpec/EmptyLineAfterFinalLetItBe:
|
||||||
- spec/lib/gitlab/gitaly_client/operation_service_spec.rb
|
- spec/lib/gitlab/gitaly_client/operation_service_spec.rb
|
||||||
- spec/lib/gitlab/gl_repository/repo_type_spec.rb
|
- spec/lib/gitlab/gl_repository/repo_type_spec.rb
|
||||||
- spec/lib/gitlab/group_search_results_spec.rb
|
- spec/lib/gitlab/group_search_results_spec.rb
|
||||||
- spec/lib/gitlab/hook_data/issue_builder_spec.rb
|
|
||||||
- spec/lib/gitlab/hook_data/merge_request_builder_spec.rb
|
|
||||||
- spec/lib/gitlab/hook_data/release_builder_spec.rb
|
|
||||||
- spec/lib/gitlab/json_cache_spec.rb
|
- spec/lib/gitlab/json_cache_spec.rb
|
||||||
- spec/lib/gitlab/language_detection_spec.rb
|
- spec/lib/gitlab/language_detection_spec.rb
|
||||||
- spec/lib/gitlab/project_search_results_spec.rb
|
- spec/lib/gitlab/project_search_results_spec.rb
|
||||||
|
@ -1010,25 +1003,12 @@ RSpec/EmptyLineAfterFinalLetItBe:
|
||||||
- spec/services/feature_flags/enable_service_spec.rb
|
- spec/services/feature_flags/enable_service_spec.rb
|
||||||
- spec/services/feature_flags/update_service_spec.rb
|
- spec/services/feature_flags/update_service_spec.rb
|
||||||
- spec/services/git/branch_push_service_spec.rb
|
- spec/services/git/branch_push_service_spec.rb
|
||||||
- spec/services/groups/auto_devops_service_spec.rb
|
|
||||||
- spec/services/groups/group_links/update_service_spec.rb
|
|
||||||
- spec/services/groups/transfer_service_spec.rb
|
|
||||||
- spec/services/groups/update_shared_runners_service_spec.rb
|
|
||||||
- spec/services/ide/base_config_service_spec.rb
|
|
||||||
- spec/services/ide/schemas_config_service_spec.rb
|
|
||||||
- spec/services/ide/terminal_config_service_spec.rb
|
|
||||||
- spec/services/import/bitbucket_server_service_spec.rb
|
- spec/services/import/bitbucket_server_service_spec.rb
|
||||||
- spec/services/incident_management/incidents/create_service_spec.rb
|
- spec/services/incident_management/incidents/create_service_spec.rb
|
||||||
- spec/services/incident_management/pager_duty/create_incident_issue_service_spec.rb
|
- spec/services/incident_management/pager_duty/create_incident_issue_service_spec.rb
|
||||||
- spec/services/incident_management/pager_duty/process_webhook_service_spec.rb
|
- spec/services/incident_management/pager_duty/process_webhook_service_spec.rb
|
||||||
- spec/services/integrations/test/project_service_spec.rb
|
- spec/services/integrations/test/project_service_spec.rb
|
||||||
- spec/services/issuable/bulk_update_service_spec.rb
|
- spec/services/issuable/bulk_update_service_spec.rb
|
||||||
- spec/services/issues/build_service_spec.rb
|
|
||||||
- spec/services/issues/clone_service_spec.rb
|
|
||||||
- spec/services/issues/create_service_spec.rb
|
|
||||||
- spec/services/issues/export_csv_service_spec.rb
|
|
||||||
- spec/services/issues/move_service_spec.rb
|
|
||||||
- spec/services/issues/related_branches_service_spec.rb
|
|
||||||
- spec/services/jira_connect/sync_service_spec.rb
|
- spec/services/jira_connect/sync_service_spec.rb
|
||||||
- spec/services/jira_import/start_import_service_spec.rb
|
- spec/services/jira_import/start_import_service_spec.rb
|
||||||
- spec/services/jira_import/users_importer_spec.rb
|
- spec/services/jira_import/users_importer_spec.rb
|
||||||
|
|
|
@ -4,13 +4,13 @@ import axios from '../lib/utils/axios_utils';
|
||||||
import { __ } from '../locale';
|
import { __ } from '../locale';
|
||||||
import DivergenceGraph from './components/divergence_graph.vue';
|
import DivergenceGraph from './components/divergence_graph.vue';
|
||||||
|
|
||||||
export function createGraphVueApp(el, data, maxCommits) {
|
export function createGraphVueApp(el, data, maxCommits, defaultBranch) {
|
||||||
return new Vue({
|
return new Vue({
|
||||||
el,
|
el,
|
||||||
render(h) {
|
render(h) {
|
||||||
return h(DivergenceGraph, {
|
return h(DivergenceGraph, {
|
||||||
props: {
|
props: {
|
||||||
defaultBranch: 'master',
|
defaultBranch,
|
||||||
distance: data.distance ? parseInt(data.distance, 10) : null,
|
distance: data.distance ? parseInt(data.distance, 10) : null,
|
||||||
aheadCount: parseInt(data.ahead, 10),
|
aheadCount: parseInt(data.ahead, 10),
|
||||||
behindCount: parseInt(data.behind, 10),
|
behindCount: parseInt(data.behind, 10),
|
||||||
|
@ -21,7 +21,7 @@ export function createGraphVueApp(el, data, maxCommits) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (endpoint) => {
|
export default (endpoint, defaultBranch) => {
|
||||||
const names = [...document.querySelectorAll('.js-branch-item')].map(
|
const names = [...document.querySelectorAll('.js-branch-item')].map(
|
||||||
({ dataset }) => dataset.name,
|
({ dataset }) => dataset.name,
|
||||||
);
|
);
|
||||||
|
@ -47,7 +47,7 @@ export default (endpoint) => {
|
||||||
|
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
|
||||||
createGraphVueApp(el, val, maxCommits);
|
createGraphVueApp(el, val, maxCommits, defaultBranch);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() =>
|
.catch(() =>
|
||||||
|
|
16
app/assets/javascripts/delete_label_modal.js
Normal file
16
app/assets/javascripts/delete_label_modal.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import DeleteLabelModal from '~/vue_shared/components/delete_label_modal.vue';
|
||||||
|
|
||||||
|
const mountDeleteLabelModal = (optionalProps) =>
|
||||||
|
new Vue({
|
||||||
|
render(h) {
|
||||||
|
return h(DeleteLabelModal, {
|
||||||
|
props: {
|
||||||
|
selector: '.js-delete-label-modal-button',
|
||||||
|
...optionalProps,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}).$mount();
|
||||||
|
|
||||||
|
export default (optionalProps = {}) => mountDeleteLabelModal(optionalProps);
|
|
@ -39,11 +39,12 @@ export const removeSubscription = async (removePath) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchGroups = async (groupsPath, { page, perPage }) => {
|
export const fetchGroups = async (groupsPath, { page, perPage, search }) => {
|
||||||
return axios.get(groupsPath, {
|
return axios.get(groupsPath, {
|
||||||
params: {
|
params: {
|
||||||
page,
|
page,
|
||||||
per_page: perPage,
|
per_page: perPage,
|
||||||
|
search,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlTabs, GlTab, GlLoadingIcon, GlPagination, GlAlert } from '@gitlab/ui';
|
import { GlLoadingIcon, GlPagination, GlAlert, GlSearchBoxByType } from '@gitlab/ui';
|
||||||
import { fetchGroups } from '~/jira_connect/api';
|
import { fetchGroups } from '~/jira_connect/api';
|
||||||
import { defaultPerPage } from '~/jira_connect/constants';
|
import { defaultPerPage } from '~/jira_connect/constants';
|
||||||
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
|
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
|
||||||
|
@ -8,11 +8,10 @@ import GroupsListItem from './groups_list_item.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
GlTabs,
|
|
||||||
GlTab,
|
|
||||||
GlLoadingIcon,
|
GlLoadingIcon,
|
||||||
GlPagination,
|
GlPagination,
|
||||||
GlAlert,
|
GlAlert,
|
||||||
|
GlSearchBoxByType,
|
||||||
GroupsListItem,
|
GroupsListItem,
|
||||||
},
|
},
|
||||||
inject: {
|
inject: {
|
||||||
|
@ -23,7 +22,8 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
groups: [],
|
groups: [],
|
||||||
isLoading: false,
|
isLoadingInitial: true,
|
||||||
|
isLoadingMore: false,
|
||||||
page: 1,
|
page: 1,
|
||||||
perPage: defaultPerPage,
|
perPage: defaultPerPage,
|
||||||
totalItems: 0,
|
totalItems: 0,
|
||||||
|
@ -31,15 +31,18 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.loadGroups();
|
return this.loadGroups().finally(() => {
|
||||||
|
this.isLoadingInitial = false;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
loadGroups() {
|
loadGroups({ searchTerm } = {}) {
|
||||||
this.isLoading = true;
|
this.isLoadingMore = true;
|
||||||
|
|
||||||
fetchGroups(this.groupsPath, {
|
return fetchGroups(this.groupsPath, {
|
||||||
page: this.page,
|
page: this.page,
|
||||||
perPage: this.perPage,
|
perPage: this.perPage,
|
||||||
|
search: searchTerm,
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
const { page, total } = parseIntPagination(normalizeHeaders(response.headers));
|
const { page, total } = parseIntPagination(normalizeHeaders(response.headers));
|
||||||
|
@ -51,50 +54,61 @@ export default {
|
||||||
this.errorMessage = s__('Integrations|Failed to load namespaces. Please try again.');
|
this.errorMessage = s__('Integrations|Failed to load namespaces. Please try again.');
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.isLoading = false;
|
this.isLoadingMore = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
onGroupSearch(searchTerm) {
|
||||||
|
return this.loadGroups({ searchTerm });
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<gl-alert v-if="errorMessage" class="gl-mb-6" variant="danger" @dismiss="errorMessage = null">
|
<gl-alert v-if="errorMessage" class="gl-mb-5" variant="danger" @dismiss="errorMessage = null">
|
||||||
{{ errorMessage }}
|
{{ errorMessage }}
|
||||||
</gl-alert>
|
</gl-alert>
|
||||||
|
|
||||||
<gl-tabs>
|
<gl-search-box-by-type
|
||||||
<gl-tab :title="__('Groups and subgroups')" class="gl-pt-3">
|
class="gl-mb-5"
|
||||||
<gl-loading-icon v-if="isLoading" size="md" />
|
debounce="500"
|
||||||
<div v-else-if="groups.length === 0" class="gl-text-center">
|
:placeholder="__('Search by name')"
|
||||||
<h5>{{ s__('Integrations|No available namespaces.') }}</h5>
|
:is-loading="isLoadingMore"
|
||||||
<p class="gl-mt-5">
|
@input="onGroupSearch"
|
||||||
{{
|
/>
|
||||||
s__('Integrations|You must have owner or maintainer permissions to link namespaces.')
|
|
||||||
}}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<ul v-else class="gl-list-style-none gl-pl-0">
|
|
||||||
<groups-list-item
|
|
||||||
v-for="group in groups"
|
|
||||||
:key="group.id"
|
|
||||||
:group="group"
|
|
||||||
@error="errorMessage = $event"
|
|
||||||
/>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div class="gl-display-flex gl-justify-content-center gl-mt-5">
|
<gl-loading-icon v-if="isLoadingInitial" size="md" />
|
||||||
<gl-pagination
|
<div v-else-if="groups.length === 0" class="gl-text-center">
|
||||||
v-if="totalItems > perPage && groups.length > 0"
|
<h5>{{ s__('Integrations|No available namespaces.') }}</h5>
|
||||||
v-model="page"
|
<p class="gl-mt-5">
|
||||||
class="gl-mb-0"
|
{{ s__('Integrations|You must have owner or maintainer permissions to link namespaces.') }}
|
||||||
:per-page="perPage"
|
</p>
|
||||||
:total-items="totalItems"
|
</div>
|
||||||
@input="loadGroups"
|
<ul
|
||||||
/>
|
v-else
|
||||||
</div>
|
class="gl-list-style-none gl-pl-0 gl-border-t-1 gl-border-t-solid gl-border-t-gray-100"
|
||||||
</gl-tab>
|
:class="{ 'gl-opacity-5': isLoadingMore }"
|
||||||
</gl-tabs>
|
data-testid="groups-list"
|
||||||
|
>
|
||||||
|
<groups-list-item
|
||||||
|
v-for="group in groups"
|
||||||
|
:key="group.id"
|
||||||
|
:group="group"
|
||||||
|
:disabled="isLoadingMore"
|
||||||
|
@error="errorMessage = $event"
|
||||||
|
/>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="gl-display-flex gl-justify-content-center gl-mt-5">
|
||||||
|
<gl-pagination
|
||||||
|
v-if="totalItems > perPage && groups.length > 0"
|
||||||
|
v-model="page"
|
||||||
|
class="gl-mb-0"
|
||||||
|
:per-page="perPage"
|
||||||
|
:total-items="totalItems"
|
||||||
|
@input="loadGroups"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -21,6 +21,11 @@ export default {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -60,7 +65,7 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<li class="gl-border-b-1 gl-border-b-solid gl-border-b-gray-200">
|
<li class="gl-border-b-1 gl-border-b-solid gl-border-b-gray-100">
|
||||||
<div class="gl-display-flex gl-align-items-center gl-py-3">
|
<div class="gl-display-flex gl-align-items-center gl-py-3">
|
||||||
<gl-icon name="folder-o" class="gl-mr-3" />
|
<gl-icon name="folder-o" class="gl-mr-3" />
|
||||||
<div class="gl-display-none gl-flex-shrink-0 gl-sm-display-flex gl-mr-3">
|
<div class="gl-display-none gl-flex-shrink-0 gl-sm-display-flex gl-mr-3">
|
||||||
|
@ -83,11 +88,13 @@ export default {
|
||||||
|
|
||||||
<gl-button
|
<gl-button
|
||||||
category="secondary"
|
category="secondary"
|
||||||
variant="success"
|
variant="confirm"
|
||||||
:loading="isLoading"
|
:loading="isLoading"
|
||||||
|
:disabled="disabled"
|
||||||
@click.prevent="onClick"
|
@click.prevent="onClick"
|
||||||
>{{ __('Link') }}</gl-button
|
|
||||||
>
|
>
|
||||||
|
{{ __('Link') }}
|
||||||
|
</gl-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import initDeleteLabelModal from '~/delete_label_modal';
|
||||||
import initLabels from '~/init_labels';
|
import initLabels from '~/init_labels';
|
||||||
|
|
||||||
initLabels();
|
initLabels();
|
||||||
|
initDeleteLabelModal();
|
||||||
|
|
|
@ -5,5 +5,10 @@ import initDiverganceGraph from '~/branches/divergence_graph';
|
||||||
|
|
||||||
AjaxLoadingSpinner.init();
|
AjaxLoadingSpinner.init();
|
||||||
new DeleteModal(); // eslint-disable-line no-new
|
new DeleteModal(); // eslint-disable-line no-new
|
||||||
initDiverganceGraph(document.querySelector('.js-branch-list').dataset.divergingCountsEndpoint);
|
|
||||||
|
const { divergingCountsEndpoint, defaultBranch } = document.querySelector(
|
||||||
|
'.js-branch-list',
|
||||||
|
).dataset;
|
||||||
|
|
||||||
|
initDiverganceGraph(divergingCountsEndpoint, defaultBranch);
|
||||||
BranchSortDropdown();
|
BranchSortDropdown();
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
import initDeleteLabelModal from '~/delete_label_modal';
|
||||||
import initLabels from '~/init_labels';
|
import initLabels from '~/init_labels';
|
||||||
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
|
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
|
||||||
import Translate from '~/vue_shared/translate';
|
import Translate from '~/vue_shared/translate';
|
||||||
|
@ -9,6 +10,7 @@ Vue.use(Translate);
|
||||||
|
|
||||||
const initLabelIndex = () => {
|
const initLabelIndex = () => {
|
||||||
initLabels();
|
initLabels();
|
||||||
|
initDeleteLabelModal();
|
||||||
|
|
||||||
const onRequestFinished = ({ labelUrl, successful }) => {
|
const onRequestFinished = ({ labelUrl, successful }) => {
|
||||||
const button = document.querySelector(
|
const button = document.querySelector(
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
<script>
|
||||||
|
import { GlModal, GlSprintf, GlButton } from '@gitlab/ui';
|
||||||
|
import { uniqueId } from 'lodash';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
GlModal,
|
||||||
|
GlSprintf,
|
||||||
|
GlButton,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
selector: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
labelName: '',
|
||||||
|
subjectName: '',
|
||||||
|
destroyPath: '',
|
||||||
|
modalId: uniqueId('modal-delete-label-'),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
document.querySelectorAll(this.selector).forEach((button) => {
|
||||||
|
button.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const { labelName, subjectName, destroyPath } = button.dataset;
|
||||||
|
this.labelName = labelName;
|
||||||
|
this.subjectName = subjectName;
|
||||||
|
this.destroyPath = destroyPath;
|
||||||
|
this.openModal();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
openModal() {
|
||||||
|
this.$refs.modal.show();
|
||||||
|
},
|
||||||
|
closeModal() {
|
||||||
|
this.$refs.modal.hide();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<gl-modal ref="modal" :modal-id="modalId">
|
||||||
|
<template #modal-title>
|
||||||
|
<gl-sprintf :message="__('Delete label: %{labelName}')">
|
||||||
|
<template #labelName>
|
||||||
|
{{ labelName }}
|
||||||
|
</template>
|
||||||
|
</gl-sprintf>
|
||||||
|
</template>
|
||||||
|
<gl-sprintf
|
||||||
|
:message="
|
||||||
|
__(
|
||||||
|
`%{strongStart}${labelName}%{strongEnd} will be permanently deleted from ${subjectName}. This cannot be undone.`,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<template #strong="{ content }">
|
||||||
|
<strong>{{ content }}</strong>
|
||||||
|
</template>
|
||||||
|
</gl-sprintf>
|
||||||
|
<template #modal-footer>
|
||||||
|
<gl-button category="secondary" @click="closeModal">{{ __('Cancel') }}</gl-button>
|
||||||
|
<gl-button
|
||||||
|
category="primary"
|
||||||
|
variant="danger"
|
||||||
|
:href="destroyPath"
|
||||||
|
data-method="delete"
|
||||||
|
data-testid="delete-button"
|
||||||
|
>{{ __('Delete label') }}</gl-button
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</gl-modal>
|
||||||
|
</template>
|
|
@ -4,7 +4,6 @@
|
||||||
@import 'bootstrap-vue/src/index';
|
@import 'bootstrap-vue/src/index';
|
||||||
|
|
||||||
@import '@gitlab/ui/src/scss/utilities';
|
@import '@gitlab/ui/src/scss/utilities';
|
||||||
@import '@gitlab/ui/src/components/base/alert/alert';
|
|
||||||
|
|
||||||
// We should only import styles that we actually use.
|
// We should only import styles that we actually use.
|
||||||
@import '@gitlab/ui/src/components/base/alert/alert';
|
@import '@gitlab/ui/src/components/base/alert/alert';
|
||||||
|
@ -16,8 +15,8 @@
|
||||||
@import '@gitlab/ui/src/components/base/loading_icon/loading_icon';
|
@import '@gitlab/ui/src/components/base/loading_icon/loading_icon';
|
||||||
@import '@gitlab/ui/src/components/base/modal/modal';
|
@import '@gitlab/ui/src/components/base/modal/modal';
|
||||||
@import '@gitlab/ui/src/components/base/pagination/pagination';
|
@import '@gitlab/ui/src/components/base/pagination/pagination';
|
||||||
@import '@gitlab/ui/src/components/base/tabs/tabs/tabs';
|
|
||||||
@import '@gitlab/ui/src/components/base/tooltip/tooltip';
|
@import '@gitlab/ui/src/components/base/tooltip/tooltip';
|
||||||
|
@import '@gitlab/ui/src/components/base/search_box_by_type/search_box_by_type';
|
||||||
|
|
||||||
$atlaskit-border-color: #dfe1e6;
|
$atlaskit-border-color: #dfe1e6;
|
||||||
$header-height: 40px;
|
$header-height: 40px;
|
||||||
|
|
37
app/graphql/resolvers/blobs_resolver.rb
Normal file
37
app/graphql/resolvers/blobs_resolver.rb
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Resolvers
|
||||||
|
class BlobsResolver < BaseResolver
|
||||||
|
include Gitlab::Graphql::Authorize::AuthorizeResource
|
||||||
|
|
||||||
|
type Types::Tree::BlobType.connection_type, null: true
|
||||||
|
authorize :download_code
|
||||||
|
calls_gitaly!
|
||||||
|
|
||||||
|
alias_method :repository, :object
|
||||||
|
|
||||||
|
argument :paths, [GraphQL::STRING_TYPE],
|
||||||
|
required: true,
|
||||||
|
description: 'Array of desired blob paths.'
|
||||||
|
argument :ref, GraphQL::STRING_TYPE,
|
||||||
|
required: false,
|
||||||
|
default_value: nil,
|
||||||
|
description: 'The commit ref to get the blobs from. Default value is HEAD.'
|
||||||
|
|
||||||
|
# We fetch blobs from Gitaly efficiently but it still scales O(N) with the
|
||||||
|
# number of paths being fetched, so apply a scaling limit to that.
|
||||||
|
def self.resolver_complexity(args, child_complexity:)
|
||||||
|
super + args.fetch(:paths, []).size
|
||||||
|
end
|
||||||
|
|
||||||
|
def resolve(paths:, ref:)
|
||||||
|
authorize!(repository.container)
|
||||||
|
|
||||||
|
return [] if repository.empty?
|
||||||
|
|
||||||
|
ref ||= repository.root_ref
|
||||||
|
|
||||||
|
repository.blobs_at(paths.map { |path| [ref, path] })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -50,7 +50,8 @@ module ResolvesMergeRequests
|
||||||
approved_by: [:approved_by_users],
|
approved_by: [:approved_by_users],
|
||||||
milestone: [:milestone],
|
milestone: [:milestone],
|
||||||
security_auto_fix: [:author],
|
security_auto_fix: [:author],
|
||||||
head_pipeline: [:merge_request_diff, { head_pipeline: [:merge_request] }]
|
head_pipeline: [:merge_request_diff, { head_pipeline: [:merge_request] }],
|
||||||
|
timelogs: [:timelogs]
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -44,7 +44,8 @@ module Resolvers
|
||||||
{
|
{
|
||||||
alert_management_alert: [:alert_management_alert],
|
alert_management_alert: [:alert_management_alert],
|
||||||
labels: [:labels],
|
labels: [:labels],
|
||||||
assignees: [:assignees]
|
assignees: [:assignees],
|
||||||
|
timelogs: [:timelogs]
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ module Resolvers
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
BatchLoader::GraphQL.for(sha).batch(key: project) do |shas, loader, args|
|
BatchLoader::GraphQL.for(sha).batch(key: project) do |shas, loader, args|
|
||||||
finder = ::Ci::PipelinesFinder.new(project, current_user, shas: shas)
|
finder = ::Ci::PipelinesFinder.new(project, current_user, sha: shas)
|
||||||
|
|
||||||
finder.execute.each { |pipeline| loader.call(pipeline.sha.to_s, pipeline) }
|
finder.execute.each { |pipeline| loader.call(pipeline.sha.to_s, pipeline) }
|
||||||
end
|
end
|
||||||
|
|
|
@ -124,6 +124,9 @@ module Types
|
||||||
field :create_note_email, GraphQL::STRING_TYPE, null: true,
|
field :create_note_email, GraphQL::STRING_TYPE, null: true,
|
||||||
description: 'User specific email address for the issue.'
|
description: 'User specific email address for the issue.'
|
||||||
|
|
||||||
|
field :timelogs, Types::TimelogType.connection_type, null: false,
|
||||||
|
description: 'Timelogs on the issue.'
|
||||||
|
|
||||||
def author
|
def author
|
||||||
Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.author_id).find
|
Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.author_id).find
|
||||||
end
|
end
|
||||||
|
|
|
@ -186,6 +186,8 @@ module Types
|
||||||
description: 'Selected auto merge strategy.'
|
description: 'Selected auto merge strategy.'
|
||||||
field :merge_user, Types::UserType, null: true,
|
field :merge_user, Types::UserType, null: true,
|
||||||
description: 'User who merged this merge request.'
|
description: 'User who merged this merge request.'
|
||||||
|
field :timelogs, Types::TimelogType.connection_type, null: false,
|
||||||
|
description: 'Timelogs on the merge request.'
|
||||||
|
|
||||||
def approved_by
|
def approved_by
|
||||||
object.approved_by_users
|
object.approved_by_users
|
||||||
|
|
|
@ -14,5 +14,7 @@ module Types
|
||||||
description: 'Indicates a corresponding Git repository exists on disk.'
|
description: 'Indicates a corresponding Git repository exists on disk.'
|
||||||
field :tree, Types::Tree::TreeType, null: true, resolver: Resolvers::TreeResolver, calls_gitaly: true,
|
field :tree, Types::Tree::TreeType, null: true, resolver: Resolvers::TreeResolver, calls_gitaly: true,
|
||||||
description: 'Tree of the repository.'
|
description: 'Tree of the repository.'
|
||||||
|
field :blobs, Types::Tree::BlobType.connection_type, null: true, resolver: Resolvers::BlobsResolver, calls_gitaly: true,
|
||||||
|
description: 'Blobs contained within the repository'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -37,4 +37,18 @@ module ProfilesHelper
|
||||||
def user_status_set_to_busy?(status)
|
def user_status_set_to_busy?(status)
|
||||||
status&.availability == availability_values[:busy]
|
status&.availability == availability_values[:busy]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Overridden in EE::ProfilesHelper#ssh_key_expiration_tooltip
|
||||||
|
def ssh_key_expiration_tooltip(key)
|
||||||
|
return key.errors.full_messages.join(', ') if key.errors.full_messages.any?
|
||||||
|
|
||||||
|
s_('Profiles|Key usable beyond expiration date.') if key.expired?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Overridden in EE::ProfilesHelper#ssh_key_expires_field_description
|
||||||
|
def ssh_key_expires_field_description
|
||||||
|
s_('Profiles|Key can still be used after expiration.')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
ProfilesHelper.prepend_ee_mod
|
||||||
|
|
|
@ -14,6 +14,10 @@ module SidebarsHelper
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def project_sidebar_context(project, user)
|
||||||
|
Sidebars::Context.new(**project_sidebar_context_data(project, user))
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def sidebar_project_tracking_attrs
|
def sidebar_project_tracking_attrs
|
||||||
|
@ -27,4 +31,12 @@ module SidebarsHelper
|
||||||
def sidebar_user_profile_tracking_attrs
|
def sidebar_user_profile_tracking_attrs
|
||||||
tracking_attrs('user_side_navigation', 'render', 'user_side_navigation')
|
tracking_attrs('user_side_navigation', 'render', 'user_side_navigation')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def project_sidebar_context_data(project, user)
|
||||||
|
{
|
||||||
|
current_user: user,
|
||||||
|
container: project,
|
||||||
|
project: project
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
# Blob is a Rails-specific wrapper around Gitlab::Git::Blob, SnippetBlob and Ci::ArtifactBlob
|
# Blob is a Rails-specific wrapper around Gitlab::Git::Blob, SnippetBlob and Ci::ArtifactBlob
|
||||||
class Blob < SimpleDelegator
|
class Blob < SimpleDelegator
|
||||||
|
include GlobalID::Identification
|
||||||
include Presentable
|
include Presentable
|
||||||
include BlobLanguageFromGitAttributes
|
include BlobLanguageFromGitAttributes
|
||||||
include BlobActiveModel
|
include BlobActiveModel
|
||||||
|
|
|
@ -2,9 +2,10 @@
|
||||||
|
|
||||||
module HasTimelogsReport
|
module HasTimelogsReport
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
include Gitlab::Utils::StrongMemoize
|
||||||
|
|
||||||
def timelogs(start_time, end_time)
|
def timelogs(start_time, end_time)
|
||||||
@timelogs ||= timelogs_for(start_time, end_time)
|
strong_memoize(:timelogs) { timelogs_for(start_time, end_time) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_can_access_group_timelogs?(current_user)
|
def user_can_access_group_timelogs?(current_user)
|
||||||
|
|
|
@ -1378,7 +1378,7 @@ class Project < ApplicationRecord
|
||||||
def find_or_initialize_service(name)
|
def find_or_initialize_service(name)
|
||||||
return if disabled_services.include?(name)
|
return if disabled_services.include?(name)
|
||||||
|
|
||||||
find_service(services, name) || build_from_instance_or_template(name) || public_send("build_#{name}_service") # rubocop:disable GitlabSecurity/PublicSend
|
find_service(services, name) || build_from_instance_or_template(name) || build_service(name)
|
||||||
end
|
end
|
||||||
|
|
||||||
# rubocop: disable CodeReuse/ServiceClass
|
# rubocop: disable CodeReuse/ServiceClass
|
||||||
|
@ -2596,6 +2596,10 @@ class Project < ApplicationRecord
|
||||||
return Service.build_from_integration(template, project_id: id) if template
|
return Service.build_from_integration(template, project_id: id) if template
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def build_service(name)
|
||||||
|
"#{name}_service".classify.constantize.new(project_id: id)
|
||||||
|
end
|
||||||
|
|
||||||
def services_templates
|
def services_templates
|
||||||
@services_templates ||= Service.for_template
|
@services_templates ||= Service.for_template
|
||||||
end
|
end
|
||||||
|
|
|
@ -51,5 +51,25 @@ module Sidebars
|
||||||
def renderable_menus
|
def renderable_menus
|
||||||
@renderable_menus ||= @menus.select(&:render?)
|
@renderable_menus ||= @menus.select(&:render?)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def container
|
||||||
|
context.container
|
||||||
|
end
|
||||||
|
|
||||||
|
# Auxiliar method that helps with the migration from
|
||||||
|
# regular views to the new logic
|
||||||
|
def render_raw_scope_menu_partial
|
||||||
|
# No-op
|
||||||
|
end
|
||||||
|
|
||||||
|
# Auxiliar method that helps with the migration from
|
||||||
|
# regular views to the new logic.
|
||||||
|
#
|
||||||
|
# Any menu inside this partial will be added after
|
||||||
|
# all the menus added in the `configure_menus`
|
||||||
|
# method.
|
||||||
|
def render_raw_menus_partial
|
||||||
|
# No-op
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
22
app/models/sidebars/projects/panel.rb
Normal file
22
app/models/sidebars/projects/panel.rb
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Sidebars
|
||||||
|
module Projects
|
||||||
|
class Panel < ::Sidebars::Panel
|
||||||
|
override :render_raw_menus_partial
|
||||||
|
def render_raw_scope_menu_partial
|
||||||
|
'layouts/nav/sidebar/project_scope_menu'
|
||||||
|
end
|
||||||
|
|
||||||
|
override :render_raw_menus_partial
|
||||||
|
def render_raw_menus_partial
|
||||||
|
'layouts/nav/sidebar/project_menus'
|
||||||
|
end
|
||||||
|
|
||||||
|
override :aria_label
|
||||||
|
def aria_label
|
||||||
|
_('Project navigation')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -250,13 +250,17 @@ module Projects
|
||||||
|
|
||||||
def make_secure_tmp_dir(tmp_path)
|
def make_secure_tmp_dir(tmp_path)
|
||||||
FileUtils.mkdir_p(tmp_path)
|
FileUtils.mkdir_p(tmp_path)
|
||||||
path = Dir.mktmpdir(nil, tmp_path)
|
path = Dir.mktmpdir(tmp_dir_prefix, tmp_path)
|
||||||
begin
|
begin
|
||||||
yield(path)
|
yield(path)
|
||||||
ensure
|
ensure
|
||||||
FileUtils.remove_entry_secure(path)
|
FileUtils.remove_entry_secure(path)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def tmp_dir_prefix
|
||||||
|
"project-#{project.id}-build-#{build.id}-"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,469 +1,3 @@
|
||||||
%aside.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?), **sidebar_tracking_attributes_by_object(@project), 'aria-label': _('Project navigation') }
|
-# We're migration the project sidebar to a logical model based structure. If you need to update
|
||||||
.nav-sidebar-inner-scroll
|
-# any of the existing menus, you can find them in app/views/layouts/nav/sidebar/_project_menus.html.haml.
|
||||||
.context-header
|
= render partial: 'shared/nav/sidebar', object: Sidebars::Projects::Panel.new(project_sidebar_context(@project, current_user))
|
||||||
= link_to project_path(@project), title: @project.name do
|
|
||||||
.avatar-container.rect-avatar.s40.project-avatar
|
|
||||||
= project_icon(@project, alt: @project.name, class: 'avatar s40 avatar-tile', width: 40, height: 40)
|
|
||||||
.sidebar-context-title
|
|
||||||
= @project.name
|
|
||||||
%ul.sidebar-top-level-items.qa-project-sidebar
|
|
||||||
= nav_link(path: sidebar_projects_paths, html_options: { class: 'home' }) do
|
|
||||||
= link_to project_path(@project), class: 'shortcuts-project rspec-project-link', data: { qa_selector: 'project_link' } do
|
|
||||||
.nav-icon-container
|
|
||||||
= sprite_icon('home')
|
|
||||||
%span.nav-item-name
|
|
||||||
= _('Project overview')
|
|
||||||
|
|
||||||
%ul.sidebar-sub-level-items
|
|
||||||
= nav_link(path: 'projects#show', html_options: { class: "fly-out-top-item" } ) do
|
|
||||||
= link_to project_path(@project) do
|
|
||||||
%strong.fly-out-top-item-name
|
|
||||||
= _('Project overview')
|
|
||||||
%li.divider.fly-out-top-item
|
|
||||||
= nav_link(path: 'projects#show') do
|
|
||||||
= link_to project_path(@project), title: _('Project details'), class: 'shortcuts-project' do
|
|
||||||
%span= _('Details')
|
|
||||||
|
|
||||||
= nav_link(path: 'projects#activity') do
|
|
||||||
= link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity', data: { qa_selector: 'activity_link' } do
|
|
||||||
%span= _('Activity')
|
|
||||||
|
|
||||||
- if project_nav_tab?(:releases)
|
|
||||||
= nav_link(controller: :releases) do
|
|
||||||
= link_to project_releases_path(@project), title: _('Releases'), class: 'shortcuts-project-releases' do
|
|
||||||
%span= _('Releases')
|
|
||||||
|
|
||||||
- if project_nav_tab? :learn_gitlab
|
|
||||||
= nav_link(controller: :learn_gitlab, html_options: { class: 'home' }) do
|
|
||||||
= link_to project_learn_gitlab_path(@project) do
|
|
||||||
.nav-icon-container
|
|
||||||
= sprite_icon('home')
|
|
||||||
%span.nav-item-name
|
|
||||||
= _('Learn GitLab')
|
|
||||||
|
|
||||||
- if project_nav_tab? :files
|
|
||||||
= nav_link(controller: sidebar_repository_paths, unless: -> { current_path?('projects/graphs#charts') }) do
|
|
||||||
= link_to project_tree_path(@project), class: 'shortcuts-tree', data: { qa_selector: "repository_link" } do
|
|
||||||
.nav-icon-container
|
|
||||||
= sprite_icon('doc-text')
|
|
||||||
%span.nav-item-name#js-onboarding-repo-link
|
|
||||||
= _('Repository')
|
|
||||||
|
|
||||||
%ul.sidebar-sub-level-items
|
|
||||||
= nav_link(controller: sidebar_repository_paths, html_options: { class: "fly-out-top-item" } ) do
|
|
||||||
= link_to project_tree_path(@project) do
|
|
||||||
%strong.fly-out-top-item-name
|
|
||||||
= _('Repository')
|
|
||||||
%li.divider.fly-out-top-item
|
|
||||||
= nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do
|
|
||||||
= link_to project_tree_path(@project) do
|
|
||||||
= _('Files')
|
|
||||||
|
|
||||||
= nav_link(controller: [:commit, :commits]) do
|
|
||||||
= link_to project_commits_path(@project, current_ref), id: 'js-onboarding-commits-link' do
|
|
||||||
= _('Commits')
|
|
||||||
|
|
||||||
= nav_link(html_options: {class: branches_tab_class}) do
|
|
||||||
= link_to project_branches_path(@project), data: { qa_selector: "branches_link" }, id: 'js-onboarding-branches-link' do
|
|
||||||
= _('Branches')
|
|
||||||
|
|
||||||
= nav_link(controller: [:tags]) do
|
|
||||||
= link_to project_tags_path(@project), data: { qa_selector: "tags_link" } do
|
|
||||||
= _('Tags')
|
|
||||||
|
|
||||||
= nav_link(path: 'graphs#show') do
|
|
||||||
= link_to project_graph_path(@project, current_ref) do
|
|
||||||
= _('Contributors')
|
|
||||||
|
|
||||||
= nav_link(controller: %w(network)) do
|
|
||||||
= link_to project_network_path(@project, current_ref) do
|
|
||||||
= _('Graph')
|
|
||||||
|
|
||||||
= nav_link(controller: :compare) do
|
|
||||||
= link_to project_compare_index_path(@project, from: @repository.root_ref, to: current_ref) do
|
|
||||||
= _('Compare')
|
|
||||||
|
|
||||||
= render_if_exists 'projects/sidebar/repository_locked_files'
|
|
||||||
|
|
||||||
- if project_nav_tab? :issues
|
|
||||||
= nav_link(controller: @project.issues_enabled? ? ['projects/issues', :labels, :milestones, :boards, :iterations] : 'projects/issues') do
|
|
||||||
= link_to project_issues_path(@project), class: 'shortcuts-issues qa-issues-item' do
|
|
||||||
.nav-icon-container
|
|
||||||
= sprite_icon('issues')
|
|
||||||
%span.nav-item-name#js-onboarding-issues-link
|
|
||||||
= _('Issues')
|
|
||||||
- if @project.issues_enabled?
|
|
||||||
%span.badge.badge-pill.count.issue_counter
|
|
||||||
= number_with_delimiter(@project.open_issues_count(current_user))
|
|
||||||
|
|
||||||
%ul.sidebar-sub-level-items
|
|
||||||
= nav_link(controller: 'projects/issues', action: :index, html_options: { class: "fly-out-top-item" } ) do
|
|
||||||
= link_to project_issues_path(@project) do
|
|
||||||
%strong.fly-out-top-item-name
|
|
||||||
= _('Issues')
|
|
||||||
- if @project.issues_enabled?
|
|
||||||
%span.badge.badge-pill.count.issue_counter.fly-out-badge
|
|
||||||
= number_with_delimiter(@project.open_issues_count(current_user))
|
|
||||||
%li.divider.fly-out-top-item
|
|
||||||
= nav_link(controller: :issues, action: :index) do
|
|
||||||
= link_to project_issues_path(@project), title: _('Issues') do
|
|
||||||
%span
|
|
||||||
= _('List')
|
|
||||||
|
|
||||||
= nav_link(controller: :boards) do
|
|
||||||
= link_to project_boards_path(@project), title: boards_link_text, data: { qa_selector: "issue_boards_link" } do
|
|
||||||
%span
|
|
||||||
= boards_link_text
|
|
||||||
|
|
||||||
= nav_link(controller: :labels) do
|
|
||||||
= link_to project_labels_path(@project), title: _('Labels'), class: 'qa-labels-link' do
|
|
||||||
%span
|
|
||||||
= _('Labels')
|
|
||||||
|
|
||||||
= render 'projects/sidebar/issues_service_desk'
|
|
||||||
|
|
||||||
= nav_link(controller: :milestones) do
|
|
||||||
= link_to project_milestones_path(@project), title: _('Milestones'), class: 'qa-milestones-link' do
|
|
||||||
%span
|
|
||||||
= _('Milestones')
|
|
||||||
|
|
||||||
= render_if_exists 'layouts/nav/sidebar/project_iterations_link'
|
|
||||||
|
|
||||||
- if project_nav_tab?(:external_issue_tracker)
|
|
||||||
- issue_tracker = @project.external_issue_tracker
|
|
||||||
- if issue_tracker.is_a?(JiraService) && project_jira_issues_integration?
|
|
||||||
= render_if_exists 'layouts/nav/sidebar/project_jira_issues_link', issue_tracker: issue_tracker
|
|
||||||
- else
|
|
||||||
= nav_link do
|
|
||||||
= link_to issue_tracker.issue_tracker_path, target: '_blank', rel: 'noopener noreferrer', class: 'shortcuts-external_tracker' do
|
|
||||||
.nav-icon-container
|
|
||||||
= sprite_icon('external-link')
|
|
||||||
%span.nav-item-name
|
|
||||||
= issue_tracker.title
|
|
||||||
%ul.sidebar-sub-level-items.is-fly-out-only
|
|
||||||
= nav_link(html_options: { class: "fly-out-top-item" } ) do
|
|
||||||
= link_to issue_tracker.issue_tracker_path, target: '_blank', rel: 'noopener noreferrer' do
|
|
||||||
%strong.fly-out-top-item-name
|
|
||||||
= issue_tracker.title
|
|
||||||
|
|
||||||
- if (project_nav_tab? :labels) && !@project.issues_enabled?
|
|
||||||
= nav_link(controller: [:labels]) do
|
|
||||||
= link_to project_labels_path(@project), title: _('Labels'), class: 'shortcuts-labels qa-labels-items' do
|
|
||||||
.nav-icon-container
|
|
||||||
= sprite_icon('label')
|
|
||||||
%span.nav-item-name#js-onboarding-labels-link
|
|
||||||
= _('Labels')
|
|
||||||
|
|
||||||
- if project_nav_tab? :merge_requests
|
|
||||||
= nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :milestones]) do
|
|
||||||
= link_to project_merge_requests_path(@project), class: 'shortcuts-merge_requests', data: { qa_selector: 'merge_requests_link' } do
|
|
||||||
.nav-icon-container
|
|
||||||
= sprite_icon('git-merge')
|
|
||||||
%span.nav-item-name#js-onboarding-mr-link
|
|
||||||
= _('Merge requests')
|
|
||||||
%span.badge.badge-pill.count.merge_counter.js-merge-counter
|
|
||||||
= number_with_delimiter(@project.open_merge_requests_count)
|
|
||||||
%ul.sidebar-sub-level-items.is-fly-out-only
|
|
||||||
= nav_link(controller: :merge_requests, html_options: { class: "fly-out-top-item" } ) do
|
|
||||||
= link_to project_merge_requests_path(@project) do
|
|
||||||
%strong.fly-out-top-item-name
|
|
||||||
= _('Merge requests')
|
|
||||||
%span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge
|
|
||||||
= number_with_delimiter(@project.open_merge_requests_count)
|
|
||||||
|
|
||||||
= render_if_exists "layouts/nav/requirements_link", project: @project
|
|
||||||
|
|
||||||
- if project_nav_tab? :pipelines
|
|
||||||
= nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts, :test_cases, :pipeline_editor], unless: -> { current_path?('projects/pipelines#charts') }) do
|
|
||||||
= link_to project_pipelines_path(@project), class: 'shortcuts-pipelines qa-link-pipelines rspec-link-pipelines', data: { qa_selector: 'ci_cd_link' } do
|
|
||||||
.nav-icon-container
|
|
||||||
= sprite_icon('rocket')
|
|
||||||
%span.nav-item-name#js-onboarding-pipelines-link
|
|
||||||
= _('CI/CD')
|
|
||||||
|
|
||||||
%ul.sidebar-sub-level-items
|
|
||||||
= nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts, :test_cases, :pipeline_editor], html_options: { class: "fly-out-top-item" }) do
|
|
||||||
= link_to project_pipelines_path(@project) do
|
|
||||||
%strong.fly-out-top-item-name
|
|
||||||
= _('CI/CD')
|
|
||||||
%li.divider.fly-out-top-item
|
|
||||||
- if project_nav_tab? :pipelines
|
|
||||||
= nav_link(path: ['pipelines#index', 'pipelines#show']) do
|
|
||||||
= link_to project_pipelines_path(@project), title: _('Pipelines'), class: 'shortcuts-pipelines' do
|
|
||||||
%span
|
|
||||||
= _('Pipelines')
|
|
||||||
|
|
||||||
- if can_view_pipeline_editor?(@project)
|
|
||||||
= nav_link(controller: :pipeline_editor, action: :show) do
|
|
||||||
= link_to project_ci_pipeline_editor_path(@project), title: s_('Pipelines|Editor') do
|
|
||||||
%span
|
|
||||||
= s_('Pipelines|Editor')
|
|
||||||
|
|
||||||
- if project_nav_tab? :builds
|
|
||||||
= nav_link(controller: :jobs) do
|
|
||||||
= link_to project_jobs_path(@project), title: _('Jobs'), class: 'shortcuts-builds' do
|
|
||||||
%span
|
|
||||||
= _('Jobs')
|
|
||||||
|
|
||||||
- if Feature.enabled?(:artifacts_management_page, @project)
|
|
||||||
= nav_link(controller: :artifacts, action: :index) do
|
|
||||||
= link_to project_artifacts_path(@project), title: _('Artifacts'), class: 'shortcuts-builds' do
|
|
||||||
%span
|
|
||||||
= _('Artifacts')
|
|
||||||
|
|
||||||
- if project_nav_tab?(:pipelines)
|
|
||||||
= nav_link(controller: :pipeline_schedules) do
|
|
||||||
= link_to pipeline_schedules_path(@project), title: _('Schedules'), class: 'shortcuts-builds' do
|
|
||||||
%span
|
|
||||||
= _('Schedules')
|
|
||||||
|
|
||||||
= render_if_exists "layouts/nav/test_cases_link", project: @project
|
|
||||||
|
|
||||||
- if project_nav_tab? :security_and_compliance
|
|
||||||
= render_if_exists 'layouts/nav/sidebar/project_security_link' # EE-specific
|
|
||||||
|
|
||||||
- if project_nav_tab? :operations
|
|
||||||
= nav_link(controller: sidebar_operations_paths) do
|
|
||||||
= link_to sidebar_operations_link_path, class: 'shortcuts-operations', data: { qa_selector: 'operations_link' } do
|
|
||||||
.nav-icon-container
|
|
||||||
= sprite_icon('cloud-gear')
|
|
||||||
%span.nav-item-name
|
|
||||||
= _('Operations')
|
|
||||||
|
|
||||||
%ul.sidebar-sub-level-items
|
|
||||||
= nav_link(controller: sidebar_operations_paths, html_options: { class: "fly-out-top-item" } ) do
|
|
||||||
= link_to sidebar_operations_link_path do
|
|
||||||
%strong.fly-out-top-item-name
|
|
||||||
= _('Operations')
|
|
||||||
%li.divider.fly-out-top-item
|
|
||||||
|
|
||||||
- if project_nav_tab? :metrics_dashboards
|
|
||||||
= nav_link(controller: :metrics_dashboard, action: [:show]) do
|
|
||||||
= link_to project_metrics_dashboard_path(@project), title: _('Metrics'), class: 'shortcuts-metrics', data: { qa_selector: 'operations_metrics_link' } do
|
|
||||||
%span
|
|
||||||
= _('Metrics')
|
|
||||||
|
|
||||||
- if project_nav_tab?(:environments) && can?(current_user, :read_pod_logs, @project)
|
|
||||||
= nav_link(controller: :logs, action: [:index]) do
|
|
||||||
= link_to project_logs_path(@project), title: _('Logs') do
|
|
||||||
%span
|
|
||||||
= _('Logs')
|
|
||||||
|
|
||||||
- if project_nav_tab? :environments
|
|
||||||
= render "layouts/nav/sidebar/tracing_link"
|
|
||||||
|
|
||||||
- if project_nav_tab?(:error_tracking)
|
|
||||||
= nav_link(controller: :error_tracking) do
|
|
||||||
= link_to project_error_tracking_index_path(@project), title: _('Error Tracking') do
|
|
||||||
%span
|
|
||||||
= _('Error Tracking')
|
|
||||||
|
|
||||||
- if project_nav_tab?(:alert_management)
|
|
||||||
= nav_link(controller: :alert_management) do
|
|
||||||
= link_to project_alert_management_index_path(@project), title: _('Alerts') do
|
|
||||||
%span
|
|
||||||
= _('Alerts')
|
|
||||||
|
|
||||||
- if project_nav_tab?(:incidents)
|
|
||||||
= nav_link(controller: :incidents) do
|
|
||||||
= link_to project_incidents_path(@project), title: _('Incidents'), data: { qa_selector: 'operations_incidents_link' } do
|
|
||||||
%span
|
|
||||||
= _('Incidents')
|
|
||||||
|
|
||||||
= render_if_exists 'projects/sidebar/oncall_schedules'
|
|
||||||
|
|
||||||
- if project_nav_tab? :serverless
|
|
||||||
= nav_link(controller: :functions) do
|
|
||||||
= link_to project_serverless_functions_path(@project), title: _('Serverless') do
|
|
||||||
%span
|
|
||||||
= _('Serverless')
|
|
||||||
|
|
||||||
- if project_nav_tab? :terraform
|
|
||||||
= nav_link(controller: :terraform) do
|
|
||||||
= link_to project_terraform_index_path(@project), title: _('Terraform') do
|
|
||||||
%span
|
|
||||||
= _('Terraform')
|
|
||||||
|
|
||||||
- if project_nav_tab? :clusters
|
|
||||||
- show_cluster_hint = show_gke_cluster_integration_callout?(@project)
|
|
||||||
= nav_link(controller: [:cluster_agents, :clusters]) do
|
|
||||||
= link_to project_clusters_path(@project), title: _('Kubernetes'), class: 'shortcuts-kubernetes' do
|
|
||||||
%span
|
|
||||||
= _('Kubernetes')
|
|
||||||
- if show_cluster_hint
|
|
||||||
.js-feature-highlight{ disabled: true,
|
|
||||||
data: { trigger: 'manual',
|
|
||||||
container: 'body',
|
|
||||||
placement: 'right',
|
|
||||||
highlight: UserCalloutsHelper::GKE_CLUSTER_INTEGRATION,
|
|
||||||
highlight_priority: UserCallout.feature_names[:GKE_CLUSTER_INTEGRATION],
|
|
||||||
dismiss_endpoint: user_callouts_path,
|
|
||||||
auto_devops_help_path: help_page_path('topics/autodevops/index.md') } }
|
|
||||||
- if project_nav_tab? :environments
|
|
||||||
= nav_link(controller: :environments, action: [:index, :folder, :show, :new, :edit, :create, :update, :stop, :terminal]) do
|
|
||||||
= link_to project_environments_path(@project), title: _('Environments'), class: 'shortcuts-environments qa-operations-environments-link' do
|
|
||||||
%span
|
|
||||||
= _('Environments')
|
|
||||||
|
|
||||||
- if project_nav_tab? :feature_flags
|
|
||||||
= nav_link(controller: :feature_flags) do
|
|
||||||
= link_to project_feature_flags_path(@project), title: _('Feature Flags'), class: 'shortcuts-feature-flags' do
|
|
||||||
%span
|
|
||||||
= _('Feature Flags')
|
|
||||||
|
|
||||||
- if project_nav_tab?(:product_analytics)
|
|
||||||
= nav_link(controller: :product_analytics) do
|
|
||||||
= link_to project_product_analytics_path(@project), title: _('Product Analytics') do
|
|
||||||
%span
|
|
||||||
= _('Product Analytics')
|
|
||||||
|
|
||||||
= render_if_exists 'layouts/nav/sidebar/project_packages_link'
|
|
||||||
|
|
||||||
- if project_nav_tab? :analytics
|
|
||||||
= render 'layouts/nav/sidebar/analytics_links', links: project_analytics_navbar_links(@project, current_user)
|
|
||||||
|
|
||||||
- if project_nav_tab?(:confluence)
|
|
||||||
- confluence_url = project_wikis_confluence_path(@project)
|
|
||||||
= nav_link do
|
|
||||||
= link_to confluence_url, class: 'shortcuts-confluence' do
|
|
||||||
.nav-icon-container
|
|
||||||
= image_tag 'confluence.svg', alt: _('Confluence')
|
|
||||||
%span.nav-item-name
|
|
||||||
= _('Confluence')
|
|
||||||
%ul.sidebar-sub-level-items.is-fly-out-only
|
|
||||||
= nav_link(html_options: { class: 'fly-out-top-item' } ) do
|
|
||||||
= link_to confluence_url, target: '_blank', rel: 'noopener noreferrer' do
|
|
||||||
%strong.fly-out-top-item-name
|
|
||||||
= _('Confluence')
|
|
||||||
|
|
||||||
- if project_nav_tab? :wiki
|
|
||||||
= render 'layouts/nav/sidebar/wiki_link', wiki_url: wiki_path(@project.wiki)
|
|
||||||
|
|
||||||
- if project_nav_tab?(:external_wiki)
|
|
||||||
- external_wiki_url = @project.external_wiki.external_wiki_url
|
|
||||||
= nav_link do
|
|
||||||
= link_to external_wiki_url, class: 'shortcuts-external_wiki' do
|
|
||||||
.nav-icon-container
|
|
||||||
= sprite_icon('external-link')
|
|
||||||
%span.nav-item-name
|
|
||||||
= s_('ExternalWikiService|External wiki')
|
|
||||||
%ul.sidebar-sub-level-items.is-fly-out-only
|
|
||||||
= nav_link(html_options: { class: "fly-out-top-item" } ) do
|
|
||||||
= link_to external_wiki_url do
|
|
||||||
%strong.fly-out-top-item-name
|
|
||||||
= s_('ExternalWikiService|External wiki')
|
|
||||||
|
|
||||||
- if project_nav_tab? :snippets
|
|
||||||
= nav_link(controller: :snippets) do
|
|
||||||
= link_to project_snippets_path(@project), class: 'shortcuts-snippets', data: { qa_selector: 'snippets_link' } do
|
|
||||||
.nav-icon-container
|
|
||||||
= sprite_icon('snippet')
|
|
||||||
%span.nav-item-name
|
|
||||||
= _('Snippets')
|
|
||||||
%ul.sidebar-sub-level-items.is-fly-out-only
|
|
||||||
= nav_link(controller: :snippets, html_options: { class: "fly-out-top-item" } ) do
|
|
||||||
= link_to project_snippets_path(@project) do
|
|
||||||
%strong.fly-out-top-item-name
|
|
||||||
= _('Snippets')
|
|
||||||
|
|
||||||
= nav_link(controller: :project_members) do
|
|
||||||
= link_to project_project_members_path(@project), title: _('Members'), class: 'qa-members-link', id: 'js-onboarding-members-link' do
|
|
||||||
.nav-icon-container
|
|
||||||
= sprite_icon('users')
|
|
||||||
%span.nav-item-name
|
|
||||||
= _('Members')
|
|
||||||
%ul.sidebar-sub-level-items.is-fly-out-only
|
|
||||||
= nav_link(path: %w[members#show], html_options: { class: "fly-out-top-item" } ) do
|
|
||||||
= link_to project_project_members_path(@project) do
|
|
||||||
%strong.fly-out-top-item-name
|
|
||||||
= _('Members')
|
|
||||||
|
|
||||||
- if project_nav_tab? :settings
|
|
||||||
= nav_link(path: sidebar_settings_paths) do
|
|
||||||
= link_to edit_project_path(@project) do
|
|
||||||
.nav-icon-container
|
|
||||||
= sprite_icon('settings')
|
|
||||||
%span.nav-item-name.qa-settings-item#js-onboarding-settings-link
|
|
||||||
= _('Settings')
|
|
||||||
|
|
||||||
%ul.sidebar-sub-level-items
|
|
||||||
- can_edit = can?(current_user, :admin_project, @project)
|
|
||||||
- if can_edit
|
|
||||||
= nav_link(path: sidebar_settings_paths, html_options: { class: "fly-out-top-item" } ) do
|
|
||||||
= link_to edit_project_path(@project) do
|
|
||||||
%strong.fly-out-top-item-name
|
|
||||||
= _('Settings')
|
|
||||||
%li.divider.fly-out-top-item
|
|
||||||
= nav_link(path: %w[projects#edit]) do
|
|
||||||
= link_to edit_project_path(@project), title: _('General'), class: 'qa-general-settings-link' do
|
|
||||||
%span
|
|
||||||
= _('General')
|
|
||||||
- if can_edit
|
|
||||||
= nav_link(controller: [:integrations, :services]) do
|
|
||||||
= link_to project_settings_integrations_path(@project), title: _('Integrations'), data: { qa_selector: 'integrations_settings_link' } do
|
|
||||||
%span
|
|
||||||
= _('Integrations')
|
|
||||||
= nav_link(controller: [:hooks, :hook_logs]) do
|
|
||||||
= link_to project_hooks_path(@project), title: _('Webhooks'), data: { qa_selector: 'webhooks_settings_link' } do
|
|
||||||
%span
|
|
||||||
= _('Webhooks')
|
|
||||||
- if can?(current_user, :read_resource_access_tokens, @project)
|
|
||||||
= nav_link(controller: [:access_tokens]) do
|
|
||||||
= link_to project_settings_access_tokens_path(@project), title: _('Access Tokens'), data: { qa_selector: 'access_tokens_settings_link' } do
|
|
||||||
%span
|
|
||||||
= _('Access Tokens')
|
|
||||||
= nav_link(controller: :repository) do
|
|
||||||
= link_to project_settings_repository_path(@project), title: _('Repository') do
|
|
||||||
%span
|
|
||||||
= _('Repository')
|
|
||||||
- if !@project.archived? && @project.feature_available?(:builds, current_user)
|
|
||||||
= nav_link(controller: :ci_cd) do
|
|
||||||
= link_to project_settings_ci_cd_path(@project), title: _('CI/CD') do
|
|
||||||
%span
|
|
||||||
= _('CI/CD')
|
|
||||||
- if settings_operations_available?
|
|
||||||
= nav_link(controller: [:operations]) do
|
|
||||||
= link_to project_settings_operations_path(@project), title: _('Operations'), data: { qa_selector: 'operations_settings_link' } do
|
|
||||||
= _('Operations')
|
|
||||||
- if @project.pages_available?
|
|
||||||
= nav_link(controller: :pages) do
|
|
||||||
= link_to project_pages_path(@project), title: _('Pages') do
|
|
||||||
%span
|
|
||||||
= _('Pages')
|
|
||||||
|
|
||||||
-# Shortcut to Project > Activity
|
|
||||||
%li.hidden
|
|
||||||
= link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity' do
|
|
||||||
%span
|
|
||||||
= _('Activity')
|
|
||||||
|
|
||||||
-# Shortcut to Repository > Graph (formerly, Network)
|
|
||||||
- if project_nav_tab? :network
|
|
||||||
%li.hidden
|
|
||||||
= link_to project_network_path(@project, current_ref), title: _('Network'), class: 'shortcuts-network' do
|
|
||||||
= _('Graph')
|
|
||||||
|
|
||||||
-# Shortcut to Issues > New Issue
|
|
||||||
- if project_nav_tab?(:issues)
|
|
||||||
%li.hidden
|
|
||||||
= link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do
|
|
||||||
= _('Create a new issue')
|
|
||||||
|
|
||||||
-# Shortcut to Pipelines > Jobs
|
|
||||||
- if project_nav_tab? :builds
|
|
||||||
%li.hidden
|
|
||||||
= link_to project_jobs_path(@project), title: _('Jobs'), class: 'shortcuts-builds' do
|
|
||||||
= _('Jobs')
|
|
||||||
|
|
||||||
-# Shortcut to commits page
|
|
||||||
- if project_nav_tab? :commits
|
|
||||||
%li.hidden
|
|
||||||
= link_to project_commits_path(@project), title: _('Commits'), class: 'shortcuts-commits' do
|
|
||||||
= _('Commits')
|
|
||||||
|
|
||||||
-# Shortcut to issue boards
|
|
||||||
- if project_nav_tab?(:issues)
|
|
||||||
%li.hidden
|
|
||||||
= link_to _('Issue Boards'), project_boards_path(@project), title: _('Issue Boards'), class: 'shortcuts-issue-boards'
|
|
||||||
|
|
||||||
= render 'shared/sidebar_toggle_button'
|
|
||||||
|
|
458
app/views/layouts/nav/sidebar/_project_menus.html.haml
Normal file
458
app/views/layouts/nav/sidebar/_project_menus.html.haml
Normal file
|
@ -0,0 +1,458 @@
|
||||||
|
= nav_link(path: sidebar_projects_paths, html_options: { class: 'home' }) do
|
||||||
|
= link_to project_path(@project), class: 'shortcuts-project rspec-project-link', data: { qa_selector: 'project_link' } do
|
||||||
|
.nav-icon-container
|
||||||
|
= sprite_icon('home')
|
||||||
|
%span.nav-item-name
|
||||||
|
= _('Project overview')
|
||||||
|
|
||||||
|
%ul.sidebar-sub-level-items
|
||||||
|
= nav_link(path: 'projects#show', html_options: { class: "fly-out-top-item" } ) do
|
||||||
|
= link_to project_path(@project) do
|
||||||
|
%strong.fly-out-top-item-name
|
||||||
|
= _('Project overview')
|
||||||
|
%li.divider.fly-out-top-item
|
||||||
|
= nav_link(path: 'projects#show') do
|
||||||
|
= link_to project_path(@project), title: _('Project details'), class: 'shortcuts-project' do
|
||||||
|
%span= _('Details')
|
||||||
|
|
||||||
|
= nav_link(path: 'projects#activity') do
|
||||||
|
= link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity', data: { qa_selector: 'activity_link' } do
|
||||||
|
%span= _('Activity')
|
||||||
|
|
||||||
|
- if project_nav_tab?(:releases)
|
||||||
|
= nav_link(controller: :releases) do
|
||||||
|
= link_to project_releases_path(@project), title: _('Releases'), class: 'shortcuts-project-releases' do
|
||||||
|
%span= _('Releases')
|
||||||
|
|
||||||
|
- if project_nav_tab? :learn_gitlab
|
||||||
|
= nav_link(controller: :learn_gitlab, html_options: { class: 'home' }) do
|
||||||
|
= link_to project_learn_gitlab_path(@project) do
|
||||||
|
.nav-icon-container
|
||||||
|
= sprite_icon('home')
|
||||||
|
%span.nav-item-name
|
||||||
|
= _('Learn GitLab')
|
||||||
|
|
||||||
|
- if project_nav_tab? :files
|
||||||
|
= nav_link(controller: sidebar_repository_paths, unless: -> { current_path?('projects/graphs#charts') }) do
|
||||||
|
= link_to project_tree_path(@project), class: 'shortcuts-tree', data: { qa_selector: "repository_link" } do
|
||||||
|
.nav-icon-container
|
||||||
|
= sprite_icon('doc-text')
|
||||||
|
%span.nav-item-name#js-onboarding-repo-link
|
||||||
|
= _('Repository')
|
||||||
|
|
||||||
|
%ul.sidebar-sub-level-items
|
||||||
|
= nav_link(controller: sidebar_repository_paths, html_options: { class: "fly-out-top-item" } ) do
|
||||||
|
= link_to project_tree_path(@project) do
|
||||||
|
%strong.fly-out-top-item-name
|
||||||
|
= _('Repository')
|
||||||
|
%li.divider.fly-out-top-item
|
||||||
|
= nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do
|
||||||
|
= link_to project_tree_path(@project) do
|
||||||
|
= _('Files')
|
||||||
|
|
||||||
|
= nav_link(controller: [:commit, :commits]) do
|
||||||
|
= link_to project_commits_path(@project, current_ref), id: 'js-onboarding-commits-link' do
|
||||||
|
= _('Commits')
|
||||||
|
|
||||||
|
= nav_link(html_options: {class: branches_tab_class}) do
|
||||||
|
= link_to project_branches_path(@project), data: { qa_selector: "branches_link" }, id: 'js-onboarding-branches-link' do
|
||||||
|
= _('Branches')
|
||||||
|
|
||||||
|
= nav_link(controller: [:tags]) do
|
||||||
|
= link_to project_tags_path(@project), data: { qa_selector: "tags_link" } do
|
||||||
|
= _('Tags')
|
||||||
|
|
||||||
|
= nav_link(path: 'graphs#show') do
|
||||||
|
= link_to project_graph_path(@project, current_ref) do
|
||||||
|
= _('Contributors')
|
||||||
|
|
||||||
|
= nav_link(controller: %w(network)) do
|
||||||
|
= link_to project_network_path(@project, current_ref) do
|
||||||
|
= _('Graph')
|
||||||
|
|
||||||
|
= nav_link(controller: :compare) do
|
||||||
|
= link_to project_compare_index_path(@project, from: @repository.root_ref, to: current_ref) do
|
||||||
|
= _('Compare')
|
||||||
|
|
||||||
|
= render_if_exists 'projects/sidebar/repository_locked_files'
|
||||||
|
|
||||||
|
- if project_nav_tab? :issues
|
||||||
|
= nav_link(controller: @project.issues_enabled? ? ['projects/issues', :labels, :milestones, :boards, :iterations] : 'projects/issues') do
|
||||||
|
= link_to project_issues_path(@project), class: 'shortcuts-issues qa-issues-item' do
|
||||||
|
.nav-icon-container
|
||||||
|
= sprite_icon('issues')
|
||||||
|
%span.nav-item-name#js-onboarding-issues-link
|
||||||
|
= _('Issues')
|
||||||
|
- if @project.issues_enabled?
|
||||||
|
%span.badge.badge-pill.count.issue_counter
|
||||||
|
= number_with_delimiter(@project.open_issues_count(current_user))
|
||||||
|
|
||||||
|
%ul.sidebar-sub-level-items
|
||||||
|
= nav_link(controller: 'projects/issues', action: :index, html_options: { class: "fly-out-top-item" } ) do
|
||||||
|
= link_to project_issues_path(@project) do
|
||||||
|
%strong.fly-out-top-item-name
|
||||||
|
= _('Issues')
|
||||||
|
- if @project.issues_enabled?
|
||||||
|
%span.badge.badge-pill.count.issue_counter.fly-out-badge
|
||||||
|
= number_with_delimiter(@project.open_issues_count(current_user))
|
||||||
|
%li.divider.fly-out-top-item
|
||||||
|
= nav_link(controller: :issues, action: :index) do
|
||||||
|
= link_to project_issues_path(@project), title: _('Issues') do
|
||||||
|
%span
|
||||||
|
= _('List')
|
||||||
|
|
||||||
|
= nav_link(controller: :boards) do
|
||||||
|
= link_to project_boards_path(@project), title: boards_link_text, data: { qa_selector: "issue_boards_link" } do
|
||||||
|
%span
|
||||||
|
= boards_link_text
|
||||||
|
|
||||||
|
= nav_link(controller: :labels) do
|
||||||
|
= link_to project_labels_path(@project), title: _('Labels'), class: 'qa-labels-link' do
|
||||||
|
%span
|
||||||
|
= _('Labels')
|
||||||
|
|
||||||
|
= render 'projects/sidebar/issues_service_desk'
|
||||||
|
|
||||||
|
= nav_link(controller: :milestones) do
|
||||||
|
= link_to project_milestones_path(@project), title: _('Milestones'), class: 'qa-milestones-link' do
|
||||||
|
%span
|
||||||
|
= _('Milestones')
|
||||||
|
|
||||||
|
= render_if_exists 'layouts/nav/sidebar/project_iterations_link'
|
||||||
|
|
||||||
|
- if project_nav_tab?(:external_issue_tracker)
|
||||||
|
- issue_tracker = @project.external_issue_tracker
|
||||||
|
- if issue_tracker.is_a?(JiraService) && project_jira_issues_integration?
|
||||||
|
= render_if_exists 'layouts/nav/sidebar/project_jira_issues_link', issue_tracker: issue_tracker
|
||||||
|
- else
|
||||||
|
= nav_link do
|
||||||
|
= link_to issue_tracker.issue_tracker_path, target: '_blank', rel: 'noopener noreferrer', class: 'shortcuts-external_tracker' do
|
||||||
|
.nav-icon-container
|
||||||
|
= sprite_icon('external-link')
|
||||||
|
%span.nav-item-name
|
||||||
|
= issue_tracker.title
|
||||||
|
%ul.sidebar-sub-level-items.is-fly-out-only
|
||||||
|
= nav_link(html_options: { class: "fly-out-top-item" } ) do
|
||||||
|
= link_to issue_tracker.issue_tracker_path, target: '_blank', rel: 'noopener noreferrer' do
|
||||||
|
%strong.fly-out-top-item-name
|
||||||
|
= issue_tracker.title
|
||||||
|
|
||||||
|
- if (project_nav_tab? :labels) && !@project.issues_enabled?
|
||||||
|
= nav_link(controller: [:labels]) do
|
||||||
|
= link_to project_labels_path(@project), title: _('Labels'), class: 'shortcuts-labels qa-labels-items' do
|
||||||
|
.nav-icon-container
|
||||||
|
= sprite_icon('label')
|
||||||
|
%span.nav-item-name#js-onboarding-labels-link
|
||||||
|
= _('Labels')
|
||||||
|
|
||||||
|
- if project_nav_tab? :merge_requests
|
||||||
|
= nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :milestones]) do
|
||||||
|
= link_to project_merge_requests_path(@project), class: 'shortcuts-merge_requests', data: { qa_selector: 'merge_requests_link' } do
|
||||||
|
.nav-icon-container
|
||||||
|
= sprite_icon('git-merge')
|
||||||
|
%span.nav-item-name#js-onboarding-mr-link
|
||||||
|
= _('Merge requests')
|
||||||
|
%span.badge.badge-pill.count.merge_counter.js-merge-counter
|
||||||
|
= number_with_delimiter(@project.open_merge_requests_count)
|
||||||
|
%ul.sidebar-sub-level-items.is-fly-out-only
|
||||||
|
= nav_link(controller: :merge_requests, html_options: { class: "fly-out-top-item" } ) do
|
||||||
|
= link_to project_merge_requests_path(@project) do
|
||||||
|
%strong.fly-out-top-item-name
|
||||||
|
= _('Merge requests')
|
||||||
|
%span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge
|
||||||
|
= number_with_delimiter(@project.open_merge_requests_count)
|
||||||
|
|
||||||
|
= render_if_exists "layouts/nav/requirements_link", project: @project
|
||||||
|
|
||||||
|
- if project_nav_tab? :pipelines
|
||||||
|
= nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts, :test_cases, :pipeline_editor], unless: -> { current_path?('projects/pipelines#charts') }) do
|
||||||
|
= link_to project_pipelines_path(@project), class: 'shortcuts-pipelines qa-link-pipelines rspec-link-pipelines', data: { qa_selector: 'ci_cd_link' } do
|
||||||
|
.nav-icon-container
|
||||||
|
= sprite_icon('rocket')
|
||||||
|
%span.nav-item-name#js-onboarding-pipelines-link
|
||||||
|
= _('CI/CD')
|
||||||
|
|
||||||
|
%ul.sidebar-sub-level-items
|
||||||
|
= nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts, :test_cases, :pipeline_editor], html_options: { class: "fly-out-top-item" }) do
|
||||||
|
= link_to project_pipelines_path(@project) do
|
||||||
|
%strong.fly-out-top-item-name
|
||||||
|
= _('CI/CD')
|
||||||
|
%li.divider.fly-out-top-item
|
||||||
|
- if project_nav_tab? :pipelines
|
||||||
|
= nav_link(path: ['pipelines#index', 'pipelines#show']) do
|
||||||
|
= link_to project_pipelines_path(@project), title: _('Pipelines'), class: 'shortcuts-pipelines' do
|
||||||
|
%span
|
||||||
|
= _('Pipelines')
|
||||||
|
|
||||||
|
- if can_view_pipeline_editor?(@project)
|
||||||
|
= nav_link(controller: :pipeline_editor, action: :show) do
|
||||||
|
= link_to project_ci_pipeline_editor_path(@project), title: s_('Pipelines|Editor') do
|
||||||
|
%span
|
||||||
|
= s_('Pipelines|Editor')
|
||||||
|
|
||||||
|
- if project_nav_tab? :builds
|
||||||
|
= nav_link(controller: :jobs) do
|
||||||
|
= link_to project_jobs_path(@project), title: _('Jobs'), class: 'shortcuts-builds' do
|
||||||
|
%span
|
||||||
|
= _('Jobs')
|
||||||
|
|
||||||
|
- if Feature.enabled?(:artifacts_management_page, @project)
|
||||||
|
= nav_link(controller: :artifacts, action: :index) do
|
||||||
|
= link_to project_artifacts_path(@project), title: _('Artifacts'), class: 'shortcuts-builds' do
|
||||||
|
%span
|
||||||
|
= _('Artifacts')
|
||||||
|
|
||||||
|
- if project_nav_tab?(:pipelines)
|
||||||
|
= nav_link(controller: :pipeline_schedules) do
|
||||||
|
= link_to pipeline_schedules_path(@project), title: _('Schedules'), class: 'shortcuts-builds' do
|
||||||
|
%span
|
||||||
|
= _('Schedules')
|
||||||
|
|
||||||
|
= render_if_exists "layouts/nav/test_cases_link", project: @project
|
||||||
|
|
||||||
|
- if project_nav_tab? :security_and_compliance
|
||||||
|
= render_if_exists 'layouts/nav/sidebar/project_security_link' # EE-specific
|
||||||
|
|
||||||
|
- if project_nav_tab? :operations
|
||||||
|
= nav_link(controller: sidebar_operations_paths) do
|
||||||
|
= link_to sidebar_operations_link_path, class: 'shortcuts-operations', data: { qa_selector: 'operations_link' } do
|
||||||
|
.nav-icon-container
|
||||||
|
= sprite_icon('cloud-gear')
|
||||||
|
%span.nav-item-name
|
||||||
|
= _('Operations')
|
||||||
|
|
||||||
|
%ul.sidebar-sub-level-items
|
||||||
|
= nav_link(controller: sidebar_operations_paths, html_options: { class: "fly-out-top-item" } ) do
|
||||||
|
= link_to sidebar_operations_link_path do
|
||||||
|
%strong.fly-out-top-item-name
|
||||||
|
= _('Operations')
|
||||||
|
%li.divider.fly-out-top-item
|
||||||
|
|
||||||
|
- if project_nav_tab? :metrics_dashboards
|
||||||
|
= nav_link(controller: :metrics_dashboard, action: [:show]) do
|
||||||
|
= link_to project_metrics_dashboard_path(@project), title: _('Metrics'), class: 'shortcuts-metrics', data: { qa_selector: 'operations_metrics_link' } do
|
||||||
|
%span
|
||||||
|
= _('Metrics')
|
||||||
|
|
||||||
|
- if project_nav_tab?(:environments) && can?(current_user, :read_pod_logs, @project)
|
||||||
|
= nav_link(controller: :logs, action: [:index]) do
|
||||||
|
= link_to project_logs_path(@project), title: _('Logs') do
|
||||||
|
%span
|
||||||
|
= _('Logs')
|
||||||
|
|
||||||
|
- if project_nav_tab? :environments
|
||||||
|
= render "layouts/nav/sidebar/tracing_link"
|
||||||
|
|
||||||
|
- if project_nav_tab?(:error_tracking)
|
||||||
|
= nav_link(controller: :error_tracking) do
|
||||||
|
= link_to project_error_tracking_index_path(@project), title: _('Error Tracking') do
|
||||||
|
%span
|
||||||
|
= _('Error Tracking')
|
||||||
|
|
||||||
|
- if project_nav_tab?(:alert_management)
|
||||||
|
= nav_link(controller: :alert_management) do
|
||||||
|
= link_to project_alert_management_index_path(@project), title: _('Alerts') do
|
||||||
|
%span
|
||||||
|
= _('Alerts')
|
||||||
|
|
||||||
|
- if project_nav_tab?(:incidents)
|
||||||
|
= nav_link(controller: :incidents) do
|
||||||
|
= link_to project_incidents_path(@project), title: _('Incidents'), data: { qa_selector: 'operations_incidents_link' } do
|
||||||
|
%span
|
||||||
|
= _('Incidents')
|
||||||
|
|
||||||
|
= render_if_exists 'projects/sidebar/oncall_schedules'
|
||||||
|
|
||||||
|
- if project_nav_tab? :serverless
|
||||||
|
= nav_link(controller: :functions) do
|
||||||
|
= link_to project_serverless_functions_path(@project), title: _('Serverless') do
|
||||||
|
%span
|
||||||
|
= _('Serverless')
|
||||||
|
|
||||||
|
- if project_nav_tab? :terraform
|
||||||
|
= nav_link(controller: :terraform) do
|
||||||
|
= link_to project_terraform_index_path(@project), title: _('Terraform') do
|
||||||
|
%span
|
||||||
|
= _('Terraform')
|
||||||
|
|
||||||
|
- if project_nav_tab? :clusters
|
||||||
|
- show_cluster_hint = show_gke_cluster_integration_callout?(@project)
|
||||||
|
= nav_link(controller: [:cluster_agents, :clusters]) do
|
||||||
|
= link_to project_clusters_path(@project), title: _('Kubernetes'), class: 'shortcuts-kubernetes' do
|
||||||
|
%span
|
||||||
|
= _('Kubernetes')
|
||||||
|
- if show_cluster_hint
|
||||||
|
.js-feature-highlight{ disabled: true,
|
||||||
|
data: { trigger: 'manual',
|
||||||
|
container: 'body',
|
||||||
|
placement: 'right',
|
||||||
|
highlight: UserCalloutsHelper::GKE_CLUSTER_INTEGRATION,
|
||||||
|
highlight_priority: UserCallout.feature_names[:GKE_CLUSTER_INTEGRATION],
|
||||||
|
dismiss_endpoint: user_callouts_path,
|
||||||
|
auto_devops_help_path: help_page_path('topics/autodevops/index.md') } }
|
||||||
|
- if project_nav_tab? :environments
|
||||||
|
= nav_link(controller: :environments, action: [:index, :folder, :show, :new, :edit, :create, :update, :stop, :terminal]) do
|
||||||
|
= link_to project_environments_path(@project), title: _('Environments'), class: 'shortcuts-environments qa-operations-environments-link' do
|
||||||
|
%span
|
||||||
|
= _('Environments')
|
||||||
|
|
||||||
|
- if project_nav_tab? :feature_flags
|
||||||
|
= nav_link(controller: :feature_flags) do
|
||||||
|
= link_to project_feature_flags_path(@project), title: _('Feature Flags'), class: 'shortcuts-feature-flags' do
|
||||||
|
%span
|
||||||
|
= _('Feature Flags')
|
||||||
|
|
||||||
|
- if project_nav_tab?(:product_analytics)
|
||||||
|
= nav_link(controller: :product_analytics) do
|
||||||
|
= link_to project_product_analytics_path(@project), title: _('Product Analytics') do
|
||||||
|
%span
|
||||||
|
= _('Product Analytics')
|
||||||
|
|
||||||
|
= render_if_exists 'layouts/nav/sidebar/project_packages_link'
|
||||||
|
|
||||||
|
- if project_nav_tab? :analytics
|
||||||
|
= render 'layouts/nav/sidebar/analytics_links', links: project_analytics_navbar_links(@project, current_user)
|
||||||
|
|
||||||
|
- if project_nav_tab?(:confluence)
|
||||||
|
- confluence_url = project_wikis_confluence_path(@project)
|
||||||
|
= nav_link do
|
||||||
|
= link_to confluence_url, class: 'shortcuts-confluence' do
|
||||||
|
.nav-icon-container
|
||||||
|
= image_tag 'confluence.svg', alt: _('Confluence')
|
||||||
|
%span.nav-item-name
|
||||||
|
= _('Confluence')
|
||||||
|
%ul.sidebar-sub-level-items.is-fly-out-only
|
||||||
|
= nav_link(html_options: { class: 'fly-out-top-item' } ) do
|
||||||
|
= link_to confluence_url, target: '_blank', rel: 'noopener noreferrer' do
|
||||||
|
%strong.fly-out-top-item-name
|
||||||
|
= _('Confluence')
|
||||||
|
|
||||||
|
- if project_nav_tab? :wiki
|
||||||
|
= render 'layouts/nav/sidebar/wiki_link', wiki_url: wiki_path(@project.wiki)
|
||||||
|
|
||||||
|
- if project_nav_tab?(:external_wiki)
|
||||||
|
- external_wiki_url = @project.external_wiki.external_wiki_url
|
||||||
|
= nav_link do
|
||||||
|
= link_to external_wiki_url, class: 'shortcuts-external_wiki' do
|
||||||
|
.nav-icon-container
|
||||||
|
= sprite_icon('external-link')
|
||||||
|
%span.nav-item-name
|
||||||
|
= s_('ExternalWikiService|External wiki')
|
||||||
|
%ul.sidebar-sub-level-items.is-fly-out-only
|
||||||
|
= nav_link(html_options: { class: "fly-out-top-item" } ) do
|
||||||
|
= link_to external_wiki_url do
|
||||||
|
%strong.fly-out-top-item-name
|
||||||
|
= s_('ExternalWikiService|External wiki')
|
||||||
|
|
||||||
|
- if project_nav_tab? :snippets
|
||||||
|
= nav_link(controller: :snippets) do
|
||||||
|
= link_to project_snippets_path(@project), class: 'shortcuts-snippets', data: { qa_selector: 'snippets_link' } do
|
||||||
|
.nav-icon-container
|
||||||
|
= sprite_icon('snippet')
|
||||||
|
%span.nav-item-name
|
||||||
|
= _('Snippets')
|
||||||
|
%ul.sidebar-sub-level-items.is-fly-out-only
|
||||||
|
= nav_link(controller: :snippets, html_options: { class: "fly-out-top-item" } ) do
|
||||||
|
= link_to project_snippets_path(@project) do
|
||||||
|
%strong.fly-out-top-item-name
|
||||||
|
= _('Snippets')
|
||||||
|
|
||||||
|
= nav_link(controller: :project_members) do
|
||||||
|
= link_to project_project_members_path(@project), title: _('Members'), class: 'qa-members-link', id: 'js-onboarding-members-link' do
|
||||||
|
.nav-icon-container
|
||||||
|
= sprite_icon('users')
|
||||||
|
%span.nav-item-name
|
||||||
|
= _('Members')
|
||||||
|
%ul.sidebar-sub-level-items.is-fly-out-only
|
||||||
|
= nav_link(path: %w[members#show], html_options: { class: "fly-out-top-item" } ) do
|
||||||
|
= link_to project_project_members_path(@project) do
|
||||||
|
%strong.fly-out-top-item-name
|
||||||
|
= _('Members')
|
||||||
|
|
||||||
|
- if project_nav_tab? :settings
|
||||||
|
= nav_link(path: sidebar_settings_paths) do
|
||||||
|
= link_to edit_project_path(@project) do
|
||||||
|
.nav-icon-container
|
||||||
|
= sprite_icon('settings')
|
||||||
|
%span.nav-item-name.qa-settings-item#js-onboarding-settings-link
|
||||||
|
= _('Settings')
|
||||||
|
|
||||||
|
%ul.sidebar-sub-level-items
|
||||||
|
- can_edit = can?(current_user, :admin_project, @project)
|
||||||
|
- if can_edit
|
||||||
|
= nav_link(path: sidebar_settings_paths, html_options: { class: "fly-out-top-item" } ) do
|
||||||
|
= link_to edit_project_path(@project) do
|
||||||
|
%strong.fly-out-top-item-name
|
||||||
|
= _('Settings')
|
||||||
|
%li.divider.fly-out-top-item
|
||||||
|
= nav_link(path: %w[projects#edit]) do
|
||||||
|
= link_to edit_project_path(@project), title: _('General'), class: 'qa-general-settings-link' do
|
||||||
|
%span
|
||||||
|
= _('General')
|
||||||
|
- if can_edit
|
||||||
|
= nav_link(controller: [:integrations, :services]) do
|
||||||
|
= link_to project_settings_integrations_path(@project), title: _('Integrations'), data: { qa_selector: 'integrations_settings_link' } do
|
||||||
|
%span
|
||||||
|
= _('Integrations')
|
||||||
|
= nav_link(controller: [:hooks, :hook_logs]) do
|
||||||
|
= link_to project_hooks_path(@project), title: _('Webhooks'), data: { qa_selector: 'webhooks_settings_link' } do
|
||||||
|
%span
|
||||||
|
= _('Webhooks')
|
||||||
|
- if can?(current_user, :read_resource_access_tokens, @project)
|
||||||
|
= nav_link(controller: [:access_tokens]) do
|
||||||
|
= link_to project_settings_access_tokens_path(@project), title: _('Access Tokens'), data: { qa_selector: 'access_tokens_settings_link' } do
|
||||||
|
%span
|
||||||
|
= _('Access Tokens')
|
||||||
|
= nav_link(controller: :repository) do
|
||||||
|
= link_to project_settings_repository_path(@project), title: _('Repository') do
|
||||||
|
%span
|
||||||
|
= _('Repository')
|
||||||
|
- if !@project.archived? && @project.feature_available?(:builds, current_user)
|
||||||
|
= nav_link(controller: :ci_cd) do
|
||||||
|
= link_to project_settings_ci_cd_path(@project), title: _('CI/CD') do
|
||||||
|
%span
|
||||||
|
= _('CI/CD')
|
||||||
|
- if settings_operations_available?
|
||||||
|
= nav_link(controller: [:operations]) do
|
||||||
|
= link_to project_settings_operations_path(@project), title: _('Operations'), data: { qa_selector: 'operations_settings_link' } do
|
||||||
|
= _('Operations')
|
||||||
|
- if @project.pages_available?
|
||||||
|
= nav_link(controller: :pages) do
|
||||||
|
= link_to project_pages_path(@project), title: _('Pages') do
|
||||||
|
%span
|
||||||
|
= _('Pages')
|
||||||
|
|
||||||
|
-# Shortcut to Project > Activity
|
||||||
|
%li.hidden
|
||||||
|
= link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity' do
|
||||||
|
%span
|
||||||
|
= _('Activity')
|
||||||
|
|
||||||
|
-# Shortcut to Repository > Graph (formerly, Network)
|
||||||
|
- if project_nav_tab? :network
|
||||||
|
%li.hidden
|
||||||
|
= link_to project_network_path(@project, current_ref), title: _('Network'), class: 'shortcuts-network' do
|
||||||
|
= _('Graph')
|
||||||
|
|
||||||
|
-# Shortcut to Issues > New Issue
|
||||||
|
- if project_nav_tab?(:issues)
|
||||||
|
%li.hidden
|
||||||
|
= link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do
|
||||||
|
= _('Create a new issue')
|
||||||
|
|
||||||
|
-# Shortcut to Pipelines > Jobs
|
||||||
|
- if project_nav_tab? :builds
|
||||||
|
%li.hidden
|
||||||
|
= link_to project_jobs_path(@project), title: _('Jobs'), class: 'shortcuts-builds' do
|
||||||
|
= _('Jobs')
|
||||||
|
|
||||||
|
-# Shortcut to commits page
|
||||||
|
- if project_nav_tab? :commits
|
||||||
|
%li.hidden
|
||||||
|
= link_to project_commits_path(@project), title: _('Commits'), class: 'shortcuts-commits' do
|
||||||
|
= _('Commits')
|
||||||
|
|
||||||
|
-# Shortcut to issue boards
|
||||||
|
- if project_nav_tab?(:issues)
|
||||||
|
%li.hidden
|
||||||
|
= link_to _('Issue Boards'), project_boards_path(@project), title: _('Issue Boards'), class: 'shortcuts-issue-boards'
|
|
@ -0,0 +1,6 @@
|
||||||
|
.context-header
|
||||||
|
= link_to project_path(@project), title: @project.name do
|
||||||
|
.avatar-container.rect-avatar.s40.project-avatar
|
||||||
|
= project_icon(@project, alt: @project.name, class: 'avatar s40 avatar-tile', width: 40, height: 40)
|
||||||
|
.sidebar-context-title
|
||||||
|
= @project.name
|
|
@ -15,11 +15,12 @@
|
||||||
.col.form-group
|
.col.form-group
|
||||||
= f.label :expires_at, s_('Profiles|Expires at'), class: 'label-bold'
|
= f.label :expires_at, s_('Profiles|Expires at'), class: 'label-bold'
|
||||||
= f.date_field :expires_at, class: "form-control input-lg", min: Date.tomorrow, data: { qa_selector: 'key_expiry_date_field' }
|
= f.date_field :expires_at, class: "form-control input-lg", min: Date.tomorrow, data: { qa_selector: 'key_expiry_date_field' }
|
||||||
|
%p.form-text.text-muted{ data: { qa_selector: 'key_expiry_date_field_description' } }= ssh_key_expires_field_description
|
||||||
|
|
||||||
.js-add-ssh-key-validation-warning.hide
|
.js-add-ssh-key-validation-warning.hide
|
||||||
.bs-callout.bs-callout-warning{ role: 'alert', aria_live: 'assertive' }
|
.bs-callout.bs-callout-warning{ role: 'alert', aria_live: 'assertive' }
|
||||||
%strong= _('Oops, are you sure?')
|
%strong= _('Oops, are you sure?')
|
||||||
%p= s_("Profiles|This doesn't look like a public SSH key, are you sure you want to add it? It will be publicly visible.")
|
%p= s_("Profiles|Publicly visible private SSH keys can compromise your system.")
|
||||||
|
|
||||||
%button.btn.gl-button.btn-confirm.js-add-ssh-key-validation-confirm-submit= _("Yes, add it")
|
%button.btn.gl-button.btn-confirm.js-add-ssh-key-validation-confirm-submit= _("Yes, add it")
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
- icon_classes = 'settings-list-icon gl-display-none gl-sm-display-block'
|
||||||
|
|
||||||
%li.key-list-item
|
%li.key-list-item
|
||||||
.gl-display-flex.gl-align-items-flex-start
|
.gl-display-flex.gl-align-items-flex-start
|
||||||
.key-list-item-info.gl-w-full.float-none
|
.key-list-item-info.gl-w-full.float-none
|
||||||
|
@ -5,15 +7,11 @@
|
||||||
= key.title
|
= key.title
|
||||||
|
|
||||||
.gl-display-flex.gl-align-items-center.gl-mt-2
|
.gl-display-flex.gl-align-items-center.gl-mt-2
|
||||||
- if key.valid?
|
- if key.valid? && !key.expired?
|
||||||
- if key.expired?
|
= sprite_icon('key', css_class: icon_classes)
|
||||||
%span.gl-display-inline-block.has-tooltip{ title: s_('Profiles|Your key has expired') }
|
|
||||||
= sprite_icon('warning-solid', css_class: 'settings-list-icon gl-display-none gl-sm-display-block')
|
|
||||||
- else
|
|
||||||
= sprite_icon('key', css_class: 'settings-list-icon gl-display-none gl-sm-display-block')
|
|
||||||
- else
|
- else
|
||||||
%span.gl-display-inline-block.has-tooltip{ title: key.errors.full_messages.join(', ') }
|
%span.gl-display-inline-block.has-tooltip{ title: ssh_key_expiration_tooltip(key) }
|
||||||
= sprite_icon('warning-solid', css_class: 'settings-list-icon gl-display-none gl-sm-display-block')
|
= sprite_icon('warning-solid', css_class: icon_classes)
|
||||||
|
|
||||||
%span.gl-text-truncate.gl-sm-ml-3
|
%span.gl-text-truncate.gl-sm-ml-3
|
||||||
= key.fingerprint
|
= key.fingerprint
|
||||||
|
|
|
@ -49,7 +49,7 @@
|
||||||
|
|
||||||
= render_if_exists 'projects/commits/mirror_status'
|
= render_if_exists 'projects/commits/mirror_status'
|
||||||
|
|
||||||
.js-branch-list{ data: { diverging_counts_endpoint: diverging_commit_counts_namespace_project_branches_path(@project.namespace, @project, format: :json) } }
|
.js-branch-list{ data: { diverging_counts_endpoint: diverging_commit_counts_namespace_project_branches_path(@project.namespace, @project, format: :json), default_branch: @project.default_branch } }
|
||||||
- if can?(current_user, :admin_project, @project)
|
- if can?(current_user, :admin_project, @project)
|
||||||
- project_settings_link = link_to s_('Branches|project settings'), project_protected_branches_path(@project)
|
- project_settings_link = link_to s_('Branches|project settings'), project_protected_branches_path(@project)
|
||||||
.row-content-block
|
.row-content-block
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
.modal{ id: "modal-delete-label-#{label.id}", tabindex: -1 }
|
|
||||||
.modal-dialog
|
|
||||||
.modal-content
|
|
||||||
.modal-header
|
|
||||||
%h3.page-title= _('Delete label: %{label_name} ?') % { label_name: label.name }
|
|
||||||
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
|
|
||||||
%span{ "aria-hidden": true } ×
|
|
||||||
|
|
||||||
.modal-body
|
|
||||||
%p
|
|
||||||
= html_escape(_('%{label_name} %{span_open}will be permanently deleted from %{subject_name}. This cannot be undone.%{span_close}')) % { label_name: tag.strong(label.name), subject_name: label.subject_name, span_open: '<span>'.html_safe, span_close: '</span>'.html_safe }
|
|
||||||
|
|
||||||
.modal-footer
|
|
||||||
%a{ href: '#', data: { dismiss: 'modal' }, class: 'gl-button btn btn-default' }= _('Cancel')
|
|
||||||
|
|
||||||
= link_to _('Delete label'),
|
|
||||||
label.destroy_path,
|
|
||||||
title: _('Delete'),
|
|
||||||
method: :delete,
|
|
||||||
class: 'gl-button btn btn-danger'
|
|
|
@ -36,10 +36,10 @@
|
||||||
label_text_color: label.text_color,
|
label_text_color: label.text_color,
|
||||||
group_name: label.project.group.name } }
|
group_name: label.project.group.name } }
|
||||||
= _('Promote to group label')
|
= _('Promote to group label')
|
||||||
- if can?(current_user, :admin_label, label)
|
%li
|
||||||
%li
|
%span
|
||||||
%span{ data: { toggle: 'modal', target: "#modal-delete-label-#{label.id}" } }
|
%button.text-danger.remove-row.js-delete-label-modal-button{ type: 'button', data: { label_name: label.name, subject_name: label.subject_name, destroy_path: label.destroy_path } }
|
||||||
%button.text-danger.remove-row{ type: 'button' }= _('Delete')
|
= _('Delete')
|
||||||
- if current_user
|
- if current_user
|
||||||
%li.inline.label-subscription
|
%li.inline.label-subscription
|
||||||
- if label.can_subscribe_to_label_in_different_levels?
|
- if label.can_subscribe_to_label_in_different_levels?
|
||||||
|
@ -61,5 +61,3 @@
|
||||||
- else
|
- else
|
||||||
%button.gl-button.js-subscribe-button.label-subscribe-button.btn.btn-default.gl-ml-3{ data: { status: status, url: toggle_subscription_path, toggle: 'tooltip' }, title: tooltip_title }
|
%button.gl-button.js-subscribe-button.label-subscribe-button.btn.btn-default.gl-ml-3{ data: { status: status, url: toggle_subscription_path, toggle: 'tooltip' }, title: tooltip_title }
|
||||||
%span= label_subscription_toggle_button_text(label, @project)
|
%span= label_subscription_toggle_button_text(label, @project)
|
||||||
|
|
||||||
= render 'shared/delete_label_modal', label: label
|
|
||||||
|
|
10
app/views/shared/nav/_sidebar.html.haml
Normal file
10
app/views/shared/nav/_sidebar.html.haml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
%aside.nav-sidebar{ class: ('sidebar-collapsed-desktop' if collapsed_sidebar?), **sidebar_tracking_attributes_by_object(sidebar.container), 'aria-label': sidebar.aria_label }
|
||||||
|
.nav-sidebar-inner-scroll
|
||||||
|
- if sidebar.render_raw_scope_menu_partial
|
||||||
|
= render sidebar.render_raw_scope_menu_partial
|
||||||
|
|
||||||
|
%ul.sidebar-top-level-items.qa-project-sidebar
|
||||||
|
- if sidebar.render_raw_menus_partial
|
||||||
|
= render sidebar.render_raw_menus_partial
|
||||||
|
|
||||||
|
= render 'shared/sidebar_toggle_button'
|
|
@ -24,7 +24,13 @@ module EachShardWorker
|
||||||
end
|
end
|
||||||
|
|
||||||
def healthy_ready_shards
|
def healthy_ready_shards
|
||||||
ready_shards.select(&:success)
|
success_checks, failed_checks = ready_shards.partition(&:success)
|
||||||
|
|
||||||
|
if failed_checks.any?
|
||||||
|
::Gitlab::AppLogger.error(message: 'Excluding unhealthy shards', failed_checks: failed_checks.map(&:payload), class: self.class.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
success_checks
|
||||||
end
|
end
|
||||||
|
|
||||||
def ready_shards
|
def ready_shards
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Make blobs directly accessible through the graphql repository
|
||||||
|
merge_request: 58677
|
||||||
|
author:
|
||||||
|
type: added
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Update default branch in divergence graph
|
||||||
|
merge_request: 58871
|
||||||
|
author:
|
||||||
|
type: changed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add search functionality to Jira Connect App namespaces
|
||||||
|
merge_request: 57669
|
||||||
|
author:
|
||||||
|
type: added
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Fix N+1 queries to find or initialize services
|
||||||
|
merge_request: 58879
|
||||||
|
author:
|
||||||
|
type: performance
|
|
@ -3,5 +3,3 @@ title: Update GIicon size in geo_node_header.vue
|
||||||
merge_request: 57952
|
merge_request: 57952
|
||||||
author: singhanshuman
|
author: singhanshuman
|
||||||
type: changed
|
type: changed
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Fix loading pipelines by commit SHA for GraphQL
|
||||||
|
merge_request: 59110
|
||||||
|
author:
|
||||||
|
type: fixed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Ensure a project iid is set before transitioning on pipeline error
|
||||||
|
merge_request: 57783
|
||||||
|
author:
|
||||||
|
type: performance
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Expose timelogs against issues and merge requests in GraphQL
|
||||||
|
merge_request: 57321
|
||||||
|
author: Lee Tickett @leetickett
|
||||||
|
type: added
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Fix EmptyLineAfterFinalLetItBe offenses in spec/lib/gitlab/hook_data
|
||||||
|
merge_request: 58262
|
||||||
|
author: Huzaifa Iftikhar @huzaifaiftikhar
|
||||||
|
type: fixed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Fix EmptyLineAfterFinalLetItBe offenses in spec/services/groups
|
||||||
|
merge_request: 58423
|
||||||
|
author: Huzaifa Iftikhar @huzaifaiftikhar
|
||||||
|
type: fixed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Fix EmptyLineAfterFinalLetItBe offenses in spec/services/ide
|
||||||
|
merge_request: 58424
|
||||||
|
author: Huzaifa Iftikhar @huzaifaiftikhar
|
||||||
|
type: fixed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Fix EmptyLineAfterFinalLetItBe offenses in spec/services/issues
|
||||||
|
merge_request: 58425
|
||||||
|
author: Huzaifa Iftikhar @huzaifaiftikhar
|
||||||
|
type: fixed
|
5
changelogs/unreleased/sh-pages-tmp-dir-use-build-id.yml
Normal file
5
changelogs/unreleased/sh-pages-tmp-dir-use-build-id.yml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Include project and build ID in Pages tmp directory
|
||||||
|
merge_request: 59106
|
||||||
|
author:
|
||||||
|
type: changed
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
name: ci_pipeline_ensure_iid_on_drop
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57783
|
||||||
|
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/326886
|
||||||
|
milestone: '13.11'
|
||||||
|
type: development
|
||||||
|
group: group::code review
|
||||||
|
default_enabled: false
|
|
@ -2905,6 +2905,7 @@ Relationship between an epic and an issue.
|
||||||
| `subscribed` | [`Boolean!`](#boolean) | Indicates the currently logged in user is subscribed to the issue. |
|
| `subscribed` | [`Boolean!`](#boolean) | Indicates the currently logged in user is subscribed to the issue. |
|
||||||
| `taskCompletionStatus` | [`TaskCompletionStatus!`](#taskcompletionstatus) | Task completion status of the issue. |
|
| `taskCompletionStatus` | [`TaskCompletionStatus!`](#taskcompletionstatus) | Task completion status of the issue. |
|
||||||
| `timeEstimate` | [`Int!`](#int) | Time estimate of the issue. |
|
| `timeEstimate` | [`Int!`](#int) | Time estimate of the issue. |
|
||||||
|
| `timelogs` | [`TimelogConnection!`](#timelogconnection) | Timelogs on the issue. |
|
||||||
| `title` | [`String!`](#string) | Title of the issue. |
|
| `title` | [`String!`](#string) | Title of the issue. |
|
||||||
| `titleHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `title`. |
|
| `titleHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `title`. |
|
||||||
| `totalTimeSpent` | [`Int!`](#int) | Total time reported as spent on the issue. |
|
| `totalTimeSpent` | [`Int!`](#int) | Total time reported as spent on the issue. |
|
||||||
|
@ -3468,6 +3469,7 @@ An edge in a connection.
|
||||||
| `subscribed` | [`Boolean!`](#boolean) | Indicates the currently logged in user is subscribed to the issue. |
|
| `subscribed` | [`Boolean!`](#boolean) | Indicates the currently logged in user is subscribed to the issue. |
|
||||||
| `taskCompletionStatus` | [`TaskCompletionStatus!`](#taskcompletionstatus) | Task completion status of the issue. |
|
| `taskCompletionStatus` | [`TaskCompletionStatus!`](#taskcompletionstatus) | Task completion status of the issue. |
|
||||||
| `timeEstimate` | [`Int!`](#int) | Time estimate of the issue. |
|
| `timeEstimate` | [`Int!`](#int) | Time estimate of the issue. |
|
||||||
|
| `timelogs` | [`TimelogConnection!`](#timelogconnection) | Timelogs on the issue. |
|
||||||
| `title` | [`String!`](#string) | Title of the issue. |
|
| `title` | [`String!`](#string) | Title of the issue. |
|
||||||
| `titleHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `title`. |
|
| `titleHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `title`. |
|
||||||
| `totalTimeSpent` | [`Int!`](#int) | Total time reported as spent on the issue. |
|
| `totalTimeSpent` | [`Int!`](#int) | Total time reported as spent on the issue. |
|
||||||
|
@ -3980,6 +3982,7 @@ An edge in a connection.
|
||||||
| `targetProjectId` | [`Int!`](#int) | ID of the merge request target project. |
|
| `targetProjectId` | [`Int!`](#int) | ID of the merge request target project. |
|
||||||
| `taskCompletionStatus` | [`TaskCompletionStatus!`](#taskcompletionstatus) | Completion status of tasks. |
|
| `taskCompletionStatus` | [`TaskCompletionStatus!`](#taskcompletionstatus) | Completion status of tasks. |
|
||||||
| `timeEstimate` | [`Int!`](#int) | Time estimate of the merge request. |
|
| `timeEstimate` | [`Int!`](#int) | Time estimate of the merge request. |
|
||||||
|
| `timelogs` | [`TimelogConnection!`](#timelogconnection) | Timelogs on the merge request. |
|
||||||
| `title` | [`String!`](#string) | Title of the merge request. |
|
| `title` | [`String!`](#string) | Title of the merge request. |
|
||||||
| `titleHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `title`. |
|
| `titleHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `title`. |
|
||||||
| `totalTimeSpent` | [`Int!`](#int) | Total time reported as spent on the merge request. |
|
| `totalTimeSpent` | [`Int!`](#int) | Total time reported as spent on the merge request. |
|
||||||
|
@ -5440,6 +5443,7 @@ Autogenerated return type of RepositionImageDiffNote.
|
||||||
|
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
| ----- | ---- | ----------- |
|
| ----- | ---- | ----------- |
|
||||||
|
| `blobs` | [`BlobConnection`](#blobconnection) | Blobs contained within the repository. |
|
||||||
| `empty` | [`Boolean!`](#boolean) | Indicates repository has no visible content. |
|
| `empty` | [`Boolean!`](#boolean) | Indicates repository has no visible content. |
|
||||||
| `exists` | [`Boolean!`](#boolean) | Indicates a corresponding Git repository exists on disk. |
|
| `exists` | [`Boolean!`](#boolean) | Indicates a corresponding Git repository exists on disk. |
|
||||||
| `rootRef` | [`String`](#string) | Default branch of the repository. |
|
| `rootRef` | [`String`](#string) | Default branch of the repository. |
|
||||||
|
|
|
@ -12,7 +12,17 @@ module Gitlab
|
||||||
end
|
end
|
||||||
|
|
||||||
pipeline.add_error_message(message)
|
pipeline.add_error_message(message)
|
||||||
pipeline.drop!(drop_reason) if drop_reason && persist_pipeline?
|
|
||||||
|
if drop_reason && persist_pipeline?
|
||||||
|
if Feature.enabled?(:ci_pipeline_ensure_iid_on_drop, pipeline.project, default_enabled: :yaml)
|
||||||
|
# Project iid must be called outside a transaction, so we ensure it is set here
|
||||||
|
# otherwise it may be set within the state transition transaction of the drop! call
|
||||||
|
# which it will lock the InternalId row for the whole transaction
|
||||||
|
pipeline.ensure_project_iid!
|
||||||
|
end
|
||||||
|
|
||||||
|
pipeline.drop!(drop_reason)
|
||||||
|
end
|
||||||
|
|
||||||
# TODO: consider not to rely on AR errors directly as they can be
|
# TODO: consider not to rely on AR errors directly as they can be
|
||||||
# polluted with other unrelated errors (e.g. state machine)
|
# polluted with other unrelated errors (e.g. state machine)
|
||||||
|
|
|
@ -623,9 +623,6 @@ msgstr ""
|
||||||
msgid "%{label_for_message} unavailable"
|
msgid "%{label_for_message} unavailable"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "%{label_name} %{span_open}will be permanently deleted from %{subject_name}. This cannot be undone.%{span_close}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "%{lets_encrypt_link_start}Let's Encrypt%{lets_encrypt_link_end} is a free, automated, and open certificate authority (CA), that give digital certificates in order to enable HTTPS (SSL/TLS) for websites."
|
msgid "%{lets_encrypt_link_start}Let's Encrypt%{lets_encrypt_link_end} is a free, automated, and open certificate authority (CA), that give digital certificates in order to enable HTTPS (SSL/TLS) for websites."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -10229,7 +10226,7 @@ msgstr ""
|
||||||
msgid "Delete label"
|
msgid "Delete label"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Delete label: %{label_name} ?"
|
msgid "Delete label: %{labelName}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Delete pipeline"
|
msgid "Delete pipeline"
|
||||||
|
@ -23895,6 +23892,9 @@ msgstr ""
|
||||||
msgid "Profiles|Enter your name, so people you know can recognize you"
|
msgid "Profiles|Enter your name, so people you know can recognize you"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Profiles|Expired key is not valid."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Profiles|Expires at"
|
msgid "Profiles|Expires at"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -23925,6 +23925,9 @@ msgstr ""
|
||||||
msgid "Profiles|Increase your account's security by enabling Two-Factor Authentication (2FA)"
|
msgid "Profiles|Increase your account's security by enabling Two-Factor Authentication (2FA)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Profiles|Invalid key."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Profiles|Invalid password"
|
msgid "Profiles|Invalid password"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -23934,6 +23937,15 @@ msgstr ""
|
||||||
msgid "Profiles|Key"
|
msgid "Profiles|Key"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Profiles|Key can still be used after expiration."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Profiles|Key usable beyond expiration date."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Profiles|Key will be deleted on this date."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Profiles|Last used:"
|
msgid "Profiles|Last used:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -23979,6 +23991,9 @@ msgstr ""
|
||||||
msgid "Profiles|Public email"
|
msgid "Profiles|Public email"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Profiles|Publicly visible private SSH keys can compromise your system."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Profiles|Remove avatar"
|
msgid "Profiles|Remove avatar"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -24003,9 +24018,6 @@ msgstr ""
|
||||||
msgid "Profiles|The maximum file size allowed is 200KB."
|
msgid "Profiles|The maximum file size allowed is 200KB."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Profiles|This doesn't look like a public SSH key, are you sure you want to add it? It will be publicly visible."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Profiles|This email will be displayed on your public profile"
|
msgid "Profiles|This email will be displayed on your public profile"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -24093,9 +24105,6 @@ msgstr ""
|
||||||
msgid "Profiles|Your email address was automatically set based on your %{provider_label} account"
|
msgid "Profiles|Your email address was automatically set based on your %{provider_label} account"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Profiles|Your key has expired"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Profiles|Your location was automatically set based on your %{provider_label} account"
|
msgid "Profiles|Your location was automatically set based on your %{provider_label} account"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ module QA
|
||||||
include SubMenus::Settings
|
include SubMenus::Settings
|
||||||
include SubMenus::Packages
|
include SubMenus::Packages
|
||||||
|
|
||||||
view 'app/views/layouts/nav/sidebar/_project.html.haml' do
|
view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do
|
||||||
element :activity_link
|
element :activity_link
|
||||||
element :merge_requests_link
|
element :merge_requests_link
|
||||||
element :snippets_link
|
element :snippets_link
|
||||||
|
|
|
@ -13,7 +13,7 @@ module QA
|
||||||
base.class_eval do
|
base.class_eval do
|
||||||
include QA::Page::Project::SubMenus::Common
|
include QA::Page::Project::SubMenus::Common
|
||||||
|
|
||||||
view 'app/views/layouts/nav/sidebar/_project.html.haml' do
|
view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do
|
||||||
element :link_pipelines
|
element :link_pipelines
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,7 +13,7 @@ module QA
|
||||||
base.class_eval do
|
base.class_eval do
|
||||||
include QA::Page::Project::SubMenus::Common
|
include QA::Page::Project::SubMenus::Common
|
||||||
|
|
||||||
view 'app/views/layouts/nav/sidebar/_project.html.haml' do
|
view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do
|
||||||
element :issue_boards_link
|
element :issue_boards_link
|
||||||
element :issues_item
|
element :issues_item
|
||||||
element :labels_link
|
element :labels_link
|
||||||
|
|
|
@ -13,7 +13,7 @@ module QA
|
||||||
base.class_eval do
|
base.class_eval do
|
||||||
include QA::Page::Project::SubMenus::Common
|
include QA::Page::Project::SubMenus::Common
|
||||||
|
|
||||||
view 'app/views/layouts/nav/sidebar/_project.html.haml' do
|
view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do
|
||||||
element :operations_link
|
element :operations_link
|
||||||
element :operations_environments_link
|
element :operations_environments_link
|
||||||
element :operations_metrics_link
|
element :operations_metrics_link
|
||||||
|
|
|
@ -13,7 +13,7 @@ module QA
|
||||||
base.class_eval do
|
base.class_eval do
|
||||||
include QA::Page::Project::SubMenus::Common
|
include QA::Page::Project::SubMenus::Common
|
||||||
|
|
||||||
view 'app/views/layouts/nav/sidebar/_project.html.haml' do
|
view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do
|
||||||
element :project_link
|
element :project_link
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,7 +13,7 @@ module QA
|
||||||
base.class_eval do
|
base.class_eval do
|
||||||
include QA::Page::Project::SubMenus::Common
|
include QA::Page::Project::SubMenus::Common
|
||||||
|
|
||||||
view 'app/views/layouts/nav/sidebar/_project.html.haml' do
|
view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do
|
||||||
element :repository_link
|
element :repository_link
|
||||||
element :branches_link
|
element :branches_link
|
||||||
element :tags_link
|
element :tags_link
|
||||||
|
|
|
@ -13,7 +13,7 @@ module QA
|
||||||
base.class_eval do
|
base.class_eval do
|
||||||
include QA::Page::Project::SubMenus::Common
|
include QA::Page::Project::SubMenus::Common
|
||||||
|
|
||||||
view 'app/views/layouts/nav/sidebar/_project.html.haml' do
|
view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do
|
||||||
element :settings_item
|
element :settings_item
|
||||||
element :general_settings_link
|
element :general_settings_link
|
||||||
element :integrations_settings_link
|
element :integrations_settings_link
|
||||||
|
|
|
@ -1,11 +1,22 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# Read about factories at https://github.com/thoughtbot/factory_bot
|
|
||||||
|
|
||||||
FactoryBot.define do
|
FactoryBot.define do
|
||||||
factory :timelog do
|
factory :timelog do
|
||||||
time_spent { 3600 }
|
time_spent { 3600 }
|
||||||
issue
|
for_issue
|
||||||
user { issue.project.creator }
|
|
||||||
|
factory :issue_timelog, traits: [:for_issue]
|
||||||
|
factory :merge_request_timelog, traits: [:for_merge_request]
|
||||||
|
|
||||||
|
trait :for_issue do
|
||||||
|
issue
|
||||||
|
user { issue.author }
|
||||||
|
end
|
||||||
|
|
||||||
|
trait :for_merge_request do
|
||||||
|
merge_request
|
||||||
|
issue { nil }
|
||||||
|
user { merge_request.author }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,18 +18,18 @@ RSpec.describe "User removes labels" do
|
||||||
visit(project_labels_path(project))
|
visit(project_labels_path(project))
|
||||||
end
|
end
|
||||||
|
|
||||||
it "removes label" do
|
it "removes label", :js do
|
||||||
page.within(".other-labels") do
|
page.within(".other-labels") do
|
||||||
page.first(".label-list-item") do
|
page.first(".label-list-item") do
|
||||||
first('.js-label-options-dropdown').click
|
first('.js-label-options-dropdown').click
|
||||||
first(".remove-row").click
|
first(".remove-row").click
|
||||||
end
|
end
|
||||||
|
|
||||||
expect(page).to have_content("#{label.title} will be permanently deleted from #{project.name}. This cannot be undone.")
|
|
||||||
|
|
||||||
first(:link, "Delete label").click
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
expect(page).to have_content("#{label.title} will be permanently deleted from #{project.name}. This cannot be undone.")
|
||||||
|
|
||||||
|
first(:link, "Delete label").click
|
||||||
|
|
||||||
expect(page).to have_content("Label was removed").and have_no_content(label.title)
|
expect(page).to have_content("Label was removed").and have_no_content(label.title)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
83
spec/frontend/delete_label_modal_spec.js
Normal file
83
spec/frontend/delete_label_modal_spec.js
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
import { TEST_HOST } from 'helpers/test_constants';
|
||||||
|
import initDeleteLabelModal from '~/delete_label_modal';
|
||||||
|
|
||||||
|
describe('DeleteLabelModal', () => {
|
||||||
|
const buttons = [
|
||||||
|
{
|
||||||
|
labelName: 'label 1',
|
||||||
|
subjectName: 'GitLab Org',
|
||||||
|
destroyPath: `${TEST_HOST}/1`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
labelName: 'label 2',
|
||||||
|
subjectName: 'GitLab Org',
|
||||||
|
destroyPath: `${TEST_HOST}/2`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const buttonContainer = document.createElement('div');
|
||||||
|
|
||||||
|
buttons.forEach((x) => {
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.setAttribute('class', 'js-delete-label-modal-button');
|
||||||
|
button.setAttribute('data-label-name', x.labelName);
|
||||||
|
button.setAttribute('data-subject-name', x.subjectName);
|
||||||
|
button.setAttribute('data-destroy-path', x.destroyPath);
|
||||||
|
button.innerHTML = 'Action';
|
||||||
|
buttonContainer.appendChild(button);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(buttonContainer);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const findJsHooks = () => document.querySelectorAll('.js-delete-label-modal-button');
|
||||||
|
const findModal = () => document.querySelector('.gl-modal');
|
||||||
|
|
||||||
|
it('starts with only js-containers', () => {
|
||||||
|
expect(findJsHooks()).toHaveLength(buttons.length);
|
||||||
|
expect(findModal()).not.toExist();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when first button clicked', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
initDeleteLabelModal();
|
||||||
|
findJsHooks().item(0).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not replace js-containers with GlModal', () => {
|
||||||
|
expect(findJsHooks()).toHaveLength(buttons.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders GlModal', () => {
|
||||||
|
expect(findModal()).toExist();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.each`
|
||||||
|
index
|
||||||
|
${0}
|
||||||
|
${1}
|
||||||
|
`(`when multiple buttons exist`, ({ index }) => {
|
||||||
|
beforeEach(() => {
|
||||||
|
initDeleteLabelModal();
|
||||||
|
findJsHooks().item(index).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('correct props are passed to gl-modal', () => {
|
||||||
|
expect(findModal().querySelector('.modal-title').innerHTML).toContain(
|
||||||
|
buttons[index].labelName,
|
||||||
|
);
|
||||||
|
expect(findModal().querySelector('.modal-body').innerHTML).toContain(
|
||||||
|
buttons[index].subjectName,
|
||||||
|
);
|
||||||
|
expect(findModal().querySelector('.modal-footer .btn-danger').href).toContain(
|
||||||
|
buttons[index].destroyPath,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,7 +1,7 @@
|
||||||
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
|
import { GlAlert, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
|
||||||
import { shallowMount } from '@vue/test-utils';
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||||
import waitForPromises from 'helpers/wait_for_promises';
|
import waitForPromises from 'helpers/wait_for_promises';
|
||||||
|
|
||||||
import { fetchGroups } from '~/jira_connect/api';
|
import { fetchGroups } from '~/jira_connect/api';
|
||||||
import GroupsList from '~/jira_connect/components/groups_list.vue';
|
import GroupsList from '~/jira_connect/components/groups_list.vue';
|
||||||
import GroupsListItem from '~/jira_connect/components/groups_list_item.vue';
|
import GroupsListItem from '~/jira_connect/components/groups_list_item.vue';
|
||||||
|
@ -12,20 +12,27 @@ jest.mock('~/jira_connect/api', () => {
|
||||||
fetchGroups: jest.fn(),
|
fetchGroups: jest.fn(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mockGroupsPath = '/groups';
|
||||||
|
|
||||||
describe('GroupsList', () => {
|
describe('GroupsList', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
const mockEmptyResponse = { data: [] };
|
const mockEmptyResponse = { data: [] };
|
||||||
|
|
||||||
const createComponent = (options = {}) => {
|
const createComponent = (options = {}) => {
|
||||||
wrapper = shallowMount(GroupsList, {
|
wrapper = extendedWrapper(
|
||||||
...options,
|
shallowMount(GroupsList, {
|
||||||
});
|
provide: {
|
||||||
|
groupsPath: mockGroupsPath,
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
}),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.destroy();
|
wrapper.destroy();
|
||||||
wrapper = null;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const findGlAlert = () => wrapper.find(GlAlert);
|
const findGlAlert = () => wrapper.find(GlAlert);
|
||||||
|
@ -33,56 +40,72 @@ describe('GroupsList', () => {
|
||||||
const findAllItems = () => wrapper.findAll(GroupsListItem);
|
const findAllItems = () => wrapper.findAll(GroupsListItem);
|
||||||
const findFirstItem = () => findAllItems().at(0);
|
const findFirstItem = () => findAllItems().at(0);
|
||||||
const findSecondItem = () => findAllItems().at(1);
|
const findSecondItem = () => findAllItems().at(1);
|
||||||
|
const findSearchBox = () => wrapper.find(GlSearchBoxByType);
|
||||||
|
const findGroupsList = () => wrapper.findByTestId('groups-list');
|
||||||
|
|
||||||
describe('isLoading is true', () => {
|
describe('when groups are loading', () => {
|
||||||
it('renders loading icon', async () => {
|
it('renders loading icon', async () => {
|
||||||
fetchGroups.mockResolvedValue(mockEmptyResponse);
|
fetchGroups.mockReturnValue(new Promise(() => {}));
|
||||||
createComponent();
|
createComponent();
|
||||||
|
|
||||||
wrapper.setData({ isLoading: true });
|
|
||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
expect(findGlLoadingIcon().exists()).toBe(true);
|
expect(findGlLoadingIcon().exists()).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('error fetching groups', () => {
|
describe('when groups fetch fails', () => {
|
||||||
it('renders error message', async () => {
|
it('renders error message', async () => {
|
||||||
fetchGroups.mockRejectedValue();
|
fetchGroups.mockRejectedValue();
|
||||||
createComponent();
|
createComponent();
|
||||||
|
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(findGlLoadingIcon().exists()).toBe(false);
|
||||||
expect(findGlAlert().exists()).toBe(true);
|
expect(findGlAlert().exists()).toBe(true);
|
||||||
expect(findGlAlert().text()).toBe('Failed to load namespaces. Please try again.');
|
expect(findGlAlert().text()).toBe('Failed to load namespaces. Please try again.');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('no groups returned', () => {
|
describe('with no groups returned', () => {
|
||||||
it('renders empty state', async () => {
|
it('renders empty state', async () => {
|
||||||
fetchGroups.mockResolvedValue(mockEmptyResponse);
|
fetchGroups.mockResolvedValue(mockEmptyResponse);
|
||||||
createComponent();
|
createComponent();
|
||||||
|
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(findGlLoadingIcon().exists()).toBe(false);
|
||||||
expect(wrapper.text()).toContain('No available namespaces');
|
expect(wrapper.text()).toContain('No available namespaces');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with groups returned', () => {
|
describe('with groups returned', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
fetchGroups.mockResolvedValue({ data: [mockGroup1, mockGroup2] });
|
fetchGroups.mockResolvedValue({
|
||||||
|
headers: { 'X-PAGE': 1, 'X-TOTAL': 2 },
|
||||||
|
data: [mockGroup1, mockGroup2],
|
||||||
|
});
|
||||||
createComponent();
|
createComponent();
|
||||||
|
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders groups list', () => {
|
it('renders groups list', () => {
|
||||||
expect(findAllItems().length).toBe(2);
|
expect(findAllItems()).toHaveLength(2);
|
||||||
expect(findFirstItem().props('group')).toBe(mockGroup1);
|
expect(findFirstItem().props('group')).toBe(mockGroup1);
|
||||||
expect(findSecondItem().props('group')).toBe(mockGroup2);
|
expect(findSecondItem().props('group')).toBe(mockGroup2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('sets GroupListItem `disabled` prop to `false`', () => {
|
||||||
|
findAllItems().wrappers.forEach((groupListItem) => {
|
||||||
|
expect(groupListItem.props('disabled')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not set opacity of the groups list', () => {
|
||||||
|
expect(findGroupsList().classes()).not.toContain('gl-opacity-5');
|
||||||
|
});
|
||||||
|
|
||||||
it('shows error message on $emit from item', async () => {
|
it('shows error message on $emit from item', async () => {
|
||||||
const errorMessage = 'error message';
|
const errorMessage = 'error message';
|
||||||
|
|
||||||
|
@ -93,5 +116,55 @@ describe('GroupsList', () => {
|
||||||
expect(findGlAlert().exists()).toBe(true);
|
expect(findGlAlert().exists()).toBe(true);
|
||||||
expect(findGlAlert().text()).toContain(errorMessage);
|
expect(findGlAlert().text()).toContain(errorMessage);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when searching groups', () => {
|
||||||
|
const mockSearchTeam = 'mock search term';
|
||||||
|
|
||||||
|
describe('while groups are loading', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
fetchGroups.mockClear();
|
||||||
|
fetchGroups.mockReturnValue(new Promise(() => {}));
|
||||||
|
|
||||||
|
findSearchBox().vm.$emit('input', mockSearchTeam);
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls `fetchGroups` with search term', () => {
|
||||||
|
expect(fetchGroups).toHaveBeenCalledWith(mockGroupsPath, {
|
||||||
|
page: 1,
|
||||||
|
perPage: 10,
|
||||||
|
search: mockSearchTeam,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disables GroupListItems', async () => {
|
||||||
|
findAllItems().wrappers.forEach((groupListItem) => {
|
||||||
|
expect(groupListItem.props('disabled')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets opacity of the groups list', () => {
|
||||||
|
expect(findGroupsList().classes()).toContain('gl-opacity-5');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets loading prop of ths search box', () => {
|
||||||
|
expect(findSearchBox().props('isLoading')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when group search finishes loading', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
fetchGroups.mockResolvedValue({ data: [mockGroup1] });
|
||||||
|
findSearchBox().vm.$emit('input');
|
||||||
|
|
||||||
|
await waitForPromises();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders new groups list', () => {
|
||||||
|
expect(findAllItems()).toHaveLength(1);
|
||||||
|
expect(findFirstItem().props('group')).toBe(mockGroup1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { GlModal } from '@gitlab/ui';
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
import { stubComponent } from 'helpers/stub_component';
|
||||||
|
import { TEST_HOST } from 'helpers/test_constants';
|
||||||
|
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||||
|
import DeleteLabelModal from '~/vue_shared/components/delete_label_modal.vue';
|
||||||
|
|
||||||
|
const MOCK_MODAL_DATA = {
|
||||||
|
labelName: 'label 1',
|
||||||
|
subjectName: 'GitLab Org',
|
||||||
|
destroyPath: `${TEST_HOST}/1`,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('vue_shared/components/delete_label_modal', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
const createComponent = () => {
|
||||||
|
wrapper = extendedWrapper(
|
||||||
|
mount(DeleteLabelModal, {
|
||||||
|
propsData: {
|
||||||
|
selector: '.js-test-btn',
|
||||||
|
},
|
||||||
|
stubs: {
|
||||||
|
GlModal: stubComponent(GlModal, {
|
||||||
|
template:
|
||||||
|
'<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
const findModal = () => wrapper.find(GlModal);
|
||||||
|
const findPrimaryModalButton = () => wrapper.findByTestId('delete-button');
|
||||||
|
|
||||||
|
describe('template', () => {
|
||||||
|
describe('when modal data is set', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
createComponent();
|
||||||
|
wrapper.vm.labelName = MOCK_MODAL_DATA.labelName;
|
||||||
|
wrapper.vm.subjectName = MOCK_MODAL_DATA.subjectName;
|
||||||
|
wrapper.vm.destroyPath = MOCK_MODAL_DATA.destroyPath;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders GlModal', () => {
|
||||||
|
expect(findModal().exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays the label name and subject name', () => {
|
||||||
|
expect(findModal().text()).toContain(
|
||||||
|
`${MOCK_MODAL_DATA.labelName} will be permanently deleted from ${MOCK_MODAL_DATA.subjectName}. This cannot be undone`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes the destroyPath to the button', () => {
|
||||||
|
expect(findPrimaryModalButton().attributes('href')).toBe(MOCK_MODAL_DATA.destroyPath);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
74
spec/graphql/resolvers/blobs_resolver_spec.rb
Normal file
74
spec/graphql/resolvers/blobs_resolver_spec.rb
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Resolvers::BlobsResolver do
|
||||||
|
include GraphqlHelpers
|
||||||
|
|
||||||
|
describe '.resolver_complexity' do
|
||||||
|
it 'adds one per path being resolved' do
|
||||||
|
control = described_class.resolver_complexity({}, child_complexity: 1)
|
||||||
|
|
||||||
|
expect(described_class.resolver_complexity({ paths: %w[a b c] }, child_complexity: 1))
|
||||||
|
.to eq(control + 3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#resolve' do
|
||||||
|
let_it_be(:user) { create(:user) }
|
||||||
|
let_it_be(:project) { create(:project, :repository) }
|
||||||
|
|
||||||
|
let(:repository) { project.repository }
|
||||||
|
let(:args) { { paths: paths, ref: ref } }
|
||||||
|
let(:paths) { [] }
|
||||||
|
let(:ref) { nil }
|
||||||
|
|
||||||
|
subject(:resolve_blobs) { resolve(described_class, obj: repository, args: args, ctx: { current_user: user }) }
|
||||||
|
|
||||||
|
context 'when unauthorized' do
|
||||||
|
it 'raises an exception' do
|
||||||
|
expect { resolve_blobs }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when authorized' do
|
||||||
|
before do
|
||||||
|
project.add_developer(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'using no filter' do
|
||||||
|
it 'returns nothing' do
|
||||||
|
is_expected.to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'using paths filter' do
|
||||||
|
let(:paths) { ['README.md'] }
|
||||||
|
|
||||||
|
it 'returns the specified blobs for HEAD' do
|
||||||
|
is_expected.to contain_exactly(have_attributes(path: 'README.md'))
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'specifying a non-existent blob' do
|
||||||
|
let(:paths) { ['non-existent'] }
|
||||||
|
|
||||||
|
it 'returns nothing' do
|
||||||
|
is_expected.to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'specifying a different ref' do
|
||||||
|
let(:ref) { 'add-pdf-file' }
|
||||||
|
let(:paths) { ['files/pdf/test.pdf', 'README.md'] }
|
||||||
|
|
||||||
|
it 'returns the specified blobs for that ref' do
|
||||||
|
is_expected.to contain_exactly(
|
||||||
|
have_attributes(path: 'files/pdf/test.pdf'),
|
||||||
|
have_attributes(path: 'README.md')
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -7,6 +7,7 @@ RSpec.describe Resolvers::ProjectPipelineResolver do
|
||||||
|
|
||||||
let_it_be(:project) { create(:project) }
|
let_it_be(:project) { create(:project) }
|
||||||
let_it_be(:pipeline) { create(:ci_pipeline, project: project, iid: '1234', sha: 'sha') }
|
let_it_be(:pipeline) { create(:ci_pipeline, project: project, iid: '1234', sha: 'sha') }
|
||||||
|
let_it_be(:other_project_pipeline) { create(:ci_pipeline, project: project, iid: '1235', sha: 'sha2') }
|
||||||
let_it_be(:other_pipeline) { create(:ci_pipeline) }
|
let_it_be(:other_pipeline) { create(:ci_pipeline) }
|
||||||
let(:current_user) { create(:user) }
|
let(:current_user) { create(:user) }
|
||||||
|
|
||||||
|
@ -23,6 +24,11 @@ RSpec.describe Resolvers::ProjectPipelineResolver do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'resolves pipeline for the passed iid' do
|
it 'resolves pipeline for the passed iid' do
|
||||||
|
expect(Ci::PipelinesFinder)
|
||||||
|
.to receive(:new)
|
||||||
|
.with(project, current_user, iids: ['1234'])
|
||||||
|
.and_call_original
|
||||||
|
|
||||||
result = batch_sync do
|
result = batch_sync do
|
||||||
resolve_pipeline(project, { iid: '1234' })
|
resolve_pipeline(project, { iid: '1234' })
|
||||||
end
|
end
|
||||||
|
@ -31,6 +37,11 @@ RSpec.describe Resolvers::ProjectPipelineResolver do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'resolves pipeline for the passed sha' do
|
it 'resolves pipeline for the passed sha' do
|
||||||
|
expect(Ci::PipelinesFinder)
|
||||||
|
.to receive(:new)
|
||||||
|
.with(project, current_user, sha: ['sha'])
|
||||||
|
.and_call_original
|
||||||
|
|
||||||
result = batch_sync do
|
result = batch_sync do
|
||||||
resolve_pipeline(project, { sha: 'sha' })
|
resolve_pipeline(project, { sha: 'sha' })
|
||||||
end
|
end
|
||||||
|
@ -39,8 +50,6 @@ RSpec.describe Resolvers::ProjectPipelineResolver do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'keeps the queries under the threshold for iid' do
|
it 'keeps the queries under the threshold for iid' do
|
||||||
create(:ci_pipeline, project: project, iid: '1235')
|
|
||||||
|
|
||||||
control = ActiveRecord::QueryRecorder.new do
|
control = ActiveRecord::QueryRecorder.new do
|
||||||
batch_sync { resolve_pipeline(project, { iid: '1234' }) }
|
batch_sync { resolve_pipeline(project, { iid: '1234' }) }
|
||||||
end
|
end
|
||||||
|
@ -54,8 +63,6 @@ RSpec.describe Resolvers::ProjectPipelineResolver do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'keeps the queries under the threshold for sha' do
|
it 'keeps the queries under the threshold for sha' do
|
||||||
create(:ci_pipeline, project: project, sha: 'sha2')
|
|
||||||
|
|
||||||
control = ActiveRecord::QueryRecorder.new do
|
control = ActiveRecord::QueryRecorder.new do
|
||||||
batch_sync { resolve_pipeline(project, { sha: 'sha' }) }
|
batch_sync { resolve_pipeline(project, { sha: 'sha' }) }
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,51 +9,57 @@ RSpec.describe Resolvers::TimelogResolver do
|
||||||
expect(described_class).to have_non_null_graphql_type(::Types::TimelogType.connection_type)
|
expect(described_class).to have_non_null_graphql_type(::Types::TimelogType.connection_type)
|
||||||
end
|
end
|
||||||
|
|
||||||
context "within a group" do
|
context "with a group" do
|
||||||
let_it_be(:current_user) { create(:user) }
|
let_it_be(:current_user) { create(:user) }
|
||||||
let(:group) { create(:group) }
|
let_it_be(:group) { create(:group) }
|
||||||
let(:project) { create(:project, :public, group: group) }
|
let_it_be(:project) { create(:project, :public, group: group) }
|
||||||
|
|
||||||
before do
|
before_all do
|
||||||
group.add_developer(current_user)
|
group.add_developer(current_user)
|
||||||
project.add_developer(current_user)
|
project.add_developer(current_user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
group.clear_memoization(:timelogs)
|
||||||
|
end
|
||||||
|
|
||||||
describe '#resolve' do
|
describe '#resolve' do
|
||||||
let(:issue) { create(:issue, project: project) }
|
let_it_be(:issue) { create(:issue, project: project) }
|
||||||
let(:issue2) { create(:issue, project: project) }
|
let_it_be(:issue2) { create(:issue, project: project) }
|
||||||
|
let_it_be(:timelog1) { create(:issue_timelog, issue: issue, spent_at: 2.days.ago.beginning_of_day) }
|
||||||
|
let_it_be(:timelog2) { create(:issue_timelog, issue: issue2, spent_at: 2.days.ago.end_of_day) }
|
||||||
|
let_it_be(:timelog3) { create(:issue_timelog, issue: issue2, spent_at: 10.days.ago) }
|
||||||
|
|
||||||
let(:args) { { start_time: 6.days.ago, end_time: 2.days.ago.noon } }
|
let(:args) { { start_time: 6.days.ago, end_time: 2.days.ago.noon } }
|
||||||
let!(:timelog1) { create(:timelog, issue: issue, spent_at: 2.days.ago.beginning_of_day) }
|
|
||||||
let!(:timelog2) { create(:timelog, issue: issue2, spent_at: 2.days.ago.end_of_day) }
|
|
||||||
let!(:timelog3) { create(:timelog, issue: issue2, spent_at: 10.days.ago) }
|
|
||||||
|
|
||||||
it 'finds all timelogs within given dates' do
|
it 'finds all timelogs within given dates' do
|
||||||
timelogs = resolve_timelogs(args)
|
timelogs = resolve_timelogs(**args)
|
||||||
|
|
||||||
expect(timelogs).to contain_exactly(timelog1)
|
expect(timelogs).to contain_exactly(timelog1)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'return nothing when user has insufficient permissions' do
|
it 'return nothing when user has insufficient permissions' do
|
||||||
|
user = create(:user)
|
||||||
group.add_guest(current_user)
|
group.add_guest(current_user)
|
||||||
|
|
||||||
expect(resolve_timelogs(args)).to be_empty
|
expect(resolve_timelogs(user: user, **args)).to be_empty
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when start_time and end_date are present' do
|
context 'when start_time and end_date are present' do
|
||||||
let(:args) { { start_time: 6.days.ago, end_date: 2.days.ago } }
|
let(:args) { { start_time: 6.days.ago, end_date: 2.days.ago } }
|
||||||
|
|
||||||
it 'finds timelogs until the end of day of end_date' do
|
it 'finds timelogs until the end of day of end_date' do
|
||||||
timelogs = resolve_timelogs(args)
|
timelogs = resolve_timelogs(**args)
|
||||||
|
|
||||||
expect(timelogs).to contain_exactly(timelog1, timelog2)
|
expect(timelogs).to contain_exactly(timelog1, timelog2)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'finds timelogs until the time specified on end_time' do
|
context 'when start_date and end_time are present' do
|
||||||
let(:args) { { start_date: 6.days.ago, end_time: 2.days.ago.noon } }
|
let(:args) { { start_date: 6.days.ago, end_time: 2.days.ago.noon } }
|
||||||
|
|
||||||
it 'finds all timelogs within start_date and end_time' do
|
it 'finds all timelogs within start_date and end_time' do
|
||||||
timelogs = resolve_timelogs(args)
|
timelogs = resolve_timelogs(**args)
|
||||||
|
|
||||||
expect(timelogs).to contain_exactly(timelog1)
|
expect(timelogs).to contain_exactly(timelog1)
|
||||||
end
|
end
|
||||||
|
@ -66,7 +72,7 @@ RSpec.describe Resolvers::TimelogResolver do
|
||||||
let(:args) { {} }
|
let(:args) { {} }
|
||||||
|
|
||||||
it 'returns correct error' do
|
it 'returns correct error' do
|
||||||
expect {resolve_timelogs(args)}
|
expect { resolve_timelogs(**args) }
|
||||||
.to raise_error(error_class, /Start and End arguments must be present/)
|
.to raise_error(error_class, /Start and End arguments must be present/)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -75,7 +81,7 @@ RSpec.describe Resolvers::TimelogResolver do
|
||||||
let(:args) { { start_time: 6.days.ago } }
|
let(:args) { { start_time: 6.days.ago } }
|
||||||
|
|
||||||
it 'returns correct error' do
|
it 'returns correct error' do
|
||||||
expect {resolve_timelogs(args)}
|
expect { resolve_timelogs(**args) }
|
||||||
.to raise_error(error_class, /Both Start and End arguments must be present/)
|
.to raise_error(error_class, /Both Start and End arguments must be present/)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -84,7 +90,7 @@ RSpec.describe Resolvers::TimelogResolver do
|
||||||
let(:args) { { end_time: 2.days.ago } }
|
let(:args) { { end_time: 2.days.ago } }
|
||||||
|
|
||||||
it 'returns correct error' do
|
it 'returns correct error' do
|
||||||
expect {resolve_timelogs(args)}
|
expect { resolve_timelogs(**args) }
|
||||||
.to raise_error(error_class, /Both Start and End arguments must be present/)
|
.to raise_error(error_class, /Both Start and End arguments must be present/)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -93,7 +99,7 @@ RSpec.describe Resolvers::TimelogResolver do
|
||||||
let(:args) { { start_date: 6.days.ago } }
|
let(:args) { { start_date: 6.days.ago } }
|
||||||
|
|
||||||
it 'returns correct error' do
|
it 'returns correct error' do
|
||||||
expect {resolve_timelogs(args)}
|
expect { resolve_timelogs(**args) }
|
||||||
.to raise_error(error_class, /Both Start and End arguments must be present/)
|
.to raise_error(error_class, /Both Start and End arguments must be present/)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -102,7 +108,7 @@ RSpec.describe Resolvers::TimelogResolver do
|
||||||
let(:args) { { end_date: 2.days.ago } }
|
let(:args) { { end_date: 2.days.ago } }
|
||||||
|
|
||||||
it 'returns correct error' do
|
it 'returns correct error' do
|
||||||
expect {resolve_timelogs(args)}
|
expect { resolve_timelogs(**args) }
|
||||||
.to raise_error(error_class, /Both Start and End arguments must be present/)
|
.to raise_error(error_class, /Both Start and End arguments must be present/)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -111,7 +117,7 @@ RSpec.describe Resolvers::TimelogResolver do
|
||||||
let(:args) { { start_time: 6.days.ago, start_date: 6.days.ago } }
|
let(:args) { { start_time: 6.days.ago, start_date: 6.days.ago } }
|
||||||
|
|
||||||
it 'returns correct error' do
|
it 'returns correct error' do
|
||||||
expect {resolve_timelogs(args)}
|
expect { resolve_timelogs(**args) }
|
||||||
.to raise_error(error_class, /Both Start and End arguments must be present/)
|
.to raise_error(error_class, /Both Start and End arguments must be present/)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -120,7 +126,7 @@ RSpec.describe Resolvers::TimelogResolver do
|
||||||
let(:args) { { end_time: 2.days.ago, end_date: 2.days.ago } }
|
let(:args) { { end_time: 2.days.ago, end_date: 2.days.ago } }
|
||||||
|
|
||||||
it 'returns correct error' do
|
it 'returns correct error' do
|
||||||
expect {resolve_timelogs(args)}
|
expect { resolve_timelogs(**args) }
|
||||||
.to raise_error(error_class, /Both Start and End arguments must be present/)
|
.to raise_error(error_class, /Both Start and End arguments must be present/)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -129,7 +135,7 @@ RSpec.describe Resolvers::TimelogResolver do
|
||||||
let(:args) { { start_date: 6.days.ago, end_date: 2.days.ago, end_time: 2.days.ago } }
|
let(:args) { { start_date: 6.days.ago, end_date: 2.days.ago, end_time: 2.days.ago } }
|
||||||
|
|
||||||
it 'returns correct error' do
|
it 'returns correct error' do
|
||||||
expect {resolve_timelogs(args)}
|
expect { resolve_timelogs(**args) }
|
||||||
.to raise_error(error_class, /Only Time or Date arguments must be present/)
|
.to raise_error(error_class, /Only Time or Date arguments must be present/)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -138,7 +144,7 @@ RSpec.describe Resolvers::TimelogResolver do
|
||||||
let(:args) { { start_time: 2.days.ago, end_time: 6.days.ago } }
|
let(:args) { { start_time: 2.days.ago, end_time: 6.days.ago } }
|
||||||
|
|
||||||
it 'returns correct error' do
|
it 'returns correct error' do
|
||||||
expect {resolve_timelogs(args)}
|
expect { resolve_timelogs(**args) }
|
||||||
.to raise_error(error_class, /Start argument must be before End argument/)
|
.to raise_error(error_class, /Start argument must be before End argument/)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -147,7 +153,7 @@ RSpec.describe Resolvers::TimelogResolver do
|
||||||
let(:args) { { start_time: 3.months.ago, end_time: 2.days.ago } }
|
let(:args) { { start_time: 3.months.ago, end_time: 2.days.ago } }
|
||||||
|
|
||||||
it 'returns correct error' do
|
it 'returns correct error' do
|
||||||
expect {resolve_timelogs(args)}
|
expect { resolve_timelogs(**args) }
|
||||||
.to raise_error(error_class, /The time range period cannot contain more than 60 days/)
|
.to raise_error(error_class, /The time range period cannot contain more than 60 days/)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -155,7 +161,8 @@ RSpec.describe Resolvers::TimelogResolver do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def resolve_timelogs(args = {}, context = { current_user: current_user })
|
def resolve_timelogs(user: current_user, **args)
|
||||||
|
context = { current_user: user }
|
||||||
resolve(described_class, obj: group, args: args, ctx: context)
|
resolve(described_class, obj: group, args: args, ctx: context)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,7 +18,7 @@ RSpec.describe GitlabSchema.types['Issue'] do
|
||||||
confidential discussion_locked upvotes downvotes user_notes_count user_discussions_count web_path web_url relative_position
|
confidential discussion_locked upvotes downvotes user_notes_count user_discussions_count web_path web_url relative_position
|
||||||
emails_disabled subscribed time_estimate total_time_spent human_time_estimate human_total_time_spent closed_at created_at updated_at task_completion_status
|
emails_disabled subscribed time_estimate total_time_spent human_time_estimate human_total_time_spent closed_at created_at updated_at task_completion_status
|
||||||
design_collection alert_management_alert severity current_user_todos moved moved_to
|
design_collection alert_management_alert severity current_user_todos moved moved_to
|
||||||
create_note_email]
|
create_note_email timelogs]
|
||||||
|
|
||||||
fields.each do |field_name|
|
fields.each do |field_name|
|
||||||
expect(described_class).to have_graphql_field(field_name)
|
expect(described_class).to have_graphql_field(field_name)
|
||||||
|
|
|
@ -12,4 +12,6 @@ RSpec.describe GitlabSchema.types['Repository'] do
|
||||||
specify { expect(described_class).to have_graphql_field(:tree) }
|
specify { expect(described_class).to have_graphql_field(:tree) }
|
||||||
|
|
||||||
specify { expect(described_class).to have_graphql_field(:exists, calls_gitaly?: true, complexity: 2) }
|
specify { expect(described_class).to have_graphql_field(:exists, calls_gitaly?: true, complexity: 2) }
|
||||||
|
|
||||||
|
specify { expect(described_class).to have_graphql_field(:blobs) }
|
||||||
end
|
end
|
||||||
|
|
|
@ -112,6 +112,46 @@ RSpec.describe ProfilesHelper do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "#ssh_key_expiration_tooltip" do
|
||||||
|
using RSpec::Parameterized::TableSyntax
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Key).to receive(:enforce_ssh_key_expiration_feature_available?).and_return(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
error_message = 'Key type is forbidden. Must be DSA, ECDSA, or ED25519'
|
||||||
|
|
||||||
|
where(:error, :expired, :result) do
|
||||||
|
false | false | nil
|
||||||
|
true | false | error_message
|
||||||
|
false | true | 'Key usable beyond expiration date.'
|
||||||
|
true | true | error_message
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
let_it_be(:key) do
|
||||||
|
build(:personal_key)
|
||||||
|
end
|
||||||
|
|
||||||
|
it do
|
||||||
|
key.expires_at = expired ? 2.days.ago : 2.days.from_now
|
||||||
|
key.errors.add(:base, error_message) if error
|
||||||
|
|
||||||
|
expect(helper.ssh_key_expiration_tooltip(key)).to eq(result)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#ssh_key_expires_field_description" do
|
||||||
|
before do
|
||||||
|
allow(Key).to receive(:enforce_ssh_key_expiration_feature_available?).and_return(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the description' do
|
||||||
|
expect(helper.ssh_key_expires_field_description).to eq('Key can still be used after expiration.')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def stub_cas_omniauth_provider
|
def stub_cas_omniauth_provider
|
||||||
provider = OpenStruct.new(
|
provider = OpenStruct.new(
|
||||||
'name' => 'cas3',
|
'name' => 'cas3',
|
||||||
|
|
70
spec/lib/gitlab/ci/pipeline/chain/helpers_spec.rb
Normal file
70
spec/lib/gitlab/ci/pipeline/chain/helpers_spec.rb
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Gitlab::Ci::Pipeline::Chain::Helpers do
|
||||||
|
let(:helper_class) do
|
||||||
|
Class.new do
|
||||||
|
include Gitlab::Ci::Pipeline::Chain::Helpers
|
||||||
|
|
||||||
|
attr_accessor :pipeline, :command
|
||||||
|
|
||||||
|
def initialize(pipeline, command)
|
||||||
|
self.pipeline = pipeline
|
||||||
|
self.command = command
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
subject(:helper) { helper_class.new(pipeline, command) }
|
||||||
|
|
||||||
|
let(:pipeline) { build(:ci_empty_pipeline) }
|
||||||
|
let(:command) { double(save_incompleted: true) }
|
||||||
|
let(:message) { 'message' }
|
||||||
|
|
||||||
|
describe '.error' do
|
||||||
|
shared_examples 'error function' do
|
||||||
|
specify do
|
||||||
|
expect(pipeline).to receive(:drop!).with(drop_reason).and_call_original
|
||||||
|
expect(pipeline).to receive(:add_error_message).with(message).and_call_original
|
||||||
|
expect(pipeline).to receive(:ensure_project_iid!).twice.and_call_original
|
||||||
|
|
||||||
|
subject.error(message, config_error: config_error, drop_reason: drop_reason)
|
||||||
|
|
||||||
|
expect(pipeline.yaml_errors).to eq(yaml_error)
|
||||||
|
expect(pipeline.errors[:base]).to include(message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when given a drop reason' do
|
||||||
|
context 'when config error is true' do
|
||||||
|
context 'sets the yaml error and overrides the drop reason' do
|
||||||
|
let(:drop_reason) { :config_error }
|
||||||
|
let(:config_error) { true }
|
||||||
|
let(:yaml_error) { message }
|
||||||
|
|
||||||
|
it_behaves_like "error function"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when config error is false' do
|
||||||
|
context 'does not set the yaml error or override the drop reason' do
|
||||||
|
let(:drop_reason) { :size_limit_exceeded }
|
||||||
|
let(:config_error) { false }
|
||||||
|
let(:yaml_error) { nil }
|
||||||
|
|
||||||
|
it_behaves_like "error function"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the ci_pipeline_ensure_iid_on_drop feature flag is false' do
|
||||||
|
it 'does not ensure the project iid' do
|
||||||
|
stub_feature_flags(ci_pipeline_ensure_iid_on_drop: false)
|
||||||
|
expect(pipeline).to receive(:ensure_project_iid!).once
|
||||||
|
|
||||||
|
subject.error(message, config_error: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -407,13 +407,13 @@ RSpec.describe Gitlab::Database do
|
||||||
expect(described_class.db_read_only?).to be_truthy
|
expect(described_class.db_read_only?).to be_truthy
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'detects a read write database' do
|
it 'detects a read-write database' do
|
||||||
allow(ActiveRecord::Base.connection).to receive(:execute).with('SELECT pg_is_in_recovery()').and_return([{ "pg_is_in_recovery" => "f" }])
|
allow(ActiveRecord::Base.connection).to receive(:execute).with('SELECT pg_is_in_recovery()').and_return([{ "pg_is_in_recovery" => "f" }])
|
||||||
|
|
||||||
expect(described_class.db_read_only?).to be_falsey
|
expect(described_class.db_read_only?).to be_falsey
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'detects a read write database' do
|
it 'detects a read-write database' do
|
||||||
allow(ActiveRecord::Base.connection).to receive(:execute).with('SELECT pg_is_in_recovery()').and_return([{ "pg_is_in_recovery" => false }])
|
allow(ActiveRecord::Base.connection).to receive(:execute).with('SELECT pg_is_in_recovery()').and_return([{ "pg_is_in_recovery" => false }])
|
||||||
|
|
||||||
expect(described_class.db_read_only?).to be_falsey
|
expect(described_class.db_read_only?).to be_falsey
|
||||||
|
|
|
@ -5,6 +5,7 @@ require 'spec_helper'
|
||||||
RSpec.describe Gitlab::HookData::IssueBuilder do
|
RSpec.describe Gitlab::HookData::IssueBuilder do
|
||||||
let_it_be(:label) { create(:label) }
|
let_it_be(:label) { create(:label) }
|
||||||
let_it_be(:issue) { create(:labeled_issue, labels: [label], project: label.project) }
|
let_it_be(:issue) { create(:labeled_issue, labels: [label], project: label.project) }
|
||||||
|
|
||||||
let(:builder) { described_class.new(issue) }
|
let(:builder) { described_class.new(issue) }
|
||||||
|
|
||||||
describe '#build' do
|
describe '#build' do
|
||||||
|
|
|
@ -4,6 +4,7 @@ require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Gitlab::HookData::MergeRequestBuilder do
|
RSpec.describe Gitlab::HookData::MergeRequestBuilder do
|
||||||
let_it_be(:merge_request) { create(:merge_request) }
|
let_it_be(:merge_request) { create(:merge_request) }
|
||||||
|
|
||||||
let(:builder) { described_class.new(merge_request) }
|
let(:builder) { described_class.new(merge_request) }
|
||||||
|
|
||||||
describe '#build' do
|
describe '#build' do
|
||||||
|
|
|
@ -4,6 +4,7 @@ require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Gitlab::HookData::ReleaseBuilder do
|
RSpec.describe Gitlab::HookData::ReleaseBuilder do
|
||||||
let_it_be(:project) { create(:project, :public, :repository) }
|
let_it_be(:project) { create(:project, :public, :repository) }
|
||||||
|
|
||||||
let(:release) { create(:release, project: project) }
|
let(:release) { create(:release, project: project) }
|
||||||
let(:builder) { described_class.new(release) }
|
let(:builder) { described_class.new(release) }
|
||||||
|
|
||||||
|
|
|
@ -32,18 +32,16 @@ RSpec.describe HasTimelogsReport do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#user_can_access_group_timelogs?' do
|
describe '#user_can_access_group_timelogs?' do
|
||||||
before do
|
|
||||||
group.add_developer(user)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns true if user can access group timelogs' do
|
it 'returns true if user can access group timelogs' do
|
||||||
expect(group.user_can_access_group_timelogs?(user)).to be_truthy
|
group.add_developer(user)
|
||||||
|
|
||||||
|
expect(group).to be_user_can_access_group_timelogs(user)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns false if user has insufficient permissions' do
|
it 'returns false if user has insufficient permissions' do
|
||||||
group.add_guest(user)
|
group.add_guest(user)
|
||||||
|
|
||||||
expect(group.user_can_access_group_timelogs?(user)).to be_falsey
|
expect(group).not_to be_user_can_access_group_timelogs(user)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -5795,16 +5795,34 @@ RSpec.describe Project, factory_default: :keep do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#find_or_initialize_services' do
|
describe '#find_or_initialize_services' do
|
||||||
before do
|
let_it_be(:subject) { create(:project) }
|
||||||
allow(Service).to receive(:available_services_names).and_return(%w[prometheus pushover teamcity])
|
|
||||||
allow(subject).to receive(:disabled_services).and_return(%w[prometheus])
|
it 'avoids N+1 database queries' do
|
||||||
|
control_count = ActiveRecord::QueryRecorder.new { subject.find_or_initialize_services }.count
|
||||||
|
|
||||||
|
expect(control_count).to be <= 4
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns only enabled services' do
|
it 'avoids N+1 database queries with more available services' do
|
||||||
services = subject.find_or_initialize_services
|
allow(Service).to receive(:available_services_names).and_return(%w[pushover])
|
||||||
|
control_count = ActiveRecord::QueryRecorder.new { subject.find_or_initialize_services }
|
||||||
|
|
||||||
expect(services.count).to eq(2)
|
allow(Service).to receive(:available_services_names).and_call_original
|
||||||
expect(services.map(&:title)).to eq(['JetBrains TeamCity CI', 'Pushover'])
|
expect { subject.find_or_initialize_services }.not_to exceed_query_limit(control_count)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with disabled services' do
|
||||||
|
before do
|
||||||
|
allow(Service).to receive(:available_services_names).and_return(%w[prometheus pushover teamcity])
|
||||||
|
allow(subject).to receive(:disabled_services).and_return(%w[prometheus])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns only enabled services sorted' do
|
||||||
|
services = subject.find_or_initialize_services
|
||||||
|
|
||||||
|
expect(services.size).to eq(2)
|
||||||
|
expect(services.map(&:title)).to eq(['JetBrains TeamCity CI', 'Pushover'])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -56,9 +56,9 @@ RSpec.describe Timelog do
|
||||||
group = create(:group)
|
group = create(:group)
|
||||||
subgroup = create(:group, parent: group)
|
subgroup = create(:group, parent: group)
|
||||||
|
|
||||||
create(:timelog, issue: create(:issue, project: create(:project)))
|
create(:issue_timelog)
|
||||||
timelog1 = create(:timelog, issue: create(:issue, project: create(:project, group: group)))
|
timelog1 = create(:issue_timelog, issue: create(:issue, project: create(:project, group: group)))
|
||||||
timelog2 = create(:timelog, issue: create(:issue, project: create(:project, group: subgroup)))
|
timelog2 = create(:issue_timelog, issue: create(:issue, project: create(:project, group: subgroup)))
|
||||||
|
|
||||||
expect(described_class.for_issues_in_group(group)).to contain_exactly(timelog1, timelog2)
|
expect(described_class.for_issues_in_group(group)).to contain_exactly(timelog1, timelog2)
|
||||||
end
|
end
|
||||||
|
@ -66,9 +66,9 @@ RSpec.describe Timelog do
|
||||||
|
|
||||||
describe 'between_times' do
|
describe 'between_times' do
|
||||||
it 'returns collection of timelogs within given times' do
|
it 'returns collection of timelogs within given times' do
|
||||||
create(:timelog, spent_at: 65.days.ago)
|
create(:issue_timelog, spent_at: 65.days.ago)
|
||||||
timelog1 = create(:timelog, spent_at: 15.days.ago)
|
timelog1 = create(:issue_timelog, spent_at: 15.days.ago)
|
||||||
timelog2 = create(:timelog, spent_at: 5.days.ago)
|
timelog2 = create(:issue_timelog, spent_at: 5.days.ago)
|
||||||
timelogs = described_class.between_times(20.days.ago, 1.day.ago)
|
timelogs = described_class.between_times(20.days.ago, 1.day.ago)
|
||||||
|
|
||||||
expect(timelogs).to contain_exactly(timelog1, timelog2)
|
expect(timelogs).to contain_exactly(timelog1, timelog2)
|
||||||
|
|
|
@ -14,6 +14,7 @@ RSpec.describe 'Timelogs through GroupQuery' do
|
||||||
let_it_be(:timelog1) { create(:timelog, issue: issue, user: user, spent_at: '2019-08-13 14:00:00') }
|
let_it_be(:timelog1) { create(:timelog, issue: issue, user: user, spent_at: '2019-08-13 14:00:00') }
|
||||||
let_it_be(:timelog2) { create(:timelog, issue: issue, user: user, spent_at: '2019-08-10 08:00:00') }
|
let_it_be(:timelog2) { create(:timelog, issue: issue, user: user, spent_at: '2019-08-10 08:00:00') }
|
||||||
let_it_be(:params) { { startTime: '2019-08-10 12:00:00', endTime: '2019-08-21 12:00:00' } }
|
let_it_be(:params) { { startTime: '2019-08-10 12:00:00', endTime: '2019-08-21 12:00:00' } }
|
||||||
|
|
||||||
let(:timelogs_data) { graphql_data['group']['timelogs']['nodes'] }
|
let(:timelogs_data) { graphql_data['group']['timelogs']['nodes'] }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
@ -34,11 +35,11 @@ RSpec.describe 'Timelogs through GroupQuery' do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'contains correct data', :aggregate_failures do
|
it 'contains correct data', :aggregate_failures do
|
||||||
username = timelog_array.map {|data| data['user']['username'] }
|
username = timelog_array.map { |data| data['user']['username'] }
|
||||||
spent_at = timelog_array.map { |data| data['spentAt'].to_time }
|
spent_at = timelog_array.map { |data| data['spentAt'].to_time }
|
||||||
time_spent = timelog_array.map { |data| data['timeSpent'] }
|
time_spent = timelog_array.map { |data| data['timeSpent'] }
|
||||||
issue_title = timelog_array.map {|data| data['issue']['title'] }
|
issue_title = timelog_array.map { |data| data['issue']['title'] }
|
||||||
milestone_title = timelog_array.map {|data| data['issue']['milestone']['title'] }
|
milestone_title = timelog_array.map { |data| data['issue']['milestone']['title'] }
|
||||||
|
|
||||||
expect(username).to eq([user.username])
|
expect(username).to eq([user.username])
|
||||||
expect(spent_at.first).to be_like_time(timelog1.spent_at)
|
expect(spent_at.first).to be_like_time(timelog1.spent_at)
|
||||||
|
@ -50,7 +51,7 @@ RSpec.describe 'Timelogs through GroupQuery' do
|
||||||
context 'when arguments with no time are present' do
|
context 'when arguments with no time are present' do
|
||||||
let!(:timelog3) { create(:timelog, issue: issue, user: user, spent_at: '2019-08-10 15:00:00') }
|
let!(:timelog3) { create(:timelog, issue: issue, user: user, spent_at: '2019-08-10 15:00:00') }
|
||||||
let!(:timelog4) { create(:timelog, issue: issue, user: user, spent_at: '2019-08-21 15:00:00') }
|
let!(:timelog4) { create(:timelog, issue: issue, user: user, spent_at: '2019-08-21 15:00:00') }
|
||||||
let(:params) { { startDate: '2019-08-10', endDate: '2019-08-21' }}
|
let(:params) { { startDate: '2019-08-10', endDate: '2019-08-21' } }
|
||||||
|
|
||||||
it 'sets times as start of day and end of day' do
|
it 'sets times as start of day and end of day' do
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
@ -111,12 +112,10 @@ RSpec.describe 'Timelogs through GroupQuery' do
|
||||||
}
|
}
|
||||||
NODE
|
NODE
|
||||||
|
|
||||||
graphql_query_for("group", { "fullPath" => group.full_path },
|
graphql_query_for(
|
||||||
[query_graphql_field(
|
:group,
|
||||||
"timelogs",
|
{ full_path: group.full_path },
|
||||||
timelog_params,
|
query_graphql_field(:timelogs, timelog_params, timelog_nodes)
|
||||||
timelog_nodes
|
|
||||||
)]
|
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,14 +5,14 @@ require 'spec_helper'
|
||||||
RSpec.describe 'getting an issue list for a project' do
|
RSpec.describe 'getting an issue list for a project' do
|
||||||
include GraphqlHelpers
|
include GraphqlHelpers
|
||||||
|
|
||||||
let(:issues_data) { graphql_data['project']['issues']['edges'] }
|
|
||||||
|
|
||||||
let_it_be(:project) { create(:project, :repository, :public) }
|
let_it_be(:project) { create(:project, :repository, :public) }
|
||||||
let_it_be(:current_user) { create(:user) }
|
let_it_be(:current_user) { create(:user) }
|
||||||
let_it_be(:issue_a, reload: true) { create(:issue, project: project, discussion_locked: true) }
|
let_it_be(:issue_a, reload: true) { create(:issue, project: project, discussion_locked: true) }
|
||||||
let_it_be(:issue_b, reload: true) { create(:issue, :with_alert, project: project) }
|
let_it_be(:issue_b, reload: true) { create(:issue, :with_alert, project: project) }
|
||||||
let_it_be(:issues, reload: true) { [issue_a, issue_b] }
|
let_it_be(:issues, reload: true) { [issue_a, issue_b] }
|
||||||
|
|
||||||
|
let(:issues_data) { graphql_data['project']['issues']['edges'] }
|
||||||
|
|
||||||
let(:fields) do
|
let(:fields) do
|
||||||
<<~QUERY
|
<<~QUERY
|
||||||
edges {
|
edges {
|
||||||
|
@ -76,7 +76,7 @@ RSpec.describe 'getting an issue list for a project' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'no limit is provided' do
|
context 'when no limit is provided' do
|
||||||
let(:issue_limit) { nil }
|
let(:issue_limit) { nil }
|
||||||
|
|
||||||
it 'returns all issues' do
|
it 'returns all issues' do
|
||||||
|
@ -143,13 +143,15 @@ RSpec.describe 'getting an issue list for a project' do
|
||||||
let_it_be(:data_path) { [:project, :issues] }
|
let_it_be(:data_path) { [:project, :issues] }
|
||||||
|
|
||||||
def pagination_query(params)
|
def pagination_query(params)
|
||||||
graphql_query_for(:project, { full_path: sort_project.full_path },
|
graphql_query_for(
|
||||||
|
:project,
|
||||||
|
{ full_path: sort_project.full_path },
|
||||||
query_graphql_field(:issues, params, "#{page_info} nodes { iid }")
|
query_graphql_field(:issues, params, "#{page_info} nodes { iid }")
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def pagination_results_data(data)
|
def pagination_results_data(data)
|
||||||
data.map { |issue| issue.dig('iid').to_i }
|
data.map { |issue| issue['iid'].to_i }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when sorting by due date' do
|
context 'when sorting by due date' do
|
||||||
|
@ -189,27 +191,38 @@ RSpec.describe 'getting an issue list for a project' do
|
||||||
it_behaves_like 'sorted paginated query' do
|
it_behaves_like 'sorted paginated query' do
|
||||||
let(:sort_param) { :RELATIVE_POSITION_ASC }
|
let(:sort_param) { :RELATIVE_POSITION_ASC }
|
||||||
let(:first_param) { 2 }
|
let(:first_param) { 2 }
|
||||||
let(:expected_results) { [relative_issue5.iid, relative_issue3.iid, relative_issue1.iid, relative_issue4.iid, relative_issue2.iid] }
|
let(:expected_results) do
|
||||||
|
[
|
||||||
|
relative_issue5.iid, relative_issue3.iid, relative_issue1.iid,
|
||||||
|
relative_issue4.iid, relative_issue2.iid
|
||||||
|
]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when sorting by priority' do
|
context 'when sorting by priority' do
|
||||||
let_it_be(:sort_project) { create(:project, :public) }
|
let_it_be(:sort_project) { create(:project, :public) }
|
||||||
let_it_be(:early_milestone) { create(:milestone, project: sort_project, due_date: 10.days.from_now) }
|
let_it_be(:on_project) { { project: sort_project } }
|
||||||
let_it_be(:late_milestone) { create(:milestone, project: sort_project, due_date: 30.days.from_now) }
|
let_it_be(:early_milestone) { create(:milestone, **on_project, due_date: 10.days.from_now) }
|
||||||
let_it_be(:priority_label1) { create(:label, project: sort_project, priority: 1) }
|
let_it_be(:late_milestone) { create(:milestone, **on_project, due_date: 30.days.from_now) }
|
||||||
let_it_be(:priority_label2) { create(:label, project: sort_project, priority: 5) }
|
let_it_be(:priority_1) { create(:label, **on_project, priority: 1) }
|
||||||
let_it_be(:priority_issue1) { create(:issue, project: sort_project, labels: [priority_label1], milestone: late_milestone) }
|
let_it_be(:priority_2) { create(:label, **on_project, priority: 5) }
|
||||||
let_it_be(:priority_issue2) { create(:issue, project: sort_project, labels: [priority_label2]) }
|
let_it_be(:priority_issue1) { create(:issue, **on_project, labels: [priority_1], milestone: late_milestone) }
|
||||||
let_it_be(:priority_issue3) { create(:issue, project: sort_project, milestone: early_milestone) }
|
let_it_be(:priority_issue2) { create(:issue, **on_project, labels: [priority_2]) }
|
||||||
let_it_be(:priority_issue4) { create(:issue, project: sort_project) }
|
let_it_be(:priority_issue3) { create(:issue, **on_project, milestone: early_milestone) }
|
||||||
|
let_it_be(:priority_issue4) { create(:issue, **on_project) }
|
||||||
|
|
||||||
context 'when ascending' do
|
context 'when ascending' do
|
||||||
it_behaves_like 'sorted paginated query' do
|
it_behaves_like 'sorted paginated query' do
|
||||||
let(:sort_param) { :PRIORITY_ASC }
|
let(:sort_param) { :PRIORITY_ASC }
|
||||||
let(:first_param) { 2 }
|
let(:first_param) { 2 }
|
||||||
let(:expected_results) { [priority_issue3.iid, priority_issue1.iid, priority_issue2.iid, priority_issue4.iid] }
|
let(:expected_results) do
|
||||||
|
[
|
||||||
|
priority_issue3.iid, priority_issue1.iid,
|
||||||
|
priority_issue2.iid, priority_issue4.iid
|
||||||
|
]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -217,7 +230,9 @@ RSpec.describe 'getting an issue list for a project' do
|
||||||
it_behaves_like 'sorted paginated query' do
|
it_behaves_like 'sorted paginated query' do
|
||||||
let(:sort_param) { :PRIORITY_DESC }
|
let(:sort_param) { :PRIORITY_DESC }
|
||||||
let(:first_param) { 2 }
|
let(:first_param) { 2 }
|
||||||
let(:expected_results) { [priority_issue1.iid, priority_issue3.iid, priority_issue2.iid, priority_issue4.iid] }
|
let(:expected_results) do
|
||||||
|
[priority_issue1.iid, priority_issue3.iid, priority_issue2.iid, priority_issue4.iid]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -275,7 +290,7 @@ RSpec.describe 'getting an issue list for a project' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'fetching alert management alert' do
|
context 'when fetching alert management alert' do
|
||||||
let(:fields) do
|
let(:fields) do
|
||||||
<<~QUERY
|
<<~QUERY
|
||||||
edges {
|
edges {
|
||||||
|
@ -297,7 +312,7 @@ RSpec.describe 'getting an issue list for a project' do
|
||||||
it 'avoids N+1 queries' do
|
it 'avoids N+1 queries' do
|
||||||
control = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: current_user) }
|
control = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: current_user) }
|
||||||
|
|
||||||
create(:alert_management_alert, :with_issue, project: project )
|
create(:alert_management_alert, :with_issue, project: project)
|
||||||
|
|
||||||
expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(control)
|
expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(control)
|
||||||
end
|
end
|
||||||
|
@ -312,7 +327,7 @@ RSpec.describe 'getting an issue list for a project' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'fetching labels' do
|
context 'when fetching labels' do
|
||||||
let(:fields) do
|
let(:fields) do
|
||||||
<<~QUERY
|
<<~QUERY
|
||||||
edges {
|
edges {
|
||||||
|
@ -362,7 +377,7 @@ RSpec.describe 'getting an issue list for a project' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'fetching assignees' do
|
context 'when fetching assignees' do
|
||||||
let(:fields) do
|
let(:fields) do
|
||||||
<<~QUERY
|
<<~QUERY
|
||||||
edges {
|
edges {
|
||||||
|
@ -420,9 +435,10 @@ RSpec.describe 'getting an issue list for a project' do
|
||||||
query = graphql_query_for(
|
query = graphql_query_for(
|
||||||
:project,
|
:project,
|
||||||
{ full_path: project.full_path },
|
{ full_path: project.full_path },
|
||||||
query_graphql_field(:issues, search_params, [
|
query_graphql_field(
|
||||||
|
:issues, search_params,
|
||||||
query_graphql_field(:nodes, nil, requested_fields)
|
query_graphql_field(:nodes, nil, requested_fields)
|
||||||
])
|
)
|
||||||
)
|
)
|
||||||
post_graphql(query, current_user: current_user)
|
post_graphql(query, current_user: current_user)
|
||||||
end
|
end
|
||||||
|
@ -448,5 +464,16 @@ RSpec.describe 'getting an issue list for a project' do
|
||||||
|
|
||||||
include_examples 'N+1 query check'
|
include_examples 'N+1 query check'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when requesting `timelogs`' do
|
||||||
|
let(:requested_fields) { 'timelogs { nodes { timeSpent } }' }
|
||||||
|
|
||||||
|
before do
|
||||||
|
create_list(:issue_timelog, 2, issue: issue_a)
|
||||||
|
create(:issue_timelog, issue: issue_b)
|
||||||
|
end
|
||||||
|
|
||||||
|
include_examples 'N+1 query check'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -299,6 +299,7 @@ RSpec.describe 'getting merge request listings nested in a project' do
|
||||||
reviewers { nodes { username } }
|
reviewers { nodes { username } }
|
||||||
participants { nodes { username } }
|
participants { nodes { username } }
|
||||||
headPipeline { status }
|
headPipeline { status }
|
||||||
|
timelogs { nodes { timeSpent } }
|
||||||
SELECT
|
SELECT
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -307,7 +308,7 @@ RSpec.describe 'getting merge request listings nested in a project' do
|
||||||
query($first: Int) {
|
query($first: Int) {
|
||||||
project(fullPath: "#{project.full_path}") {
|
project(fullPath: "#{project.full_path}") {
|
||||||
mergeRequests(first: $first) {
|
mergeRequests(first: $first) {
|
||||||
nodes { #{mr_fields} }
|
nodes { iid #{mr_fields} }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -324,6 +325,7 @@ RSpec.describe 'getting merge request listings nested in a project' do
|
||||||
mr.assignees << current_user
|
mr.assignees << current_user
|
||||||
mr.reviewers << create(:user)
|
mr.reviewers << create(:user)
|
||||||
mr.reviewers << current_user
|
mr.reviewers << current_user
|
||||||
|
mr.timelogs << create(:merge_request_timelog, merge_request: mr)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -345,7 +347,7 @@ RSpec.describe 'getting merge request listings nested in a project' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_collection
|
def user_collection
|
||||||
{ 'nodes' => all(match(a_hash_including('username' => be_present))) }
|
{ 'nodes' => be_present.and(all(match(a_hash_including('username' => be_present)))) }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns appropriate results' do
|
it 'returns appropriate results' do
|
||||||
|
@ -358,7 +360,8 @@ RSpec.describe 'getting merge request listings nested in a project' do
|
||||||
'assignees' => user_collection,
|
'assignees' => user_collection,
|
||||||
'reviewers' => user_collection,
|
'reviewers' => user_collection,
|
||||||
'participants' => user_collection,
|
'participants' => user_collection,
|
||||||
'headPipeline' => { 'status' => be_present }
|
'headPipeline' => { 'status' => be_present },
|
||||||
|
'timelogs' => { 'nodes' => be_one }
|
||||||
)))
|
)))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ require 'spec_helper'
|
||||||
RSpec.describe Groups::AutoDevopsService, '#execute' do
|
RSpec.describe Groups::AutoDevopsService, '#execute' do
|
||||||
let_it_be(:group) { create(:group) }
|
let_it_be(:group) { create(:group) }
|
||||||
let_it_be(:user) { create(:user) }
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
||||||
let(:group_params) { { auto_devops_enabled: '0' } }
|
let(:group_params) { { auto_devops_enabled: '0' } }
|
||||||
let(:service) { described_class.new(group, user, group_params) }
|
let(:service) { described_class.new(group, user, group_params) }
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ RSpec.describe Groups::GroupLinks::UpdateService, '#execute' do
|
||||||
let_it_be(:group) { create(:group, :private) }
|
let_it_be(:group) { create(:group, :private) }
|
||||||
let_it_be(:shared_group) { create(:group, :private) }
|
let_it_be(:shared_group) { create(:group, :private) }
|
||||||
let_it_be(:project) { create(:project, group: shared_group) }
|
let_it_be(:project) { create(:project, group: shared_group) }
|
||||||
|
|
||||||
let(:group_member_user) { create(:user) }
|
let(:group_member_user) { create(:user) }
|
||||||
let!(:link) { create(:group_group_link, shared_group: shared_group, shared_with_group: group) }
|
let!(:link) { create(:group_group_link, shared_group: shared_group, shared_with_group: group) }
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,14 @@ require 'spec_helper'
|
||||||
RSpec.describe Groups::TransferService do
|
RSpec.describe Groups::TransferService do
|
||||||
let_it_be(:user) { create(:user) }
|
let_it_be(:user) { create(:user) }
|
||||||
let_it_be(:new_parent_group) { create(:group, :public) }
|
let_it_be(:new_parent_group) { create(:group, :public) }
|
||||||
|
|
||||||
let!(:group_member) { create(:group_member, :owner, group: group, user: user) }
|
let!(:group_member) { create(:group_member, :owner, group: group, user: user) }
|
||||||
let(:transfer_service) { described_class.new(group, user) }
|
let(:transfer_service) { described_class.new(group, user) }
|
||||||
|
|
||||||
context 'handling packages' do
|
context 'handling packages' do
|
||||||
let_it_be(:group) { create(:group, :public) }
|
let_it_be(:group) { create(:group, :public) }
|
||||||
let_it_be(:new_group) { create(:group, :public) }
|
let_it_be(:new_group) { create(:group, :public) }
|
||||||
|
|
||||||
let(:project) { create(:project, :public, namespace: group) }
|
let(:project) { create(:project, :public, namespace: group) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
@ -272,6 +274,7 @@ RSpec.describe Groups::TransferService do
|
||||||
|
|
||||||
context 'with a group integration' do
|
context 'with a group integration' do
|
||||||
let_it_be(:instance_integration) { create(:slack_service, :instance, webhook: 'http://project.slack.com') }
|
let_it_be(:instance_integration) { create(:slack_service, :instance, webhook: 'http://project.slack.com') }
|
||||||
|
|
||||||
let(:new_created_integration) { Service.find_by(group: group) }
|
let(:new_created_integration) { Service.find_by(group: group) }
|
||||||
|
|
||||||
context 'with an inherited integration' do
|
context 'with an inherited integration' do
|
||||||
|
|
|
@ -59,6 +59,7 @@ RSpec.describe Groups::UpdateSharedRunnersService do
|
||||||
|
|
||||||
context 'disable shared Runners' do
|
context 'disable shared Runners' do
|
||||||
let_it_be(:group) { create(:group) }
|
let_it_be(:group) { create(:group) }
|
||||||
|
|
||||||
let(:params) { { shared_runners_setting: 'disabled_and_unoverridable' } }
|
let(:params) { { shared_runners_setting: 'disabled_and_unoverridable' } }
|
||||||
|
|
||||||
it 'receives correct method and succeeds' do
|
it 'receives correct method and succeeds' do
|
||||||
|
|
|
@ -5,6 +5,7 @@ require 'spec_helper'
|
||||||
RSpec.describe Ide::BaseConfigService do
|
RSpec.describe Ide::BaseConfigService do
|
||||||
let_it_be(:project) { create(:project, :repository) }
|
let_it_be(:project) { create(:project, :repository) }
|
||||||
let_it_be(:user) { create(:user) }
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
||||||
let(:sha) { 'sha' }
|
let(:sha) { 'sha' }
|
||||||
|
|
||||||
describe '#execute' do
|
describe '#execute' do
|
||||||
|
|
|
@ -5,6 +5,7 @@ require 'spec_helper'
|
||||||
RSpec.describe Ide::SchemasConfigService do
|
RSpec.describe Ide::SchemasConfigService do
|
||||||
let_it_be(:project) { create(:project, :repository) }
|
let_it_be(:project) { create(:project, :repository) }
|
||||||
let_it_be(:user) { create(:user) }
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
||||||
let(:filename) { 'sample.yml' }
|
let(:filename) { 'sample.yml' }
|
||||||
let(:schema_content) { double(body: '{"title":"Sample schema"}') }
|
let(:schema_content) { double(body: '{"title":"Sample schema"}') }
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ require 'spec_helper'
|
||||||
RSpec.describe Ide::TerminalConfigService do
|
RSpec.describe Ide::TerminalConfigService do
|
||||||
let_it_be(:project) { create(:project, :repository) }
|
let_it_be(:project) { create(:project, :repository) }
|
||||||
let_it_be(:user) { create(:user) }
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
||||||
let(:sha) { 'sha' }
|
let(:sha) { 'sha' }
|
||||||
|
|
||||||
describe '#execute' do
|
describe '#execute' do
|
||||||
|
|
|
@ -6,6 +6,7 @@ RSpec.describe Issues::BuildService do
|
||||||
let_it_be(:project) { create(:project, :repository) }
|
let_it_be(:project) { create(:project, :repository) }
|
||||||
let_it_be(:developer) { create(:user) }
|
let_it_be(:developer) { create(:user) }
|
||||||
let_it_be(:guest) { create(:user) }
|
let_it_be(:guest) { create(:user) }
|
||||||
|
|
||||||
let(:user) { developer }
|
let(:user) { developer }
|
||||||
|
|
||||||
before_all do
|
before_all do
|
||||||
|
|
|
@ -242,6 +242,7 @@ RSpec.describe Issues::CloneService do
|
||||||
|
|
||||||
context 'issue with a design', :clean_gitlab_redis_shared_state do
|
context 'issue with a design', :clean_gitlab_redis_shared_state do
|
||||||
let_it_be(:new_project) { create(:project) }
|
let_it_be(:new_project) { create(:project) }
|
||||||
|
|
||||||
let!(:design) { create(:design, :with_lfs_file, issue: old_issue) }
|
let!(:design) { create(:design, :with_lfs_file, issue: old_issue) }
|
||||||
let!(:note) { create(:diff_note_on_design, noteable: design, issue: old_issue, project: old_issue.project) }
|
let!(:note) { create(:diff_note_on_design, noteable: design, issue: old_issue, project: old_issue.project) }
|
||||||
let(:subject) { clone_service.execute(old_issue, new_project) }
|
let(:subject) { clone_service.execute(old_issue, new_project) }
|
||||||
|
|
|
@ -11,6 +11,7 @@ RSpec.describe Issues::CreateService do
|
||||||
describe '#execute' do
|
describe '#execute' do
|
||||||
let_it_be(:assignee) { create(:user) }
|
let_it_be(:assignee) { create(:user) }
|
||||||
let_it_be(:milestone) { create(:milestone, project: project) }
|
let_it_be(:milestone) { create(:milestone, project: project) }
|
||||||
|
|
||||||
let(:issue) { described_class.new(project, user, opts).execute }
|
let(:issue) { described_class.new(project, user, opts).execute }
|
||||||
|
|
||||||
context 'when params are valid' do
|
context 'when params are valid' do
|
||||||
|
|
|
@ -8,6 +8,7 @@ RSpec.describe Issues::ExportCsvService do
|
||||||
let_it_be(:project) { create(:project, :public, group: group) }
|
let_it_be(:project) { create(:project, :public, group: group) }
|
||||||
let_it_be(:issue) { create(:issue, project: project, author: user) }
|
let_it_be(:issue) { create(:issue, project: project, author: user) }
|
||||||
let_it_be(:bad_issue) { create(:issue, project: project, author: user) }
|
let_it_be(:bad_issue) { create(:issue, project: project, author: user) }
|
||||||
|
|
||||||
subject { described_class.new(Issue.all, project) }
|
subject { described_class.new(Issue.all, project) }
|
||||||
|
|
||||||
it 'renders csv to string' do
|
it 'renders csv to string' do
|
||||||
|
|
|
@ -206,6 +206,7 @@ RSpec.describe Issues::MoveService do
|
||||||
|
|
||||||
context 'issue with a design', :clean_gitlab_redis_shared_state do
|
context 'issue with a design', :clean_gitlab_redis_shared_state do
|
||||||
let_it_be(:new_project) { create(:project) }
|
let_it_be(:new_project) { create(:project) }
|
||||||
|
|
||||||
let!(:design) { create(:design, :with_lfs_file, issue: old_issue) }
|
let!(:design) { create(:design, :with_lfs_file, issue: old_issue) }
|
||||||
let!(:note) { create(:diff_note_on_design, noteable: design, issue: old_issue, project: old_issue.project) }
|
let!(:note) { create(:diff_note_on_design, noteable: design, issue: old_issue, project: old_issue.project) }
|
||||||
let(:subject) { move_service.execute(old_issue, new_project) }
|
let(:subject) { move_service.execute(old_issue, new_project) }
|
||||||
|
|
|
@ -5,6 +5,7 @@ require 'spec_helper'
|
||||||
RSpec.describe Issues::RelatedBranchesService do
|
RSpec.describe Issues::RelatedBranchesService do
|
||||||
let_it_be(:developer) { create(:user) }
|
let_it_be(:developer) { create(:user) }
|
||||||
let_it_be(:issue) { create(:issue) }
|
let_it_be(:issue) { create(:issue) }
|
||||||
|
|
||||||
let(:user) { developer }
|
let(:user) { developer }
|
||||||
|
|
||||||
subject { described_class.new(issue.project, user) }
|
subject { described_class.new(issue.project, user) }
|
||||||
|
|
|
@ -724,9 +724,7 @@ RSpec.describe Projects::CreateService, '#execute' do
|
||||||
|
|
||||||
it 'cleans invalid record and logs warning', :aggregate_failures do
|
it 'cleans invalid record and logs warning', :aggregate_failures do
|
||||||
invalid_service_record = build(:prometheus_service, properties: { api_url: nil, manual_configuration: true }.to_json)
|
invalid_service_record = build(:prometheus_service, properties: { api_url: nil, manual_configuration: true }.to_json)
|
||||||
allow_next_instance_of(Project) do |instance|
|
allow(PrometheusService).to receive(:new).and_return(invalid_service_record)
|
||||||
allow(instance).to receive(:build_prometheus_service).and_return(invalid_service_record)
|
|
||||||
end
|
|
||||||
|
|
||||||
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(an_instance_of(ActiveRecord::RecordInvalid), include(extra: { project_id: a_kind_of(Integer) }))
|
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(an_instance_of(ActiveRecord::RecordInvalid), include(extra: { project_id: a_kind_of(Integer) }))
|
||||||
project = create_project(user, opts)
|
project = create_project(user, opts)
|
||||||
|
|
|
@ -55,6 +55,12 @@ RSpec.describe Projects::UpdatePagesService do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'creates a temporary directory with the project and build ID' do
|
||||||
|
expect(Dir).to receive(:mktmpdir).with("project-#{project.id}-build-#{build.id}-", anything).and_call_original
|
||||||
|
|
||||||
|
subject.execute
|
||||||
|
end
|
||||||
|
|
||||||
it "doesn't deploy to legacy storage if it's disabled" do
|
it "doesn't deploy to legacy storage if it's disabled" do
|
||||||
allow(Settings.pages.local_store).to receive(:enabled).and_return(false)
|
allow(Settings.pages.local_store).to receive(:enabled).and_return(false)
|
||||||
|
|
||||||
|
|
50
spec/views/profiles/keys/_form.html.haml_spec.rb
Normal file
50
spec/views/profiles/keys/_form.html.haml_spec.rb
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'profiles/keys/_form.html.haml' do
|
||||||
|
let_it_be(:key) { Key.new }
|
||||||
|
|
||||||
|
let(:page) { Capybara::Node::Simple.new(rendered) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
assign(:key, key)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the form partial is used' do
|
||||||
|
before do
|
||||||
|
allow(view).to receive(:ssh_key_expires_field_description).and_return('Key can still be used after expiration.')
|
||||||
|
|
||||||
|
render
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders the form with the correct action' do
|
||||||
|
expect(page.find('form')['action']).to eq('/-/profile/keys')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has the key field', :aggregate_failures do
|
||||||
|
expect(rendered).to have_field('Key', type: 'textarea', placeholder: 'Typically starts with "ssh-ed25519 …" or "ssh-rsa …"')
|
||||||
|
expect(rendered).to have_text("Paste your public SSH key, which is usually contained in the file '~/.ssh/id_ed25519.pub' or '~/.ssh/id_rsa.pub' and begins with 'ssh-ed25519' or 'ssh-rsa'. Do not paste your private SSH key, as that can compromise your identity.")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has the title field', :aggregate_failures do
|
||||||
|
expect(rendered).to have_field('Title', type: 'text', placeholder: 'e.g. My MacBook key')
|
||||||
|
expect(rendered).to have_text('Give your individual key a title.')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has the expires at field', :aggregate_failures do
|
||||||
|
expect(rendered).to have_field('Expires at', type: 'date')
|
||||||
|
expect(page.find_field('Expires at')['min']).to eq(l(1.day.from_now, format: "%Y-%m-%d"))
|
||||||
|
expect(rendered).to have_text('Key can still be used after expiration.')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has the validation warning', :aggregate_failures do
|
||||||
|
expect(rendered).to have_text("Oops, are you sure? Publicly visible private SSH keys can compromise your system.")
|
||||||
|
expect(rendered).to have_button('Yes, add it')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has the submit button' do
|
||||||
|
expect(rendered).to have_button('Add key')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
109
spec/views/profiles/keys/_key.html.haml_spec.rb
Normal file
109
spec/views/profiles/keys/_key.html.haml_spec.rb
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'profiles/keys/_key.html.haml' do
|
||||||
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(view).to receive(:key).and_return(key)
|
||||||
|
allow(view).to receive(:is_admin).and_return(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the key partial is used' do
|
||||||
|
let_it_be(:key) do
|
||||||
|
create(:personal_key,
|
||||||
|
user: user,
|
||||||
|
last_used_at: 7.days.ago,
|
||||||
|
expires_at: 2.days.from_now)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'displays the correct values', :aggregate_failures do
|
||||||
|
render
|
||||||
|
|
||||||
|
expect(rendered).to have_text(key.title)
|
||||||
|
expect(rendered).to have_css('[data-testid="key-icon"]')
|
||||||
|
expect(rendered).to have_text(key.fingerprint)
|
||||||
|
expect(rendered).to have_text(l(key.last_used_at, format: "%b %d, %Y"))
|
||||||
|
expect(rendered).to have_text(l(key.created_at, format: "%b %d, %Y"))
|
||||||
|
expect(rendered).to have_text(key.expires_at.to_date)
|
||||||
|
expect(response).to render_template(partial: 'shared/ssh_keys/_key_delete')
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the key has not been used' do
|
||||||
|
let_it_be(:key) do
|
||||||
|
create(:personal_key,
|
||||||
|
user: user,
|
||||||
|
last_used_at: nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders "Never" for last used' do
|
||||||
|
render
|
||||||
|
|
||||||
|
expect(rendered).to have_text('Last used: Never')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the key does not have an expiration date' do
|
||||||
|
let_it_be(:key) do
|
||||||
|
create(:personal_key,
|
||||||
|
user: user,
|
||||||
|
expires_at: nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders "Never" for expires' do
|
||||||
|
render
|
||||||
|
|
||||||
|
expect(rendered).to have_text('Expires: Never')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the key is not deletable' do
|
||||||
|
# Turns out key.can_delete? is only false for LDAP keys
|
||||||
|
# but LDAP keys don't exist outside EE
|
||||||
|
before do
|
||||||
|
allow(key).to receive(:can_delete?).and_return(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not render the partial' do
|
||||||
|
render
|
||||||
|
|
||||||
|
expect(response).not_to render_template(partial: 'shared/ssh_keys/_key_delete')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'icon tooltip' do
|
||||||
|
using RSpec::Parameterized::TableSyntax
|
||||||
|
|
||||||
|
where(:valid, :expiry, :result) do
|
||||||
|
false | 2.days.from_now | 'Key type is forbidden. Must be DSA, ECDSA, or ED25519'
|
||||||
|
false | 2.days.ago | 'Key type is forbidden. Must be DSA, ECDSA, or ED25519'
|
||||||
|
true | 2.days.ago | 'Key usable beyond expiration date.'
|
||||||
|
true | 2.days.from_now | ''
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
let_it_be(:key) do
|
||||||
|
create(:personal_key, user: user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'renders the correct icon', :aggregate_failures do
|
||||||
|
unless valid
|
||||||
|
stub_application_setting(rsa_key_restriction: ApplicationSetting::FORBIDDEN_KEY_VALUE)
|
||||||
|
end
|
||||||
|
|
||||||
|
key.expires_at = expiry
|
||||||
|
|
||||||
|
render
|
||||||
|
|
||||||
|
if result.empty?
|
||||||
|
expect(rendered).to have_css('[data-testid="key-icon"]')
|
||||||
|
else
|
||||||
|
expect(rendered).to have_css('[data-testid="warning-solid-icon"]')
|
||||||
|
expect(rendered).to have_selector("span.has-tooltip[title='#{result}']")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -64,10 +64,7 @@ RSpec.describe Projects::PostCreationWorker do
|
||||||
|
|
||||||
it 'cleans invalid record and logs warning', :aggregate_failures do
|
it 'cleans invalid record and logs warning', :aggregate_failures do
|
||||||
invalid_service_record = build(:prometheus_service, properties: { api_url: nil, manual_configuration: true }.to_json)
|
invalid_service_record = build(:prometheus_service, properties: { api_url: nil, manual_configuration: true }.to_json)
|
||||||
|
allow(PrometheusService).to receive(:new).and_return(invalid_service_record)
|
||||||
allow_next_found_instance_of(Project) do |instance|
|
|
||||||
allow(instance).to receive(:build_prometheus_service).and_return(invalid_service_record)
|
|
||||||
end
|
|
||||||
|
|
||||||
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(an_instance_of(ActiveRecord::RecordInvalid), include(extra: { project_id: a_kind_of(Integer) })).twice
|
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(an_instance_of(ActiveRecord::RecordInvalid), include(extra: { project_id: a_kind_of(Integer) })).twice
|
||||||
subject
|
subject
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue