Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
3884d9d716
commit
5150ecc452
|
|
@ -12,3 +12,4 @@ Metrics/AbcSize:
|
|||
- 'lib/gitlab/analytics/cycle_analytics/request_params.rb'
|
||||
- 'lib/gitlab/sidekiq_middleware/server_metrics.rb'
|
||||
- 'qa/qa/resource/repository/push.rb'
|
||||
- 'ee/db/seeds/awesome_co/**/*.rb'
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@
|
|||
"workflow": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "$ref": "#/definitions/workflowName" },
|
||||
"rules": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
|
@ -714,6 +715,12 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"workflowName": {
|
||||
"type": "string",
|
||||
"markdownDescription": "Defines the pipeline name. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#workflowname).",
|
||||
"minLength": 1,
|
||||
"maxLength": 255
|
||||
},
|
||||
"globalVariables": {
|
||||
"markdownDescription": "Defines default variables for all jobs. Job level property overrides global variables. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#variables).",
|
||||
"type": "object",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { visitUrl } from '~/lib/utils/url_utility';
|
|||
import { truncate } from '~/lib/utils/text_utility';
|
||||
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import Tracking from '~/tracking';
|
||||
import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue';
|
||||
import {
|
||||
FIRST_DROPDOWN_INDEX,
|
||||
|
|
@ -163,8 +164,17 @@ export default {
|
|||
...mapActions(['setSearch', 'fetchAutocompleteOptions', 'clearAutocomplete']),
|
||||
openDropdown() {
|
||||
this.showDropdown = true;
|
||||
this.isFocused = true;
|
||||
this.$emit('expandSearchBar', true);
|
||||
|
||||
// check isFocused state to avoid firing duplicate events
|
||||
if (!this.isFocused) {
|
||||
this.isFocused = true;
|
||||
this.$emit('expandSearchBar', true);
|
||||
|
||||
Tracking.event(undefined, 'focus_input', {
|
||||
label: 'global_search',
|
||||
property: 'top_navigation',
|
||||
});
|
||||
}
|
||||
},
|
||||
closeDropdown() {
|
||||
this.showDropdown = false;
|
||||
|
|
@ -178,6 +188,11 @@ export default {
|
|||
this.showDropdown = false;
|
||||
this.isFocused = false;
|
||||
this.$emit('collapseSearchBar');
|
||||
|
||||
Tracking.event(undefined, 'blur_input', {
|
||||
label: 'global_search',
|
||||
property: 'top_navigation',
|
||||
});
|
||||
}, 200);
|
||||
},
|
||||
submitSearch() {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { GlDropdown, GlDropdownItem, GlListbox } from '@gitlab/ui';
|
||||
import { GlListbox } from '@gitlab/ui';
|
||||
import Vue from 'vue';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
|
||||
|
|
@ -31,59 +31,25 @@ export function initListbox(el, { onChange } = {}) {
|
|||
},
|
||||
},
|
||||
render(h) {
|
||||
if (gon.features?.glListboxForSortDropdowns) {
|
||||
return h(GlListbox, {
|
||||
props: {
|
||||
items,
|
||||
right,
|
||||
selected: this.selected,
|
||||
toggleText: this.text,
|
||||
},
|
||||
class: className,
|
||||
on: {
|
||||
select: (selectedValue) => {
|
||||
this.selected = selectedValue;
|
||||
const selectedItem = items.find(({ value }) => value === selectedValue);
|
||||
|
||||
if (typeof onChange === 'function') {
|
||||
onChange(selectedItem);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return h(
|
||||
GlDropdown,
|
||||
{
|
||||
props: {
|
||||
text: this.text,
|
||||
right,
|
||||
},
|
||||
class: className,
|
||||
return h(GlListbox, {
|
||||
props: {
|
||||
items,
|
||||
right,
|
||||
selected: this.selected,
|
||||
toggleText: this.text,
|
||||
},
|
||||
items.map((item) =>
|
||||
h(
|
||||
GlDropdownItem,
|
||||
{
|
||||
props: {
|
||||
isCheckItem: true,
|
||||
isChecked: this.selected === item.value,
|
||||
},
|
||||
on: {
|
||||
click: () => {
|
||||
this.selected = item.value;
|
||||
class: className,
|
||||
on: {
|
||||
select: (selectedValue) => {
|
||||
this.selected = selectedValue;
|
||||
const selectedItem = items.find(({ value }) => value === selectedValue);
|
||||
|
||||
if (typeof onChange === 'function') {
|
||||
onChange(item);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
item.text,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (typeof onChange === 'function') {
|
||||
onChange(selectedItem);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import { GlNav, GlIcon, GlNavItemDropdown, GlDropdownForm, GlTooltipDirective } from '@gitlab/ui';
|
||||
import Tracking from '~/tracking';
|
||||
import TopNavDropdownMenu from './top_nav_dropdown_menu.vue';
|
||||
|
||||
export default {
|
||||
|
|
@ -19,6 +20,14 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
trackToggleEvent() {
|
||||
Tracking.event(undefined, 'click_nav', {
|
||||
label: 'hamburger_menu',
|
||||
property: 'top_navigation',
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -32,6 +41,7 @@ export default {
|
|||
toggle-class="top-nav-toggle js-top-nav-dropdown-toggle gl-px-3!"
|
||||
no-flip
|
||||
no-caret
|
||||
@toggle="trackToggleEvent"
|
||||
>
|
||||
<template #button-content>
|
||||
<gl-icon name="hamburger" />
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
<script>
|
||||
import pdfjsLib from 'pdfjs-dist/build/pdf';
|
||||
import workerSrc from 'pdfjs-dist/build/pdf.worker.min';
|
||||
import { getDocument, GlobalWorkerOptions } from 'pdfjs-dist/legacy/build/pdf';
|
||||
|
||||
import Page from './page/index.vue';
|
||||
|
||||
GlobalWorkerOptions.workerSrc = '/assets/webpack/pdfjs/pdf.worker.min.js';
|
||||
|
||||
export default {
|
||||
components: { Page },
|
||||
props: {
|
||||
|
|
@ -30,18 +31,16 @@ export default {
|
|||
},
|
||||
watch: { pdf: 'load' },
|
||||
mounted() {
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc;
|
||||
if (this.hasPDF) this.load();
|
||||
},
|
||||
methods: {
|
||||
load() {
|
||||
this.pages = [];
|
||||
return pdfjsLib
|
||||
.getDocument({
|
||||
url: this.document,
|
||||
cMapUrl: '/assets/webpack/cmaps/',
|
||||
cMapPacked: true,
|
||||
})
|
||||
return getDocument({
|
||||
url: this.document,
|
||||
cMapUrl: '/assets/webpack/pdfjs/cmaps/',
|
||||
cMapPacked: true,
|
||||
})
|
||||
.promise.then(this.renderPages)
|
||||
.then((pages) => {
|
||||
this.pages = pages;
|
||||
|
|
|
|||
|
|
@ -17,8 +17,6 @@ import allRunnersCountQuery from 'ee_else_ce/runner/graphql/list/all_runners_cou
|
|||
import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
|
||||
import RunnerStackedLayoutBanner from '../components/runner_stacked_layout_banner.vue';
|
||||
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
|
||||
import RunnerBulkDelete from '../components/runner_bulk_delete.vue';
|
||||
import RunnerBulkDeleteCheckbox from '../components/runner_bulk_delete_checkbox.vue';
|
||||
import RunnerList from '../components/runner_list.vue';
|
||||
import RunnerListEmptyState from '../components/runner_list_empty_state.vue';
|
||||
import RunnerName from '../components/runner_name.vue';
|
||||
|
|
@ -45,8 +43,6 @@ export default {
|
|||
RegistrationDropdown,
|
||||
RunnerStackedLayoutBanner,
|
||||
RunnerFilteredSearchBar,
|
||||
RunnerBulkDelete,
|
||||
RunnerBulkDeleteCheckbox,
|
||||
RunnerList,
|
||||
RunnerListEmptyState,
|
||||
RunnerName,
|
||||
|
|
@ -56,7 +52,7 @@ export default {
|
|||
RunnerActionsCell,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
inject: ['emptyStateSvgPath', 'emptyStateFilteredSvgPath', 'localMutations'],
|
||||
inject: ['emptyStateSvgPath', 'emptyStateFilteredSvgPath'],
|
||||
props: {
|
||||
registrationToken: {
|
||||
type: String,
|
||||
|
|
@ -155,12 +151,6 @@ export default {
|
|||
reportToSentry(error) {
|
||||
captureException({ error, component: this.$options.name });
|
||||
},
|
||||
onChecked({ runner, isChecked }) {
|
||||
this.localMutations.setRunnerChecked({
|
||||
runner,
|
||||
isChecked,
|
||||
});
|
||||
},
|
||||
onPaginationInput(value) {
|
||||
this.search.pagination = value;
|
||||
},
|
||||
|
|
@ -211,16 +201,12 @@ export default {
|
|||
:filtered-svg-path="emptyStateFilteredSvgPath"
|
||||
/>
|
||||
<template v-else>
|
||||
<runner-bulk-delete :runners="runners.items" @deleted="onDeleted" />
|
||||
<runner-list
|
||||
:runners="runners.items"
|
||||
:loading="runnersLoading"
|
||||
:checkable="true"
|
||||
@checked="onChecked"
|
||||
@deleted="onDeleted"
|
||||
>
|
||||
<template #head-checkbox>
|
||||
<runner-bulk-delete-checkbox :runners="runners.items" />
|
||||
</template>
|
||||
<template #runner-name="{ runner }">
|
||||
<gl-link :href="runner.adminUrl">
|
||||
<runner-name :runner="runner" />
|
||||
|
|
|
|||
|
|
@ -26,14 +26,17 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
deletableRunners() {
|
||||
return this.runners.filter((runner) => runner.userPermissions?.deleteRunner);
|
||||
},
|
||||
disabled() {
|
||||
return !this.runners.length;
|
||||
return !this.deletableRunners.length;
|
||||
},
|
||||
checked() {
|
||||
return Boolean(this.runners.length) && this.runners.every(this.isChecked);
|
||||
return Boolean(this.deletableRunners.length) && this.deletableRunners.every(this.isChecked);
|
||||
},
|
||||
indeterminate() {
|
||||
return !this.checked && this.runners.some(this.isChecked);
|
||||
return !this.checked && this.deletableRunners.some(this.isChecked);
|
||||
},
|
||||
label() {
|
||||
return this.checked ? s__('Runners|Unselect all') : s__('Runners|Select all');
|
||||
|
|
@ -45,7 +48,7 @@ export default {
|
|||
},
|
||||
onChange($event) {
|
||||
this.localMutations.setRunnersChecked({
|
||||
runners: this.runners,
|
||||
runners: this.deletableRunners,
|
||||
isChecked: $event,
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { helpPagePath } from '~/helpers/help_page_helper';
|
|||
import { s__ } from '~/locale';
|
||||
import HelpPopover from '~/vue_shared/components/help_popover.vue';
|
||||
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
|
||||
import { ACCESS_LEVEL_REF_PROTECTED, GROUP_TYPE, PROJECT_TYPE } from '../constants';
|
||||
import RunnerDetail from './runner_detail.vue';
|
||||
|
|
@ -29,7 +28,6 @@ export default {
|
|||
RunnerTags,
|
||||
TimeAgo,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
props: {
|
||||
runner: {
|
||||
type: Object,
|
||||
|
|
@ -117,10 +115,7 @@ export default {
|
|||
</template>
|
||||
</runner-detail>
|
||||
<runner-detail :label="s__('Runners|Maximum job timeout')" :value="maximumTimeout" />
|
||||
<runner-detail
|
||||
v-if="glFeatures.enforceRunnerTokenExpiresAt"
|
||||
:empty-value="s__('Runners|Never expires')"
|
||||
>
|
||||
<runner-detail :empty-value="s__('Runners|Never expires')">
|
||||
<template #label>
|
||||
{{ s__('Runners|Token expiry') }}
|
||||
<help-popover :options="tokenExpirationHelpPopoverOptions">
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import { s__ } from '~/locale';
|
|||
import HelpPopover from '~/vue_shared/components/help_popover.vue';
|
||||
import checkedRunnerIdsQuery from '../graphql/list/checked_runner_ids.query.graphql';
|
||||
import { formatJobCount, tableField } from '../utils';
|
||||
import RunnerBulkDelete from './runner_bulk_delete.vue';
|
||||
import RunnerBulkDeleteCheckbox from './runner_bulk_delete_checkbox.vue';
|
||||
import RunnerStackedSummaryCell from './cells/runner_stacked_summary_cell.vue';
|
||||
import RunnerStatusPopover from './runner_status_popover.vue';
|
||||
import RunnerStatusCell from './cells/runner_status_cell.vue';
|
||||
|
|
@ -23,6 +25,8 @@ export default {
|
|||
GlTableLite,
|
||||
GlSkeletonLoader,
|
||||
HelpPopover,
|
||||
RunnerBulkDelete,
|
||||
RunnerBulkDeleteCheckbox,
|
||||
RunnerStatusPopover,
|
||||
RunnerStackedSummaryCell,
|
||||
RunnerStatusCell,
|
||||
|
|
@ -39,6 +43,7 @@ export default {
|
|||
},
|
||||
},
|
||||
},
|
||||
inject: ['localMutations'],
|
||||
props: {
|
||||
checkable: {
|
||||
type: Boolean,
|
||||
|
|
@ -55,7 +60,7 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['checked'],
|
||||
emits: ['deleted'],
|
||||
data() {
|
||||
return { checkedRunnerIds: [] };
|
||||
},
|
||||
|
|
@ -84,6 +89,12 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
canDelete(runner) {
|
||||
return runner.userPermissions?.deleteRunner;
|
||||
},
|
||||
onDeleted(event) {
|
||||
this.$emit('deleted', event);
|
||||
},
|
||||
formatJobCount(jobCount) {
|
||||
return formatJobCount(jobCount);
|
||||
},
|
||||
|
|
@ -96,7 +107,7 @@ export default {
|
|||
return {};
|
||||
},
|
||||
onCheckboxChange(runner, isChecked) {
|
||||
this.$emit('checked', {
|
||||
this.localMutations.setRunnerChecked({
|
||||
runner,
|
||||
isChecked,
|
||||
});
|
||||
|
|
@ -109,6 +120,7 @@ export default {
|
|||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<runner-bulk-delete v-if="checkable" :runners="runners" @deleted="onDeleted" />
|
||||
<gl-table-lite
|
||||
:aria-busy="loading"
|
||||
:class="tableClass"
|
||||
|
|
@ -121,11 +133,15 @@ export default {
|
|||
fixed
|
||||
>
|
||||
<template #head(checkbox)>
|
||||
<slot name="head-checkbox"></slot>
|
||||
<runner-bulk-delete-checkbox :runners="runners" />
|
||||
</template>
|
||||
|
||||
<template #cell(checkbox)="{ item }">
|
||||
<gl-form-checkbox :checked="isChecked(item)" @change="onCheckboxChange(item, $event)" />
|
||||
<gl-form-checkbox
|
||||
v-if="canDelete(item)"
|
||||
:checked="isChecked(item)"
|
||||
@change="onCheckboxChange(item, $event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #head(status)="{ label }">
|
||||
|
|
|
|||
|
|
@ -48,16 +48,18 @@ export const createLocalState = () => {
|
|||
|
||||
const localMutations = {
|
||||
setRunnerChecked({ runner, isChecked }) {
|
||||
checkedRunnerIdsVar({
|
||||
...checkedRunnerIdsVar(),
|
||||
[runner.id]: isChecked,
|
||||
});
|
||||
const { id, userPermissions } = runner;
|
||||
if (userPermissions?.deleteRunner) {
|
||||
checkedRunnerIdsVar({
|
||||
...checkedRunnerIdsVar(),
|
||||
[id]: isChecked,
|
||||
});
|
||||
}
|
||||
},
|
||||
setRunnersChecked({ runners, isChecked }) {
|
||||
const newVal = runners.reduce(
|
||||
(acc, { id }) => ({ ...acc, [id]: isChecked }),
|
||||
checkedRunnerIdsVar(),
|
||||
);
|
||||
const newVal = runners
|
||||
.filter(({ userPermissions }) => userPermissions?.deleteRunner)
|
||||
.reduce((acc, { id }) => ({ ...acc, [id]: isChecked }), checkedRunnerIdsVar());
|
||||
checkedRunnerIdsVar(newVal);
|
||||
},
|
||||
clearChecked() {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
} from 'ee_else_ce/runner/runner_search_utils';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import groupRunnersQuery from 'ee_else_ce/runner/graphql/list/group_runners.query.graphql';
|
||||
import groupRunnersCountQuery from 'ee_else_ce/runner/graphql/list/group_runners_count.query.graphql';
|
||||
|
||||
import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
|
||||
import RunnerStackedLayoutBanner from '../components/runner_stacked_layout_banner.vue';
|
||||
|
|
@ -173,13 +174,17 @@ export default {
|
|||
editUrl(runner) {
|
||||
return this.runners.urlsById[runner.id]?.edit;
|
||||
},
|
||||
refetchCounts() {
|
||||
this.$apollo.getClient().refetchQueries({ include: [groupRunnersCountQuery] });
|
||||
},
|
||||
onToggledPaused() {
|
||||
// When a runner becomes Paused, the tab count can
|
||||
// become stale, refetch outdated counts.
|
||||
this.$refs['runner-type-tabs'].refetch();
|
||||
this.refetchCounts();
|
||||
},
|
||||
onDeleted({ message }) {
|
||||
this.$root.$toast?.show(message);
|
||||
this.refetchCounts();
|
||||
},
|
||||
reportToSentry(error) {
|
||||
captureException({ error, component: this.$options.name });
|
||||
|
|
@ -245,7 +250,7 @@ export default {
|
|||
:filtered-svg-path="emptyStateFilteredSvgPath"
|
||||
/>
|
||||
<template v-else>
|
||||
<runner-list :runners="runners.items" :loading="runnersLoading">
|
||||
<runner-list :runners="runners.items" :loading="runnersLoading" @deleted="onDeleted">
|
||||
<template #runner-name="{ runner }">
|
||||
<gl-link :href="webUrl(runner)">
|
||||
<runner-name :runner="runner" />
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { GlToast } from '@gitlab/ui';
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
import { createLocalState } from '../graphql/list/local_state';
|
||||
import GroupRunnersApp from './group_runners_app.vue';
|
||||
|
||||
Vue.use(GlToast);
|
||||
|
|
@ -26,8 +27,10 @@ export const initGroupRunners = (selector = '#js-group-runners') => {
|
|||
emptyStateFilteredSvgPath,
|
||||
} = el.dataset;
|
||||
|
||||
const { cacheConfig, typeDefs, localMutations } = createLocalState();
|
||||
|
||||
const apolloProvider = new VueApollo({
|
||||
defaultClient: createDefaultClient(),
|
||||
defaultClient: createDefaultClient({}, { cacheConfig, typeDefs }),
|
||||
});
|
||||
|
||||
return new Vue({
|
||||
|
|
@ -35,6 +38,7 @@ export const initGroupRunners = (selector = '#js-group-runners') => {
|
|||
apolloProvider,
|
||||
provide: {
|
||||
runnerInstallHelpPage,
|
||||
localMutations,
|
||||
groupId,
|
||||
onlineContactTimeoutSecs: parseInt(onlineContactTimeoutSecs, 10),
|
||||
staleTimeoutSecs: parseInt(staleTimeoutSecs, 10),
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import {
|
|||
WIDGET_TYPE_WEIGHT,
|
||||
WIDGET_TYPE_HIERARCHY,
|
||||
WORK_ITEM_VIEWED_STORAGE_KEY,
|
||||
WIDGET_TYPE_ITERATION,
|
||||
} from '../constants';
|
||||
|
||||
import workItemQuery from '../graphql/work_item.query.graphql';
|
||||
|
|
@ -65,6 +66,7 @@ export default {
|
|||
WorkItemInformation,
|
||||
LocalStorageSync,
|
||||
WorkItemTypeIcon,
|
||||
WorkItemIteration: () => import('ee_component/work_items/components/work_item_iteration.vue'),
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
props: {
|
||||
|
|
@ -134,7 +136,7 @@ export default {
|
|||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.workItemDueDate;
|
||||
return !this.isWidgetPresent(WIDGET_TYPE_START_AND_DUE_DATE);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -145,7 +147,7 @@ export default {
|
|||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.workItemAssignees;
|
||||
return !this.isWidgetPresent(WIDGET_TYPE_ASSIGNEES);
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
@ -170,28 +172,8 @@ export default {
|
|||
workItemsMvc2Enabled() {
|
||||
return this.glFeatures.workItemsMvc2;
|
||||
},
|
||||
hasDescriptionWidget() {
|
||||
return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_DESCRIPTION);
|
||||
},
|
||||
workItemAssignees() {
|
||||
return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_ASSIGNEES);
|
||||
},
|
||||
workItemLabels() {
|
||||
return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_LABELS);
|
||||
},
|
||||
workItemDueDate() {
|
||||
return this.workItem?.widgets?.find(
|
||||
(widget) => widget.type === WIDGET_TYPE_START_AND_DUE_DATE,
|
||||
);
|
||||
},
|
||||
workItemWeight() {
|
||||
return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_WEIGHT);
|
||||
},
|
||||
workItemHierarchy() {
|
||||
return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY);
|
||||
},
|
||||
parentWorkItem() {
|
||||
return this.workItemHierarchy?.parent;
|
||||
return this.isWidgetPresent(WIDGET_TYPE_HIERARCHY)?.parent;
|
||||
},
|
||||
parentWorkItemConfidentiality() {
|
||||
return this.parentWorkItem?.confidential;
|
||||
|
|
@ -205,6 +187,27 @@ export default {
|
|||
noAccessSvgPath() {
|
||||
return `data:image/svg+xml;utf8,${encodeURIComponent(noAccessSvg)}`;
|
||||
},
|
||||
hasDescriptionWidget() {
|
||||
return this.isWidgetPresent(WIDGET_TYPE_DESCRIPTION);
|
||||
},
|
||||
workItemAssignees() {
|
||||
return this.isWidgetPresent(WIDGET_TYPE_ASSIGNEES);
|
||||
},
|
||||
workItemLabels() {
|
||||
return this.isWidgetPresent(WIDGET_TYPE_LABELS);
|
||||
},
|
||||
workItemDueDate() {
|
||||
return this.isWidgetPresent(WIDGET_TYPE_START_AND_DUE_DATE);
|
||||
},
|
||||
workItemWeight() {
|
||||
return this.isWidgetPresent(WIDGET_TYPE_WEIGHT);
|
||||
},
|
||||
workItemHierarchy() {
|
||||
return this.isWidgetPresent(WIDGET_TYPE_HIERARCHY);
|
||||
},
|
||||
workItemIteration() {
|
||||
return this.isWidgetPresent(WIDGET_TYPE_ITERATION);
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
/** make sure that if the user has not even dismissed the alert ,
|
||||
|
|
@ -212,6 +215,9 @@ export default {
|
|||
this.dismissBanner();
|
||||
},
|
||||
methods: {
|
||||
isWidgetPresent(type) {
|
||||
return this.workItem?.widgets?.find((widget) => widget.type === type);
|
||||
},
|
||||
dismissBanner() {
|
||||
this.showInfoBanner = false;
|
||||
},
|
||||
|
|
@ -416,6 +422,17 @@ export default {
|
|||
:work-item-type="workItemType"
|
||||
@error="updateError = $event"
|
||||
/>
|
||||
<template v-if="workItemsMvc2Enabled">
|
||||
<work-item-iteration
|
||||
v-if="workItemIteration"
|
||||
class="gl-mb-5"
|
||||
:iteration="workItemIteration.iteration"
|
||||
:can-update="canUpdate"
|
||||
:work-item-id="workItem.id"
|
||||
:work-item-type="workItemType"
|
||||
@error="updateError = $event"
|
||||
/>
|
||||
</template>
|
||||
<work-item-description
|
||||
v-if="hasDescriptionWidget"
|
||||
:work-item-id="workItem.id"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,13 @@ export default function initWorkItemLinks() {
|
|||
return;
|
||||
}
|
||||
|
||||
const { projectPath, wiHasIssueWeightsFeature, iid } = workItemLinksRoot.dataset;
|
||||
const {
|
||||
projectPath,
|
||||
wiHasIssueWeightsFeature,
|
||||
iid,
|
||||
wiHasIterationsFeature,
|
||||
projectNamespace,
|
||||
} = workItemLinksRoot.dataset;
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
|
|
@ -31,6 +37,8 @@ export default function initWorkItemLinks() {
|
|||
iid,
|
||||
fullPath: projectPath,
|
||||
hasIssueWeightsFeature: wiHasIssueWeightsFeature,
|
||||
hasIterationsFeature: wiHasIterationsFeature,
|
||||
projectNamespace,
|
||||
},
|
||||
render: (createElement) =>
|
||||
createElement('work-item-links', {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { s__ } from '~/locale';
|
|||
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
||||
import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
|
||||
import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql';
|
||||
import getIssueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
|
||||
import { isMetaKey } from '~/lib/utils/common_utils';
|
||||
import { setUrlParams, updateHistory } from '~/lib/utils/url_utility';
|
||||
|
||||
|
|
@ -59,7 +59,7 @@ export default {
|
|||
},
|
||||
},
|
||||
parentIssue: {
|
||||
query: issueConfidentialQuery,
|
||||
query: getIssueDetailsQuery,
|
||||
variables() {
|
||||
return {
|
||||
fullPath: this.projectPath,
|
||||
|
|
@ -86,6 +86,9 @@ export default {
|
|||
confidential() {
|
||||
return this.parentIssue?.confidential || this.workItem?.confidential || false;
|
||||
},
|
||||
issuableIteration() {
|
||||
return this.parentIssue?.iteration;
|
||||
},
|
||||
children() {
|
||||
return (
|
||||
this.workItem?.widgets.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY)?.children
|
||||
|
|
@ -305,6 +308,7 @@ export default {
|
|||
:issuable-gid="issuableGid"
|
||||
:children-ids="childrenIds"
|
||||
:parent-confidential="confidential"
|
||||
:parent-iteration="issuableIteration"
|
||||
@cancel="hideAddForm"
|
||||
@addWorkItemChild="addChild"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export default {
|
|||
GlFormGroup,
|
||||
GlFormInput,
|
||||
},
|
||||
inject: ['projectPath'],
|
||||
inject: ['projectPath', 'hasIterationsFeature'],
|
||||
props: {
|
||||
issuableGid: {
|
||||
type: String,
|
||||
|
|
@ -33,6 +33,11 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
parentIteration: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
workItemTypes: {
|
||||
|
|
@ -77,6 +82,9 @@ export default {
|
|||
taskWorkItemType() {
|
||||
return this.workItemTypes.find((type) => type.name === TASK_TYPE_NAME)?.id;
|
||||
},
|
||||
parentIterationId() {
|
||||
return this.parentIteration?.id;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getIdFromGraphQLId,
|
||||
|
|
@ -133,6 +141,13 @@ export default {
|
|||
} else {
|
||||
this.unsetError();
|
||||
this.$emit('addWorkItemChild', data.workItemCreate.workItem);
|
||||
/**
|
||||
* call update mutation only when there is an iteration associated with the issue
|
||||
*/
|
||||
// TODO: setting the iteration should be moved to the creation mutation once the backend is done
|
||||
if (this.parentIterationId && this.hasIterationsFeature) {
|
||||
this.addIterationToWorkItem(data.workItemCreate.workItem.id);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
|
|
@ -143,6 +158,19 @@ export default {
|
|||
this.childToCreateTitle = null;
|
||||
});
|
||||
},
|
||||
async addIterationToWorkItem(workItemId) {
|
||||
await this.$apollo.mutate({
|
||||
mutation: updateWorkItemMutation,
|
||||
variables: {
|
||||
input: {
|
||||
id: workItemId,
|
||||
iterationWidget: {
|
||||
iterationId: this.parentIterationId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
inputLabel: __('Title'),
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ export const WIDGET_TYPE_LABELS = 'LABELS';
|
|||
export const WIDGET_TYPE_START_AND_DUE_DATE = 'START_AND_DUE_DATE';
|
||||
export const WIDGET_TYPE_WEIGHT = 'WEIGHT';
|
||||
export const WIDGET_TYPE_HIERARCHY = 'HIERARCHY';
|
||||
export const WIDGET_TYPE_ITERATION = 'ITERATION';
|
||||
|
||||
export const WORK_ITEM_VIEWED_STORAGE_KEY = 'gl-show-work-item-banner';
|
||||
|
||||
export const WORK_ITEM_TYPE_ENUM_INCIDENT = 'INCIDENT';
|
||||
|
|
@ -54,6 +56,10 @@ export const I18N_WORK_ITEM_ARE_YOU_SURE_DELETE = s__(
|
|||
);
|
||||
export const I18N_WORK_ITEM_DELETED = s__('WorkItem|%{workItemType} deleted');
|
||||
|
||||
export const I18N_WORK_ITEM_FETCH_ITERATIONS_ERROR = s__(
|
||||
'WorkItem|Something went wrong when fetching iterations. Please try again.',
|
||||
);
|
||||
|
||||
export const sprintfWorkItem = (msg, workItemTypeArg) => {
|
||||
const workItemType = workItemTypeArg || s__('WorkItem|Work item');
|
||||
return capitalizeFirstCharacter(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
query issuableDetails($fullPath: ID!, $iid: String) {
|
||||
workspace: project(fullPath: $fullPath) {
|
||||
id
|
||||
issuable: issue(iid: $iid) {
|
||||
id
|
||||
confidential
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,13 @@ import { createRouter } from './router';
|
|||
|
||||
export const initWorkItemsRoot = () => {
|
||||
const el = document.querySelector('#js-work-items');
|
||||
const { fullPath, hasIssueWeightsFeature, issuesListPath } = el.dataset;
|
||||
const {
|
||||
fullPath,
|
||||
hasIssueWeightsFeature,
|
||||
issuesListPath,
|
||||
projectNamespace,
|
||||
hasIterationsFeature,
|
||||
} = el.dataset;
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
|
|
@ -17,6 +23,8 @@ export const initWorkItemsRoot = () => {
|
|||
fullPath,
|
||||
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
|
||||
issuesListPath,
|
||||
projectNamespace,
|
||||
hasIterationsFeature: parseBoolean(hasIterationsFeature),
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(App);
|
||||
|
|
|
|||
|
|
@ -63,3 +63,23 @@
|
|||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.work-item-iteration {
|
||||
.gl-dropdown-toggle {
|
||||
background: none !important;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
box-shadow: inset 0 0 0 $gl-border-size-1 var(--gray-darkest, $gray-darkest) !important;
|
||||
}
|
||||
|
||||
&.is-not-focused:not(:hover, :focus) {
|
||||
box-shadow: none;
|
||||
|
||||
.gl-button-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,10 +5,6 @@ class Admin::RunnersController < Admin::ApplicationController
|
|||
|
||||
before_action :runner, except: [:index, :tag_list, :runner_setup_scripts]
|
||||
|
||||
before_action only: [:show] do
|
||||
push_frontend_feature_flag(:enforce_runner_token_expires_at)
|
||||
end
|
||||
|
||||
feature_category :runner
|
||||
urgency :low
|
||||
|
||||
|
|
|
|||
|
|
@ -5,10 +5,6 @@ class Groups::RunnersController < Groups::ApplicationController
|
|||
before_action :authorize_update_runner!, only: [:edit, :update, :destroy, :pause, :resume]
|
||||
before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
|
||||
|
||||
before_action only: [:show] do
|
||||
push_frontend_feature_flag(:enforce_runner_token_expires_at)
|
||||
end
|
||||
|
||||
feature_category :runner
|
||||
urgency :low
|
||||
|
||||
|
|
|
|||
|
|
@ -617,6 +617,15 @@ module Ci
|
|||
# auto_canceled_by_pipeline_id - store the pipeline_id of the pipeline that triggered cancellation
|
||||
# execute_async - if true cancel the children asyncronously
|
||||
def cancel_running(retries: 1, cascade_to_children: true, auto_canceled_by_pipeline_id: nil, execute_async: true)
|
||||
Gitlab::AppJsonLogger.info(
|
||||
event: 'pipeline_cancel_running',
|
||||
pipeline_id: id,
|
||||
auto_canceled_by_pipeline_id: auto_canceled_by_pipeline_id,
|
||||
cascade_to_children: cascade_to_children,
|
||||
execute_async: execute_async,
|
||||
**Gitlab::ApplicationContext.current
|
||||
)
|
||||
|
||||
update(auto_canceled_by_id: auto_canceled_by_pipeline_id) if auto_canceled_by_pipeline_id
|
||||
|
||||
cancel_jobs(cancelable_statuses, retries: retries, auto_canceled_by_pipeline_id: auto_canceled_by_pipeline_id)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ module Ci
|
|||
include Presentable
|
||||
include EachBatch
|
||||
|
||||
add_authentication_token_field :token, encrypted: :optional, expires_at: :compute_token_expiration, expiration_enforced?: :token_expiration_enforced?
|
||||
add_authentication_token_field :token, encrypted: :optional, expires_at: :compute_token_expiration
|
||||
|
||||
enum access_level: {
|
||||
not_protected: 0,
|
||||
|
|
@ -480,10 +480,6 @@ module Ci
|
|||
end
|
||||
end
|
||||
|
||||
def self.token_expiration_enforced?
|
||||
Feature.enabled?(:enforce_runner_token_expires_at)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
scope :with_upgrade_status, ->(upgrade_status) do
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ class WebHook < ApplicationRecord
|
|||
|
||||
MAX_FAILURES = 100
|
||||
FAILURE_THRESHOLD = 3 # three strikes
|
||||
INITIAL_BACKOFF = 10.minutes
|
||||
INITIAL_BACKOFF = 1.minute
|
||||
MAX_BACKOFF = 1.day
|
||||
BACKOFF_GROWTH_FACTOR = 2.0
|
||||
|
||||
|
|
|
|||
|
|
@ -17,14 +17,14 @@
|
|||
|
||||
.form-group
|
||||
= f.label :receive_max_input_size, _('Maximum push size (MB)'), class: 'label-light'
|
||||
= f.number_field :receive_max_input_size, class: 'form-control gl-form-input qa-receive-max-input-size-field', title: _('Maximum size limit for a single commit.'), data: { toggle: 'tooltip', container: 'body' }
|
||||
= f.number_field :receive_max_input_size, class: 'form-control gl-form-input', title: _('Maximum size limit for a single commit.'), data: { toggle: 'tooltip', container: 'body', qa_selector: 'receive_max_input_size_field' }
|
||||
.form-group
|
||||
= f.label :max_export_size, _('Maximum export size (MB)'), class: 'label-light'
|
||||
= f.number_field :max_export_size, class: 'form-control gl-form-input', title: _('Maximum size of export files.'), data: { toggle: 'tooltip', container: 'body' }
|
||||
%span.form-text.text-muted= _('Set to 0 for no size limit.')
|
||||
.form-group
|
||||
= f.label :max_import_size, _('Maximum import size (MB)'), class: 'label-light'
|
||||
= f.number_field :max_import_size, class: 'form-control gl-form-input qa-receive-max-import-size-field', title: _('Maximum size of import files.'), data: { toggle: 'tooltip', container: 'body' }
|
||||
= f.number_field :max_import_size, class: 'form-control gl-form-input', title: _('Maximum size of import files.'), data: { toggle: 'tooltip', container: 'body' }
|
||||
%span.form-text.text-muted= _('Only effective when remote storage is enabled. Set to 0 for no size limit.')
|
||||
.form-group
|
||||
= f.label :session_expire_delay, _('Session duration (minutes)'), class: 'label-light'
|
||||
|
|
@ -69,4 +69,4 @@
|
|||
= render 'admin/application_settings/invitation_flow_enforcement', form: f
|
||||
= render 'admin/application_settings/user_restrictions', form: f
|
||||
= render_if_exists 'admin/application_settings/availability_on_namespace_setting', form: f
|
||||
= f.submit _('Save changes'), class: 'qa-save-changes-button', pajamas_button: true
|
||||
= f.submit _('Save changes'), pajamas_button: true, data: { qa_selector: 'save_changes_button' }
|
||||
|
|
|
|||
|
|
@ -53,8 +53,7 @@
|
|||
= link_to sprite_icon('question-o'), help_page_path('ci/pipelines/settings', anchor: 'specify-a-custom-cicd-configuration-file'), target: '_blank', rel: 'noopener noreferrer'
|
||||
.form-group
|
||||
= f.gitlab_ui_checkbox_component :suggest_pipeline_enabled, s_('AdminSettings|Enable pipeline suggestion banner'), help_text: s_('AdminSettings|Display a banner on merge requests in projects with no pipelines to initiate steps to add a .gitlab-ci.yml file.')
|
||||
- if Feature.enabled?(:enforce_runner_token_expires_at)
|
||||
#js-runner-token-expiration-intervals{ data: runner_token_expiration_interval_attributes }
|
||||
#js-runner-token-expiration-intervals{ data: runner_token_expiration_interval_attributes }
|
||||
|
||||
= f.submit _('Save changes'), pajamas_button: true
|
||||
|
||||
|
|
|
|||
|
|
@ -10,4 +10,4 @@
|
|||
= f.label :performance_bar_allowed_group_path, _('Allow access to members of the following group'), class: 'label-bold'
|
||||
= f.text_field :performance_bar_allowed_group_path, class: 'form-control gl-form-input', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path
|
||||
|
||||
= f.submit _('Save changes'), class: 'qa-save-changes-button', pajamas_button: true
|
||||
= f.submit _('Save changes'), pajamas_button: true, data: { qa_selector: 'save_changes_button' }
|
||||
|
|
|
|||
|
|
@ -15,12 +15,12 @@
|
|||
%input.js-ci-variable-input-destroy{ type: "hidden", name: destroy_input_name }
|
||||
%select.js-ci-variable-input-variable-type.ci-variable-body-item.form-control.select-control.custom-select.table-section.section-15{ name: variable_type_input_name }
|
||||
= options_for_select(ci_variable_type_options, variable_type)
|
||||
%input.js-ci-variable-input-key.ci-variable-body-item.qa-ci-variable-input-key.form-control.table-section.section-15{ type: "text",
|
||||
%input.js-ci-variable-input-key.ci-variable-body-item.form-control.table-section.section-15{ type: "text",
|
||||
name: key_input_name,
|
||||
value: key,
|
||||
placeholder: s_('CiVariables|Input variable key') }
|
||||
.ci-variable-body-item.gl-show-field-errors.table-section.section-15.border-top-0.p-0
|
||||
%textarea.js-ci-variable-input-value.js-secret-value.qa-ci-variable-input-value.form-control{ rows: 1,
|
||||
%textarea.js-ci-variable-input-value.js-secret-value.form-control{ rows: 1,
|
||||
name: value_input_name,
|
||||
placeholder: s_('CiVariables|Input variable value') }
|
||||
= value
|
||||
|
|
|
|||
|
|
@ -18,14 +18,14 @@
|
|||
%input.js-ci-variable-input-destroy{ type: "hidden", name: destroy_input_name }
|
||||
%select.js-ci-variable-input-variable-type.ci-variable-body-item.form-control.select-control.custom-select.table-section.section-15{ name: variable_type_input_name }
|
||||
= options_for_select(ci_variable_type_options, variable_type)
|
||||
%input.js-ci-variable-input-key.ci-variable-body-item.qa-ci-variable-input-key.form-control.gl-form-input.table-section.section-15{ type: "text",
|
||||
%input.js-ci-variable-input-key.ci-variable-body-item.form-control.gl-form-input.table-section.section-15{ type: "text",
|
||||
name: key_input_name,
|
||||
value: key,
|
||||
placeholder: s_('CiVariables|Input variable key') }
|
||||
.ci-variable-body-item.gl-show-field-errors.table-section.section-15.border-top-0.p-0
|
||||
.form-control.js-secret-value-placeholder.qa-ci-variable-input-value.overflow-hidden{ class: ('hide' unless id) }
|
||||
.form-control.js-secret-value-placeholder.overflow-hidden{ class: ('hide' unless id) }
|
||||
= '*' * 17
|
||||
%textarea.js-ci-variable-input-value.js-secret-value.qa-ci-variable-input-value.form-control.gl-form-input{ class: ('hide' if id),
|
||||
%textarea.js-ci-variable-input-value.js-secret-value.form-control.gl-form-input{ class: ('hide' if id),
|
||||
rows: 1,
|
||||
name: value_input_name,
|
||||
placeholder: s_('CiVariables|Input variable value') }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
- add_page_specific_style 'page_bundles/prometheus'
|
||||
|
||||
%section.settings.no-animate.expanded.cluster-health-graphs.qa-cluster-health-section#cluster-health
|
||||
%section.settings.no-animate.expanded.cluster-health-graphs#cluster-health
|
||||
- if @cluster&.integration_prometheus_available?
|
||||
#prometheus-graphs{ data: @cluster.health_data(clusterable) }
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
= platform_kubernetes_field.form_group :authorization_type,
|
||||
{ help: '%{help_text} %{help_link}'.html_safe % { help_text: rbac_help_text, help_link: rbac_help_link } } do
|
||||
= platform_kubernetes_field.check_box :authorization_type,
|
||||
{ class: 'qa-rbac-checkbox', label: s_('ClusterIntegration|RBAC-enabled cluster'),
|
||||
{ data: { qa_selector: 'rbac_checkbox'}, label: s_('ClusterIntegration|RBAC-enabled cluster'),
|
||||
label_class: 'label-bold', inline: true }, 'rbac', 'abac'
|
||||
|
||||
.form-group
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@
|
|||
- if header_link?(:user_dropdown)
|
||||
%li.nav-item.header-user.js-nav-user-dropdown.dropdown{ data: { track_label: "profile_dropdown", track_action: "click_dropdown", track_value: "", qa_selector: 'user_menu', testid: 'user-menu' }, class: ('mr-0' if has_impersonation_link) }
|
||||
= link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
|
||||
= render Pajamas::AvatarComponent.new(current_user, size: 24, class: 'header-user-avatar qa-user-avatar')
|
||||
= render Pajamas::AvatarComponent.new(current_user, size: 24, class: 'header-user-avatar', avatar_options: { data: { qa_selector: 'user_avatar_content' } })
|
||||
= render_if_exists 'layouts/header/user_notification_dot', project: project, namespace: group
|
||||
= sprite_icon('chevron-down', css_class: 'caret-down')
|
||||
.dropdown-menu.dropdown-menu-right
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
%aside.nav-sidebar.qa-admin-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?), 'aria-label': _('Admin navigation') }
|
||||
%aside.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?), 'aria-label': _('Admin navigation'), data: { qa_selector: 'admin_sidebar_content' } }
|
||||
.nav-sidebar-inner-scroll
|
||||
.context-header
|
||||
= link_to admin_root_path, title: _('Admin Overview'), class: 'has-tooltip', data: { container: 'body', placement: 'right' } do
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
= sprite_icon('admin', size: 18)
|
||||
%span.sidebar-context-title
|
||||
= _('Admin Area')
|
||||
%ul.sidebar-top-level-items{ data: { qa_selector: 'admin_sidebar_overview_submenu_content' } }
|
||||
%ul.sidebar-top-level-items{ data: { qa_selector: 'admin_overview_submenu_content' } }
|
||||
= nav_link(controller: %w[dashboard admin admin/projects users groups admin/topics jobs runners gitaly_servers cohorts], html_options: {class: 'home'}) do
|
||||
= link_to admin_root_path, class: 'has-sub-items' do
|
||||
.nav-icon-container
|
||||
|
|
@ -28,15 +28,15 @@
|
|||
%span
|
||||
= _('Projects')
|
||||
= nav_link(controller: %w[users cohorts]) do
|
||||
= link_to admin_users_path, title: _('Users'), data: { qa_selector: 'users_overview_link' } do
|
||||
= link_to admin_users_path, title: _('Users'), data: { qa_selector: 'admin_overview_users_link' } do
|
||||
%span
|
||||
= _('Users')
|
||||
= nav_link(controller: :groups) do
|
||||
= link_to admin_groups_path, title: _('Groups'), data: { qa_selector: 'groups_overview_link' } do
|
||||
= link_to admin_groups_path, title: _('Groups'), data: { qa_selector: 'admin_overview_groups_link' } do
|
||||
%span
|
||||
= _('Groups')
|
||||
= nav_link(controller: [:admin, 'admin/topics']) do
|
||||
= link_to admin_topics_path, title: _('Topics'), data: { qa_selector: 'topics_overview_link' } do
|
||||
= link_to admin_topics_path, title: _('Topics') do
|
||||
%span
|
||||
= _('Topics')
|
||||
= nav_link path: 'jobs#index' do
|
||||
|
|
@ -75,13 +75,13 @@
|
|||
= _('Usage Trends')
|
||||
|
||||
= nav_link(controller: admin_monitoring_nav_links) do
|
||||
= link_to admin_system_info_path, data: { qa_selector: 'admin_monitoring_link' }, class: 'has-sub-items' do
|
||||
= link_to admin_system_info_path, data: { qa_selector: 'admin_monitoring_menu_link' }, class: 'has-sub-items' do
|
||||
.nav-icon-container
|
||||
= sprite_icon('monitor')
|
||||
%span.nav-item-name
|
||||
= _('Monitoring')
|
||||
|
||||
%ul.sidebar-sub-level-items{ data: { qa_selector: 'admin_sidebar_monitoring_submenu_content' } }
|
||||
%ul.sidebar-sub-level-items{ data: { qa_selector: 'admin_monitoring_submenu_content' } }
|
||||
= nav_link(controller: admin_monitoring_nav_links, html_options: { class: "fly-out-top-item" } ) do
|
||||
= link_to admin_system_info_path do
|
||||
%strong.fly-out-top-item-name
|
||||
|
|
@ -222,10 +222,10 @@
|
|||
= link_to general_admin_application_settings_path, class: 'has-sub-items' do
|
||||
.nav-icon-container
|
||||
= sprite_icon('settings')
|
||||
%span.nav-item-name.qa-admin-settings-item
|
||||
%span.nav-item-name{ data: { qa_selector: 'admin_settings_menu_link' } }
|
||||
= _('Settings')
|
||||
|
||||
%ul.sidebar-sub-level-items{ data: { qa_selector: 'admin_sidebar_settings_submenu_content' } }
|
||||
%ul.sidebar-sub-level-items{ data: { qa_selector: 'admin_settings_submenu_content' } }
|
||||
-# This active_nav_link check is also used in `app/views/layouts/admin.html.haml`
|
||||
= nav_link(controller: [:application_settings, :integrations, :appearances], html_options: { class: "fly-out-top-item" } ) do
|
||||
= link_to general_admin_application_settings_path do
|
||||
|
|
@ -233,24 +233,24 @@
|
|||
= _('Settings')
|
||||
%li.divider.fly-out-top-item
|
||||
= nav_link(path: 'application_settings#general') do
|
||||
= link_to general_admin_application_settings_path, title: _('General'), class: 'qa-admin-settings-general-item' do
|
||||
= link_to general_admin_application_settings_path, title: _('General'), data: { qa_selector: 'admin_settings_general_link' } do
|
||||
%span
|
||||
= _('General')
|
||||
|
||||
= render_if_exists 'layouts/nav/sidebar/advanced_search', class: 'qa-admin-settings-advanced-search'
|
||||
= render_if_exists 'layouts/nav/sidebar/advanced_search', data: { qa_selector: 'admin_settings_advanced_search_link' }
|
||||
|
||||
- if instance_level_integrations?
|
||||
= nav_link(path: ['application_settings#integrations', 'integrations#edit']) do
|
||||
= link_to integrations_admin_application_settings_path, title: _('Integrations'), data: { qa_selector: 'integration_settings_link' } do
|
||||
= link_to integrations_admin_application_settings_path, title: _('Integrations'), data: { qa_selector: 'admin_settings_integrations_link' } do
|
||||
%span
|
||||
= _('Integrations')
|
||||
= nav_link(path: 'application_settings#repository') do
|
||||
= link_to repository_admin_application_settings_path, title: _('Repository'), class: 'qa-admin-settings-repository-item' do
|
||||
= link_to repository_admin_application_settings_path, title: _('Repository'), data: { qa_selector: 'admin_settings_repository_link' } do
|
||||
%span
|
||||
= _('Repository')
|
||||
- if Gitlab.ee? && License.feature_available?(:custom_file_templates)
|
||||
= nav_link(path: 'application_settings#templates') do
|
||||
= link_to templates_admin_application_settings_path, title: _('Templates'), class: 'qa-admin-settings-template-item' do
|
||||
= link_to templates_admin_application_settings_path, title: _('Templates'), data: { qa_selector: 'admin_settings_templates_link' } do
|
||||
%span
|
||||
= _('Templates')
|
||||
= nav_link(path: 'application_settings#ci_cd') do
|
||||
|
|
@ -262,7 +262,7 @@
|
|||
%span
|
||||
= _('Reporting')
|
||||
= nav_link(path: 'application_settings#metrics_and_profiling') do
|
||||
= link_to metrics_and_profiling_admin_application_settings_path, title: _('Metrics and profiling'), class: 'qa-admin-settings-metrics-and-profiling-item' do
|
||||
= link_to metrics_and_profiling_admin_application_settings_path, title: _('Metrics and profiling'), data: { qa_selector: 'admin_settings_metrics_and_profiling_link' } do
|
||||
%span
|
||||
= _('Metrics and profiling')
|
||||
= nav_link(path: ['application_settings#service_usage_data']) do
|
||||
|
|
@ -270,7 +270,7 @@
|
|||
%span
|
||||
= _('Service usage data')
|
||||
= nav_link(path: 'application_settings#network') do
|
||||
= link_to network_admin_application_settings_path, title: _('Network'), data: { qa_selector: 'admin_settings_network_item' } do
|
||||
= link_to network_admin_application_settings_path, title: _('Network'), data: { qa_selector: 'admin_settings_network_link' } do
|
||||
%span
|
||||
= _('Network')
|
||||
= nav_link(controller: :appearances ) do
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
%ul.nav.navbar-nav
|
||||
%li.header-user.dropdown
|
||||
= link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
|
||||
= render Pajamas::AvatarComponent.new(current_user, size: 24, class: 'gl-mr-3', avatar_options: { data: { qa_selector: 'user_avatar' } })
|
||||
= render Pajamas::AvatarComponent.new(current_user, size: 24, class: 'gl-mr-3', avatar_options: { data: { qa_selector: 'user_avatar_content' } })
|
||||
= sprite_icon('chevron-down')
|
||||
.dropdown-menu.dropdown-menu-right
|
||||
= render 'layouts/header/current_user_dropdown'
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@
|
|||
.template-selector-dropdowns-wrap
|
||||
.template-type-selector.js-template-type-selector-wrap.hidden
|
||||
- toggle_text = should_suggest_gitlab_ci_yml? ? '.gitlab-ci.yml' : 'Select a template type'
|
||||
= dropdown_tag(_(toggle_text), options: { toggle_class: 'js-template-type-selector qa-template-type-dropdown', dropdown_class: 'dropdown-menu-selectable' })
|
||||
= dropdown_tag(_(toggle_text), options: { toggle_class: 'js-template-type-selector', dropdown_class: 'dropdown-menu-selectable', data: { qa_selector: 'template_type_dropdown' } })
|
||||
.license-selector.js-license-selector-wrap.js-template-selector-wrap.hidden
|
||||
= dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-license-selector qa-license-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: licenses_for_select(@project), project: @project.name, fullname: @project.namespace.human_name } } )
|
||||
= dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-license-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: licenses_for_select(@project), project: @project.name, fullname: @project.namespace.human_name, qa_selector: 'license_dropdown' } } )
|
||||
.gitignore-selector.js-gitignore-selector-wrap.js-template-selector-wrap.hidden
|
||||
= dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitignore-selector qa-gitignore-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitignore_names(@project) } } )
|
||||
= dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitignore-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitignore_names(@project), qa_selector: 'gitignore_dropdown' } } )
|
||||
.metrics-dashboard-selector.js-metrics-dashboard-selector-wrap.js-template-selector-wrap.hidden
|
||||
= dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-metrics-dashboard-selector qa-metrics-dashboard-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: metrics_dashboard_ymls(@project) } } )
|
||||
= dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-metrics-dashboard-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: metrics_dashboard_ymls(@project), qa_selector: 'metrics_dashboard_dropdown' } } )
|
||||
#gitlab-ci-yml-selector.gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.js-template-selector-wrap.hidden
|
||||
= dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitlab-ci-yml-selector qa-gitlab-ci-yml-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls(@project), selected: params[:template] } } )
|
||||
= dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitlab-ci-yml-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls(@project), selected: params[:template], qa_selector: 'gitlab_ci_yml_dropdown' } } )
|
||||
.dockerfile-selector.js-dockerfile-selector-wrap.js-template-selector-wrap.hidden
|
||||
= dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-dockerfile-selector qa-dockerfile-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: dockerfile_names(@project) } } )
|
||||
= dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-dockerfile-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: dockerfile_names(@project), qa_selector: 'dockerfile_dropdown' } } )
|
||||
|
|
|
|||
|
|
@ -2,17 +2,17 @@
|
|||
- dropdown_class = local_assigns.fetch(:dropdown_class, '')
|
||||
|
||||
.git-clone-holder.js-git-clone-holder
|
||||
%a#clone-dropdown.gl-button.btn.btn-confirm.clone-dropdown-btn.qa-clone-dropdown{ href: '#', data: { toggle: 'dropdown' } }
|
||||
%a#clone-dropdown.gl-button.btn.btn-confirm.clone-dropdown-btn{ href: '#', data: { toggle: 'dropdown', qa_selector: 'clone_dropdown' } }
|
||||
%span.gl-mr-2.js-clone-dropdown-label
|
||||
= _('Clone')
|
||||
= sprite_icon("chevron-down", css_class: "icon")
|
||||
%ul.dropdown-menu.dropdown-menu-large.dropdown-menu-selectable.clone-options-dropdown.qa-clone-options{ class: dropdown_class }
|
||||
%ul.dropdown-menu.dropdown-menu-large.dropdown-menu-selectable.clone-options-dropdown{ class: dropdown_class, data: { qa_selector: 'clone_dropdown_content' } }
|
||||
- if ssh_enabled?
|
||||
%li{ class: 'gl-px-4!' }
|
||||
%label.label-bold
|
||||
= _('Clone with SSH')
|
||||
.input-group.btn-group
|
||||
= text_field_tag :ssh_project_clone, project.ssh_url_to_repo, class: "js-select-on-focus form-control qa-ssh-clone-url", readonly: true, aria: { label: _('Repository clone URL') }
|
||||
= text_field_tag :ssh_project_clone, project.ssh_url_to_repo, class: "js-select-on-focus form-control", readonly: true, aria: { label: _('Repository clone URL') }, data: { qa_selector: 'ssh_clone_url_content' }
|
||||
.input-group-append
|
||||
= clipboard_button(target: '#ssh_project_clone', title: _("Copy URL"), class: "input-group-text gl-button btn btn-icon btn-default")
|
||||
= render_if_exists 'projects/buttons/geo'
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
%label.label-bold
|
||||
= _('Clone with %{http_label}') % { http_label: gitlab_config.protocol.upcase }
|
||||
.input-group.btn-group
|
||||
= text_field_tag :http_project_clone, project.http_url_to_repo, class: "js-select-on-focus form-control qa-http-clone-url", readonly: true, aria: { label: _('Repository clone URL') }
|
||||
= text_field_tag :http_project_clone, project.http_url_to_repo, class: "js-select-on-focus form-control", readonly: true, aria: { label: _('Repository clone URL') }, data: { qa_selector: 'http_clone_url_content' }
|
||||
.input-group-append
|
||||
= clipboard_button(target: '#http_project_clone', title: _("Copy URL"), class: "input-group-text gl-button btn btn-icon btn-default")
|
||||
= render_if_exists 'projects/buttons/geo'
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
- if Feature.enabled?(:work_items_hierarchy, @project)
|
||||
.js-work-item-links-root{ data: { issuable_id: @issue.id, iid: @issue.iid, project_path: @project.full_path, wi: work_items_index_data(@project) } }
|
||||
.js-work-item-links-root{ data: { issuable_id: @issue.id, iid: @issue.iid, project_namespace: @project.namespace.path, project_path: @project.full_path, wi: work_items_index_data(@project) } }
|
||||
|
|
|
|||
|
|
@ -9,14 +9,14 @@
|
|||
%span.js-clone-dropdown-label
|
||||
= default_clone_protocol.upcase
|
||||
= sprite_icon('chevron-down', css_class: 'gl-icon')
|
||||
%ul.dropdown-menu.dropdown-menu-selectable.clone-options-dropdown
|
||||
%ul.dropdown-menu.dropdown-menu-selectable{ data: { qa_selector: 'clone_dropdown_content' } }
|
||||
%li
|
||||
= ssh_clone_button(container)
|
||||
%li
|
||||
= http_clone_button(container)
|
||||
= render_if_exists 'shared/kerberos_clone_button', container: container
|
||||
|
||||
= text_field_tag :clone_url, default_url_to_repo(container), class: "js-select-on-focus btn gl-button", readonly: true, aria: { label: _('Repository clone URL') }
|
||||
= text_field_tag :clone_url, default_url_to_repo(container), class: "js-select-on-focus btn gl-button", readonly: true, aria: { label: _('Repository clone URL') }, data: { qa_selector: 'clone_url_content' }
|
||||
.input-group-append
|
||||
= clipboard_button(target: '#clone_url', title: _("Copy URL"), class: "input-group-text gl-button btn-default btn-clipboard")
|
||||
|
||||
|
|
|
|||
|
|
@ -4,16 +4,12 @@
|
|||
- link_end = '</a>'.html_safe
|
||||
|
||||
- if hook.rate_limited?
|
||||
- support_path = 'https://support.gitlab.com/hc/en-us/requests/new'
|
||||
- placeholders = { strong_start: strong_start,
|
||||
strong_end: strong_end,
|
||||
limit: hook.rate_limit,
|
||||
support_link_start: link_start % { url: support_path },
|
||||
support_link_end: link_end }
|
||||
= render Pajamas::AlertComponent.new(title: s_('Webhooks|Webhook was automatically disabled'),
|
||||
- placeholders = { limit: number_with_delimiter(hook.rate_limit),
|
||||
root_namespace: hook.parent.root_namespace.path }
|
||||
= render Pajamas::AlertComponent.new(title: s_('Webhooks|Webhook rate limit has been reached'),
|
||||
variant: :danger) do |c|
|
||||
= c.body do
|
||||
= s_('Webhooks|The webhook was triggered more than %{limit} times per minute and is now disabled. To re-enable this webhook, fix the problems shown in %{strong_start}Recent events%{strong_end}, then re-test your settings. %{support_link_start}Contact Support%{support_link_end} if you need help re-enabling your webhook.').html_safe % placeholders
|
||||
= s_("Webhooks|Webhooks for %{root_namespace} are now disabled because they've been triggered more than %{limit} times per minute. They'll be automatically re-enabled in the next minute.").html_safe % placeholders
|
||||
- elsif hook.permanently_disabled?
|
||||
= render Pajamas::AlertComponent.new(title: s_('Webhooks|Webhook failed to connect'),
|
||||
variant: :danger) do |c|
|
||||
|
|
|
|||
|
|
@ -8,15 +8,15 @@ opt_parser = OptionParser.new do |opt|
|
|||
Profile a URL on this GitLab instance.
|
||||
|
||||
Usage:
|
||||
#{__FILE__} url --output=<profile-html> --sql=<sql-log> [--user=<user>] [--post=<post-data>]
|
||||
#{__FILE__} url --output=<profile-dump> --sql=<sql-log> [--user=<user>] [--post=<post-data>]
|
||||
|
||||
Example:
|
||||
#{__FILE__} /dashboard/issues --output=dashboard-profile.html --sql=dashboard.log --user=root
|
||||
#{__FILE__} /dashboard/issues --output=dashboard-profile.dump --sql=dashboard.log --user=root
|
||||
DOCSTRING
|
||||
opt.separator ''
|
||||
opt.separator 'Options:'
|
||||
|
||||
opt.on('-o', '--output=/tmp/profile.html', 'profile output filename') do |output|
|
||||
opt.on('-o', '--output=/tmp/profile.dump', 'profile output filename') do |output|
|
||||
options[:profile_output] = output
|
||||
end
|
||||
|
||||
|
|
@ -45,13 +45,9 @@ end
|
|||
|
||||
require File.expand_path('../config/environment', File.dirname(__FILE__))
|
||||
|
||||
result = Gitlab::Profiler.profile(options[:url],
|
||||
logger: Logger.new(options[:sql_output]),
|
||||
post_data: options[:post_data],
|
||||
user: UserFinder.new(options[:username]).find_by_username,
|
||||
private_token: ENV['PRIVATE_TOKEN'])
|
||||
|
||||
printer = RubyProf::CallStackPrinter.new(result)
|
||||
file = File.open(options[:profile_output], 'w')
|
||||
printer.print(file)
|
||||
file.close
|
||||
Gitlab::Profiler.profile(options[:url],
|
||||
logger: Logger.new(options[:sql_output]),
|
||||
post_data: options[:post_data],
|
||||
user: UserFinder.new(options[:username]).find_by_username,
|
||||
private_token: ENV['PRIVATE_TOKEN'],
|
||||
profiler_options: { out: options[:profile_output] })
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Profile bundled RuboCop version.
|
||||
#
|
||||
# See https://github.com/rubocop/rubocop/blob/master/bin/rubocop-profile
|
||||
|
||||
if ARGV.include?('-h') || ARGV.include?('--help')
|
||||
puts "Usage: same as main `rubocop` command but gathers profiling info"
|
||||
puts "Additional option: `--memory` to print memory usage"
|
||||
exit(0)
|
||||
end
|
||||
with_mem = ARGV.delete('--memory')
|
||||
ARGV.unshift '--cache', 'false' unless ARGV.include?('--cache')
|
||||
|
||||
require 'stackprof'
|
||||
if with_mem
|
||||
require 'memory_profiler'
|
||||
MemoryProfiler.start
|
||||
end
|
||||
StackProf.start
|
||||
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||
begin
|
||||
require 'rubocop'
|
||||
|
||||
exit RuboCop::CLI.new.run
|
||||
ensure
|
||||
delta = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
|
||||
puts "Finished in #{delta.round(1)} seconds"
|
||||
StackProf.stop
|
||||
if with_mem
|
||||
puts "Building memory report..."
|
||||
report = MemoryProfiler.stop
|
||||
end
|
||||
Dir.mkdir('tmp') unless File.exist?('tmp')
|
||||
StackProf.results('tmp/stackprof.dump')
|
||||
report&.pretty_print(scale_bytes: true)
|
||||
puts "StackProf written to `tmp/stackprof.dump`."
|
||||
end
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: enforce_runner_token_expires_at
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78557
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/352008
|
||||
milestone: '14.8'
|
||||
type: development
|
||||
group: group::runner
|
||||
default_enabled: false
|
||||
|
|
@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/373792
|
|||
milestone: '15.4'
|
||||
type: development
|
||||
group: group::optimize
|
||||
default_enabled: false
|
||||
default_enabled: true
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
name: gl_listbox_for_sort_dropdowns
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/98363
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/364715
|
||||
name: pipeline_name
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/97502
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/376095
|
||||
milestone: '15.5'
|
||||
type: development
|
||||
group: group::foundations
|
||||
group: group::delivery
|
||||
default_enabled: false
|
||||
|
|
@ -646,7 +646,11 @@ module.exports = {
|
|||
patterns: [
|
||||
{
|
||||
from: path.join(ROOT_PATH, 'node_modules/pdfjs-dist/cmaps/'),
|
||||
to: path.join(WEBPACK_OUTPUT_PATH, 'cmaps/'),
|
||||
to: path.join(WEBPACK_OUTPUT_PATH, 'pdfjs/cmaps/'),
|
||||
},
|
||||
{
|
||||
from: path.join(ROOT_PATH, 'node_modules/pdfjs-dist/legacy/build/pdf.worker.min.js'),
|
||||
to: path.join(WEBPACK_OUTPUT_PATH, 'pdfjs/'),
|
||||
},
|
||||
{
|
||||
from: path.join(ROOT_PATH, 'node_modules', SOURCEGRAPH_PACKAGE, '/'),
|
||||
|
|
|
|||
|
|
@ -26,8 +26,6 @@ module.exports = {
|
|||
entry: {
|
||||
vendor: [
|
||||
'jquery/dist/jquery.slim.js',
|
||||
'pdfjs-dist/build/pdf',
|
||||
'pdfjs-dist/build/pdf.worker.min',
|
||||
'core-js',
|
||||
'echarts',
|
||||
'lodash',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddFreeUserCapOverLimitNotifiedAtToNamespaceDetails < Gitlab::Database::Migration[2.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
TABLE_NAME = 'namespace_details'
|
||||
COLUMN_NAME = 'free_user_cap_over_limit_notified_at'
|
||||
|
||||
def up
|
||||
with_lock_retries do
|
||||
add_column(TABLE_NAME, COLUMN_NAME, :datetime_with_timezone)
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
remove_column TABLE_NAME, COLUMN_NAME
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
2652f733d5998b4dacc89a7c43af45e6d411235efcdc120be02bbf04eb1c55d6
|
||||
|
|
@ -17866,7 +17866,8 @@ CREATE TABLE namespace_details (
|
|||
cached_markdown_version integer,
|
||||
description text,
|
||||
description_html text,
|
||||
free_user_cap_over_limt_notified_at timestamp with time zone
|
||||
free_user_cap_over_limt_notified_at timestamp with time zone,
|
||||
free_user_cap_over_limit_notified_at timestamp with time zone
|
||||
);
|
||||
|
||||
CREATE TABLE namespace_limits (
|
||||
|
|
|
|||
|
|
@ -915,12 +915,8 @@ To determine which runners need to be upgraded:
|
|||
|
||||
## Authentication token security
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30942) in GitLab 15.3 [with a flag](../../administration/feature_flags.md) named `enforce_runner_token_expires_at`. Disabled by default.
|
||||
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to
|
||||
[enable the feature flag](../../administration/feature_flags.md) named `enforce_runner_token_expires_at`.
|
||||
On GitLab.com, this feature is not available.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30942) in GitLab 15.3 [with a flag](../../administration/feature_flags.md) named `enforce_runner_token_expires_at`. Disabled by default.
|
||||
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/377902) in GitLab 15.5. Feature flag `enforce_runner_token_expires_at` removed.
|
||||
|
||||
Each runner has an [authentication token](../../api/runners.md#registration-and-authentication-tokens)
|
||||
to connect with the GitLab instance.
|
||||
|
|
|
|||
|
|
@ -398,6 +398,30 @@ Use [`workflow`](workflow.md) to control pipeline behavior.
|
|||
- [`workflow: rules` examples](workflow.md#workflow-rules-examples)
|
||||
- [Switch between branch pipelines and merge request pipelines](workflow.md#switch-between-branch-pipelines-and-merge-request-pipelines)
|
||||
|
||||
#### `workflow:name`
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/372538) in GitLab 15.5 [with a flag](../../administration/feature_flags.md) named `pipeline_name`. Disabled by default.
|
||||
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is not available. To make it available,
|
||||
ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `pipeline_name`.
|
||||
The feature is not ready for production use.
|
||||
|
||||
You can use `name` in `workflow:` to define a name for pipelines.
|
||||
|
||||
All pipelines are assigned the defined name. Any leading or trailing spaces in the name are removed.
|
||||
|
||||
**Possible inputs**:
|
||||
|
||||
- A string.
|
||||
|
||||
**Example of `workflow:name`**:
|
||||
|
||||
```yaml
|
||||
workflow:
|
||||
name: 'Pipeline name'
|
||||
```
|
||||
|
||||
#### `workflow:rules`
|
||||
|
||||
The `rules` keyword in `workflow` is similar to [`rules` defined in jobs](#rules),
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ The first argument to the profiler is either a full URL
|
|||
(including the instance hostname) or an absolute path, including the
|
||||
leading slash.
|
||||
|
||||
By default the report dump will be stored in a temporary file, which can be
|
||||
interacted with using the [stackprof API](#reading-a-gitlabprofiler-report).
|
||||
|
||||
When using the script, command-line documentation is available by passing no
|
||||
arguments.
|
||||
|
||||
|
|
@ -31,10 +34,11 @@ For example:
|
|||
|
||||
```ruby
|
||||
Gitlab::Profiler.profile('/my-user')
|
||||
# Returns a RubyProf::Profile for the regular operation of this request
|
||||
# Returns the location of the temp file where the report dump is stored
|
||||
class UsersController; def show; sleep 100; end; end
|
||||
Gitlab::Profiler.profile('/my-user')
|
||||
# Returns a RubyProf::Profile where 100 seconds is spent in UsersController#show
|
||||
# Returns the location of the temp file where the report dump is stored
|
||||
# where 100 seconds is spent in UsersController#show
|
||||
```
|
||||
|
||||
For routes that require authorization you must provide a user to
|
||||
|
|
@ -52,57 +56,14 @@ documented with the method source.
|
|||
Gitlab::Profiler.profile('/gitlab-org/gitlab-test', user: User.first, logger: Logger.new($stdout))
|
||||
```
|
||||
|
||||
There is also a RubyProf printer available:
|
||||
`Gitlab::Profiler::TotalTimeFlatPrinter`. This acts like
|
||||
`RubyProf::FlatPrinter`, but its `min_percent` option works on the method's
|
||||
total time, not its self time. (This is because we often spend most of our time
|
||||
in library code, but this comes from calls in our application.) It also offers a
|
||||
`max_percent` option to help filter out outer calls that aren't useful (like
|
||||
`ActionDispatch::Integration::Session#process`).
|
||||
|
||||
There is a convenience method for using this,
|
||||
`Gitlab::Profiler.print_by_total_time`:
|
||||
|
||||
```ruby
|
||||
result = Gitlab::Profiler.profile('/my-user')
|
||||
Gitlab::Profiler.print_by_total_time(result, max_percent: 60, min_percent: 2)
|
||||
# Measure Mode: wall_time
|
||||
# Thread ID: 70005223698240
|
||||
# Fiber ID: 70004894952580
|
||||
# Total: 1.768912
|
||||
# Sort by: total_time
|
||||
#
|
||||
# %self total self wait child calls name
|
||||
# 0.00 1.017 0.000 0.000 1.017 14 *ActionView::Helpers::RenderingHelper#render
|
||||
# 0.00 1.017 0.000 0.000 1.017 14 *ActionView::Renderer#render_partial
|
||||
# 0.00 1.017 0.000 0.000 1.017 14 *ActionView::PartialRenderer#render
|
||||
# 0.00 1.007 0.000 0.000 1.007 14 *ActionView::PartialRenderer#render_partial
|
||||
# 0.00 0.930 0.000 0.000 0.930 14 Hamlit::TemplateHandler#call
|
||||
# 0.00 0.928 0.000 0.000 0.928 14 Temple::Engine#call
|
||||
# 0.02 0.865 0.000 0.000 0.864 638 *Enumerable#inject
|
||||
```
|
||||
|
||||
To print the profile in HTML format, use the following example:
|
||||
|
||||
```ruby
|
||||
result = Gitlab::Profiler.profile('/my-user')
|
||||
|
||||
printer = RubyProf::CallStackPrinter.new(result)
|
||||
printer.print(File.open('/tmp/profile.html', 'w'))
|
||||
```
|
||||
|
||||
### Stackprof support
|
||||
|
||||
By default, `Gitlab::Profiler.profile` uses a tracing profiler called [`ruby-prof`](https://ruby-prof.github.io/). However, sampling profilers
|
||||
[run faster and use less memory](https://jvns.ca/blog/2017/12/17/how-do-ruby---python-profilers-work-/), so they might be preferred.
|
||||
|
||||
You can switch to [Stackprof](https://github.com/tmm1/stackprof) (a sampling profiler) to generate a profile by passing `sampling_mode: true`.
|
||||
Pass in a `profiler_options` hash to configure the output file (`out`) of the sampling data. For example:
|
||||
|
||||
```ruby
|
||||
Gitlab::Profiler.profile('/gitlab-org/gitlab-test', user: User.first, sampling_mode: true, profiler_options: { out: 'tmp/profile.dump' })
|
||||
Gitlab::Profiler.profile('/gitlab-org/gitlab-test', user: User.first, profiler_options: { out: 'tmp/profile.dump' })
|
||||
```
|
||||
|
||||
## Reading a GitLab::Profiler report
|
||||
|
||||
You can get a summary of where time was spent by running Stackprof against the sampling data. For example:
|
||||
|
||||
```shell
|
||||
|
|
|
|||
|
|
@ -0,0 +1,143 @@
|
|||
---
|
||||
stage: Ecosystem
|
||||
group: Foundations
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
description: AwesomeCo test data harness created by the Test Data Working Group https://about.gitlab.com/company/team/structure/working-groups/demo-test-data/
|
||||
comments: false
|
||||
---
|
||||
|
||||
# AwesomeCo
|
||||
|
||||
AwesomeCo is a test data seeding harness, that can seed test data into a user or group namespace.
|
||||
|
||||
AwesomeCo uses FactoryBot in the backend which makes maintenance extremely easy. When a Model is changed,
|
||||
FactoryBot will already be reflected to account for the change.
|
||||
|
||||
## Docker Setup
|
||||
|
||||
See [AwesomeCo Docker Demo](https://gitlab.com/-/snippets/2390362)
|
||||
|
||||
## GDK Setup
|
||||
|
||||
```shell
|
||||
$ gdk start db
|
||||
ok: run: services/postgresql: (pid n) 0s, normally down
|
||||
ok: run: services/redis: (pid n) 74s, normally down
|
||||
$ bundle install
|
||||
Bundle complete!
|
||||
$ bundle exec rake db:migrate
|
||||
main: migrated
|
||||
ci: migrated
|
||||
```
|
||||
|
||||
### Run
|
||||
|
||||
The `gitlab:seed:awesome_co` Rake task takes two arguments. `:name` and `:namespace_id`.
|
||||
|
||||
```shell
|
||||
$ bundle exec rake "gitlab:seed:awesome_co[awesome_co,1]"
|
||||
Seeding AwesomeCo for Administrator
|
||||
```
|
||||
|
||||
#### `:name`
|
||||
|
||||
Where `:name` is the name of the AwesomeCo. (This will reflect .rb files located in db/seeds/awesome_co/*.rb)
|
||||
|
||||
#### `:namespace_id`
|
||||
|
||||
Where `:namespace_id` is the ID of the User or Group Namespace
|
||||
|
||||
## List of Awesome Companies
|
||||
|
||||
Each company (i.e. test data template) is represented as a Ruby file (.rb) in `db/seeds/awesome_co`.
|
||||
|
||||
### AwesomeCo (db/seeds/awesome_co/awesome_co.rb)
|
||||
|
||||
```shell
|
||||
$ bundle exec rake "gitlab:seed:awesome_co[awesome_co,:namespace_id]"
|
||||
Seeding AwesomeCo for :namespace_id
|
||||
```
|
||||
|
||||
AwesomeCo is an automated seeding of [this demo repository](https://gitlab.com/tech-marketing/demos/gitlab-agile-demo/awesome-co).
|
||||
|
||||
## Develop
|
||||
|
||||
AwesomeCo seeding uses FactoryBot definitions from `spec/factories` which ...
|
||||
|
||||
1. Saves time on development
|
||||
1. Are easy-to-read
|
||||
1. Are easy to maintain
|
||||
1. Do not rely on an API that may change in the future
|
||||
1. Are always up-to-date
|
||||
1. Execute on the lowest-level (`ActiveRecord`) possible to create data as quickly as possible
|
||||
|
||||
> _from the [FactoryBot README](https://github.com/thoughtbot/factory_bot#readme_) : factory_bot is a fixtures replacement with a straightforward definition syntax, support for multiple build
|
||||
> strategies (saved instances, unsaved instances, attribute hashes, and stubbed objects), and support for multiple factories for the same class, including factory
|
||||
> inheritance
|
||||
|
||||
Factories reside in `spec/factories/*` and are fixtures for Rails models found in `app/models/*`. For example, For a model named `app/models/issue.rb`, the factory will
|
||||
be named `spec/factories/issues.rb`. For a model named `app/models/project.rb`, the factory will be named `app/models/projects.rb`.
|
||||
|
||||
### Taxonomy of a Factory
|
||||
|
||||
Factories consist of three main parts - the **Name** of the factory, the **Traits** and the **Attributes**.
|
||||
|
||||
Given: `create(:iteration, :with_title, :current, title: 'My Iteration')`
|
||||
|
||||
|||
|
||||
|:-|:-|
|
||||
| **:iteration** | This is the **Name** of the factory. The file name will be the plural form of this **Name** and reside under either `spec/factories/iterations.rb` or `ee/spec/factories/iterations.rb`. |
|
||||
| **:with_title** | This is a **Trait** of the factory. [See how it's defined](https://gitlab.com/gitlab-org/gitlab/-/blob/9c2a1f98483921dd006d70fdaed316e21fc5652f/ee/spec/factories/iterations.rb#L21-23). |
|
||||
| **:current** | This is a **Trait** of the factory. [See how it's defined](https://gitlab.com/gitlab-org/gitlab/-/blob/9c2a1f98483921dd006d70fdaed316e21fc5652f/ee/spec/factories/iterations.rb#L29-31). |
|
||||
| **title: 'My Iteration'** | This is an **Attribute** of the factory that will be passed to the Model for creation. |
|
||||
|
||||
### Examples
|
||||
|
||||
In these examples, you will see an instance variable `@owner`. This is the `root` user (`User.first`).
|
||||
|
||||
#### Create a Group
|
||||
|
||||
```ruby
|
||||
my_group = create(:group, name: 'My Group', path: 'my-group-path')
|
||||
```
|
||||
|
||||
#### Create a Project
|
||||
|
||||
```ruby
|
||||
# create a Project belonging to a Group
|
||||
my_project = create(:project, :public, name: 'My Project', namespace: my_group, creator: @owner)
|
||||
```
|
||||
|
||||
#### Create an Issue
|
||||
|
||||
```ruby
|
||||
# create an Issue belonging to a Project
|
||||
my_issue = create(:issue, title: 'My Issue', project: my_project, weight: 2)
|
||||
```
|
||||
|
||||
#### Create an Iteration
|
||||
|
||||
```ruby
|
||||
# create an Iteration under a Group
|
||||
my_iteration = create(:iteration, :with_title, :current, title: 'My Iteration', group: my_group)
|
||||
```
|
||||
|
||||
### Frequently encountered issues
|
||||
|
||||
#### ActiveRecord::RecordInvalid: Validation failed: Email has already been taken, Username has already been taken
|
||||
|
||||
This is because, by default, our factories are written to backfill any data that is missing. For instance, when a project
|
||||
is created, the project must have somebody that created it. If the owner is not specified, the factory attempts to create it.
|
||||
|
||||
**How to fix**
|
||||
|
||||
Check the respective Factory to find out what key is required. Usually `:author` or `:owner`.
|
||||
|
||||
```ruby
|
||||
# This throws ActiveRecord::RecordInvalid
|
||||
create(:project, name: 'Throws Error', namespace: create(:group, name: 'Some Group'))
|
||||
|
||||
# Specify the user where @owner is a [User] record
|
||||
create(:project, name: 'No longer throws error', owner: @owner, namespace: create(:group, name: 'Some Group'))
|
||||
create(:epic, group: create(:group), author: @owner)
|
||||
```
|
||||
|
|
@ -88,11 +88,11 @@ Prometheus as long as you meet the requirements above.
|
|||
To enable the Prometheus integration for your cluster:
|
||||
|
||||
1. Go to the cluster's page:
|
||||
- For a [project-level cluster](../project/clusters/index.md), navigate to your project's
|
||||
- For a [project-level cluster](../project/clusters/index.md), go to your project's
|
||||
**Infrastructure > Kubernetes clusters**.
|
||||
- For a [group-level cluster](../group/clusters/index.md), navigate to your group's
|
||||
- For a [group-level cluster](../group/clusters/index.md), go to your group's
|
||||
**Kubernetes** page.
|
||||
- For an [instance-level cluster](../instance/clusters/index.md), navigate to your instance's
|
||||
- For an [instance-level cluster](../instance/clusters/index.md), go to your instance's
|
||||
**Kubernetes** page.
|
||||
1. Select the **Integrations** tab.
|
||||
1. Check the **Enable Prometheus integration** checkbox.
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ When you use the `/add_contacts` or `/remove_contacts` quick actions, follow the
|
|||
|
||||
The root group is the topmost group in the group hierarchy.
|
||||
|
||||
When you move an issue, project, or group **within the same group hierarchy**,
|
||||
When you move an issue, project, or group **in the same group hierarchy**,
|
||||
issues retain their contacts.
|
||||
|
||||
When you move an issue or project and the **root group changes**,
|
||||
|
|
|
|||
|
|
@ -35,6 +35,22 @@ graph TD
|
|||
|
||||
Also, read more about possible [planning hierarchies](../planning_hierarchy/index.md).
|
||||
|
||||
### Child issues from different group hierarchies
|
||||
|
||||
<!-- When feature flag is removed, integrate this info as a sentence in
|
||||
https://docs.gitlab.com/ee/user/group/epics/manage_epics.html#add-an-existing-issue-to-an-epic -->
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/371081) in GitLab 15.5 [with a flag](../../../administration/feature_flags.md) named `epic_issues_from_different_hierarchies`. Disabled by default.
|
||||
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/373304) in GitLab 15.5.
|
||||
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is unavailable. To make it available, ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `epic_issues_from_different_hierarchies`.
|
||||
On GitLab.com, this feature is available.
|
||||
|
||||
You can add issues from a different group hierarchy to an epic.
|
||||
To do it, paste the issue URL when
|
||||
[adding an existing issue](manage_epics.md#add-an-existing-issue-to-an-epic).
|
||||
|
||||
## Roadmap in epics **(ULTIMATE)**
|
||||
|
||||
If your epic contains one or more [child epics](manage_epics.md#multi-level-child-epics) that
|
||||
|
|
|
|||
|
|
@ -577,6 +577,7 @@ Or:
|
|||
> - Filtering by type was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/322755) in GitLab 13.10 [with a flag](../../../administration/feature_flags.md) named `vue_issues_list`. Disabled by default.
|
||||
> - Filtering by type was [enabled on self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/322755) in GitLab 14.10.
|
||||
> - Filtering by type is generally available in GitLab 15.1. [Feature flag `vue_issues_list`](https://gitlab.com/gitlab-org/gitlab/-/issues/359966) removed.
|
||||
> - Filtering by health status [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218711) in GitLab 15.5.
|
||||
|
||||
To filter the list of issues:
|
||||
|
||||
|
|
|
|||
|
|
@ -390,6 +390,11 @@ line of your Apache configuration to ensure your page slugs render correctly.
|
|||
WARNING:
|
||||
This operation deletes all data in the wiki.
|
||||
|
||||
WARNING:
|
||||
Any command that changes data directly could be damaging if not run correctly, or under the
|
||||
right conditions. We highly recommend running them in a test environment with a backup of the
|
||||
instance ready to be restored, just in case.
|
||||
|
||||
To clear all data from a project wiki and recreate it in a blank state:
|
||||
|
||||
1. [Start a Rails console session](../../../administration/operations/rails_console.md#starting-a-rails-console-session).
|
||||
|
|
|
|||
|
|
@ -185,3 +185,21 @@ To set issue weight of a task:
|
|||
The task window opens.
|
||||
1. Next to **Weight**, enter a whole, positive number.
|
||||
1. Select the close icon (**{close}**).
|
||||
|
||||
## Add a task to an iteration **(PREMIUM)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/362550) in GitLab 15.5.
|
||||
|
||||
You can add a task to an [iteration](group/iterations/index.md).
|
||||
You can see the iteration title and period only when you view a task.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must have at least the Reporter role for the project.
|
||||
|
||||
To add a task to an iteration:
|
||||
|
||||
1. In the issue description, in the **Tasks** section, select the title of the task you want to edit.
|
||||
The task window opens.
|
||||
1. Next to **Iteration**, select **Add to iteration**.
|
||||
1. From the dropdown list, select the iteration to be associated with the task.
|
||||
|
|
|
|||
|
|
@ -218,6 +218,7 @@ module API
|
|||
[
|
||||
current_user&.cache_key,
|
||||
mr.merge_status,
|
||||
mr.labels.map(&:cache_key),
|
||||
mr.merge_request_assignees.map(&:cache_key),
|
||||
mr.merge_request_reviewers.map(&:cache_key)
|
||||
].join(":")
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ module API
|
|||
@results = search_service(additional_params).search_objects(preload_method)
|
||||
end
|
||||
|
||||
set_global_search_log_information
|
||||
set_global_search_log_information(additional_params)
|
||||
|
||||
Gitlab::Metrics::GlobalSearchSlis.record_apdex(
|
||||
elapsed: @search_duration_s,
|
||||
|
|
@ -105,7 +105,7 @@ module API
|
|||
# EE, without having to modify this file directly.
|
||||
end
|
||||
|
||||
def search_type
|
||||
def search_type(additional_params = {})
|
||||
'basic'
|
||||
end
|
||||
|
||||
|
|
@ -113,10 +113,10 @@ module API
|
|||
params[:scope]
|
||||
end
|
||||
|
||||
def set_global_search_log_information
|
||||
def set_global_search_log_information(additional_params)
|
||||
Gitlab::Instrumentation::GlobalSearchApi.set_information(
|
||||
type: search_type,
|
||||
level: search_service.level,
|
||||
type: search_type(additional_params),
|
||||
level: search_service(additional_params).level,
|
||||
scope: search_scope,
|
||||
search_duration_s: @search_duration_s
|
||||
)
|
||||
|
|
|
|||
|
|
@ -85,6 +85,10 @@ module Gitlab
|
|||
root.workflow_entry.rules_value
|
||||
end
|
||||
|
||||
def workflow_name
|
||||
root.workflow_entry.name
|
||||
end
|
||||
|
||||
def normalized_jobs
|
||||
@normalized_jobs ||= Ci::Config::Normalizer.new(jobs).normalize_jobs
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,12 +6,17 @@ module Gitlab
|
|||
module Entry
|
||||
class Workflow < ::Gitlab::Config::Entry::Node
|
||||
include ::Gitlab::Config::Entry::Configurable
|
||||
include ::Gitlab::Config::Entry::Validatable
|
||||
include ::Gitlab::Config::Entry::Attributable
|
||||
|
||||
ALLOWED_KEYS = %i[rules].freeze
|
||||
ALLOWED_KEYS = %i[rules name].freeze
|
||||
|
||||
attributes :name
|
||||
|
||||
validations do
|
||||
validates :config, type: Hash
|
||||
validates :config, allowed_keys: ALLOWED_KEYS
|
||||
validates :name, allow_nil: true, length: { minimum: 1, maximum: 255 }
|
||||
end
|
||||
|
||||
entry :rules, Entry::Rules,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ module Gitlab
|
|||
return error('Failed to build the pipeline!')
|
||||
end
|
||||
|
||||
set_pipeline_name
|
||||
|
||||
raise Populate::PopulateError if pipeline.persisted?
|
||||
end
|
||||
|
||||
|
|
@ -34,6 +36,15 @@ module Gitlab
|
|||
|
||||
private
|
||||
|
||||
def set_pipeline_name
|
||||
return if Feature.disabled?(:pipeline_name, pipeline.project) ||
|
||||
@command.yaml_processor_result.workflow_name.blank?
|
||||
|
||||
name = @command.yaml_processor_result.workflow_name
|
||||
|
||||
pipeline.build_pipeline_metadata(project: pipeline.project, title: name)
|
||||
end
|
||||
|
||||
def stage_names
|
||||
# We filter out `.pre/.post` stages, as they alone are not considered
|
||||
# a complete pipeline:
|
||||
|
|
|
|||
|
|
@ -36,6 +36,10 @@ module Gitlab
|
|||
@workflow_rules ||= @ci_config.workflow_rules
|
||||
end
|
||||
|
||||
def workflow_name
|
||||
@workflow_name ||= @ci_config.workflow_name&.strip
|
||||
end
|
||||
|
||||
def root_variables
|
||||
@root_variables ||= transform_to_array(@ci_config.variables)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -56,7 +56,6 @@ module Gitlab
|
|||
push_frontend_feature_flag(:security_auto_fix)
|
||||
push_frontend_feature_flag(:new_header_search)
|
||||
push_frontend_feature_flag(:source_editor_toolbar)
|
||||
push_frontend_feature_flag(:gl_listbox_for_sort_dropdowns)
|
||||
push_frontend_feature_flag(:integration_slack_app_notifications)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -43,12 +43,9 @@ module Gitlab
|
|||
# - private_token: instead of providing a user instance, the token can be
|
||||
# given as a string. Takes precedence over the user option.
|
||||
#
|
||||
# - sampling_mode: When true, uses a sampling profiler (StackProf) instead of a tracing profiler (RubyProf).
|
||||
#
|
||||
# - profiler_options: A keyword Hash of arguments passed to the profiler. Defaults by profiler type:
|
||||
# RubyProf - {}
|
||||
# StackProf - { mode: :wall, out: <some temporary file>, interval: 1000, raw: true }
|
||||
def self.profile(url, logger: nil, post_data: nil, user: nil, private_token: nil, sampling_mode: false, profiler_options: {})
|
||||
# - profiler_options: A keyword Hash of arguments passed to the profiler. Defaults:
|
||||
# { mode: :wall, out: <some temporary file>, interval: 1000, raw: true }
|
||||
def self.profile(url, logger: nil, post_data: nil, user: nil, private_token: nil, profiler_options: {})
|
||||
app = ActionDispatch::Integration::Session.new(Rails.application)
|
||||
verb = :get
|
||||
headers = {}
|
||||
|
|
@ -80,7 +77,7 @@ module Gitlab
|
|||
|
||||
with_custom_logger(logger) do
|
||||
with_user(user) do
|
||||
with_profiler(sampling_mode, profiler_options) do
|
||||
with_profiler(profiler_options) do
|
||||
app.public_send(verb, url, params: post_data, headers: headers) # rubocop:disable GitlabSecurity/PublicSend
|
||||
end
|
||||
end
|
||||
|
|
@ -174,21 +171,11 @@ module Gitlab
|
|||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
def self.print_by_total_time(result, options = {})
|
||||
default_options = { sort_method: :total_time, filter_by: :total_time }
|
||||
|
||||
RubyProf::FlatPrinter.new(result).print($stdout, default_options.merge(options))
|
||||
end
|
||||
|
||||
def self.with_profiler(sampling_mode, profiler_options)
|
||||
if sampling_mode
|
||||
require 'stackprof'
|
||||
args = { mode: :wall, interval: 1000, raw: true }.merge!(profiler_options)
|
||||
args[:out] ||= ::Tempfile.new(["profile-#{Time.now.to_i}-", ".dump"]).path
|
||||
::StackProf.run(**args) { yield }
|
||||
else
|
||||
RubyProf.profile(**profiler_options) { yield }
|
||||
end
|
||||
def self.with_profiler(profiler_options)
|
||||
require 'stackprof'
|
||||
args = { mode: :wall, interval: 1000, raw: true }.merge!(profiler_options)
|
||||
args[:out] ||= ::Tempfile.new(["profile-#{Time.now.to_i}-", ".dump"]).path
|
||||
::StackProf.run(**args) { yield }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -45295,9 +45295,6 @@ msgstr ""
|
|||
msgid "Webhooks|The webhook failed to connect, and is disabled. To re-enable it, check %{strong_start}Recent events%{strong_end} for error details, then test your settings below."
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhooks|The webhook was triggered more than %{limit} times per minute and is now disabled. To re-enable this webhook, fix the problems shown in %{strong_start}Recent events%{strong_end}, then re-test your settings. %{support_link_start}Contact Support%{support_link_end} if you need help re-enabling your webhook."
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhooks|Trigger"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -45322,7 +45319,10 @@ msgstr ""
|
|||
msgid "Webhooks|Webhook fails to connect"
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhooks|Webhook was automatically disabled"
|
||||
msgid "Webhooks|Webhook rate limit has been reached"
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhooks|Webhooks for %{root_namespace} are now disabled because they've been triggered more than %{limit} times per minute. They'll be automatically re-enabled in the next minute."
|
||||
msgstr ""
|
||||
|
||||
msgid "Webhooks|Wiki page events"
|
||||
|
|
@ -45731,6 +45731,9 @@ msgstr ""
|
|||
msgid "WorkItem|Add task"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Add to iteration"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Are you sure you want to cancel editing?"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -45781,9 +45784,18 @@ msgstr ""
|
|||
msgid "WorkItem|Issue"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Iteration"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Learn about tasks."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|No iteration"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|No matching results"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|No tasks are currently assigned. Use tasks to break down this issue into smaller parts."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -45814,6 +45826,9 @@ msgstr ""
|
|||
msgid "WorkItem|Something went wrong when deleting the task. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Something went wrong when fetching iterations. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Something went wrong when fetching labels. Please try again."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@
|
|||
"mousetrap": "1.6.5",
|
||||
"papaparse": "^5.3.1",
|
||||
"patch-package": "^6.4.7",
|
||||
"pdfjs-dist": "^2.0.943",
|
||||
"pdfjs-dist": "^2.16.105",
|
||||
"pikaday": "^1.8.0",
|
||||
"popper.js": "^1.16.1",
|
||||
"portal-vue": "^2.1.7",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
install:
|
||||
image: maven:3.6-jdk-11
|
||||
script:
|
||||
- 'mvn install -U -s settings.xml'
|
||||
only:
|
||||
- "<%= imported_project.default_branch %>"
|
||||
tags:
|
||||
- "runner-for-<%= imported_project.name %>"
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<settings>
|
||||
<servers>
|
||||
<server>
|
||||
<id>central-proxy</id>
|
||||
<configuration>
|
||||
<httpHeaders>
|
||||
<property>
|
||||
<name>Private-Token</name>
|
||||
<value><%= personal_access_token %></value>
|
||||
</property>
|
||||
</httpHeaders>
|
||||
</configuration>
|
||||
</server>
|
||||
</servers>
|
||||
<mirrors>
|
||||
<mirror>
|
||||
<id>central-proxy</id>
|
||||
<name>GitLab proxy of central repo</name>
|
||||
<url><%= gitlab_address_with_port %>/api/v4/projects/<%= imported_project.id %>/packages/maven</url>
|
||||
<mirrorOf>central</mirrorOf>
|
||||
</mirror>
|
||||
</mirrors>
|
||||
</settings>
|
||||
|
|
@ -22,10 +22,10 @@ module QA
|
|||
end
|
||||
|
||||
def open_mobile_menu
|
||||
if has_no_element?(:user_avatar)
|
||||
if has_no_element?(:user_avatar_content)
|
||||
Support::Retrier.retry_until do
|
||||
click_element(:mobile_navbar_button)
|
||||
has_element?(:user_avatar)
|
||||
has_element?(:user_avatar_content)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,78 +5,79 @@ module QA
|
|||
module Admin
|
||||
class Menu < Page::Base
|
||||
view 'app/views/layouts/nav/sidebar/_admin.html.haml' do
|
||||
element :admin_sidebar
|
||||
element :admin_sidebar_settings_submenu_content
|
||||
element :admin_settings_item
|
||||
element :admin_settings_repository_item
|
||||
element :admin_settings_general_item
|
||||
element :admin_settings_metrics_and_profiling_item
|
||||
element :admin_sidebar_content
|
||||
element :admin_monitoring_menu_link
|
||||
element :admin_monitoring_submenu_content
|
||||
element :admin_overview_submenu_content
|
||||
element :admin_overview_users_link
|
||||
element :admin_overview_groups_link
|
||||
element :admin_settings_menu_link
|
||||
element :admin_settings_submenu_content
|
||||
element :admin_settings_general_link
|
||||
element :admin_settings_integrations_link
|
||||
element :admin_settings_metrics_and_profiling_link
|
||||
element :admin_settings_network_link
|
||||
element :admin_settings_preferences_link
|
||||
element :admin_monitoring_link
|
||||
element :admin_sidebar_monitoring_submenu_content
|
||||
element :admin_sidebar_overview_submenu_content
|
||||
element :users_overview_link
|
||||
element :groups_overview_link
|
||||
element :integration_settings_link
|
||||
element :admin_settings_repository_link
|
||||
end
|
||||
|
||||
def go_to_preferences_settings
|
||||
hover_element(:admin_settings_item) do
|
||||
within_submenu(:admin_sidebar_settings_submenu_content) do
|
||||
hover_element(:admin_settings_menu_link) do
|
||||
within_submenu(:admin_settings_submenu_content) do
|
||||
click_element :admin_settings_preferences_link
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def go_to_repository_settings
|
||||
hover_element(:admin_settings_item) do
|
||||
within_submenu(:admin_sidebar_settings_submenu_content) do
|
||||
click_element :admin_settings_repository_item
|
||||
hover_element(:admin_settings_menu_link) do
|
||||
within_submenu(:admin_settings_submenu_content) do
|
||||
click_element :admin_settings_repository_link
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def go_to_integration_settings
|
||||
hover_element(:admin_settings_item) do
|
||||
within_submenu(:admin_sidebar_settings_submenu_content) do
|
||||
click_element :integration_settings_link
|
||||
hover_element(:admin_settings_menu_link) do
|
||||
within_submenu(:admin_settings_submenu_content) do
|
||||
click_element :admin_settings_integrations_link
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def go_to_general_settings
|
||||
hover_element(:admin_settings_item) do
|
||||
within_submenu(:admin_sidebar_settings_submenu_content) do
|
||||
click_element :admin_settings_general_item
|
||||
hover_element(:admin_settings_menu_link) do
|
||||
within_submenu(:admin_settings_submenu_content) do
|
||||
click_element :admin_settings_general_link
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def go_to_metrics_and_profiling_settings
|
||||
hover_element(:admin_settings_item) do
|
||||
within_submenu(:admin_sidebar_settings_submenu_content) do
|
||||
click_element :admin_settings_metrics_and_profiling_item
|
||||
hover_element(:admin_settings_menu_link) do
|
||||
within_submenu(:admin_settings_submenu_content) do
|
||||
click_element :admin_settings_metrics_and_profiling_link
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def go_to_network_settings
|
||||
hover_element(:admin_settings_item) do
|
||||
within_submenu(:admin_sidebar_settings_submenu_content) do
|
||||
click_element :admin_settings_network_item
|
||||
hover_element(:admin_settings_menu_link) do
|
||||
within_submenu(:admin_settings_submenu_content) do
|
||||
click_element :admin_settings_network_link
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def go_to_users_overview
|
||||
within_submenu(:admin_sidebar_overview_submenu_content) do
|
||||
click_element :users_overview_link
|
||||
within_submenu(:admin_overview_submenu_content) do
|
||||
click_element :admin_overview_users_link
|
||||
end
|
||||
end
|
||||
|
||||
def go_to_groups_overview
|
||||
within_submenu(:admin_sidebar_overview_submenu_content) do
|
||||
click_element :groups_overview_link
|
||||
within_submenu(:admin_overview_submenu_content) do
|
||||
click_element :admin_overview_groups_link
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -92,7 +93,7 @@ module QA
|
|||
end
|
||||
|
||||
def within_sidebar
|
||||
within_element(:admin_sidebar) do
|
||||
within_element(:admin_sidebar_content) do
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,18 +11,18 @@ module QA
|
|||
|
||||
base.view 'app/views/projects/buttons/_clone.html.haml' do
|
||||
element :clone_dropdown
|
||||
element :clone_options
|
||||
element :ssh_clone_url
|
||||
element :http_clone_url
|
||||
element :clone_dropdown_content
|
||||
element :ssh_clone_url_content
|
||||
element :http_clone_url_content
|
||||
end
|
||||
end
|
||||
|
||||
def repository_clone_http_location
|
||||
repository_clone_location(:http_clone_url)
|
||||
repository_clone_location(:http_clone_url_content)
|
||||
end
|
||||
|
||||
def repository_clone_ssh_location
|
||||
repository_clone_location(:ssh_clone_url)
|
||||
repository_clone_location(:ssh_clone_url_content)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
@ -31,7 +31,7 @@ module QA
|
|||
wait_until(reload: false) do
|
||||
click_element :clone_dropdown
|
||||
|
||||
within_element :clone_options do
|
||||
within_element :clone_dropdown_content do
|
||||
Git::Location.new(find_element(kind).value)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ module QA
|
|||
|
||||
base.view 'app/views/shared/_clone_panel.html.haml' do
|
||||
element :clone_dropdown
|
||||
element :clone_options_dropdown, '.clone-options-dropdown' # rubocop:disable QA/ElementWithPattern
|
||||
element :clone_url, 'text_field_tag :clone_url' # rubocop:disable QA/ElementWithPattern
|
||||
element :clone_dropdown_content
|
||||
element :clone_url_content
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ module QA
|
|||
end
|
||||
|
||||
def repository_location
|
||||
Git::Location.new(find('#clone_url').value)
|
||||
Git::Location.new(find_element(:clone_url_content).text)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
@ -37,7 +37,7 @@ module QA
|
|||
wait_until(reload: false) do
|
||||
click_element :clone_dropdown
|
||||
|
||||
page.within('.clone-options-dropdown') do
|
||||
within_element(:clone_dropdown_content) do
|
||||
click_link(kind)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ module QA
|
|||
view 'app/views/layouts/header/_default.html.haml' do
|
||||
element :navbar, required: true
|
||||
element :canary_badge_link
|
||||
element :user_avatar, required: !QA::Runtime::Env.mobile_layout?
|
||||
element :user_avatar_content, required: !QA::Runtime::Env.mobile_layout?
|
||||
element :user_menu, required: !QA::Runtime::Env.mobile_layout?
|
||||
element :stop_impersonation_link
|
||||
element :issues_shortcut_button, required: !QA::Runtime::Env.mobile_layout?
|
||||
|
|
@ -184,11 +184,11 @@ module QA
|
|||
end
|
||||
|
||||
def has_personal_area?(wait: Capybara.default_max_wait_time)
|
||||
has_element?(:user_avatar, wait: wait)
|
||||
has_element?(:user_avatar_content, wait: wait)
|
||||
end
|
||||
|
||||
def has_no_personal_area?(wait: Capybara.default_max_wait_time)
|
||||
has_no_element?(:user_avatar, wait: wait)
|
||||
has_no_element?(:user_avatar_content, wait: wait)
|
||||
end
|
||||
|
||||
def has_admin_area_link?(wait: Capybara.default_max_wait_time)
|
||||
|
|
@ -227,7 +227,7 @@ module QA
|
|||
|
||||
def within_user_menu(&block)
|
||||
within_top_menu do
|
||||
click_element :user_avatar unless has_element?(:user_profile_link, wait: 1)
|
||||
click_element :user_avatar_content unless has_element?(:user_profile_link, wait: 1)
|
||||
|
||||
within_element(:user_menu, &block)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ module QA
|
|||
module Main
|
||||
class Terms < Page::Base
|
||||
view 'app/views/layouts/terms.html.haml' do
|
||||
element :user_avatar, required: true
|
||||
element :user_avatar_content, required: true
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/terms/components/app.vue' do
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
RSpec.describe 'Package', :orchestrated, :packages, :object_storage, :reliable do
|
||||
RSpec.describe 'Package', :orchestrated, :packages, :object_storage, :reliable,
|
||||
feature_flag: {
|
||||
name: 'maven_central_request_forwarding',
|
||||
scope: :global
|
||||
} do
|
||||
describe 'Maven project level endpoint' do
|
||||
include Runtime::Fixtures
|
||||
|
||||
|
|
@ -143,5 +147,103 @@ module QA
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Maven request forwarding' do
|
||||
include Runtime::Fixtures
|
||||
|
||||
let(:group_id) { 'com.gitlab.qa' }
|
||||
let(:artifact_id) { "maven-#{SecureRandom.hex(8)}" }
|
||||
let(:package_name) { "#{group_id}/#{artifact_id}".tr('.', '/') }
|
||||
let(:package_version) { '1.3.7' }
|
||||
let(:package_type) { 'maven' }
|
||||
let(:personal_access_token) { Runtime::Env.personal_access_token }
|
||||
let(:group) { Resource::Group.fabricate_via_api! }
|
||||
|
||||
let(:gitlab_address_with_port) do
|
||||
uri = URI.parse(Runtime::Scenario.gitlab_address)
|
||||
"#{uri.scheme}://#{uri.host}:#{uri.port}"
|
||||
end
|
||||
|
||||
let(:package) do
|
||||
Resource::Package.init do |package|
|
||||
package.name = package_name
|
||||
package.project = imported_project
|
||||
end
|
||||
end
|
||||
|
||||
let(:runner) do
|
||||
Resource::Runner.fabricate! do |runner|
|
||||
runner.name = "qa-runner-#{Time.now.to_i}"
|
||||
runner.tags = ["runner-for-#{imported_project.name}"]
|
||||
runner.executor = :docker
|
||||
runner.token = group.reload!.runners_token
|
||||
end
|
||||
end
|
||||
|
||||
let(:imported_project) do
|
||||
Resource::ProjectImportedFromURL.fabricate_via_browser_ui! do |project|
|
||||
project.name = "#{package_type}_imported_project"
|
||||
project.group = group
|
||||
project.gitlab_repository_path = 'https://gitlab.com/gitlab-org/quality/imported-projects/maven.git'
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
Runtime::Feature.enable(:maven_central_request_forwarding)
|
||||
Flow::Login.sign_in_unless_signed_in
|
||||
|
||||
imported_project
|
||||
runner
|
||||
end
|
||||
|
||||
after do
|
||||
Runtime::Feature.disable(:maven_central_request_forwarding)
|
||||
|
||||
runner.remove_via_api!
|
||||
package.remove_via_api!
|
||||
imported_project.remove_via_api!
|
||||
end
|
||||
|
||||
it(
|
||||
'uses GitLab as a mirror of the central proxy',
|
||||
:skip_live_env,
|
||||
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/375767'
|
||||
) do
|
||||
Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
|
||||
Resource::Repository::Commit.fabricate_via_api! do |commit|
|
||||
gitlab_ci_yaml = ERB.new(read_fixture('package_managers/maven/project/request_forwarding',
|
||||
'gitlab_ci.yaml.erb'
|
||||
)
|
||||
)
|
||||
.result(binding)
|
||||
settings_xml = ERB.new(read_fixture('package_managers/maven/project/request_forwarding',
|
||||
'settings.xml.erb'
|
||||
)
|
||||
)
|
||||
.result(binding)
|
||||
|
||||
commit.project = imported_project
|
||||
commit.commit_message = 'Add files'
|
||||
commit.add_files(
|
||||
[
|
||||
{ file_path: '.gitlab-ci.yml', content: gitlab_ci_yaml },
|
||||
{ file_path: 'settings.xml', content: settings_xml }
|
||||
])
|
||||
end
|
||||
end
|
||||
|
||||
imported_project.visit!
|
||||
|
||||
Flow::Pipeline.visit_latest_pipeline
|
||||
|
||||
Page::Project::Pipeline::Show.perform do |pipeline|
|
||||
pipeline.click_job('install')
|
||||
end
|
||||
|
||||
Page::Project::Job::Show.perform do |job|
|
||||
expect(job).to be_successful(timeout: 800)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module RuboCop
|
||||
module PathUtil
|
||||
def match_path?(pattern, path)
|
||||
case pattern
|
||||
when String
|
||||
matched = if /[*{}]/.match?(pattern)
|
||||
File.fnmatch?(pattern, path, File::FNM_PATHNAME | File::FNM_EXTGLOB)
|
||||
else
|
||||
pattern == path
|
||||
end
|
||||
|
||||
matched || hidden_file_in_not_hidden_dir?(pattern, path)
|
||||
when Regexp
|
||||
begin
|
||||
pattern.match?(path)
|
||||
rescue ArgumentError => e
|
||||
return false if e.message.start_with?('invalid byte sequence')
|
||||
|
||||
raise e
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module RuboCop
|
||||
module Ext
|
||||
module VariableForce
|
||||
def scanned_node?(node)
|
||||
scanned_nodes.include?(node)
|
||||
end
|
||||
|
||||
def scanned_nodes
|
||||
@scanned_nodes ||= Set.new.compare_by_identity
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RuboCop::Cop::VariableForce.prepend RuboCop::Ext::VariableForce
|
||||
|
|
@ -1,6 +1,11 @@
|
|||
# rubocop:disable Naming/FileName
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Performance improvements to be upstreamed soon:
|
||||
# See https://gitlab.com/gitlab-org/gitlab/-/issues/377469
|
||||
require_relative 'ext/path_util'
|
||||
require_relative 'ext/variable_force'
|
||||
|
||||
# Auto-require all cops under `rubocop/cop/**/*.rb`
|
||||
Dir[File.join(__dir__, 'cop', '**', '*.rb')].sort.each(&method(:require))
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ RSpec.describe 'Dashboard > User filters projects' do
|
|||
let(:project2) { create(:project, name: 'Treasure', namespace: user2.namespace, created_at: 1.second.ago, updated_at: 1.second.ago) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(gl_listbox_for_sort_dropdowns: false)
|
||||
|
||||
project.add_maintainer(user)
|
||||
|
||||
sign_in(user)
|
||||
|
|
@ -147,7 +145,14 @@ RSpec.describe 'Dashboard > User filters projects' do
|
|||
end
|
||||
|
||||
it 'filters any project' do
|
||||
# Selecting the same option in the `GlListbox` does not emit `select` event
|
||||
# and that is why URL update won't be triggered. Given that `Any` is a default option
|
||||
# we need to explicitly switch from some other option (e.g. `Internal`) to `Any`
|
||||
# to trigger the page update
|
||||
select_dropdown_option '#filtered-search-visibility-dropdown > .dropdown', 'Internal', '.dropdown-item'
|
||||
|
||||
select_dropdown_option '#filtered-search-visibility-dropdown > .dropdown', 'Any', '.dropdown-item'
|
||||
|
||||
list = page.all('.projects-list .project-name').map(&:text)
|
||||
|
||||
expect(list).to contain_exactly("Internal project", "Private project", "Treasure", "Victorialand")
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ RSpec.describe 'Sort labels', :js do
|
|||
let!(:label2) { create(:group_label, title: 'Bar', description: 'Fusce consequat', group: group) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(gl_listbox_for_sort_dropdowns: false)
|
||||
|
||||
group.add_maintainer(user)
|
||||
sign_in(user)
|
||||
|
||||
|
|
@ -30,7 +28,7 @@ RSpec.describe 'Sort labels', :js do
|
|||
it 'sorts by date' do
|
||||
click_button 'Name'
|
||||
|
||||
sort_options = find('ul.dropdown-menu').all('li').collect(&:text)
|
||||
sort_options = find('ul[role="listbox"]').all('li').collect(&:text)
|
||||
|
||||
expect(sort_options[0]).to eq('Name')
|
||||
expect(sort_options[1]).to eq('Name, descending')
|
||||
|
|
@ -39,7 +37,7 @@ RSpec.describe 'Sort labels', :js do
|
|||
expect(sort_options[4]).to eq('Updated date')
|
||||
expect(sort_options[5]).to eq('Oldest updated')
|
||||
|
||||
click_button 'Name, descending'
|
||||
find('li', text: 'Name, descending').click
|
||||
|
||||
# assert default sorting
|
||||
within '.other-labels' do
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ RSpec.describe 'Milestones sorting', :js do
|
|||
let(:user) { create(:group_member, :maintainer, user: create(:user), group: group ).user }
|
||||
|
||||
before do
|
||||
stub_feature_flags(gl_listbox_for_sort_dropdowns: false)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
|
|
@ -30,9 +29,9 @@ RSpec.describe 'Milestones sorting', :js do
|
|||
|
||||
within '[data-testid=milestone_sort_by_dropdown]' do
|
||||
click_button 'Due soon'
|
||||
expect(find('.gl-new-dropdown-contents').all('.gl-new-dropdown-item-text-wrapper p').map(&:text)).to eq(['Due soon', 'Due later', 'Start soon', 'Start later', 'Name, ascending', 'Name, descending'])
|
||||
expect(find('ul[role="listbox"]').all('li').map(&:text)).to eq(['Due soon', 'Due later', 'Start soon', 'Start later', 'Name, ascending', 'Name, descending'])
|
||||
|
||||
click_button 'Due later'
|
||||
find('li', text: 'Due later').click
|
||||
expect(page).to have_button('Due later')
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ RSpec.describe 'Sort labels', :js do
|
|||
let!(:label2) { create(:label, title: 'Bar', description: 'Fusce consequat', project: project) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(gl_listbox_for_sort_dropdowns: false)
|
||||
|
||||
project.add_maintainer(user)
|
||||
sign_in(user)
|
||||
|
||||
|
|
@ -30,7 +28,7 @@ RSpec.describe 'Sort labels', :js do
|
|||
it 'sorts by date' do
|
||||
click_button 'Name'
|
||||
|
||||
sort_options = find('ul.dropdown-menu').all('li').collect(&:text)
|
||||
sort_options = find('ul[role="listbox"]').all('li').collect(&:text)
|
||||
|
||||
expect(sort_options[0]).to eq('Name')
|
||||
expect(sort_options[1]).to eq('Name, descending')
|
||||
|
|
@ -39,7 +37,7 @@ RSpec.describe 'Sort labels', :js do
|
|||
expect(sort_options[4]).to eq('Updated date')
|
||||
expect(sort_options[5]).to eq('Oldest updated')
|
||||
|
||||
click_button 'Name, descending'
|
||||
find('li', text: 'Name, descending').click
|
||||
|
||||
# assert default sorting
|
||||
within '.other-labels' do
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ RSpec.describe 'Milestones sorting', :js do
|
|||
end
|
||||
|
||||
before do
|
||||
stub_feature_flags(gl_listbox_for_sort_dropdowns: false)
|
||||
create(:milestone, start_date: 7.days.from_now, due_date: 10.days.from_now, title: "a", project: project)
|
||||
create(:milestone, start_date: 6.days.from_now, due_date: 11.days.from_now, title: "c", project: project)
|
||||
create(:milestone, start_date: 5.days.from_now, due_date: 12.days.from_now, title: "b", project: project)
|
||||
|
|
@ -43,10 +42,10 @@ RSpec.describe 'Milestones sorting', :js do
|
|||
milestones_for_sort_by.each do |sort_by, expected_milestones|
|
||||
within '[data-testid=milestone_sort_by_dropdown]' do
|
||||
click_button selected_sort_order
|
||||
milestones = find('.gl-new-dropdown-contents').all('.gl-new-dropdown-item-text-wrapper p').map(&:text)
|
||||
milestones = find('ul[role="listbox"]').all('li').map(&:text)
|
||||
expect(milestones).to eq(ordered_milestones)
|
||||
|
||||
click_button sort_by
|
||||
find('li', text: sort_by).click
|
||||
expect(page).to have_button(sort_by)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ RSpec.describe 'User sorts projects and order persists' do
|
|||
|
||||
context "from explore projects", :js do
|
||||
before do
|
||||
stub_feature_flags(gl_listbox_for_sort_dropdowns: false)
|
||||
sign_in(user)
|
||||
visit(explore_projects_path)
|
||||
find('#sort-projects-dropdown').click
|
||||
|
|
@ -54,7 +53,6 @@ RSpec.describe 'User sorts projects and order persists' do
|
|||
|
||||
context 'from dashboard projects', :js do
|
||||
before do
|
||||
stub_feature_flags(gl_listbox_for_sort_dropdowns: false)
|
||||
sign_in(user)
|
||||
visit(dashboard_projects_path)
|
||||
find('#sort-projects-dropdown').click
|
||||
|
|
|
|||
|
|
@ -3,10 +3,6 @@
|
|||
require "spec_helper"
|
||||
|
||||
RSpec.describe 'Project wikis', :js do
|
||||
before do
|
||||
stub_feature_flags(gl_listbox_for_sort_dropdowns: false)
|
||||
end
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
let(:wiki) { create(:project_wiki, user: user, project: project) }
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@ rules:changes as array of strings:
|
|||
|
||||
# valid workflow:rules:exists
|
||||
# valid rules:changes:path
|
||||
# valid workflow:name
|
||||
workflow:
|
||||
name: 'Pipeline name'
|
||||
rules:
|
||||
- changes:
|
||||
paths:
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { GlSearchBoxByType, GlToken, GlIcon } from '@gitlab/ui';
|
|||
import Vue, { nextTick } from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { mockTracking } from 'helpers/tracking_helper';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import HeaderSearchApp from '~/header_search/components/app.vue';
|
||||
import HeaderSearchAutocompleteItems from '~/header_search/components/header_search_autocomplete_items.vue';
|
||||
|
|
@ -360,22 +361,43 @@ describe('HeaderSearchApp', () => {
|
|||
|
||||
describe('Header Search Input', () => {
|
||||
describe('when dropdown is closed', () => {
|
||||
it('onFocus opens dropdown', async () => {
|
||||
let trackingSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
|
||||
});
|
||||
|
||||
it('onFocus opens dropdown and triggers snowplow event', async () => {
|
||||
expect(findHeaderSearchDropdown().exists()).toBe(false);
|
||||
findHeaderSearchInput().vm.$emit('focus');
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findHeaderSearchDropdown().exists()).toBe(true);
|
||||
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'focus_input', {
|
||||
label: 'global_search',
|
||||
property: 'top_navigation',
|
||||
});
|
||||
});
|
||||
|
||||
it('onClick opens dropdown', async () => {
|
||||
it('onClick opens dropdown and triggers snowplow event', async () => {
|
||||
expect(findHeaderSearchDropdown().exists()).toBe(false);
|
||||
findHeaderSearchInput().vm.$emit('click');
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findHeaderSearchDropdown().exists()).toBe(true);
|
||||
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'focus_input', {
|
||||
label: 'global_search',
|
||||
property: 'top_navigation',
|
||||
});
|
||||
});
|
||||
|
||||
it('onClick followed by onFocus only triggers a single snowplow event', async () => {
|
||||
findHeaderSearchInput().vm.$emit('click');
|
||||
findHeaderSearchInput().vm.$emit('focus');
|
||||
|
||||
expect(trackingSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { nextTick } from 'vue';
|
||||
import { getAllByRole, getByRole, getByTestId } from '@testing-library/dom';
|
||||
import { GlDropdown, GlListbox } from '@gitlab/ui';
|
||||
import { getAllByRole, getByTestId } from '@testing-library/dom';
|
||||
import { GlListbox } from '@gitlab/ui';
|
||||
import { createWrapper } from '@vue/test-utils';
|
||||
import { initListbox, parseAttributes } from '~/listbox';
|
||||
import { getFixture, setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
|
||||
|
|
@ -39,141 +39,65 @@ describe('initListbox', () => {
|
|||
});
|
||||
|
||||
describe('given a valid element', () => {
|
||||
describe('when `glListboxForSortDropdowns` FF is enabled', () => {
|
||||
let onChangeSpy;
|
||||
let onChangeSpy;
|
||||
|
||||
const listbox = () => createWrapper(instance).findComponent(GlListbox);
|
||||
const findToggleButton = () => getByTestId(document.body, 'base-dropdown-toggle');
|
||||
const findSelectedItems = () => getAllByRole(document.body, 'option', { selected: true });
|
||||
const listbox = () => createWrapper(instance).findComponent(GlListbox);
|
||||
const findToggleButton = () => getByTestId(document.body, 'base-dropdown-toggle');
|
||||
const findSelectedItems = () => getAllByRole(document.body, 'option', { selected: true });
|
||||
|
||||
beforeEach(async () => {
|
||||
setHTMLFixture(fixture);
|
||||
onChangeSpy = jest.fn();
|
||||
setup(document.querySelector('.js-redirect-listbox'), { onChange: onChangeSpy });
|
||||
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetHTMLFixture();
|
||||
});
|
||||
|
||||
it('returns an instance', () => {
|
||||
expect(instance).not.toBe(null);
|
||||
});
|
||||
|
||||
it('renders button with selected item text', () => {
|
||||
expect(findToggleButton().textContent.trim()).toBe('Bar');
|
||||
});
|
||||
|
||||
it('has the correct item selected', () => {
|
||||
const selectedItems = findSelectedItems();
|
||||
expect(selectedItems).toHaveLength(1);
|
||||
expect(selectedItems[0].textContent.trim()).toBe('Bar');
|
||||
});
|
||||
|
||||
it('applies additional classes from the original element', () => {
|
||||
expect(instance.$el.classList).toContain('test-class-1', 'test-class-2');
|
||||
});
|
||||
|
||||
describe.each(parsedAttributes.items)('selecting an item', (item) => {
|
||||
beforeEach(async () => {
|
||||
window.gon.features = { glListboxForSortDropdowns: true };
|
||||
setHTMLFixture(fixture);
|
||||
onChangeSpy = jest.fn();
|
||||
setup(document.querySelector('.js-redirect-listbox'), { onChange: onChangeSpy });
|
||||
|
||||
listbox().vm.$emit('select', item.value);
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetHTMLFixture();
|
||||
it('calls the onChange callback with the item', () => {
|
||||
expect(onChangeSpy).toHaveBeenCalledWith(item);
|
||||
});
|
||||
|
||||
it('returns an instance', () => {
|
||||
expect(instance).not.toBe(null);
|
||||
it('updates the toggle button text', () => {
|
||||
expect(findToggleButton().textContent.trim()).toBe(item.text);
|
||||
});
|
||||
|
||||
it('renders button with selected item text', () => {
|
||||
expect(findToggleButton().textContent.trim()).toBe('Bar');
|
||||
});
|
||||
|
||||
it('has the correct item selected', () => {
|
||||
it('marks the item as selected', () => {
|
||||
const selectedItems = findSelectedItems();
|
||||
expect(selectedItems).toHaveLength(1);
|
||||
expect(selectedItems[0].textContent.trim()).toBe('Bar');
|
||||
});
|
||||
|
||||
it('applies additional classes from the original element', () => {
|
||||
expect(instance.$el.classList).toContain('test-class-1', 'test-class-2');
|
||||
});
|
||||
|
||||
describe.each(parsedAttributes.items)('selecting an item', (item) => {
|
||||
beforeEach(async () => {
|
||||
listbox().vm.$emit('select', item.value);
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
it('calls the onChange callback with the item', () => {
|
||||
expect(onChangeSpy).toHaveBeenCalledWith(item);
|
||||
});
|
||||
|
||||
it('updates the toggle button text', () => {
|
||||
expect(findToggleButton().textContent.trim()).toBe(item.text);
|
||||
});
|
||||
|
||||
it('marks the item as selected', () => {
|
||||
const selectedItems = findSelectedItems();
|
||||
expect(selectedItems).toHaveLength(1);
|
||||
expect(selectedItems[0].textContent.trim()).toBe(item.text);
|
||||
});
|
||||
});
|
||||
|
||||
it('passes the "right" prop through to the underlying component', () => {
|
||||
expect(listbox().props('right')).toBe(parsedAttributes.right);
|
||||
expect(selectedItems[0].textContent.trim()).toBe(item.text);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when `glListboxForSortDropdowns` FF is disabled', () => {
|
||||
let onChangeSpy;
|
||||
|
||||
const ITEM_ROLE = 'menuitem';
|
||||
const dropdown = () => createWrapper(instance).findComponent(GlDropdown);
|
||||
|
||||
const findToggleButton = () => document.body.querySelector('.gl-dropdown-toggle');
|
||||
const findItem = (text) => getByRole(document.body, ITEM_ROLE, { name: text });
|
||||
const findItems = () => getAllByRole(document.body, ITEM_ROLE);
|
||||
const findSelectedItems = () =>
|
||||
findItems().filter(
|
||||
(item) =>
|
||||
!item
|
||||
.querySelector('.gl-new-dropdown-item-check-icon')
|
||||
.classList.contains('gl-visibility-hidden'),
|
||||
);
|
||||
beforeEach(async () => {
|
||||
window.gon.features = { glListboxForSortDropdowns: false };
|
||||
setHTMLFixture(fixture);
|
||||
onChangeSpy = jest.fn();
|
||||
setup(document.querySelector('.js-redirect-listbox'), { onChange: onChangeSpy });
|
||||
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetHTMLFixture();
|
||||
});
|
||||
|
||||
it('returns an instance', () => {
|
||||
expect(instance).not.toBe(null);
|
||||
});
|
||||
|
||||
it('renders button with selected item text', () => {
|
||||
expect(findToggleButton().textContent.trim()).toBe('Bar');
|
||||
});
|
||||
|
||||
it('has the correct item selected', () => {
|
||||
const selectedItems = findSelectedItems();
|
||||
expect(selectedItems).toHaveLength(1);
|
||||
expect(selectedItems[0].textContent.trim()).toBe('Bar');
|
||||
});
|
||||
|
||||
it('applies additional classes from the original element', () => {
|
||||
expect(instance.$el.classList).toContain('test-class-1', 'test-class-2');
|
||||
});
|
||||
|
||||
describe.each(parsedAttributes.items)('selecting an item', (item) => {
|
||||
beforeEach(async () => {
|
||||
findItem(item.text).click();
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
it('calls the onChange callback with the item', () => {
|
||||
expect(onChangeSpy).toHaveBeenCalledWith(item);
|
||||
});
|
||||
|
||||
it('updates the toggle button text', () => {
|
||||
expect(findToggleButton().textContent.trim()).toBe(item.text);
|
||||
});
|
||||
|
||||
it('marks the item as selected', () => {
|
||||
const selectedItems = findSelectedItems();
|
||||
expect(selectedItems).toHaveLength(1);
|
||||
expect(selectedItems[0].textContent.trim()).toBe(item.text);
|
||||
});
|
||||
});
|
||||
|
||||
it('passes the "right" prop through to the underlying component', () => {
|
||||
expect(dropdown().props('right')).toBe(parsedAttributes.right);
|
||||
});
|
||||
it('passes the "right" prop through to the underlying component', () => {
|
||||
expect(listbox().props('right')).toBe(parsedAttributes.right);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { GlNavItemDropdown } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { mount, shallowMount } from '@vue/test-utils';
|
||||
import { mockTracking } from 'helpers/tracking_helper';
|
||||
import TopNavApp from '~/nav/components/top_nav_app.vue';
|
||||
import TopNavDropdownMenu from '~/nav/components/top_nav_dropdown_menu.vue';
|
||||
import { TEST_NAV_DATA } from '../mock_data';
|
||||
|
|
@ -8,6 +9,14 @@ describe('~/nav/components/top_nav_app.vue', () => {
|
|||
let wrapper;
|
||||
|
||||
const createComponent = () => {
|
||||
wrapper = mount(TopNavApp, {
|
||||
propsData: {
|
||||
navData: TEST_NAV_DATA,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const createComponentShallow = () => {
|
||||
wrapper = shallowMount(TopNavApp, {
|
||||
propsData: {
|
||||
navData: TEST_NAV_DATA,
|
||||
|
|
@ -16,6 +25,7 @@ describe('~/nav/components/top_nav_app.vue', () => {
|
|||
};
|
||||
|
||||
const findNavItemDropdown = () => wrapper.findComponent(GlNavItemDropdown);
|
||||
const findNavItemDropdowToggle = () => findNavItemDropdown().find('.js-top-nav-dropdown-toggle');
|
||||
const findMenu = () => wrapper.findComponent(TopNavDropdownMenu);
|
||||
|
||||
afterEach(() => {
|
||||
|
|
@ -24,7 +34,7 @@ describe('~/nav/components/top_nav_app.vue', () => {
|
|||
|
||||
describe('default', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
createComponentShallow();
|
||||
});
|
||||
|
||||
it('renders nav item dropdown', () => {
|
||||
|
|
@ -45,4 +55,18 @@ describe('~/nav/components/top_nav_app.vue', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('tracking', () => {
|
||||
it('emits a tracking event when the toggle is clicked', () => {
|
||||
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
|
||||
createComponent();
|
||||
|
||||
findNavItemDropdowToggle().trigger('click');
|
||||
|
||||
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_nav', {
|
||||
label: 'hamburger_menu',
|
||||
property: 'top_navigation',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { mount } from '@vue/test-utils';
|
|||
import PageComponent from '~/pdf/page/index.vue';
|
||||
|
||||
jest.mock('pdfjs-dist/webpack', () => {
|
||||
return { default: jest.requireActual('pdfjs-dist/build/pdf') };
|
||||
return { default: jest.requireActual('pdfjs-dist/legacy/build/pdf') };
|
||||
});
|
||||
|
||||
describe('Page component', () => {
|
||||
|
|
|
|||
|
|
@ -104,6 +104,10 @@ describe('AdminRunnerShowApp', () => {
|
|||
Platform darwin
|
||||
Configuration Runs untagged jobs
|
||||
Maximum job timeout None
|
||||
Token expiry
|
||||
Runner authentication token expiration
|
||||
Runner authentication tokens will expire based on a set interval.
|
||||
They will automatically rotate once expired. Learn more Never expires
|
||||
Tags None`.replace(/\s+/g, ' ');
|
||||
|
||||
expect(wrapper.text().replace(/\s+/g, ' ')).toContain(expected);
|
||||
|
|
|
|||
|
|
@ -20,8 +20,6 @@ import AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue';
|
|||
import RunnerStackedLayoutBanner from '~/runner/components/runner_stacked_layout_banner.vue';
|
||||
import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
|
||||
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
|
||||
import RunnerBulkDelete from '~/runner/components/runner_bulk_delete.vue';
|
||||
import RunnerBulkDeleteCheckbox from '~/runner/components/runner_bulk_delete_checkbox.vue';
|
||||
import RunnerList from '~/runner/components/runner_list.vue';
|
||||
import RunnerListEmptyState from '~/runner/components/runner_list_empty_state.vue';
|
||||
import RunnerStats from '~/runner/components/stat/runner_stats.vue';
|
||||
|
|
@ -84,8 +82,6 @@ const COUNT_QUERIES = 7; // 4 tabs + 3 status queries
|
|||
|
||||
describe('AdminRunnersApp', () => {
|
||||
let wrapper;
|
||||
let cacheConfig;
|
||||
let localMutations;
|
||||
let showToast;
|
||||
|
||||
const findRunnerStackedLayoutBanner = () => wrapper.findComponent(RunnerStackedLayoutBanner);
|
||||
|
|
@ -93,8 +89,6 @@ describe('AdminRunnersApp', () => {
|
|||
const findRunnerActionsCell = () => wrapper.findComponent(RunnerActionsCell);
|
||||
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
|
||||
const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs);
|
||||
const findRunnerBulkDelete = () => wrapper.findComponent(RunnerBulkDelete);
|
||||
const findRunnerBulkDeleteCheckbox = () => wrapper.findComponent(RunnerBulkDeleteCheckbox);
|
||||
const findRunnerList = () => wrapper.findComponent(RunnerList);
|
||||
const findRunnerListEmptyState = () => wrapper.findComponent(RunnerListEmptyState);
|
||||
const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
|
||||
|
|
@ -107,7 +101,7 @@ describe('AdminRunnersApp', () => {
|
|||
provide,
|
||||
...options
|
||||
} = {}) => {
|
||||
({ cacheConfig, localMutations } = createLocalState());
|
||||
const { cacheConfig, localMutations } = createLocalState();
|
||||
|
||||
const handlers = [
|
||||
[allRunnersQuery, mockRunnersHandler],
|
||||
|
|
@ -373,38 +367,9 @@ describe('AdminRunnersApp', () => {
|
|||
await createComponent({ mountFn: mountExtended });
|
||||
});
|
||||
|
||||
it('runner bulk delete is available', () => {
|
||||
expect(findRunnerBulkDelete().props('runners')).toEqual(mockRunners);
|
||||
});
|
||||
|
||||
it('runner bulk delete checkbox is available', () => {
|
||||
expect(findRunnerBulkDeleteCheckbox().props('runners')).toEqual(mockRunners);
|
||||
});
|
||||
|
||||
it('runner list is checkable', () => {
|
||||
expect(findRunnerList().props('checkable')).toBe(true);
|
||||
});
|
||||
|
||||
it('responds to checked items by updating the local cache', () => {
|
||||
const setRunnerCheckedMock = jest
|
||||
.spyOn(localMutations, 'setRunnerChecked')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const runner = mockRunners[0];
|
||||
|
||||
expect(setRunnerCheckedMock).toHaveBeenCalledTimes(0);
|
||||
|
||||
findRunnerList().vm.$emit('checked', {
|
||||
runner,
|
||||
isChecked: true,
|
||||
});
|
||||
|
||||
expect(setRunnerCheckedMock).toHaveBeenCalledTimes(1);
|
||||
expect(setRunnerCheckedMock).toHaveBeenCalledWith({
|
||||
runner,
|
||||
isChecked: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When runners are deleted', () => {
|
||||
|
|
@ -415,7 +380,7 @@ describe('AdminRunnersApp', () => {
|
|||
it('count data is refetched', async () => {
|
||||
expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES);
|
||||
|
||||
findRunnerBulkDelete().vm.$emit('deleted', { message: 'Runners deleted' });
|
||||
findRunnerList().vm.$emit('deleted', { message: 'Runners deleted' });
|
||||
|
||||
expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES * 2);
|
||||
});
|
||||
|
|
@ -423,7 +388,7 @@ describe('AdminRunnersApp', () => {
|
|||
it('toast is shown', async () => {
|
||||
expect(showToast).toHaveBeenCalledTimes(0);
|
||||
|
||||
findRunnerBulkDelete().vm.$emit('deleted', { message: 'Runners deleted' });
|
||||
findRunnerList().vm.$emit('deleted', { message: 'Runners deleted' });
|
||||
|
||||
expect(showToast).toHaveBeenCalledTimes(1);
|
||||
expect(showToast).toHaveBeenCalledWith('Runners deleted');
|
||||
|
|
|
|||
|
|
@ -5,11 +5,21 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
|||
import RunnerBulkDeleteCheckbox from '~/runner/components/runner_bulk_delete_checkbox.vue';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { createLocalState } from '~/runner/graphql/list/local_state';
|
||||
import { allRunnersData } from '../mock_data';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
jest.mock('~/flash');
|
||||
const makeRunner = (id, deleteRunner = true) => ({
|
||||
id,
|
||||
userPermissions: { deleteRunner },
|
||||
});
|
||||
|
||||
// Multi-select checkbox possible states:
|
||||
const stateToAttrs = {
|
||||
unchecked: { disabled: undefined, checked: undefined, indeterminate: undefined },
|
||||
checked: { disabled: undefined, checked: 'true', indeterminate: undefined },
|
||||
indeterminate: { disabled: undefined, checked: undefined, indeterminate: 'true' },
|
||||
disabled: { disabled: 'true', checked: undefined, indeterminate: undefined },
|
||||
};
|
||||
|
||||
describe('RunnerBulkDeleteCheckbox', () => {
|
||||
let wrapper;
|
||||
|
|
@ -18,12 +28,14 @@ describe('RunnerBulkDeleteCheckbox', () => {
|
|||
|
||||
const findCheckbox = () => wrapper.findComponent(GlFormCheckbox);
|
||||
|
||||
const mockRunners = allRunnersData.data.runners.nodes;
|
||||
const mockIds = allRunnersData.data.runners.nodes.map(({ id }) => id);
|
||||
const mockId = mockIds[0];
|
||||
const mockIdAnotherPage = 'RUNNER_IN_ANOTHER_PAGE_ID';
|
||||
const expectCheckboxToBe = (state) => {
|
||||
const expected = stateToAttrs[state];
|
||||
expect(findCheckbox().attributes('disabled')).toBe(expected.disabled);
|
||||
expect(findCheckbox().attributes('checked')).toBe(expected.checked);
|
||||
expect(findCheckbox().attributes('indeterminate')).toBe(expected.indeterminate);
|
||||
};
|
||||
|
||||
const createComponent = ({ props = {} } = {}) => {
|
||||
const createComponent = ({ runners = [] } = {}) => {
|
||||
const { cacheConfig, localMutations } = mockState;
|
||||
const apolloProvider = createMockApollo(undefined, undefined, cacheConfig);
|
||||
|
||||
|
|
@ -33,8 +45,7 @@ describe('RunnerBulkDeleteCheckbox', () => {
|
|||
localMutations,
|
||||
},
|
||||
propsData: {
|
||||
runners: mockRunners,
|
||||
...props,
|
||||
runners,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -49,31 +60,61 @@ describe('RunnerBulkDeleteCheckbox', () => {
|
|||
jest.spyOn(mockState.localMutations, 'setRunnersChecked');
|
||||
});
|
||||
|
||||
describe.each`
|
||||
case | is | checkedRunnerIds | disabled | checked | indeterminate
|
||||
${'no runners'} | ${'unchecked'} | ${[]} | ${undefined} | ${undefined} | ${undefined}
|
||||
${'no runners in this page'} | ${'unchecked'} | ${[mockIdAnotherPage]} | ${undefined} | ${undefined} | ${undefined}
|
||||
${'all runners'} | ${'checked'} | ${mockIds} | ${undefined} | ${'true'} | ${undefined}
|
||||
${'some runners'} | ${'indeterminate'} | ${[mockId]} | ${undefined} | ${undefined} | ${'true'}
|
||||
${'all plus other runners'} | ${'checked'} | ${[...mockIds, mockIdAnotherPage]} | ${undefined} | ${'true'} | ${undefined}
|
||||
`('When $case are checked', ({ is, checkedRunnerIds, disabled, checked, indeterminate }) => {
|
||||
beforeEach(async () => {
|
||||
describe('when all runners can be deleted', () => {
|
||||
const mockIds = ['1', '2', '3'];
|
||||
const mockIdAnotherPage = '4';
|
||||
const mockRunners = mockIds.map((id) => makeRunner(id));
|
||||
|
||||
it.each`
|
||||
case | checkedRunnerIds | state
|
||||
${'no runners'} | ${[]} | ${'unchecked'}
|
||||
${'no runners in this page'} | ${[mockIdAnotherPage]} | ${'unchecked'}
|
||||
${'all runners'} | ${mockIds} | ${'checked'}
|
||||
${'some runners'} | ${[mockIds[0]]} | ${'indeterminate'}
|
||||
${'all plus other runners'} | ${[...mockIds, mockIdAnotherPage]} | ${'checked'}
|
||||
`('if $case are checked, checkbox is $state', ({ checkedRunnerIds, state }) => {
|
||||
mockCheckedRunnerIds = checkedRunnerIds;
|
||||
|
||||
createComponent();
|
||||
createComponent({ runners: mockRunners });
|
||||
expectCheckboxToBe(state);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when some runners cannot be deleted', () => {
|
||||
it('all allowed runners are selected, checkbox is checked', () => {
|
||||
mockCheckedRunnerIds = ['a', 'b', 'c'];
|
||||
createComponent({
|
||||
runners: [makeRunner('a'), makeRunner('b'), makeRunner('c', false)],
|
||||
});
|
||||
|
||||
expectCheckboxToBe('checked');
|
||||
});
|
||||
|
||||
it(`is ${is}`, () => {
|
||||
expect(findCheckbox().attributes('disabled')).toBe(disabled);
|
||||
expect(findCheckbox().attributes('checked')).toBe(checked);
|
||||
expect(findCheckbox().attributes('indeterminate')).toBe(indeterminate);
|
||||
it('some allowed runners are selected, checkbox is indeterminate', () => {
|
||||
mockCheckedRunnerIds = ['a', 'b'];
|
||||
createComponent({
|
||||
runners: [makeRunner('a'), makeRunner('b'), makeRunner('c')],
|
||||
});
|
||||
|
||||
expectCheckboxToBe('indeterminate');
|
||||
});
|
||||
|
||||
it('no allowed runners are selected, checkbox is disabled', () => {
|
||||
mockCheckedRunnerIds = ['a', 'b'];
|
||||
createComponent({
|
||||
runners: [makeRunner('a', false), makeRunner('b', false)],
|
||||
});
|
||||
|
||||
expectCheckboxToBe('disabled');
|
||||
});
|
||||
});
|
||||
|
||||
describe('When user selects', () => {
|
||||
const mockRunners = [makeRunner('1'), makeRunner('2')];
|
||||
|
||||
beforeEach(() => {
|
||||
mockCheckedRunnerIds = mockIds;
|
||||
createComponent();
|
||||
mockCheckedRunnerIds = ['1', '2'];
|
||||
createComponent({ runners: mockRunners });
|
||||
});
|
||||
|
||||
it.each([[true], [false]])('sets checked to %s', (checked) => {
|
||||
|
|
@ -89,13 +130,11 @@ describe('RunnerBulkDeleteCheckbox', () => {
|
|||
|
||||
describe('When runners are loading', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ props: { runners: [] } });
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it(`is disabled`, () => {
|
||||
expect(findCheckbox().attributes('disabled')).toBe('true');
|
||||
expect(findCheckbox().attributes('checked')).toBe(undefined);
|
||||
expect(findCheckbox().attributes('indeterminate')).toBe(undefined);
|
||||
it('is disabled', () => {
|
||||
expectCheckboxToBe('disabled');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,12 +25,7 @@ describe('RunnerDetails', () => {
|
|||
|
||||
const findDetailGroups = () => wrapper.findComponent(RunnerGroups);
|
||||
|
||||
const createComponent = ({
|
||||
props = {},
|
||||
stubs,
|
||||
mountFn = shallowMountExtended,
|
||||
enforceRunnerTokenExpiresAt = false,
|
||||
} = {}) => {
|
||||
const createComponent = ({ props = {}, stubs, mountFn = shallowMountExtended } = {}) => {
|
||||
wrapper = mountFn(RunnerDetails, {
|
||||
propsData: {
|
||||
...props,
|
||||
|
|
@ -39,9 +34,6 @@ describe('RunnerDetails', () => {
|
|||
RunnerDetail,
|
||||
...stubs,
|
||||
},
|
||||
provide: {
|
||||
glFeatures: { enforceRunnerTokenExpiresAt },
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -82,7 +74,6 @@ describe('RunnerDetails', () => {
|
|||
...runner,
|
||||
},
|
||||
},
|
||||
enforceRunnerTokenExpiresAt: true,
|
||||
stubs: {
|
||||
GlIntersperse,
|
||||
GlSprintf,
|
||||
|
|
@ -135,22 +126,5 @@ describe('RunnerDetails', () => {
|
|||
expect(findDetailGroups().props('runner')).toEqual(mockGroupRunner);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Token expiration field', () => {
|
||||
it.each`
|
||||
case | flag | shown
|
||||
${'is shown when feature flag is enabled'} | ${true} | ${true}
|
||||
${'is not shown when feature flag is disabled'} | ${false} | ${false}
|
||||
`('$case', ({ flag, shown }) => {
|
||||
createComponent({
|
||||
props: {
|
||||
runner: mockGroupRunner,
|
||||
},
|
||||
enforceRunnerTokenExpiresAt: flag,
|
||||
});
|
||||
|
||||
expect(findDd('Token expiry', wrapper).exists()).toBe(shown);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,9 +5,15 @@ import {
|
|||
shallowMountExtended,
|
||||
mountExtended,
|
||||
} from 'helpers/vue_test_utils_helper';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { s__ } from '~/locale';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { createLocalState } from '~/runner/graphql/list/local_state';
|
||||
|
||||
import RunnerList from '~/runner/components/runner_list.vue';
|
||||
import RunnerBulkDelete from '~/runner/components/runner_bulk_delete.vue';
|
||||
import RunnerBulkDeleteCheckbox from '~/runner/components/runner_bulk_delete_checkbox.vue';
|
||||
|
||||
import { I18N_PROJECT_TYPE, I18N_STATUS_NEVER_CONTACTED } from '~/runner/constants';
|
||||
import { allRunnersData, onlineContactTimeoutSecs, staleTimeoutSecs } from '../mock_data';
|
||||
|
||||
|
|
@ -16,6 +22,8 @@ const mockActiveRunnersCount = mockRunners.length;
|
|||
|
||||
describe('RunnerList', () => {
|
||||
let wrapper;
|
||||
let cacheConfig;
|
||||
let localMutations;
|
||||
|
||||
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
|
||||
const findTable = () => wrapper.findComponent(GlTableLite);
|
||||
|
|
@ -23,18 +31,24 @@ describe('RunnerList', () => {
|
|||
const findRows = () => wrapper.findAll('[data-testid^="runner-row-"]');
|
||||
const findCell = ({ row = 0, fieldKey }) =>
|
||||
extendedWrapper(findRows().at(row).find(`[data-testid="td-${fieldKey}"]`));
|
||||
const findRunnerBulkDelete = () => wrapper.findComponent(RunnerBulkDelete);
|
||||
const findRunnerBulkDeleteCheckbox = () => wrapper.findComponent(RunnerBulkDeleteCheckbox);
|
||||
|
||||
const createComponent = (
|
||||
{ props = {}, provide = {}, ...options } = {},
|
||||
mountFn = shallowMountExtended,
|
||||
) => {
|
||||
({ cacheConfig, localMutations } = createLocalState());
|
||||
|
||||
wrapper = mountFn(RunnerList, {
|
||||
apolloProvider: createMockApollo([], {}, cacheConfig),
|
||||
propsData: {
|
||||
runners: mockRunners,
|
||||
activeRunnersCount: mockActiveRunnersCount,
|
||||
...props,
|
||||
},
|
||||
provide: {
|
||||
localMutations,
|
||||
onlineContactTimeoutSecs,
|
||||
staleTimeoutSecs,
|
||||
...provide,
|
||||
|
|
@ -126,21 +140,40 @@ describe('RunnerList', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('runner bulk delete is available', () => {
|
||||
expect(findRunnerBulkDelete().props('runners')).toEqual(mockRunners);
|
||||
});
|
||||
|
||||
it('runner bulk delete checkbox is available', () => {
|
||||
expect(findRunnerBulkDeleteCheckbox().props('runners')).toEqual(mockRunners);
|
||||
});
|
||||
|
||||
it('Displays a checkbox field', () => {
|
||||
expect(findCell({ fieldKey: 'checkbox' }).find('input').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('Emits a checked event', async () => {
|
||||
const checkbox = findCell({ fieldKey: 'checkbox' }).find('input');
|
||||
it('Sets a runner as checked', async () => {
|
||||
const runner = mockRunners[0];
|
||||
const setRunnerCheckedMock = jest
|
||||
.spyOn(localMutations, 'setRunnerChecked')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const checkbox = findCell({ fieldKey: 'checkbox' }).find('input');
|
||||
await checkbox.setChecked();
|
||||
|
||||
expect(wrapper.emitted('checked')).toHaveLength(1);
|
||||
expect(wrapper.emitted('checked')[0][0]).toEqual({
|
||||
expect(setRunnerCheckedMock).toHaveBeenCalledTimes(1);
|
||||
expect(setRunnerCheckedMock).toHaveBeenCalledWith({
|
||||
runner,
|
||||
isChecked: true,
|
||||
runner: mockRunners[0],
|
||||
});
|
||||
});
|
||||
|
||||
it('Emits a deleted event', async () => {
|
||||
const event = { message: 'Deleted!' };
|
||||
findRunnerBulkDelete().vm.$emit('deleted', event);
|
||||
|
||||
expect(wrapper.emitted('deleted')).toEqual([[event]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Scoped cell slots', () => {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue