Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
913224e81c
commit
b9bc4d88ea
|
@ -3666,7 +3666,6 @@ Layout/LineLength:
|
||||||
- 'qa/qa/ee/page/project/secure/security_dashboard.rb'
|
- 'qa/qa/ee/page/project/secure/security_dashboard.rb'
|
||||||
- 'qa/qa/ee/page/project/secure/show.rb'
|
- 'qa/qa/ee/page/project/secure/show.rb'
|
||||||
- 'qa/qa/ee/resource/license.rb'
|
- 'qa/qa/ee/resource/license.rb'
|
||||||
- 'qa/qa/fixtures/auto_devops_rack/config.ru'
|
|
||||||
- 'qa/qa/flow/sign_up.rb'
|
- 'qa/qa/flow/sign_up.rb'
|
||||||
- 'qa/qa/git/repository.rb'
|
- 'qa/qa/git/repository.rb'
|
||||||
- 'qa/qa/page/base.rb'
|
- 'qa/qa/page/base.rb'
|
||||||
|
|
|
@ -217,7 +217,6 @@ Style/Lambda:
|
||||||
- 'lib/gitlab/sidekiq_signals.rb'
|
- 'lib/gitlab/sidekiq_signals.rb'
|
||||||
- 'lib/gitlab/utils/measuring.rb'
|
- 'lib/gitlab/utils/measuring.rb'
|
||||||
- 'lib/gitlab/visibility_level.rb'
|
- 'lib/gitlab/visibility_level.rb'
|
||||||
- 'qa/qa/fixtures/auto_devops_rack/config.ru'
|
|
||||||
- 'rubocop/cop/rspec/modify_sidekiq_middleware.rb'
|
- 'rubocop/cop/rspec/modify_sidekiq_middleware.rb'
|
||||||
- 'rubocop/cop/rspec/timecop_freeze.rb'
|
- 'rubocop/cop/rspec/timecop_freeze.rb'
|
||||||
- 'rubocop/cop/rspec/timecop_travel.rb'
|
- 'rubocop/cop/rspec/timecop_travel.rb'
|
||||||
|
|
|
@ -1281,7 +1281,7 @@ GEM
|
||||||
shellany (0.0.1)
|
shellany (0.0.1)
|
||||||
shoulda-matchers (5.1.0)
|
shoulda-matchers (5.1.0)
|
||||||
activesupport (>= 5.2.0)
|
activesupport (>= 5.2.0)
|
||||||
sidekiq (6.4.0)
|
sidekiq (6.4.2)
|
||||||
connection_pool (>= 2.2.2)
|
connection_pool (>= 2.2.2)
|
||||||
rack (~> 2.0)
|
rack (~> 2.0)
|
||||||
redis (>= 4.2.0)
|
redis (>= 4.2.0)
|
||||||
|
|
|
@ -17,11 +17,6 @@ export default {
|
||||||
GlLoadingIcon,
|
GlLoadingIcon,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
},
|
},
|
||||||
inject: {
|
|
||||||
renderEmptyState: {
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
props: {
|
props: {
|
||||||
action: {
|
action: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -45,6 +40,11 @@ export default {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
renderEmptyState: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -224,6 +224,9 @@ export default {
|
||||||
},
|
},
|
||||||
showLegacyEmptyState() {
|
showLegacyEmptyState() {
|
||||||
const { containerEl } = this;
|
const { containerEl } = this;
|
||||||
|
|
||||||
|
if (!containerEl) return;
|
||||||
|
|
||||||
const contentListEl = containerEl.querySelector(CONTENT_LIST_CLASS);
|
const contentListEl = containerEl.querySelector(CONTENT_LIST_CLASS);
|
||||||
const emptyStateEl = containerEl.querySelector('.empty-state');
|
const emptyStateEl = containerEl.querySelector('.empty-state');
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
<script>
|
||||||
|
import { GlTabs, GlTab } from '@gitlab/ui';
|
||||||
|
import { __ } from '~/locale';
|
||||||
|
import GroupsStore from '../store/groups_store';
|
||||||
|
import GroupsService from '../service/groups_service';
|
||||||
|
import {
|
||||||
|
ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
|
||||||
|
ACTIVE_TAB_SHARED,
|
||||||
|
ACTIVE_TAB_ARCHIVED,
|
||||||
|
} from '../constants';
|
||||||
|
import GroupsApp from './app.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { GlTabs, GlTab, GroupsApp },
|
||||||
|
inject: ['endpoints'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
title: this.$options.i18n.subgroupsAndProjects,
|
||||||
|
key: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
|
||||||
|
renderEmptyState: true,
|
||||||
|
lazy: false,
|
||||||
|
service: new GroupsService(this.endpoints[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]),
|
||||||
|
store: new GroupsStore({ showSchemaMarkup: true }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: this.$options.i18n.sharedProjects,
|
||||||
|
key: ACTIVE_TAB_SHARED,
|
||||||
|
renderEmptyState: false,
|
||||||
|
lazy: true,
|
||||||
|
service: new GroupsService(this.endpoints[ACTIVE_TAB_SHARED]),
|
||||||
|
store: new GroupsStore(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: this.$options.i18n.archivedProjects,
|
||||||
|
key: ACTIVE_TAB_ARCHIVED,
|
||||||
|
renderEmptyState: false,
|
||||||
|
lazy: true,
|
||||||
|
service: new GroupsService(this.endpoints[ACTIVE_TAB_ARCHIVED]),
|
||||||
|
store: new GroupsStore(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
activeTabIndex: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleTabInput(tabIndex) {
|
||||||
|
this.activeTabIndex = tabIndex;
|
||||||
|
|
||||||
|
const tab = this.tabs[tabIndex];
|
||||||
|
tab.lazy = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
i18n: {
|
||||||
|
subgroupsAndProjects: __('Subgroups and projects'),
|
||||||
|
sharedProjects: __('Shared projects'),
|
||||||
|
archivedProjects: __('Archived projects'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<gl-tabs content-class="gl-pt-0" :value="activeTabIndex" @input="handleTabInput">
|
||||||
|
<gl-tab
|
||||||
|
v-for="{ key, title, renderEmptyState, lazy, service, store } in tabs"
|
||||||
|
:key="key"
|
||||||
|
:title="title"
|
||||||
|
:lazy="lazy"
|
||||||
|
>
|
||||||
|
<groups-app
|
||||||
|
:action="key"
|
||||||
|
:service="service"
|
||||||
|
:store="store"
|
||||||
|
:hide-projects="false"
|
||||||
|
:render-empty-state="renderEmptyState"
|
||||||
|
/>
|
||||||
|
</gl-tab>
|
||||||
|
</gl-tabs>
|
||||||
|
</template>
|
|
@ -52,7 +52,6 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
|
||||||
newSubgroupIllustration,
|
newSubgroupIllustration,
|
||||||
newProjectIllustration,
|
newProjectIllustration,
|
||||||
emptySubgroupIllustration,
|
emptySubgroupIllustration,
|
||||||
renderEmptyState,
|
|
||||||
canCreateSubgroups,
|
canCreateSubgroups,
|
||||||
canCreateProjects,
|
canCreateProjects,
|
||||||
currentGroupVisibility,
|
currentGroupVisibility,
|
||||||
|
@ -65,7 +64,6 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
|
||||||
newSubgroupIllustration,
|
newSubgroupIllustration,
|
||||||
newProjectIllustration,
|
newProjectIllustration,
|
||||||
emptySubgroupIllustration,
|
emptySubgroupIllustration,
|
||||||
renderEmptyState: parseBoolean(renderEmptyState),
|
|
||||||
canCreateSubgroups: parseBoolean(canCreateSubgroups),
|
canCreateSubgroups: parseBoolean(canCreateSubgroups),
|
||||||
canCreateProjects: parseBoolean(canCreateProjects),
|
canCreateProjects: parseBoolean(canCreateProjects),
|
||||||
currentGroupVisibility,
|
currentGroupVisibility,
|
||||||
|
@ -75,6 +73,7 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
|
||||||
const { dataset } = dataEl || this.$options.el;
|
const { dataset } = dataEl || this.$options.el;
|
||||||
const hideProjects = parseBoolean(dataset.hideProjects);
|
const hideProjects = parseBoolean(dataset.hideProjects);
|
||||||
const showSchemaMarkup = parseBoolean(dataset.showSchemaMarkup);
|
const showSchemaMarkup = parseBoolean(dataset.showSchemaMarkup);
|
||||||
|
const renderEmptyState = parseBoolean(dataset.renderEmptyState);
|
||||||
const service = new GroupsService(endpoint || dataset.endpoint);
|
const service = new GroupsService(endpoint || dataset.endpoint);
|
||||||
const store = new GroupsStore({ hideProjects, showSchemaMarkup });
|
const store = new GroupsStore({ hideProjects, showSchemaMarkup });
|
||||||
|
|
||||||
|
@ -83,6 +82,7 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
|
||||||
store,
|
store,
|
||||||
service,
|
service,
|
||||||
hideProjects,
|
hideProjects,
|
||||||
|
renderEmptyState,
|
||||||
loading: true,
|
loading: true,
|
||||||
containerId,
|
containerId,
|
||||||
};
|
};
|
||||||
|
@ -119,6 +119,7 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
|
||||||
store: this.store,
|
store: this.store,
|
||||||
service: this.service,
|
service: this.service,
|
||||||
hideProjects: this.hideProjects,
|
hideProjects: this.hideProjects,
|
||||||
|
renderEmptyState: this.renderEmptyState,
|
||||||
containerId: this.containerId,
|
containerId: this.containerId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import { GlToast } from '@gitlab/ui';
|
||||||
|
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||||
|
import GroupFolder from './components/group_folder.vue';
|
||||||
|
import GroupItem from './components/group_item.vue';
|
||||||
|
import {
|
||||||
|
ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
|
||||||
|
ACTIVE_TAB_SHARED,
|
||||||
|
ACTIVE_TAB_ARCHIVED,
|
||||||
|
} from './constants';
|
||||||
|
import OverviewTabs from './components/overview_tabs.vue';
|
||||||
|
|
||||||
|
export const initGroupOverviewTabs = () => {
|
||||||
|
const el = document.getElementById('js-group-overview-tabs');
|
||||||
|
|
||||||
|
if (!el) return false;
|
||||||
|
|
||||||
|
Vue.component('GroupFolder', GroupFolder);
|
||||||
|
Vue.component('GroupItem', GroupItem);
|
||||||
|
Vue.use(GlToast);
|
||||||
|
|
||||||
|
const {
|
||||||
|
newSubgroupPath,
|
||||||
|
newProjectPath,
|
||||||
|
newSubgroupIllustration,
|
||||||
|
newProjectIllustration,
|
||||||
|
emptySubgroupIllustration,
|
||||||
|
canCreateSubgroups,
|
||||||
|
canCreateProjects,
|
||||||
|
currentGroupVisibility,
|
||||||
|
subgroupsAndProjectsEndpoint,
|
||||||
|
sharedProjectsEndpoint,
|
||||||
|
archivedProjectsEndpoint,
|
||||||
|
} = el.dataset;
|
||||||
|
|
||||||
|
return new Vue({
|
||||||
|
el,
|
||||||
|
provide: {
|
||||||
|
newSubgroupPath,
|
||||||
|
newProjectPath,
|
||||||
|
newSubgroupIllustration,
|
||||||
|
newProjectIllustration,
|
||||||
|
emptySubgroupIllustration,
|
||||||
|
canCreateSubgroups: parseBoolean(canCreateSubgroups),
|
||||||
|
canCreateProjects: parseBoolean(canCreateProjects),
|
||||||
|
currentGroupVisibility,
|
||||||
|
endpoints: {
|
||||||
|
[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]: subgroupsAndProjectsEndpoint,
|
||||||
|
[ACTIVE_TAB_SHARED]: sharedProjectsEndpoint,
|
||||||
|
[ACTIVE_TAB_ARCHIVED]: archivedProjectsEndpoint,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
render(createElement) {
|
||||||
|
return createElement(OverviewTabs);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
|
@ -39,6 +39,11 @@ function format(searchTerm, isFallbackKey = false) {
|
||||||
return formattedQuery;
|
return formattedQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSearchTerm(newIssuePath) {
|
||||||
|
const { search, pathname } = document.location;
|
||||||
|
return newIssuePath === pathname ? '' : format(search);
|
||||||
|
}
|
||||||
|
|
||||||
function getFallbackKey() {
|
function getFallbackKey() {
|
||||||
const searchTerm = format(document.location.search, true);
|
const searchTerm = format(document.location.search, true);
|
||||||
return ['autosave', document.location.pathname, searchTerm].join('/');
|
return ['autosave', document.location.pathname, searchTerm].join('/');
|
||||||
|
@ -72,7 +77,8 @@ export default class IssuableForm {
|
||||||
this.reviewersSelect = new UsersSelect(undefined, '.js-reviewer-search');
|
this.reviewersSelect = new UsersSelect(undefined, '.js-reviewer-search');
|
||||||
this.zenMode = new ZenMode();
|
this.zenMode = new ZenMode();
|
||||||
|
|
||||||
this.newIssuePath = form[0].getAttribute(DATA_ISSUES_NEW_PATH);
|
this.searchTerm = getSearchTerm(form[0].getAttribute(DATA_ISSUES_NEW_PATH));
|
||||||
|
this.fallbackKey = getFallbackKey();
|
||||||
this.titleField = this.form.find('input[name*="[title]"]');
|
this.titleField = this.form.find('input[name*="[title]"]');
|
||||||
this.descriptionField = this.form.find('textarea[name*="[description]"]');
|
this.descriptionField = this.form.find('textarea[name*="[description]"]');
|
||||||
if (!(this.titleField.length && this.descriptionField.length)) {
|
if (!(this.titleField.length && this.descriptionField.length)) {
|
||||||
|
@ -109,20 +115,16 @@ export default class IssuableForm {
|
||||||
}
|
}
|
||||||
|
|
||||||
initAutosave() {
|
initAutosave() {
|
||||||
const { search, pathname } = document.location;
|
this.autosaveTitle = new Autosave(
|
||||||
const searchTerm = this.newIssuePath === pathname ? '' : format(search);
|
|
||||||
const fallbackKey = getFallbackKey();
|
|
||||||
|
|
||||||
this.autosave = new Autosave(
|
|
||||||
this.titleField,
|
this.titleField,
|
||||||
[document.location.pathname, searchTerm, 'title'],
|
[document.location.pathname, this.searchTerm, 'title'],
|
||||||
`${fallbackKey}=title`,
|
`${this.fallbackKey}=title`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return new Autosave(
|
this.autosaveDescription = new Autosave(
|
||||||
this.descriptionField,
|
this.descriptionField,
|
||||||
[document.location.pathname, searchTerm, 'description'],
|
[document.location.pathname, this.searchTerm, 'description'],
|
||||||
`${fallbackKey}=description`,
|
`${this.fallbackKey}=description`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,8 +133,8 @@ export default class IssuableForm {
|
||||||
}
|
}
|
||||||
|
|
||||||
resetAutosave() {
|
resetAutosave() {
|
||||||
this.titleField.data('autosave').reset();
|
this.autosaveTitle.reset();
|
||||||
return this.descriptionField.data('autosave').reset();
|
this.autosaveDescription.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
initWip() {
|
initWip() {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { initGroupOverviewTabs } from '~/groups/init_overview_tabs';
|
||||||
import initGroupDetails from '../shared/group_details';
|
import initGroupDetails from '../shared/group_details';
|
||||||
|
|
||||||
initGroupDetails('details');
|
initGroupDetails('details');
|
||||||
|
initGroupOverviewTabs();
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import leaveByUrl from '~/namespaces/leave_by_url';
|
import leaveByUrl from '~/namespaces/leave_by_url';
|
||||||
|
import { initGroupOverviewTabs } from '~/groups/init_overview_tabs';
|
||||||
import initGroupDetails from '../shared/group_details';
|
import initGroupDetails from '../shared/group_details';
|
||||||
|
|
||||||
leaveByUrl('group');
|
leaveByUrl('group');
|
||||||
initGroupDetails();
|
initGroupDetails();
|
||||||
|
initGroupOverviewTabs();
|
||||||
|
|
|
@ -13,6 +13,7 @@ import Poll from '~/lib/utils/poll';
|
||||||
import { normalizeHeaders } from '~/lib/utils/common_utils';
|
import { normalizeHeaders } from '~/lib/utils/common_utils';
|
||||||
import { EXTENSION_ICON_CLASS, EXTENSION_ICONS } from '../../constants';
|
import { EXTENSION_ICON_CLASS, EXTENSION_ICONS } from '../../constants';
|
||||||
import Actions from '../action_buttons.vue';
|
import Actions from '../action_buttons.vue';
|
||||||
|
import StateContainer from '../state_container.vue';
|
||||||
import StatusIcon from './status_icon.vue';
|
import StatusIcon from './status_icon.vue';
|
||||||
import ChildContent from './child_content.vue';
|
import ChildContent from './child_content.vue';
|
||||||
import { createTelemetryHub } from './telemetry';
|
import { createTelemetryHub } from './telemetry';
|
||||||
|
@ -36,6 +37,7 @@ export default {
|
||||||
ChildContent,
|
ChildContent,
|
||||||
DynamicScroller,
|
DynamicScroller,
|
||||||
DynamicScrollerItem,
|
DynamicScrollerItem,
|
||||||
|
StateContainer,
|
||||||
},
|
},
|
||||||
directives: {
|
directives: {
|
||||||
SafeHtml: GlSafeHtmlDirective,
|
SafeHtml: GlSafeHtmlDirective,
|
||||||
|
@ -312,18 +314,15 @@ export default {
|
||||||
data-testid="widget-extension"
|
data-testid="widget-extension"
|
||||||
data-qa-selector="mr_widget_extension"
|
data-qa-selector="mr_widget_extension"
|
||||||
>
|
>
|
||||||
<div
|
<state-container
|
||||||
|
:mr="mr"
|
||||||
|
:status="statusIconName"
|
||||||
|
:is-loading="isLoadingSummary"
|
||||||
:class="{ 'gl-cursor-pointer': isCollapsible }"
|
:class="{ 'gl-cursor-pointer': isCollapsible }"
|
||||||
class="media gl-p-5"
|
class="gl-p-5"
|
||||||
@mousedown="onRowMouseDown"
|
@mousedown="onRowMouseDown"
|
||||||
@mouseup="onRowMouseUp"
|
@mouseup="onRowMouseUp"
|
||||||
>
|
>
|
||||||
<status-icon
|
|
||||||
:level="1"
|
|
||||||
:name="$options.label || $options.name"
|
|
||||||
:is-loading="isLoadingSummary"
|
|
||||||
:icon-name="statusIconName"
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
class="media-body gl-display-flex gl-flex-direction-row! gl-align-self-center"
|
class="media-body gl-display-flex gl-flex-direction-row! gl-align-self-center"
|
||||||
data-testid="widget-extension-top-level"
|
data-testid="widget-extension-top-level"
|
||||||
|
@ -362,7 +361,7 @@ export default {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</state-container>
|
||||||
<div
|
<div
|
||||||
v-if="!isCollapsed"
|
v-if="!isCollapsed"
|
||||||
class="mr-widget-grouped-section gl-relative"
|
class="mr-widget-grouped-section gl-relative"
|
||||||
|
|
|
@ -62,7 +62,9 @@ export default {
|
||||||
<strong v-else v-safe-html="generateText(data.header)"></strong>
|
<strong v-else v-safe-html="generateText(data.header)"></strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="gl-display-flex">
|
<div class="gl-display-flex">
|
||||||
<status-icon v-if="data.icon" :icon-name="data.icon.name" :size="12" class="gl-pl-0" />
|
<div v-if="data.icon" class="report-block-child-icon gl-display-flex">
|
||||||
|
<status-icon :icon-name="data.icon.name" :size="12" class="gl-m-auto" />
|
||||||
|
</div>
|
||||||
<div class="gl-w-full">
|
<div class="gl-w-full">
|
||||||
<div class="gl-display-flex gl-flex-nowrap">
|
<div class="gl-display-flex gl-flex-nowrap">
|
||||||
<div class="gl-flex-wrap gl-display-flex gl-w-full">
|
<div class="gl-flex-wrap gl-display-flex gl-w-full">
|
||||||
|
|
|
@ -44,7 +44,14 @@ export default {
|
||||||
<template>
|
<template>
|
||||||
<div class="mr-widget-body media">
|
<div class="mr-widget-body media">
|
||||||
<div v-if="isLoading" class="gl-w-full mr-conflict-loader">
|
<div v-if="isLoading" class="gl-w-full mr-conflict-loader">
|
||||||
<slot name="loading"></slot>
|
<slot name="loading">
|
||||||
|
<div class="gl-display-flex">
|
||||||
|
<status-icon status="loading" />
|
||||||
|
<div class="media-body">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<slot name="icon">
|
<slot name="icon">
|
||||||
|
|
|
@ -11,7 +11,7 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
required: false,
|
required: false,
|
||||||
validator: (value) => value === '' || Object.keys(EXTENSION_ICONS).indexOf(value) > -1,
|
validator: (value) => value === '' || Object.keys(EXTENSION_ICONS).includes(value),
|
||||||
},
|
},
|
||||||
widgetName: {
|
widgetName: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|
|
@ -21,7 +21,6 @@
|
||||||
@import './pages/notifications';
|
@import './pages/notifications';
|
||||||
@import './pages/pipelines';
|
@import './pages/pipelines';
|
||||||
@import './pages/profile';
|
@import './pages/profile';
|
||||||
@import './pages/profiles/preferences';
|
|
||||||
@import './pages/projects';
|
@import './pages/projects';
|
||||||
@import './pages/prometheus';
|
@import './pages/prometheus';
|
||||||
@import './pages/registry';
|
@import './pages/registry';
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
@import 'page_bundles/mixins_and_variables_and_functions';
|
||||||
|
|
||||||
.application-theme {
|
.application-theme {
|
||||||
$ui-gray-bg: #303030;
|
$ui-gray-bg: #303030;
|
||||||
$ui-light-gray-bg: #f0f0f0;
|
$ui-light-gray-bg: #f0f0f0;
|
|
@ -16,6 +16,10 @@
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.report-block-child-icon {
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.report-block-list {
|
.report-block-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0 1px;
|
padding: 0 1px;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Admin::SpamLogsController < Admin::ApplicationController
|
class Admin::SpamLogsController < Admin::ApplicationController
|
||||||
feature_category :not_owned # rubocop:todo Gitlab/AvoidFeatureCategoryNotOwned
|
feature_category :instance_resiliency
|
||||||
|
|
||||||
# rubocop: disable CodeReuse/ActiveRecord
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
def index
|
def index
|
||||||
|
|
|
@ -172,6 +172,15 @@ module GroupsHelper
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def group_overview_tabs_app_data(group)
|
||||||
|
{
|
||||||
|
subgroups_and_projects_endpoint: group_children_path(group, format: :json),
|
||||||
|
shared_projects_endpoint: group_shared_projects_path(group, format: :json),
|
||||||
|
archived_projects_endpoint: group_children_path(group, format: :json, archived: 'only'),
|
||||||
|
current_group_visibility: group.visibility
|
||||||
|
}.merge(subgroups_and_projects_list_app_data(group))
|
||||||
|
end
|
||||||
|
|
||||||
def enabled_git_access_protocol_options_for_group
|
def enabled_git_access_protocol_options_for_group
|
||||||
case ::Gitlab::CurrentSettings.enabled_git_access_protocol
|
case ::Gitlab::CurrentSettings.enabled_git_access_protocol
|
||||||
when nil, ""
|
when nil, ""
|
||||||
|
|
|
@ -33,6 +33,9 @@
|
||||||
|
|
||||||
= render_if_exists 'groups/group_activity_analytics', group: @group
|
= render_if_exists 'groups/group_activity_analytics', group: @group
|
||||||
|
|
||||||
|
- if Feature.enabled?(:group_overview_tabs_vue, @group)
|
||||||
|
#js-group-overview-tabs{ data: group_overview_tabs_app_data(@group) }
|
||||||
|
- else
|
||||||
.groups-listing{ data: { endpoints: { default: group_children_path(@group, format: :json), shared: group_shared_projects_path(@group, format: :json) } } }
|
.groups-listing{ data: { endpoints: { default: group_children_path(@group, format: :json), shared: group_shared_projects_path(@group, format: :json) } } }
|
||||||
.top-area.group-nav-container.justify-content-between
|
.top-area.group-nav-container.justify-content-between
|
||||||
.scrolling-tabs-container.inner-page-scroll-tabs
|
.scrolling-tabs-container.inner-page-scroll-tabs
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
%p
|
%p
|
||||||
= s_('Notify|All discussions on merge request %{mr_link} were resolved by %{name}') %{mr_link: sanitize(merge_request_reference_link(@merge_request)), name: sanitize_name(@resolved_by.name)}
|
= s_('Notify|All discussions on merge request %{mr_link} were resolved by %{name}').html_safe % { mr_link: merge_request_reference_link(@merge_request), name: sanitize_name(@resolved_by.name) }
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
- page_title _('Preferences')
|
- page_title _('Preferences')
|
||||||
|
- add_page_specific_style 'page_bundles/profiles/preferences'
|
||||||
- @content_class = "limit-container-width" unless fluid_layout
|
- @content_class = "limit-container-width" unless fluid_layout
|
||||||
- user_theme_id = Gitlab::Themes.for_user(@user).id
|
- user_theme_id = Gitlab::Themes.for_user(@user).id
|
||||||
- user_color_schema_id = Gitlab::ColorSchemes.for_user(@user).id
|
- user_color_schema_id = Gitlab::ColorSchemes.for_user(@user).id
|
||||||
|
|
|
@ -291,10 +291,12 @@ module Gitlab
|
||||||
config.assets.precompile << "page_bundles/productivity_analytics.css"
|
config.assets.precompile << "page_bundles/productivity_analytics.css"
|
||||||
config.assets.precompile << "page_bundles/profile.css"
|
config.assets.precompile << "page_bundles/profile.css"
|
||||||
config.assets.precompile << "page_bundles/profile_two_factor_auth.css"
|
config.assets.precompile << "page_bundles/profile_two_factor_auth.css"
|
||||||
|
config.assets.precompile << "page_bundles/profiles/preferences.css"
|
||||||
config.assets.precompile << "page_bundles/project.css"
|
config.assets.precompile << "page_bundles/project.css"
|
||||||
config.assets.precompile << "page_bundles/projects_edit.css"
|
config.assets.precompile << "page_bundles/projects_edit.css"
|
||||||
config.assets.precompile << "page_bundles/reports.css"
|
config.assets.precompile << "page_bundles/reports.css"
|
||||||
config.assets.precompile << "page_bundles/roadmap.css"
|
config.assets.precompile << "page_bundles/roadmap.css"
|
||||||
|
config.assets.precompile << "page_bundles/requirements.css"
|
||||||
config.assets.precompile << "page_bundles/runner_details.css"
|
config.assets.precompile << "page_bundles/runner_details.css"
|
||||||
config.assets.precompile << "page_bundles/security_dashboard.css"
|
config.assets.precompile << "page_bundles/security_dashboard.css"
|
||||||
config.assets.precompile << "page_bundles/security_discover.css"
|
config.assets.precompile << "page_bundles/security_discover.css"
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
name: group_overview_tabs_vue
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95850
|
||||||
|
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/370872
|
||||||
|
milestone: '15.4'
|
||||||
|
type: development
|
||||||
|
group: group::workspace
|
||||||
|
default_enabled: false
|
|
@ -9,5 +9,5 @@
|
||||||
require 'sidekiq/web'
|
require 'sidekiq/web'
|
||||||
|
|
||||||
if Rails.env.development?
|
if Rails.env.development?
|
||||||
Sidekiq.default_worker_options[:backtrace] = true
|
Sidekiq.default_job_options[:backtrace] = true
|
||||||
end
|
end
|
||||||
|
|
|
@ -71,7 +71,7 @@ Migration file for adding `NOT VALID` foreign key:
|
||||||
```ruby
|
```ruby
|
||||||
class AddNotValidForeignKeyToEmailsUser < Gitlab::Database::Migration[2.0]
|
class AddNotValidForeignKeyToEmailsUser < Gitlab::Database::Migration[2.0]
|
||||||
def up
|
def up
|
||||||
add_concurrent_foreign_key :emails, :users, on_delete: :cascade, validate: false
|
add_concurrent_foreign_key :emails, :users, column: :user_id, on_delete: :cascade, validate: false
|
||||||
end
|
end
|
||||||
|
|
||||||
def down
|
def down
|
||||||
|
|
|
@ -144,14 +144,14 @@ graph LR
|
||||||
|
|
||||||
### Manually deploy to production
|
### Manually deploy to production
|
||||||
|
|
||||||
GitLab Docs is deployed to production whenever the `Build docs.gitlab.com every 4 hours` scheduled pipeline runs. By
|
GitLab Docs is deployed to production whenever the `Build docs.gitlab.com every hour` scheduled pipeline runs. By
|
||||||
default, this pipeline runs every four hours.
|
default, this pipeline runs every hour.
|
||||||
|
|
||||||
Maintainers can [manually](../../../ci/pipelines/schedules.md#run-manually) run this pipeline to force a deployment to
|
Maintainers can [manually](../../../ci/pipelines/schedules.md#run-manually) run this pipeline to force a deployment to
|
||||||
production:
|
production:
|
||||||
|
|
||||||
1. Go to the [scheduled pipelines](https://gitlab.com/gitlab-org/gitlab-docs/-/pipeline_schedules) for `gitlab-docs`.
|
1. Go to the [scheduled pipelines](https://gitlab.com/gitlab-org/gitlab-docs/-/pipeline_schedules) for `gitlab-docs`.
|
||||||
1. Next to `Build docs.gitlab.com every 4 hours`, select **Play** (**{play}**).
|
1. Next to `Build docs.gitlab.com every hour`, select **Play** (**{play}**).
|
||||||
|
|
||||||
The updated documentation is available in production after the `pages` and `pages:deploy` jobs
|
The updated documentation is available in production after the `pages` and `pages:deploy` jobs
|
||||||
complete in the new pipeline.
|
complete in the new pipeline.
|
||||||
|
|
|
@ -190,7 +190,7 @@ To update the linting images:
|
||||||
|
|
||||||
1. In `gitlab-docs`, open a merge request to update `.gitlab-ci.yml` to use the new tooling
|
1. In `gitlab-docs`, open a merge request to update `.gitlab-ci.yml` to use the new tooling
|
||||||
version. ([Example MR](https://gitlab.com/gitlab-org/gitlab-docs/-/merge_requests/2571))
|
version. ([Example MR](https://gitlab.com/gitlab-org/gitlab-docs/-/merge_requests/2571))
|
||||||
1. When merged, start a `Build docs.gitlab.com every 4 hours` [scheduled pipeline](https://gitlab.com/gitlab-org/gitlab-docs/-/pipeline_schedules).
|
1. When merged, start a `Build docs.gitlab.com every hour` [scheduled pipeline](https://gitlab.com/gitlab-org/gitlab-docs/-/pipeline_schedules).
|
||||||
1. Go the pipeline you started, and manually run the relevant build-images job,
|
1. Go the pipeline you started, and manually run the relevant build-images job,
|
||||||
for example, `image:docs-lint-markdown`.
|
for example, `image:docs-lint-markdown`.
|
||||||
1. In the job output, get the name of the new image.
|
1. In the job output, get the name of the new image.
|
||||||
|
|
|
@ -49,3 +49,16 @@ Enable the Google Chat integration in GitLab:
|
||||||
|
|
||||||
To test the integration, make a change based on the events you selected and
|
To test the integration, make a change based on the events you selected and
|
||||||
see the notification in your Google Chat room.
|
see the notification in your Google Chat room.
|
||||||
|
|
||||||
|
### Enable threads in Google Chat
|
||||||
|
|
||||||
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/27823) in GitLab 15.4.
|
||||||
|
|
||||||
|
To enable threaded notifications for the same GitLab object (for example, an issue or merge request):
|
||||||
|
|
||||||
|
1. Go to [Google Chat](https://chat.google.com/).
|
||||||
|
1. In **Spaces**, select **+ > Create space**.
|
||||||
|
1. Enter the space name and (optionally) other details, and select **Use threaded replies**.
|
||||||
|
1. Select **Create**.
|
||||||
|
|
||||||
|
You cannot enable threaded replies for existing Google Chat spaces.
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
FROM ruby:2.6.5-alpine
|
|
||||||
ADD ./ /app/
|
|
||||||
WORKDIR /app
|
|
||||||
ENV RACK_ENV production
|
|
||||||
ENV PORT 5000
|
|
||||||
EXPOSE 5000
|
|
||||||
|
|
||||||
RUN bundle install
|
|
||||||
CMD ["bundle","exec", "rackup", "-p", "5000"]
|
|
|
@ -1,5 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
source 'https://rubygems.org'
|
|
||||||
gem 'rack'
|
|
||||||
gem 'rake'
|
|
|
@ -1,15 +0,0 @@
|
||||||
GEM
|
|
||||||
remote: https://rubygems.org/
|
|
||||||
specs:
|
|
||||||
rack (2.2.3)
|
|
||||||
rake (12.3.3)
|
|
||||||
|
|
||||||
PLATFORMS
|
|
||||||
ruby
|
|
||||||
|
|
||||||
DEPENDENCIES
|
|
||||||
rack
|
|
||||||
rake
|
|
||||||
|
|
||||||
BUNDLED WITH
|
|
||||||
1.17.3
|
|
|
@ -1,9 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'rake/testtask'
|
|
||||||
|
|
||||||
task default: %w[test]
|
|
||||||
|
|
||||||
task :test do
|
|
||||||
puts "ok"
|
|
||||||
end
|
|
|
@ -1,3 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
run lambda { |env| [200, { 'Content-Type' => 'text/plain' }, StringIO.new("Hello World! #{ENV['OPTIONAL_MESSAGE']}\n")] }
|
|
|
@ -10,18 +10,13 @@ module QA
|
||||||
element :clusters_actions_button
|
element :clusters_actions_button
|
||||||
end
|
end
|
||||||
|
|
||||||
def connect_existing_cluster
|
def connect_cluster
|
||||||
within_element(:clusters_actions_button) { click_button(class: 'dropdown-toggle-split') }
|
click_element(:clusters_actions_button)
|
||||||
click_link 'Connect a cluster (certificate - deprecated)'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_cluster?(cluster)
|
def has_cluster?(cluster)
|
||||||
has_element?(:cluster, cluster_name: cluster.to_s)
|
has_element?(:cluster, cluster_name: cluster.to_s)
|
||||||
end
|
end
|
||||||
|
|
||||||
def click_on_cluster(cluster)
|
|
||||||
click_on cluster.cluster_name
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -26,25 +26,18 @@ module QA
|
||||||
end
|
end
|
||||||
|
|
||||||
def api_get_path
|
def api_get_path
|
||||||
"gid://gitlab/Clusters::Agent/#{id}"
|
"/projects/#{project.id}/cluster_agents/#{id}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def api_post_path
|
def api_post_path
|
||||||
"/graphql"
|
"/projects/#{project.id}/cluster_agents"
|
||||||
end
|
end
|
||||||
|
|
||||||
def api_post_body
|
def api_post_body
|
||||||
<<~GQL
|
{
|
||||||
mutation createAgent {
|
id: project.id,
|
||||||
createClusterAgent(input: { projectPath: "#{project.full_path}", name: "#{@name}" }) {
|
name: name
|
||||||
clusterAgent {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
}
|
||||||
errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
GQL
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,7 +5,7 @@ module QA
|
||||||
module Clusters
|
module Clusters
|
||||||
class AgentToken < QA::Resource::Base
|
class AgentToken < QA::Resource::Base
|
||||||
attribute :id
|
attribute :id
|
||||||
attribute :secret
|
attribute :token
|
||||||
attribute :agent do
|
attribute :agent do
|
||||||
QA::Resource::Clusters::Agent.fabricate_via_api!
|
QA::Resource::Clusters::Agent.fabricate_via_api!
|
||||||
end
|
end
|
||||||
|
@ -20,26 +20,19 @@ module QA
|
||||||
end
|
end
|
||||||
|
|
||||||
def api_get_path
|
def api_get_path
|
||||||
"gid://gitlab/Clusters::AgentToken/#{id}"
|
"/projects/#{agent.project.id}/cluster_agents/#{agent.id}/tokens/#{id}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def api_post_path
|
def api_post_path
|
||||||
"/graphql"
|
"/projects/#{agent.project.id}/cluster_agents/#{agent.id}/tokens"
|
||||||
end
|
end
|
||||||
|
|
||||||
def api_post_body
|
def api_post_body
|
||||||
<<~GQL
|
{
|
||||||
mutation createToken {
|
id: agent.project.id,
|
||||||
clusterAgentTokenCreate(input: { clusterAgentId: "gid://gitlab/Clusters::Agent/#{agent.id}" name: "token-#{agent.id}" }) {
|
agent_id: agent.id,
|
||||||
secret # This is the value you need to use on the next step
|
name: agent.name
|
||||||
token {
|
|
||||||
createdAt
|
|
||||||
id
|
|
||||||
}
|
}
|
||||||
errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
GQL
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,50 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module QA
|
|
||||||
module Resource
|
|
||||||
module KubernetesCluster
|
|
||||||
# TODO: This resource is currently broken, since one-click apps have been removed.
|
|
||||||
# See https://gitlab.com/gitlab-org/gitlab/-/issues/333818
|
|
||||||
class ProjectCluster < Base
|
|
||||||
attr_writer :cluster,
|
|
||||||
:install_ingress, :install_prometheus, :install_runner, :domain
|
|
||||||
|
|
||||||
attribute :project do
|
|
||||||
Resource::Project.fabricate!
|
|
||||||
end
|
|
||||||
|
|
||||||
attribute :ingress_ip do
|
|
||||||
@cluster.fetch_external_ip_for_ingress
|
|
||||||
end
|
|
||||||
|
|
||||||
def fabricate!
|
|
||||||
project.visit!
|
|
||||||
|
|
||||||
Page::Project::Menu.perform(
|
|
||||||
&:go_to_infrastructure_kubernetes)
|
|
||||||
|
|
||||||
Page::Project::Infrastructure::Kubernetes::Index.perform(
|
|
||||||
&:connect_existing_cluster)
|
|
||||||
|
|
||||||
Page::Project::Infrastructure::Kubernetes::AddExisting.perform do |cluster_page|
|
|
||||||
cluster_page.set_cluster_name(@cluster.cluster_name)
|
|
||||||
cluster_page.set_api_url(@cluster.api_url)
|
|
||||||
cluster_page.set_ca_certificate(@cluster.ca_certificate)
|
|
||||||
cluster_page.set_token(@cluster.token)
|
|
||||||
cluster_page.uncheck_rbac! unless @cluster.rbac
|
|
||||||
cluster_page.add_cluster!
|
|
||||||
end
|
|
||||||
|
|
||||||
Page::Project::Infrastructure::Kubernetes::Show.perform do |show|
|
|
||||||
if @install_ingress
|
|
||||||
ingress_ip
|
|
||||||
|
|
||||||
show.set_domain("#{@ingress_ip}.nip.io")
|
|
||||||
show.save_domain
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -33,14 +33,32 @@ module QA
|
||||||
delete_cluster
|
delete_cluster
|
||||||
end
|
end
|
||||||
|
|
||||||
def install_ingress
|
# kas is hardcoded to staging since this test should only run in staging for now
|
||||||
QA::Runtime::Logger.info "Attempting to install Ingress on cluster #{cluster_name}"
|
def install_kubernetes_agent(agent_token)
|
||||||
shell 'kubectl create -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-0.31.0/deploy/static/provider/cloud/deploy.yaml'
|
install_helm
|
||||||
wait_for_ingress
|
|
||||||
|
shell <<~CMD.tr("\n", ' ')
|
||||||
|
helm repo add gitlab https://charts.gitlab.io &&
|
||||||
|
helm repo update &&
|
||||||
|
helm upgrade --install test gitlab/gitlab-agent
|
||||||
|
--namespace gitlab-agent
|
||||||
|
--create-namespace
|
||||||
|
--set image.tag=#{Runtime::Env.gitlab_agentk_version}
|
||||||
|
--set config.token=#{agent_token}
|
||||||
|
--set config.kasAddress=wss://kas.staging.gitlab.com
|
||||||
|
CMD
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def install_helm
|
||||||
|
shell <<~CMD.tr("\n", ' ')
|
||||||
|
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 &&
|
||||||
|
chmod 700 get_helm.sh &&
|
||||||
|
./get_helm.sh
|
||||||
|
CMD
|
||||||
|
end
|
||||||
|
|
||||||
def login_if_not_already_logged_in
|
def login_if_not_already_logged_in
|
||||||
if Runtime::Env.has_gcloud_credentials?
|
if Runtime::Env.has_gcloud_credentials?
|
||||||
attempt_login_with_env_vars
|
attempt_login_with_env_vars
|
||||||
|
@ -104,18 +122,6 @@ module QA
|
||||||
def get_region
|
def get_region
|
||||||
Runtime::Env.gcloud_region || @available_regions.delete(@available_regions.sample)
|
Runtime::Env.gcloud_region || @available_regions.delete(@available_regions.sample)
|
||||||
end
|
end
|
||||||
|
|
||||||
def wait_for_ingress
|
|
||||||
QA::Runtime::Logger.info 'Waiting for Ingress controller pod to be initialized'
|
|
||||||
|
|
||||||
Support::Retrier.retry_until(max_attempts: 60, sleep_interval: 1) do
|
|
||||||
service_available?('kubectl get pods --all-namespaces -l app.kubernetes.io/component=controller | grep -o "ingress-nginx-controller.*1/1"')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def service_available?(command)
|
|
||||||
system("#{command} > /dev/null 2>&1")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -41,8 +41,8 @@ module QA
|
||||||
cluster_name
|
cluster_name
|
||||||
end
|
end
|
||||||
|
|
||||||
def install_ingress
|
def install_kubernetes_agent(agent_token)
|
||||||
@provider.install_ingress
|
@provider.install_kubernetes_agent(agent_token)
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_secret(secret, secret_name)
|
def create_secret(secret, secret_name)
|
||||||
|
@ -73,16 +73,6 @@ module QA
|
||||||
shell('kubectl apply -f -', stdin_data: network_policy)
|
shell('kubectl apply -f -', stdin_data: network_policy)
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_external_ip_for_ingress
|
|
||||||
install_ingress
|
|
||||||
|
|
||||||
# need to wait since the ingress-nginx service has an initial delay set of 10 seconds
|
|
||||||
sleep 12
|
|
||||||
ingress_ip = `kubectl get svc --all-namespaces --no-headers=true -l app.kubernetes.io/name=ingress-nginx -o custom-columns=:'status.loadBalancer.ingress[0].ip' | grep -v 'none'`
|
|
||||||
QA::Runtime::Logger.debug "Has ingress address set to: #{ingress_ip}"
|
|
||||||
ingress_ip
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def fetch_api_url
|
def fetch_api_url
|
||||||
|
|
|
@ -1,51 +1,52 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module QA
|
module QA
|
||||||
RSpec.describe 'Configure',
|
RSpec.describe 'Configure', only: { subdomain: %i[staging staging-canary] } do
|
||||||
only: { subdomain: %i[staging staging-canary] },
|
describe 'Auto DevOps with a Kubernetes Agent' do
|
||||||
quarantine: {
|
let!(:app_project) do
|
||||||
issue: 'https://gitlab.com/gitlab-org/quality/team-tasks/-/issues/1198',
|
|
||||||
type: :waiting_on
|
|
||||||
} do
|
|
||||||
let(:project) do
|
|
||||||
Resource::Project.fabricate_via_api! do |project|
|
Resource::Project.fabricate_via_api! do |project|
|
||||||
project.name = 'autodevops-project'
|
project.name = 'autodevops-app-project'
|
||||||
|
project.template_name = 'express'
|
||||||
project.auto_devops_enabled = true
|
project.auto_devops_enabled = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
let!(:cluster) { Service::KubernetesCluster.new(provider_class: Service::ClusterProvider::Gcloud).create! }
|
||||||
set_kube_ingress_base_domain(project)
|
|
||||||
disable_optional_jobs(project)
|
let!(:kubernetes_agent) do
|
||||||
|
Resource::Clusters::Agent.fabricate_via_api! do |agent|
|
||||||
|
agent.name = 'agent1'
|
||||||
|
agent.project = app_project
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'Auto DevOps support' do
|
let!(:agent_token) do
|
||||||
context 'when rbac is enabled' do
|
Resource::Clusters::AgentToken.fabricate_via_api! do |token|
|
||||||
let(:cluster) { Service::KubernetesCluster.new.create! }
|
token.agent = kubernetes_agent
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
cluster.install_kubernetes_agent(agent_token.token)
|
||||||
|
upload_agent_config(app_project, kubernetes_agent.name)
|
||||||
|
|
||||||
|
set_kube_ingress_base_domain(app_project)
|
||||||
|
set_kube_context(app_project)
|
||||||
|
disable_optional_jobs(app_project)
|
||||||
|
end
|
||||||
|
|
||||||
after do
|
after do
|
||||||
cluster&.remove!
|
cluster&.remove!
|
||||||
project.remove_via_api!
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'runs auto devops', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348061' do
|
it 'runs auto devops', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348061' do
|
||||||
Flow::Login.sign_in
|
Flow::Login.sign_in
|
||||||
|
|
||||||
Resource::KubernetesCluster::ProjectCluster.fabricate! do |k8s_cluster|
|
app_project.visit!
|
||||||
k8s_cluster.project = project
|
|
||||||
k8s_cluster.cluster = cluster
|
|
||||||
k8s_cluster.install_ingress = true
|
|
||||||
end
|
|
||||||
|
|
||||||
Resource::Repository::ProjectPush.fabricate! do |push|
|
Page::Project::Menu.perform(&:click_ci_cd_pipelines)
|
||||||
push.project = project
|
Page::Project::Pipeline::Index.perform(&:click_run_pipeline_button)
|
||||||
push.directory = Pathname
|
Page::Project::Pipeline::New.perform(&:click_run_pipeline_button)
|
||||||
.new(__dir__)
|
|
||||||
.join('../../../../../fixtures/auto_devops_rack')
|
|
||||||
push.commit_message = 'Create Auto DevOps compatible rack application'
|
|
||||||
end
|
|
||||||
|
|
||||||
Flow::Pipeline.visit_latest_pipeline
|
|
||||||
|
|
||||||
Page::Project::Pipeline::Show.perform do |pipeline|
|
Page::Project::Pipeline::Show.perform do |pipeline|
|
||||||
pipeline.click_job('build')
|
pipeline.click_job('build')
|
||||||
|
@ -56,23 +57,11 @@ module QA
|
||||||
job.click_element(:pipeline_path)
|
job.click_element(:pipeline_path)
|
||||||
end
|
end
|
||||||
|
|
||||||
Page::Project::Pipeline::Show.perform do |pipeline|
|
|
||||||
pipeline.click_job('test')
|
|
||||||
end
|
|
||||||
Page::Project::Job::Show.perform do |job|
|
|
||||||
expect(job).to be_successful(timeout: 600)
|
|
||||||
|
|
||||||
job.click_element(:pipeline_path)
|
|
||||||
end
|
|
||||||
|
|
||||||
Page::Project::Pipeline::Show.perform do |pipeline|
|
Page::Project::Pipeline::Show.perform do |pipeline|
|
||||||
pipeline.click_job('production')
|
pipeline.click_job('production')
|
||||||
end
|
end
|
||||||
Page::Project::Job::Show.perform do |job|
|
Page::Project::Job::Show.perform do |job|
|
||||||
expect(job).to be_successful(timeout: 1200)
|
expect(job).to be_successful(timeout: 600)
|
||||||
|
|
||||||
job.click_element(:pipeline_path)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -88,12 +77,43 @@ module QA
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_kube_context(project)
|
||||||
|
Resource::CiVariable.fabricate_via_api! do |resource|
|
||||||
|
resource.project = project
|
||||||
|
resource.key = 'KUBE_CONTEXT'
|
||||||
|
resource.value = "#{project.path_with_namespace}:#{kubernetes_agent.name}"
|
||||||
|
resource.masked = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def upload_agent_config(project, agent)
|
||||||
|
Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
|
||||||
|
Resource::Repository::Commit.fabricate_via_api! do |commit|
|
||||||
|
commit.project = project
|
||||||
|
commit.commit_message = 'Add kubernetes agent configuration'
|
||||||
|
commit.add_files(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
file_path: ".gitlab/agents/#{agent}/config.yaml",
|
||||||
|
content: <<~YAML
|
||||||
|
ci_access:
|
||||||
|
projects:
|
||||||
|
- id: #{project.path_with_namespace}
|
||||||
|
YAML
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def disable_optional_jobs(project)
|
def disable_optional_jobs(project)
|
||||||
%w[
|
%w[
|
||||||
CODE_QUALITY_DISABLED LICENSE_MANAGEMENT_DISABLED
|
TEST_DISABLED CODE_QUALITY_DISABLED LICENSE_MANAGEMENT_DISABLED
|
||||||
SAST_DISABLED DAST_DISABLED DEPENDENCY_SCANNING_DISABLED
|
BROWSER_PERFORMANCE_DISABLED LOAD_PERFORMANCE_DISABLED
|
||||||
CONTAINER_SCANNING_DISABLED BROWSER_PERFORMANCE_DISABLED
|
SAST_DISABLED SECRET_DETECTION_DISABLED DEPENDENCY_SCANNING_DISABLED
|
||||||
SECRET_DETECTION_DISABLED
|
CONTAINER_SCANNING_DISABLED DAST_DISABLED REVIEW_DISABLED
|
||||||
|
CODE_INTELLIGENCE_DISABLED CLUSTER_IMAGE_SCANNING_DISABLED
|
||||||
].each do |key|
|
].each do |key|
|
||||||
Resource::CiVariable.fabricate_via_api! do |resource|
|
Resource::CiVariable.fabricate_via_api! do |resource|
|
||||||
resource.project = project
|
resource.project = project
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module QA
|
|
||||||
RSpec.describe 'Configure', except: { job: 'review-qa-*' } do
|
|
||||||
describe 'Kubernetes Cluster Integration', :orchestrated, :requires_admin, :skip_live_env do
|
|
||||||
context 'Project Clusters' do
|
|
||||||
let!(:cluster) { Service::KubernetesCluster.new(provider_class: Service::ClusterProvider::K3s).create! }
|
|
||||||
let(:project) do
|
|
||||||
Resource::Project.fabricate_via_api! do |project|
|
|
||||||
project.name = 'project-with-k8s'
|
|
||||||
project.description = 'Project with Kubernetes cluster integration'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
before do
|
|
||||||
Flow::Login.sign_in_as_admin
|
|
||||||
end
|
|
||||||
|
|
||||||
after do
|
|
||||||
cluster.remove!
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'can create and associate a project cluster', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348062' do
|
|
||||||
Resource::KubernetesCluster::ProjectCluster.fabricate_via_browser_ui! do |k8s_cluster|
|
|
||||||
k8s_cluster.project = project
|
|
||||||
k8s_cluster.cluster = cluster
|
|
||||||
end.project.visit!
|
|
||||||
|
|
||||||
Page::Project::Menu.perform(&:go_to_infrastructure_kubernetes)
|
|
||||||
|
|
||||||
Page::Project::Infrastructure::Kubernetes::Index.perform do |index|
|
|
||||||
expect(index).to have_cluster(cluster)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -331,6 +331,7 @@ RSpec.describe 'Group show page' do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not include structured markup in shared projects tab', :aggregate_failures, :js do
|
it 'does not include structured markup in shared projects tab', :aggregate_failures, :js do
|
||||||
|
stub_feature_flags(group_overview_tabs_vue: false)
|
||||||
other_project = create(:project, :public)
|
other_project = create(:project, :public)
|
||||||
other_project.project_group_links.create!(group: group)
|
other_project.project_group_links.create!(group: group)
|
||||||
|
|
||||||
|
@ -342,6 +343,7 @@ RSpec.describe 'Group show page' do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not include structured markup in archived projects tab', :aggregate_failures, :js do
|
it 'does not include structured markup in archived projects tab', :aggregate_failures, :js do
|
||||||
|
stub_feature_flags(group_overview_tabs_vue: false)
|
||||||
project.update!(archived: true)
|
project.update!(archived: true)
|
||||||
|
|
||||||
visit group_archived_path(group)
|
visit group_archived_path(group)
|
||||||
|
|
|
@ -24,6 +24,7 @@ RSpec.describe 'User sorts projects and order persists' do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "is set on the group_canonical_path" do
|
it "is set on the group_canonical_path" do
|
||||||
|
stub_feature_flags(group_overview_tabs_vue: false)
|
||||||
visit(group_canonical_path(group))
|
visit(group_canonical_path(group))
|
||||||
|
|
||||||
within '[data-testid=group_sort_by_dropdown]' do
|
within '[data-testid=group_sort_by_dropdown]' do
|
||||||
|
@ -32,6 +33,7 @@ RSpec.describe 'User sorts projects and order persists' do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "is set on the details_group_path" do
|
it "is set on the details_group_path" do
|
||||||
|
stub_feature_flags(group_overview_tabs_vue: false)
|
||||||
visit(details_group_path(group))
|
visit(details_group_path(group))
|
||||||
|
|
||||||
within '[data-testid=group_sort_by_dropdown]' do
|
within '[data-testid=group_sort_by_dropdown]' do
|
||||||
|
@ -64,6 +66,7 @@ RSpec.describe 'User sorts projects and order persists' do
|
||||||
|
|
||||||
context 'from group homepage', :js do
|
context 'from group homepage', :js do
|
||||||
before do
|
before do
|
||||||
|
stub_feature_flags(group_overview_tabs_vue: false)
|
||||||
sign_in(user)
|
sign_in(user)
|
||||||
visit(group_canonical_path(group))
|
visit(group_canonical_path(group))
|
||||||
within '[data-testid=group_sort_by_dropdown]' do
|
within '[data-testid=group_sort_by_dropdown]' do
|
||||||
|
@ -77,6 +80,7 @@ RSpec.describe 'User sorts projects and order persists' do
|
||||||
|
|
||||||
context 'from group details', :js do
|
context 'from group details', :js do
|
||||||
before do
|
before do
|
||||||
|
stub_feature_flags(group_overview_tabs_vue: false)
|
||||||
sign_in(user)
|
sign_in(user)
|
||||||
visit(details_group_path(group))
|
visit(details_group_path(group))
|
||||||
within '[data-testid=group_sort_by_dropdown]' do
|
within '[data-testid=group_sort_by_dropdown]' do
|
||||||
|
|
|
@ -40,7 +40,7 @@ describe('AppComponent', () => {
|
||||||
const store = new GroupsStore({ hideProjects: false });
|
const store = new GroupsStore({ hideProjects: false });
|
||||||
const service = new GroupsService(mockEndpoint);
|
const service = new GroupsService(mockEndpoint);
|
||||||
|
|
||||||
const createShallowComponent = ({ propsData = {}, provide = {} } = {}) => {
|
const createShallowComponent = ({ propsData = {} } = {}) => {
|
||||||
store.state.pageInfo = mockPageInfo;
|
store.state.pageInfo = mockPageInfo;
|
||||||
wrapper = shallowMount(appComponent, {
|
wrapper = shallowMount(appComponent, {
|
||||||
propsData: {
|
propsData: {
|
||||||
|
@ -53,10 +53,6 @@ describe('AppComponent', () => {
|
||||||
mocks: {
|
mocks: {
|
||||||
$toast,
|
$toast,
|
||||||
},
|
},
|
||||||
provide: {
|
|
||||||
renderEmptyState: false,
|
|
||||||
...provide,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
vm = wrapper.vm;
|
vm = wrapper.vm;
|
||||||
};
|
};
|
||||||
|
@ -402,8 +398,7 @@ describe('AppComponent', () => {
|
||||||
({ action, groups, fromSearch, renderEmptyState, expected }) => {
|
({ action, groups, fromSearch, renderEmptyState, expected }) => {
|
||||||
it(expected ? 'renders empty state' : 'does not render empty state', async () => {
|
it(expected ? 'renders empty state' : 'does not render empty state', async () => {
|
||||||
createShallowComponent({
|
createShallowComponent({
|
||||||
propsData: { action },
|
propsData: { action, renderEmptyState },
|
||||||
provide: { renderEmptyState },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
vm.updateGroups(groups, fromSearch);
|
vm.updateGroups(groups, fromSearch);
|
||||||
|
@ -420,7 +415,6 @@ describe('AppComponent', () => {
|
||||||
it('renders legacy empty state', async () => {
|
it('renders legacy empty state', async () => {
|
||||||
createShallowComponent({
|
createShallowComponent({
|
||||||
propsData: { action: 'subgroups_and_projects' },
|
propsData: { action: 'subgroups_and_projects' },
|
||||||
provide: { renderEmptyState: false },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
vm.updateGroups([], false);
|
vm.updateGroups([], false);
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
import { GlTab } from '@gitlab/ui';
|
||||||
|
import { nextTick } from 'vue';
|
||||||
|
import AxiosMockAdapter from 'axios-mock-adapter';
|
||||||
|
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||||
|
import OverviewTabs from '~/groups/components/overview_tabs.vue';
|
||||||
|
import GroupsApp from '~/groups/components/app.vue';
|
||||||
|
import GroupsStore from '~/groups/store/groups_store';
|
||||||
|
import GroupsService from '~/groups/service/groups_service';
|
||||||
|
import {
|
||||||
|
ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
|
||||||
|
ACTIVE_TAB_SHARED,
|
||||||
|
ACTIVE_TAB_ARCHIVED,
|
||||||
|
} from '~/groups/constants';
|
||||||
|
import axios from '~/lib/utils/axios_utils';
|
||||||
|
|
||||||
|
describe('OverviewTabs', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
const endpoints = {
|
||||||
|
subgroups_and_projects: '/groups/foobar/-/children.json',
|
||||||
|
shared: '/groups/foobar/-/shared_projects.json',
|
||||||
|
archived: '/groups/foobar/-/children.json?archived=only',
|
||||||
|
};
|
||||||
|
|
||||||
|
const createComponent = async () => {
|
||||||
|
wrapper = mountExtended(OverviewTabs, {
|
||||||
|
provide: {
|
||||||
|
endpoints,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
};
|
||||||
|
|
||||||
|
const findTabPanels = () => wrapper.findAllComponents(GlTab);
|
||||||
|
const findTab = (name) => wrapper.findByRole('tab', { name });
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new AxiosMockAdapter(axios);
|
||||||
|
|
||||||
|
await createComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders `Subgroups and projects` tab with `GroupsApp` component', async () => {
|
||||||
|
const tabPanel = findTabPanels().at(0);
|
||||||
|
|
||||||
|
expect(tabPanel.vm.$attrs).toMatchObject({
|
||||||
|
title: OverviewTabs.i18n.subgroupsAndProjects,
|
||||||
|
lazy: false,
|
||||||
|
});
|
||||||
|
expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({
|
||||||
|
action: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
|
||||||
|
store: new GroupsStore({ showSchemaMarkup: true }),
|
||||||
|
service: new GroupsService(endpoints[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]),
|
||||||
|
hideProjects: false,
|
||||||
|
renderEmptyState: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders `Shared projects` tab and renders `GroupsApp` component after clicking tab', async () => {
|
||||||
|
const tabPanel = findTabPanels().at(1);
|
||||||
|
|
||||||
|
expect(tabPanel.vm.$attrs).toMatchObject({
|
||||||
|
title: OverviewTabs.i18n.sharedProjects,
|
||||||
|
lazy: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await findTab(OverviewTabs.i18n.sharedProjects).trigger('click');
|
||||||
|
|
||||||
|
expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({
|
||||||
|
action: ACTIVE_TAB_SHARED,
|
||||||
|
store: new GroupsStore(),
|
||||||
|
service: new GroupsService(endpoints[ACTIVE_TAB_SHARED]),
|
||||||
|
hideProjects: false,
|
||||||
|
renderEmptyState: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(tabPanel.vm.$attrs.lazy).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders `Archived projects` tab and renders `GroupsApp` component after clicking tab', async () => {
|
||||||
|
const tabPanel = findTabPanels().at(2);
|
||||||
|
|
||||||
|
expect(tabPanel.vm.$attrs).toMatchObject({
|
||||||
|
title: OverviewTabs.i18n.archivedProjects,
|
||||||
|
lazy: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await findTab(OverviewTabs.i18n.archivedProjects).trigger('click');
|
||||||
|
|
||||||
|
expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({
|
||||||
|
action: ACTIVE_TAB_ARCHIVED,
|
||||||
|
store: new GroupsStore(),
|
||||||
|
service: new GroupsService(endpoints[ACTIVE_TAB_ARCHIVED]),
|
||||||
|
hideProjects: false,
|
||||||
|
renderEmptyState: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(tabPanel.vm.$attrs.lazy).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,70 +1,126 @@
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
|
import Autosave from '~/autosave';
|
||||||
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
|
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
|
||||||
import IssuableForm from '~/issuable/issuable_form';
|
import IssuableForm from '~/issuable/issuable_form';
|
||||||
import setWindowLocation from 'helpers/set_window_location_helper';
|
import setWindowLocation from 'helpers/set_window_location_helper';
|
||||||
|
|
||||||
describe('IssuableForm', () => {
|
jest.mock('~/autosave');
|
||||||
let instance;
|
|
||||||
|
|
||||||
const createIssuable = (form) => {
|
const createIssuable = (form) => {
|
||||||
instance = new IssuableForm(form);
|
return new IssuableForm(form);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
describe('IssuableForm', () => {
|
||||||
|
let $form;
|
||||||
|
let instance;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
setHTMLFixture(`
|
setHTMLFixture(`
|
||||||
<form>
|
<form>
|
||||||
<input name="[title]" />
|
<input name="[title]" />
|
||||||
|
<textarea name="[description]"></textarea>
|
||||||
</form>
|
</form>
|
||||||
`);
|
`);
|
||||||
createIssuable($('form'));
|
$form = $('form');
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
resetHTMLFixture();
|
resetHTMLFixture();
|
||||||
|
$form = null;
|
||||||
|
instance = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('autosave', () => {
|
||||||
|
let $title;
|
||||||
|
let $description;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
$title = $form.find('input[name*="[title]"]');
|
||||||
|
$description = $form.find('textarea[name*="[description]"]');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
$title = null;
|
||||||
|
$description = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('initAutosave', () => {
|
describe('initAutosave', () => {
|
||||||
|
it('calls initAutosave', () => {
|
||||||
|
const initAutosave = jest.spyOn(IssuableForm.prototype, 'initAutosave');
|
||||||
|
createIssuable($form);
|
||||||
|
|
||||||
|
expect(initAutosave).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
it('creates autosave with the searchTerm included', () => {
|
it('creates autosave with the searchTerm included', () => {
|
||||||
setWindowLocation('https://gitlab.test/foo?bar=true');
|
setWindowLocation('https://gitlab.test/foo?bar=true');
|
||||||
const autosave = instance.initAutosave();
|
createIssuable($form);
|
||||||
|
|
||||||
expect(autosave.key.includes('bar=true')).toBe(true);
|
expect(Autosave).toHaveBeenCalledWith(
|
||||||
|
$title,
|
||||||
|
['/foo', 'bar=true', 'title'],
|
||||||
|
'autosave//foo/bar=true=title',
|
||||||
|
);
|
||||||
|
expect(Autosave).toHaveBeenCalledWith(
|
||||||
|
$description,
|
||||||
|
['/foo', 'bar=true', 'description'],
|
||||||
|
'autosave//foo/bar=true=description',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates autosave fields without the searchTerm if it's an issue new form", () => {
|
it("creates autosave fields without the searchTerm if it's an issue new form", () => {
|
||||||
setHTMLFixture(`
|
|
||||||
<form data-new-issue-path="/issues/new">
|
|
||||||
<input name="[title]" />
|
|
||||||
</form>
|
|
||||||
`);
|
|
||||||
createIssuable($('form'));
|
|
||||||
|
|
||||||
setWindowLocation('https://gitlab.test/issues/new?bar=true');
|
setWindowLocation('https://gitlab.test/issues/new?bar=true');
|
||||||
|
$form.attr('data-new-issue-path', '/issues/new');
|
||||||
|
createIssuable($form);
|
||||||
|
|
||||||
const autosave = instance.initAutosave();
|
expect(Autosave).toHaveBeenCalledWith(
|
||||||
|
$title,
|
||||||
expect(autosave.key.includes('bar=true')).toBe(false);
|
['/issues/new', '', 'title'],
|
||||||
|
'autosave//issues/new/bar=true=title',
|
||||||
|
);
|
||||||
|
expect(Autosave).toHaveBeenCalledWith(
|
||||||
|
$description,
|
||||||
|
['/issues/new', '', 'description'],
|
||||||
|
'autosave//issues/new/bar=true=description',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('resetAutosave', () => {
|
describe('resetAutosave', () => {
|
||||||
it('resets autosave on elements with the .js-reset-autosave class', () => {
|
it('calls reset on title and description', () => {
|
||||||
setHTMLFixture(`
|
instance = createIssuable($form);
|
||||||
<form>
|
|
||||||
<input name="[title]" />
|
instance.resetAutosave();
|
||||||
<textarea name="[description]"></textarea>
|
|
||||||
<a class="js-reset-autosave">Cancel</a>
|
expect(instance.autosaveTitle.reset).toHaveBeenCalledTimes(1);
|
||||||
</form>
|
expect(instance.autosaveDescription.reset).toHaveBeenCalledTimes(1);
|
||||||
`);
|
});
|
||||||
const $form = $('form');
|
|
||||||
|
it('resets autosave when submit', () => {
|
||||||
const resetAutosave = jest.spyOn(IssuableForm.prototype, 'resetAutosave');
|
const resetAutosave = jest.spyOn(IssuableForm.prototype, 'resetAutosave');
|
||||||
createIssuable($form);
|
createIssuable($form);
|
||||||
|
|
||||||
|
$form.submit();
|
||||||
|
|
||||||
|
expect(resetAutosave).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resets autosave on elements with the .js-reset-autosave class', () => {
|
||||||
|
const resetAutosave = jest.spyOn(IssuableForm.prototype, 'resetAutosave');
|
||||||
|
$form.append('<a class="js-reset-autosave">Cancel</a>');
|
||||||
|
createIssuable($form);
|
||||||
|
|
||||||
$form.find('.js-reset-autosave').trigger('click');
|
$form.find('.js-reset-autosave').trigger('click');
|
||||||
|
|
||||||
expect(resetAutosave).toHaveBeenCalled();
|
expect(resetAutosave).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('wip', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
instance = createIssuable($form);
|
||||||
|
});
|
||||||
|
|
||||||
describe('removeWip', () => {
|
describe('removeWip', () => {
|
||||||
it.each`
|
it.each`
|
||||||
|
@ -109,3 +165,4 @@ describe('IssuableForm', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -520,6 +520,29 @@ RSpec.describe GroupsHelper do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#group_overview_tabs_app_data' do
|
||||||
|
let_it_be(:group) { create(:group) }
|
||||||
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(helper).to receive(:current_user).and_return(user)
|
||||||
|
|
||||||
|
allow(helper).to receive(:can?).with(user, :create_subgroup, group) { true }
|
||||||
|
allow(helper).to receive(:can?).with(user, :create_projects, group) { true }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns expected hash' do
|
||||||
|
expect(helper.group_overview_tabs_app_data(group)).to match(
|
||||||
|
{
|
||||||
|
subgroups_and_projects_endpoint: including("/groups/#{group.path}/-/children.json"),
|
||||||
|
shared_projects_endpoint: including("/groups/#{group.path}/-/shared_projects.json"),
|
||||||
|
archived_projects_endpoint: including("/groups/#{group.path}/-/children.json?archived=only"),
|
||||||
|
current_group_visibility: group.visibility
|
||||||
|
}.merge(helper.group_overview_tabs_app_data(group))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "#enabled_git_access_protocol_options_for_group" do
|
describe "#enabled_git_access_protocol_options_for_group" do
|
||||||
subject { helper.enabled_git_access_protocol_options_for_group }
|
subject { helper.enabled_git_access_protocol_options_for_group }
|
||||||
|
|
||||||
|
|
|
@ -289,7 +289,6 @@ RSpec.describe ApplicationWorker do
|
||||||
perform_action
|
perform_action
|
||||||
|
|
||||||
expect(worker.jobs.count).to eq args.count
|
expect(worker.jobs.count).to eq args.count
|
||||||
expect(worker.jobs).to all(include('enqueued_at'))
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -302,7 +301,6 @@ RSpec.describe ApplicationWorker do
|
||||||
perform_action
|
perform_action
|
||||||
|
|
||||||
expect(worker.jobs.count).to eq args.count
|
expect(worker.jobs.count).to eq args.count
|
||||||
expect(worker.jobs).to all(include('enqueued_at'))
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue