Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.rubocop_todo/layout/line_length.yml1
-rw-r--r--.rubocop_todo/style/lambda.yml1
-rw-r--r--Gemfile.lock2
-rw-r--r--app/assets/javascripts/groups/components/app.vue13
-rw-r--r--app/assets/javascripts/groups/components/overview_tabs.vue80
-rw-r--r--app/assets/javascripts/groups/index.js5
-rw-r--r--app/assets/javascripts/groups/init_overview_tabs.js57
-rw-r--r--app/assets/javascripts/issuable/issuable_form.js28
-rw-r--r--app/assets/javascripts/pages/groups/details/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/show/index.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue17
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/state_container.vue9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_section.vue2
-rw-r--r--app/assets/stylesheets/_page_specific_files.scss1
-rw-r--r--app/assets/stylesheets/page_bundles/profiles/preferences.scss (renamed from app/assets/stylesheets/pages/profiles/preferences.scss)2
-rw-r--r--app/assets/stylesheets/page_bundles/reports.scss4
-rw-r--r--app/controllers/admin/spam_logs_controller.rb2
-rw-r--r--app/helpers/groups_helper.rb9
-rw-r--r--app/views/groups/show.html.haml53
-rw-r--r--app/views/notify/resolved_all_discussions_email.html.haml2
-rw-r--r--app/views/profiles/preferences/show.html.haml1
-rw-r--r--config/application.rb2
-rw-r--r--config/feature_flags/development/group_overview_tabs_vue.yml8
-rw-r--r--config/initializers_before_autoloader/002_sidekiq.rb2
-rw-r--r--doc/development/database/add_foreign_key_to_existing_column.md2
-rw-r--r--doc/development/documentation/site_architecture/deployment_process.md6
-rw-r--r--doc/development/documentation/testing.md2
-rw-r--r--doc/user/project/integrations/hangouts_chat.md13
-rw-r--r--qa/qa/fixtures/auto_devops_rack/Dockerfile9
-rw-r--r--qa/qa/fixtures/auto_devops_rack/Gemfile5
-rw-r--r--qa/qa/fixtures/auto_devops_rack/Gemfile.lock15
-rw-r--r--qa/qa/fixtures/auto_devops_rack/Rakefile9
-rw-r--r--qa/qa/fixtures/auto_devops_rack/config.ru3
-rw-r--r--qa/qa/page/project/infrastructure/kubernetes/index.rb9
-rw-r--r--qa/qa/resource/clusters/agent.rb17
-rw-r--r--qa/qa/resource/clusters/agent_token.rb21
-rw-r--r--qa/qa/resource/kubernetes_cluster/project_cluster.rb50
-rw-r--r--qa/qa/service/cluster_provider/gcloud.rb38
-rw-r--r--qa/qa/service/kubernetes_cluster.rb14
-rw-r--r--qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb160
-rw-r--r--qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb38
-rw-r--r--spec/features/groups/show_spec.rb2
-rw-r--r--spec/features/projects/user_sorts_projects_spec.rb4
-rw-r--r--spec/frontend/groups/components/app_spec.js10
-rw-r--r--spec/frontend/groups/components/overview_tabs_spec.js106
-rw-r--r--spec/frontend/issuable/issuable_form_spec.js199
-rw-r--r--spec/helpers/groups_helper_spec.rb23
-rw-r--r--spec/workers/concerns/application_worker_spec.rb2
49 files changed, 656 insertions, 410 deletions
diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml
index 7424ab7ea26..5fcc55bb0b0 100644
--- a/.rubocop_todo/layout/line_length.yml
+++ b/.rubocop_todo/layout/line_length.yml
@@ -3666,7 +3666,6 @@ Layout/LineLength:
- 'qa/qa/ee/page/project/secure/security_dashboard.rb'
- 'qa/qa/ee/page/project/secure/show.rb'
- 'qa/qa/ee/resource/license.rb'
- - 'qa/qa/fixtures/auto_devops_rack/config.ru'
- 'qa/qa/flow/sign_up.rb'
- 'qa/qa/git/repository.rb'
- 'qa/qa/page/base.rb'
diff --git a/.rubocop_todo/style/lambda.yml b/.rubocop_todo/style/lambda.yml
index 5b898417d96..525e2c31797 100644
--- a/.rubocop_todo/style/lambda.yml
+++ b/.rubocop_todo/style/lambda.yml
@@ -217,7 +217,6 @@ Style/Lambda:
- 'lib/gitlab/sidekiq_signals.rb'
- 'lib/gitlab/utils/measuring.rb'
- 'lib/gitlab/visibility_level.rb'
- - 'qa/qa/fixtures/auto_devops_rack/config.ru'
- 'rubocop/cop/rspec/modify_sidekiq_middleware.rb'
- 'rubocop/cop/rspec/timecop_freeze.rb'
- 'rubocop/cop/rspec/timecop_travel.rb'
diff --git a/Gemfile.lock b/Gemfile.lock
index 7b78495dea2..4ba19e79aea 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1281,7 +1281,7 @@ GEM
shellany (0.0.1)
shoulda-matchers (5.1.0)
activesupport (>= 5.2.0)
- sidekiq (6.4.0)
+ sidekiq (6.4.2)
connection_pool (>= 2.2.2)
rack (~> 2.0)
redis (>= 4.2.0)
diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue
index cd5521c599e..0bd7371d39b 100644
--- a/app/assets/javascripts/groups/components/app.vue
+++ b/app/assets/javascripts/groups/components/app.vue
@@ -17,11 +17,6 @@ export default {
GlLoadingIcon,
EmptyState,
},
- inject: {
- renderEmptyState: {
- default: false,
- },
- },
props: {
action: {
type: String,
@@ -45,6 +40,11 @@ export default {
type: Boolean,
required: true,
},
+ renderEmptyState: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -224,6 +224,9 @@ export default {
},
showLegacyEmptyState() {
const { containerEl } = this;
+
+ if (!containerEl) return;
+
const contentListEl = containerEl.querySelector(CONTENT_LIST_CLASS);
const emptyStateEl = containerEl.querySelector('.empty-state');
diff --git a/app/assets/javascripts/groups/components/overview_tabs.vue b/app/assets/javascripts/groups/components/overview_tabs.vue
new file mode 100644
index 00000000000..53efb354f5c
--- /dev/null
+++ b/app/assets/javascripts/groups/components/overview_tabs.vue
@@ -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>
diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js
index dc2909f2621..c3bf3f28509 100644
--- a/app/assets/javascripts/groups/index.js
+++ b/app/assets/javascripts/groups/index.js
@@ -52,7 +52,6 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
newSubgroupIllustration,
newProjectIllustration,
emptySubgroupIllustration,
- renderEmptyState,
canCreateSubgroups,
canCreateProjects,
currentGroupVisibility,
@@ -65,7 +64,6 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
newSubgroupIllustration,
newProjectIllustration,
emptySubgroupIllustration,
- renderEmptyState: parseBoolean(renderEmptyState),
canCreateSubgroups: parseBoolean(canCreateSubgroups),
canCreateProjects: parseBoolean(canCreateProjects),
currentGroupVisibility,
@@ -75,6 +73,7 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
const { dataset } = dataEl || this.$options.el;
const hideProjects = parseBoolean(dataset.hideProjects);
const showSchemaMarkup = parseBoolean(dataset.showSchemaMarkup);
+ const renderEmptyState = parseBoolean(dataset.renderEmptyState);
const service = new GroupsService(endpoint || dataset.endpoint);
const store = new GroupsStore({ hideProjects, showSchemaMarkup });
@@ -83,6 +82,7 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
store,
service,
hideProjects,
+ renderEmptyState,
loading: true,
containerId,
};
@@ -119,6 +119,7 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
store: this.store,
service: this.service,
hideProjects: this.hideProjects,
+ renderEmptyState: this.renderEmptyState,
containerId: this.containerId,
},
});
diff --git a/app/assets/javascripts/groups/init_overview_tabs.js b/app/assets/javascripts/groups/init_overview_tabs.js
new file mode 100644
index 00000000000..5f568d10a42
--- /dev/null
+++ b/app/assets/javascripts/groups/init_overview_tabs.js
@@ -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);
+ },
+ });
+};
diff --git a/app/assets/javascripts/issuable/issuable_form.js b/app/assets/javascripts/issuable/issuable_form.js
index cc2608b5c62..4c6685820cf 100644
--- a/app/assets/javascripts/issuable/issuable_form.js
+++ b/app/assets/javascripts/issuable/issuable_form.js
@@ -39,6 +39,11 @@ function format(searchTerm, isFallbackKey = false) {
return formattedQuery;
}
+function getSearchTerm(newIssuePath) {
+ const { search, pathname } = document.location;
+ return newIssuePath === pathname ? '' : format(search);
+}
+
function getFallbackKey() {
const searchTerm = format(document.location.search, true);
return ['autosave', document.location.pathname, searchTerm].join('/');
@@ -72,7 +77,8 @@ export default class IssuableForm {
this.reviewersSelect = new UsersSelect(undefined, '.js-reviewer-search');
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.descriptionField = this.form.find('textarea[name*="[description]"]');
if (!(this.titleField.length && this.descriptionField.length)) {
@@ -109,20 +115,16 @@ export default class IssuableForm {
}
initAutosave() {
- const { search, pathname } = document.location;
- const searchTerm = this.newIssuePath === pathname ? '' : format(search);
- const fallbackKey = getFallbackKey();
-
- this.autosave = new Autosave(
+ this.autosaveTitle = new Autosave(
this.titleField,
- [document.location.pathname, searchTerm, 'title'],
- `${fallbackKey}=title`,
+ [document.location.pathname, this.searchTerm, 'title'],
+ `${this.fallbackKey}=title`,
);
- return new Autosave(
+ this.autosaveDescription = new Autosave(
this.descriptionField,
- [document.location.pathname, searchTerm, 'description'],
- `${fallbackKey}=description`,
+ [document.location.pathname, this.searchTerm, 'description'],
+ `${this.fallbackKey}=description`,
);
}
@@ -131,8 +133,8 @@ export default class IssuableForm {
}
resetAutosave() {
- this.titleField.data('autosave').reset();
- return this.descriptionField.data('autosave').reset();
+ this.autosaveTitle.reset();
+ this.autosaveDescription.reset();
}
initWip() {
diff --git a/app/assets/javascripts/pages/groups/details/index.js b/app/assets/javascripts/pages/groups/details/index.js
index 0417134f2a7..92490368b15 100644
--- a/app/assets/javascripts/pages/groups/details/index.js
+++ b/app/assets/javascripts/pages/groups/details/index.js
@@ -1,3 +1,5 @@
+import { initGroupOverviewTabs } from '~/groups/init_overview_tabs';
import initGroupDetails from '../shared/group_details';
initGroupDetails('details');
+initGroupOverviewTabs();
diff --git a/app/assets/javascripts/pages/groups/show/index.js b/app/assets/javascripts/pages/groups/show/index.js
index e4a84dd5eec..161fca83a58 100644
--- a/app/assets/javascripts/pages/groups/show/index.js
+++ b/app/assets/javascripts/pages/groups/show/index.js
@@ -1,5 +1,7 @@
import leaveByUrl from '~/namespaces/leave_by_url';
+import { initGroupOverviewTabs } from '~/groups/init_overview_tabs';
import initGroupDetails from '../shared/group_details';
leaveByUrl('group');
initGroupDetails();
+initGroupOverviewTabs();
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
index aa5ab87597f..300e2a672cb 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
@@ -13,6 +13,7 @@ import Poll from '~/lib/utils/poll';
import { normalizeHeaders } from '~/lib/utils/common_utils';
import { EXTENSION_ICON_CLASS, EXTENSION_ICONS } from '../../constants';
import Actions from '../action_buttons.vue';
+import StateContainer from '../state_container.vue';
import StatusIcon from './status_icon.vue';
import ChildContent from './child_content.vue';
import { createTelemetryHub } from './telemetry';
@@ -36,6 +37,7 @@ export default {
ChildContent,
DynamicScroller,
DynamicScrollerItem,
+ StateContainer,
},
directives: {
SafeHtml: GlSafeHtmlDirective,
@@ -312,18 +314,15 @@ export default {
data-testid="widget-extension"
data-qa-selector="mr_widget_extension"
>
- <div
+ <state-container
+ :mr="mr"
+ :status="statusIconName"
+ :is-loading="isLoadingSummary"
:class="{ 'gl-cursor-pointer': isCollapsible }"
- class="media gl-p-5"
+ class="gl-p-5"
@mousedown="onRowMouseDown"
@mouseup="onRowMouseUp"
>
- <status-icon
- :level="1"
- :name="$options.label || $options.name"
- :is-loading="isLoadingSummary"
- :icon-name="statusIconName"
- />
<div
class="media-body gl-display-flex gl-flex-direction-row! gl-align-self-center"
data-testid="widget-extension-top-level"
@@ -362,7 +361,7 @@ export default {
/>
</div>
</div>
- </div>
+ </state-container>
<div
v-if="!isCollapsed"
class="mr-widget-grouped-section gl-relative"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue
index 7f2049904fd..52c9f047b76 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue
@@ -62,7 +62,9 @@ export default {
<strong v-else v-safe-html="generateText(data.header)"></strong>
</div>
<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-display-flex gl-flex-nowrap">
<div class="gl-flex-wrap gl-display-flex gl-w-full">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue
index 2bba8d2dc82..03728e2831b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue
@@ -44,7 +44,14 @@ export default {
<template>
<div class="mr-widget-body media">
<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>
<template v-else>
<slot name="icon">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_section.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_section.vue
index 42fd02f978b..61e3744b5dc 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_section.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_section.vue
@@ -11,7 +11,7 @@ export default {
type: String,
default: '',
required: false,
- validator: (value) => value === '' || Object.keys(EXTENSION_ICONS).indexOf(value) > -1,
+ validator: (value) => value === '' || Object.keys(EXTENSION_ICONS).includes(value),
},
widgetName: {
type: String,
diff --git a/app/assets/stylesheets/_page_specific_files.scss b/app/assets/stylesheets/_page_specific_files.scss
index 004dc22c9b8..d0b3f5bda8e 100644
--- a/app/assets/stylesheets/_page_specific_files.scss
+++ b/app/assets/stylesheets/_page_specific_files.scss
@@ -21,7 +21,6 @@
@import './pages/notifications';
@import './pages/pipelines';
@import './pages/profile';
-@import './pages/profiles/preferences';
@import './pages/projects';
@import './pages/prometheus';
@import './pages/registry';
diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/page_bundles/profiles/preferences.scss
index c7d7aacceec..c9c78a70163 100644
--- a/app/assets/stylesheets/pages/profiles/preferences.scss
+++ b/app/assets/stylesheets/page_bundles/profiles/preferences.scss
@@ -1,3 +1,5 @@
+@import 'page_bundles/mixins_and_variables_and_functions';
+
.application-theme {
$ui-gray-bg: #303030;
$ui-light-gray-bg: #f0f0f0;
diff --git a/app/assets/stylesheets/page_bundles/reports.scss b/app/assets/stylesheets/page_bundles/reports.scss
index d0748779f47..03c9fc7508d 100644
--- a/app/assets/stylesheets/page_bundles/reports.scss
+++ b/app/assets/stylesheets/page_bundles/reports.scss
@@ -16,6 +16,10 @@
line-height: 20px;
}
+.report-block-child-icon {
+ height: 20px;
+}
+
.report-block-list {
list-style: none;
padding: 0 1px;
diff --git a/app/controllers/admin/spam_logs_controller.rb b/app/controllers/admin/spam_logs_controller.rb
index e4e866a8b60..3a55fc4b951 100644
--- a/app/controllers/admin/spam_logs_controller.rb
+++ b/app/controllers/admin/spam_logs_controller.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class Admin::SpamLogsController < Admin::ApplicationController
- feature_category :not_owned # rubocop:todo Gitlab/AvoidFeatureCategoryNotOwned
+ feature_category :instance_resiliency
# rubocop: disable CodeReuse/ActiveRecord
def index
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index bb92792de2d..f77bd6621f9 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -172,6 +172,15 @@ module GroupsHelper
}
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
case ::Gitlab::CurrentSettings.enabled_git_access_protocol
when nil, ""
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index d8da77dc5cc..f474f8fbd3b 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -33,33 +33,36 @@
= render_if_exists 'groups/group_activity_analytics', group: @group
-.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
- .scrolling-tabs-container.inner-page-scroll-tabs
- .fade-left= sprite_icon('chevron-lg-left', size: 12)
- .fade-right= sprite_icon('chevron-lg-right', size: 12)
- -# `item_active` is set to `false` as the active state is set by `app/assets/javascripts/pages/groups/shared/group_details.js`
- -# TODO: Replace this approach in https://gitlab.com/gitlab-org/gitlab/-/issues/23466
- = gl_tabs_nav({ class: 'nav-links scrolling-tabs gl-display-flex gl-flex-grow-1 gl-flex-nowrap gl-border-0' }) do
- = gl_tab_link_to group_path, item_active: false, tab_class: 'js-subgroups_and_projects-tab', data: { target: 'div#subgroups_and_projects', action: 'subgroups_and_projects', toggle: 'tab' } do
- = _("Subgroups and projects")
- = gl_tab_link_to group_shared_path, item_active: false, tab_class: 'js-shared-tab', data: { target: 'div#shared', action: 'shared', toggle: 'tab' } do
- = _("Shared projects")
- = gl_tab_link_to group_archived_path, item_active: false, tab_class: 'js-archived-tab', data: { target: 'div#archived', action: 'archived', toggle: 'tab' } do
- = _("Archived projects")
+- 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) } } }
+ .top-area.group-nav-container.justify-content-between
+ .scrolling-tabs-container.inner-page-scroll-tabs
+ .fade-left= sprite_icon('chevron-lg-left', size: 12)
+ .fade-right= sprite_icon('chevron-lg-right', size: 12)
+ -# `item_active` is set to `false` as the active state is set by `app/assets/javascripts/pages/groups/shared/group_details.js`
+ -# TODO: Replace this approach in https://gitlab.com/gitlab-org/gitlab/-/issues/23466
+ = gl_tabs_nav({ class: 'nav-links scrolling-tabs gl-display-flex gl-flex-grow-1 gl-flex-nowrap gl-border-0' }) do
+ = gl_tab_link_to group_path, item_active: false, tab_class: 'js-subgroups_and_projects-tab', data: { target: 'div#subgroups_and_projects', action: 'subgroups_and_projects', toggle: 'tab' } do
+ = _("Subgroups and projects")
+ = gl_tab_link_to group_shared_path, item_active: false, tab_class: 'js-shared-tab', data: { target: 'div#shared', action: 'shared', toggle: 'tab' } do
+ = _("Shared projects")
+ = gl_tab_link_to group_archived_path, item_active: false, tab_class: 'js-archived-tab', data: { target: 'div#archived', action: 'archived', toggle: 'tab' } do
+ = _("Archived projects")
- .nav-controls.d-block.d-md-flex
- .group-search
- = render "shared/groups/search_form"
+ .nav-controls.d-block.d-md-flex
+ .group-search
+ = render "shared/groups/search_form"
- = render "shared/groups/dropdown", options_hash: subgroups_sort_options_hash
+ = render "shared/groups/dropdown", options_hash: subgroups_sort_options_hash
- .tab-content
- #subgroups_and_projects.tab-pane
- = render "subgroups_and_projects", group: @group
+ .tab-content
+ #subgroups_and_projects.tab-pane
+ = render "subgroups_and_projects", group: @group
- #shared.tab-pane
- = render "shared_projects", group: @group
+ #shared.tab-pane
+ = render "shared_projects", group: @group
- #archived.tab-pane
- = render "archived_projects", group: @group
+ #archived.tab-pane
+ = render "archived_projects", group: @group
diff --git a/app/views/notify/resolved_all_discussions_email.html.haml b/app/views/notify/resolved_all_discussions_email.html.haml
index bd9778ae142..78dc21caf18 100644
--- a/app/views/notify/resolved_all_discussions_email.html.haml
+++ b/app/views/notify/resolved_all_discussions_email.html.haml
@@ -1,2 +1,2 @@
%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) }
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index f8737a4e54a..5f306c6eb48 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -1,4 +1,5 @@
- page_title _('Preferences')
+- add_page_specific_style 'page_bundles/profiles/preferences'
- @content_class = "limit-container-width" unless fluid_layout
- user_theme_id = Gitlab::Themes.for_user(@user).id
- user_color_schema_id = Gitlab::ColorSchemes.for_user(@user).id
diff --git a/config/application.rb b/config/application.rb
index 8c5bb7fe110..ba768d50dd4 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -291,10 +291,12 @@ module Gitlab
config.assets.precompile << "page_bundles/productivity_analytics.css"
config.assets.precompile << "page_bundles/profile.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/projects_edit.css"
config.assets.precompile << "page_bundles/reports.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/security_dashboard.css"
config.assets.precompile << "page_bundles/security_discover.css"
diff --git a/config/feature_flags/development/group_overview_tabs_vue.yml b/config/feature_flags/development/group_overview_tabs_vue.yml
new file mode 100644
index 00000000000..4c54ab31b53
--- /dev/null
+++ b/config/feature_flags/development/group_overview_tabs_vue.yml
@@ -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
diff --git a/config/initializers_before_autoloader/002_sidekiq.rb b/config/initializers_before_autoloader/002_sidekiq.rb
index 9ffcf39d6fb..929bdeda996 100644
--- a/config/initializers_before_autoloader/002_sidekiq.rb
+++ b/config/initializers_before_autoloader/002_sidekiq.rb
@@ -9,5 +9,5 @@
require 'sidekiq/web'
if Rails.env.development?
- Sidekiq.default_worker_options[:backtrace] = true
+ Sidekiq.default_job_options[:backtrace] = true
end
diff --git a/doc/development/database/add_foreign_key_to_existing_column.md b/doc/development/database/add_foreign_key_to_existing_column.md
index 8a8fe3c0a1e..4be3296b2bb 100644
--- a/doc/development/database/add_foreign_key_to_existing_column.md
+++ b/doc/development/database/add_foreign_key_to_existing_column.md
@@ -71,7 +71,7 @@ Migration file for adding `NOT VALID` foreign key:
```ruby
class AddNotValidForeignKeyToEmailsUser < Gitlab::Database::Migration[2.0]
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
def down
diff --git a/doc/development/documentation/site_architecture/deployment_process.md b/doc/development/documentation/site_architecture/deployment_process.md
index bf45066c7db..8a9c2e1e8d7 100644
--- a/doc/development/documentation/site_architecture/deployment_process.md
+++ b/doc/development/documentation/site_architecture/deployment_process.md
@@ -144,14 +144,14 @@ graph LR
### Manually deploy to production
-GitLab Docs is deployed to production whenever the `Build docs.gitlab.com every 4 hours` scheduled pipeline runs. By
-default, this pipeline runs every four hours.
+GitLab Docs is deployed to production whenever the `Build docs.gitlab.com every hour` scheduled pipeline runs. By
+default, this pipeline runs every hour.
Maintainers can [manually](../../../ci/pipelines/schedules.md#run-manually) run this pipeline to force a deployment to
production:
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
complete in the new pipeline.
diff --git a/doc/development/documentation/testing.md b/doc/development/documentation/testing.md
index 428a57a11fb..59a078bdec0 100644
--- a/doc/development/documentation/testing.md
+++ b/doc/development/documentation/testing.md
@@ -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
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,
for example, `image:docs-lint-markdown`.
1. In the job output, get the name of the new image.
diff --git a/doc/user/project/integrations/hangouts_chat.md b/doc/user/project/integrations/hangouts_chat.md
index fbfa7d914a5..6e532a6c14f 100644
--- a/doc/user/project/integrations/hangouts_chat.md
+++ b/doc/user/project/integrations/hangouts_chat.md
@@ -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
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.
diff --git a/qa/qa/fixtures/auto_devops_rack/Dockerfile b/qa/qa/fixtures/auto_devops_rack/Dockerfile
deleted file mode 100644
index 6ab2795dd40..00000000000
--- a/qa/qa/fixtures/auto_devops_rack/Dockerfile
+++ /dev/null
@@ -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"]
diff --git a/qa/qa/fixtures/auto_devops_rack/Gemfile b/qa/qa/fixtures/auto_devops_rack/Gemfile
deleted file mode 100644
index 2c7c77adf94..00000000000
--- a/qa/qa/fixtures/auto_devops_rack/Gemfile
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-source 'https://rubygems.org'
-gem 'rack'
-gem 'rake'
diff --git a/qa/qa/fixtures/auto_devops_rack/Gemfile.lock b/qa/qa/fixtures/auto_devops_rack/Gemfile.lock
deleted file mode 100644
index 04a85be4b2f..00000000000
--- a/qa/qa/fixtures/auto_devops_rack/Gemfile.lock
+++ /dev/null
@@ -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
diff --git a/qa/qa/fixtures/auto_devops_rack/Rakefile b/qa/qa/fixtures/auto_devops_rack/Rakefile
deleted file mode 100644
index a6d08103d55..00000000000
--- a/qa/qa/fixtures/auto_devops_rack/Rakefile
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-require 'rake/testtask'
-
-task default: %w[test]
-
-task :test do
- puts "ok"
-end
diff --git a/qa/qa/fixtures/auto_devops_rack/config.ru b/qa/qa/fixtures/auto_devops_rack/config.ru
deleted file mode 100644
index aea28ef1893..00000000000
--- a/qa/qa/fixtures/auto_devops_rack/config.ru
+++ /dev/null
@@ -1,3 +0,0 @@
-# frozen_string_literal: true
-
-run lambda { |env| [200, { 'Content-Type' => 'text/plain' }, StringIO.new("Hello World! #{ENV['OPTIONAL_MESSAGE']}\n")] }
diff --git a/qa/qa/page/project/infrastructure/kubernetes/index.rb b/qa/qa/page/project/infrastructure/kubernetes/index.rb
index 34d2ad55429..4c759a049e1 100644
--- a/qa/qa/page/project/infrastructure/kubernetes/index.rb
+++ b/qa/qa/page/project/infrastructure/kubernetes/index.rb
@@ -10,18 +10,13 @@ module QA
element :clusters_actions_button
end
- def connect_existing_cluster
- within_element(:clusters_actions_button) { click_button(class: 'dropdown-toggle-split') }
- click_link 'Connect a cluster (certificate - deprecated)'
+ def connect_cluster
+ click_element(:clusters_actions_button)
end
def has_cluster?(cluster)
has_element?(:cluster, cluster_name: cluster.to_s)
end
-
- def click_on_cluster(cluster)
- click_on cluster.cluster_name
- end
end
end
end
diff --git a/qa/qa/resource/clusters/agent.rb b/qa/qa/resource/clusters/agent.rb
index b190634f357..9574289a2ed 100644
--- a/qa/qa/resource/clusters/agent.rb
+++ b/qa/qa/resource/clusters/agent.rb
@@ -26,25 +26,18 @@ module QA
end
def api_get_path
- "gid://gitlab/Clusters::Agent/#{id}"
+ "/projects/#{project.id}/cluster_agents/#{id}"
end
def api_post_path
- "/graphql"
+ "/projects/#{project.id}/cluster_agents"
end
def api_post_body
- <<~GQL
- mutation createAgent {
- createClusterAgent(input: { projectPath: "#{project.full_path}", name: "#{@name}" }) {
- clusterAgent {
- id
- name
- }
- errors
- }
+ {
+ id: project.id,
+ name: name
}
- GQL
end
end
end
diff --git a/qa/qa/resource/clusters/agent_token.rb b/qa/qa/resource/clusters/agent_token.rb
index c1cf5c2f37b..cbd2964c31d 100644
--- a/qa/qa/resource/clusters/agent_token.rb
+++ b/qa/qa/resource/clusters/agent_token.rb
@@ -5,7 +5,7 @@ module QA
module Clusters
class AgentToken < QA::Resource::Base
attribute :id
- attribute :secret
+ attribute :token
attribute :agent do
QA::Resource::Clusters::Agent.fabricate_via_api!
end
@@ -20,26 +20,19 @@ module QA
end
def api_get_path
- "gid://gitlab/Clusters::AgentToken/#{id}"
+ "/projects/#{agent.project.id}/cluster_agents/#{agent.id}/tokens/#{id}"
end
def api_post_path
- "/graphql"
+ "/projects/#{agent.project.id}/cluster_agents/#{agent.id}/tokens"
end
def api_post_body
- <<~GQL
- mutation createToken {
- clusterAgentTokenCreate(input: { clusterAgentId: "gid://gitlab/Clusters::Agent/#{agent.id}" name: "token-#{agent.id}" }) {
- secret # This is the value you need to use on the next step
- token {
- createdAt
- id
- }
- errors
- }
+ {
+ id: agent.project.id,
+ agent_id: agent.id,
+ name: agent.name
}
- GQL
end
end
end
diff --git a/qa/qa/resource/kubernetes_cluster/project_cluster.rb b/qa/qa/resource/kubernetes_cluster/project_cluster.rb
deleted file mode 100644
index 0443b26064e..00000000000
--- a/qa/qa/resource/kubernetes_cluster/project_cluster.rb
+++ /dev/null
@@ -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
diff --git a/qa/qa/service/cluster_provider/gcloud.rb b/qa/qa/service/cluster_provider/gcloud.rb
index 77677745f7a..14c13eecb8d 100644
--- a/qa/qa/service/cluster_provider/gcloud.rb
+++ b/qa/qa/service/cluster_provider/gcloud.rb
@@ -33,14 +33,32 @@ module QA
delete_cluster
end
- def install_ingress
- QA::Runtime::Logger.info "Attempting to install Ingress on cluster #{cluster_name}"
- shell 'kubectl create -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-0.31.0/deploy/static/provider/cloud/deploy.yaml'
- wait_for_ingress
+ # kas is hardcoded to staging since this test should only run in staging for now
+ def install_kubernetes_agent(agent_token)
+ install_helm
+
+ 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
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
if Runtime::Env.has_gcloud_credentials?
attempt_login_with_env_vars
@@ -104,18 +122,6 @@ module QA
def get_region
Runtime::Env.gcloud_region || @available_regions.delete(@available_regions.sample)
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
diff --git a/qa/qa/service/kubernetes_cluster.rb b/qa/qa/service/kubernetes_cluster.rb
index dafce4acc33..59bfacf9195 100644
--- a/qa/qa/service/kubernetes_cluster.rb
+++ b/qa/qa/service/kubernetes_cluster.rb
@@ -41,8 +41,8 @@ module QA
cluster_name
end
- def install_ingress
- @provider.install_ingress
+ def install_kubernetes_agent(agent_token)
+ @provider.install_kubernetes_agent(agent_token)
end
def create_secret(secret, secret_name)
@@ -73,16 +73,6 @@ module QA
shell('kubectl apply -f -', stdin_data: network_policy)
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
def fetch_api_url
diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
index f1a2eb71390..b839855c500 100644
--- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
+++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
@@ -1,78 +1,67 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Configure',
- only: { subdomain: %i[staging staging-canary] },
- quarantine: {
- 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|
- project.name = 'autodevops-project'
- project.auto_devops_enabled = true
+ RSpec.describe 'Configure', only: { subdomain: %i[staging staging-canary] } do
+ describe 'Auto DevOps with a Kubernetes Agent' do
+ let!(:app_project) do
+ Resource::Project.fabricate_via_api! do |project|
+ project.name = 'autodevops-app-project'
+ project.template_name = 'express'
+ project.auto_devops_enabled = true
+ end
end
- end
- before do
- set_kube_ingress_base_domain(project)
- disable_optional_jobs(project)
- end
+ let!(:cluster) { Service::KubernetesCluster.new(provider_class: Service::ClusterProvider::Gcloud).create! }
- describe 'Auto DevOps support' do
- context 'when rbac is enabled' do
- let(:cluster) { Service::KubernetesCluster.new.create! }
+ let!(:kubernetes_agent) do
+ Resource::Clusters::Agent.fabricate_via_api! do |agent|
+ agent.name = 'agent1'
+ agent.project = app_project
+ end
+ end
- after do
- cluster&.remove!
- project.remove_via_api!
+ let!(:agent_token) do
+ Resource::Clusters::AgentToken.fabricate_via_api! do |token|
+ token.agent = kubernetes_agent
end
+ end
- it 'runs auto devops', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348061' do
- Flow::Login.sign_in
-
- Resource::KubernetesCluster::ProjectCluster.fabricate! do |k8s_cluster|
- k8s_cluster.project = project
- k8s_cluster.cluster = cluster
- k8s_cluster.install_ingress = true
- end
-
- Resource::Repository::ProjectPush.fabricate! do |push|
- push.project = project
- push.directory = Pathname
- .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|
- pipeline.click_job('build')
- 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|
- 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|
- pipeline.click_job('production')
- end
- Page::Project::Job::Show.perform do |job|
- expect(job).to be_successful(timeout: 1200)
-
- job.click_element(:pipeline_path)
- 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
+ cluster&.remove!
+ end
+
+ it 'runs auto devops', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348061' do
+ Flow::Login.sign_in
+
+ app_project.visit!
+
+ Page::Project::Menu.perform(&:click_ci_cd_pipelines)
+ Page::Project::Pipeline::Index.perform(&:click_run_pipeline_button)
+ Page::Project::Pipeline::New.perform(&:click_run_pipeline_button)
+
+ Page::Project::Pipeline::Show.perform do |pipeline|
+ pipeline.click_job('build')
+ 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|
+ pipeline.click_job('production')
+ end
+ Page::Project::Job::Show.perform do |job|
+ expect(job).to be_successful(timeout: 600)
end
end
end
@@ -88,12 +77,43 @@ module QA
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)
%w[
- CODE_QUALITY_DISABLED LICENSE_MANAGEMENT_DISABLED
- SAST_DISABLED DAST_DISABLED DEPENDENCY_SCANNING_DISABLED
- CONTAINER_SCANNING_DISABLED BROWSER_PERFORMANCE_DISABLED
- SECRET_DETECTION_DISABLED
+ TEST_DISABLED CODE_QUALITY_DISABLED LICENSE_MANAGEMENT_DISABLED
+ BROWSER_PERFORMANCE_DISABLED LOAD_PERFORMANCE_DISABLED
+ SAST_DISABLED SECRET_DETECTION_DISABLED DEPENDENCY_SCANNING_DISABLED
+ CONTAINER_SCANNING_DISABLED DAST_DISABLED REVIEW_DISABLED
+ CODE_INTELLIGENCE_DISABLED CLUSTER_IMAGE_SCANNING_DISABLED
].each do |key|
Resource::CiVariable.fabricate_via_api! do |resource|
resource.project = project
diff --git a/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb
deleted file mode 100644
index 94f9e9ec1f6..00000000000
--- a/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb
+++ /dev/null
@@ -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
diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb
index d814906a274..67310862516 100644
--- a/spec/features/groups/show_spec.rb
+++ b/spec/features/groups/show_spec.rb
@@ -331,6 +331,7 @@ RSpec.describe 'Group show page' do
end
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.project_group_links.create!(group: group)
@@ -342,6 +343,7 @@ RSpec.describe 'Group show page' do
end
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)
visit group_archived_path(group)
diff --git a/spec/features/projects/user_sorts_projects_spec.rb b/spec/features/projects/user_sorts_projects_spec.rb
index 7c970f7ee3d..b9b28398279 100644
--- a/spec/features/projects/user_sorts_projects_spec.rb
+++ b/spec/features/projects/user_sorts_projects_spec.rb
@@ -24,6 +24,7 @@ RSpec.describe 'User sorts projects and order persists' do
end
it "is set on the group_canonical_path" do
+ stub_feature_flags(group_overview_tabs_vue: false)
visit(group_canonical_path(group))
within '[data-testid=group_sort_by_dropdown]' do
@@ -32,6 +33,7 @@ RSpec.describe 'User sorts projects and order persists' do
end
it "is set on the details_group_path" do
+ stub_feature_flags(group_overview_tabs_vue: false)
visit(details_group_path(group))
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
before do
+ stub_feature_flags(group_overview_tabs_vue: false)
sign_in(user)
visit(group_canonical_path(group))
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
before do
+ stub_feature_flags(group_overview_tabs_vue: false)
sign_in(user)
visit(details_group_path(group))
within '[data-testid=group_sort_by_dropdown]' do
diff --git a/spec/frontend/groups/components/app_spec.js b/spec/frontend/groups/components/app_spec.js
index 2796a561953..a4a7530184d 100644
--- a/spec/frontend/groups/components/app_spec.js
+++ b/spec/frontend/groups/components/app_spec.js
@@ -40,7 +40,7 @@ describe('AppComponent', () => {
const store = new GroupsStore({ hideProjects: false });
const service = new GroupsService(mockEndpoint);
- const createShallowComponent = ({ propsData = {}, provide = {} } = {}) => {
+ const createShallowComponent = ({ propsData = {} } = {}) => {
store.state.pageInfo = mockPageInfo;
wrapper = shallowMount(appComponent, {
propsData: {
@@ -53,10 +53,6 @@ describe('AppComponent', () => {
mocks: {
$toast,
},
- provide: {
- renderEmptyState: false,
- ...provide,
- },
});
vm = wrapper.vm;
};
@@ -402,8 +398,7 @@ describe('AppComponent', () => {
({ action, groups, fromSearch, renderEmptyState, expected }) => {
it(expected ? 'renders empty state' : 'does not render empty state', async () => {
createShallowComponent({
- propsData: { action },
- provide: { renderEmptyState },
+ propsData: { action, renderEmptyState },
});
vm.updateGroups(groups, fromSearch);
@@ -420,7 +415,6 @@ describe('AppComponent', () => {
it('renders legacy empty state', async () => {
createShallowComponent({
propsData: { action: 'subgroups_and_projects' },
- provide: { renderEmptyState: false },
});
vm.updateGroups([], false);
diff --git a/spec/frontend/groups/components/overview_tabs_spec.js b/spec/frontend/groups/components/overview_tabs_spec.js
new file mode 100644
index 00000000000..c26254acf3d
--- /dev/null
+++ b/spec/frontend/groups/components/overview_tabs_spec.js
@@ -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);
+ });
+});
diff --git a/spec/frontend/issuable/issuable_form_spec.js b/spec/frontend/issuable/issuable_form_spec.js
index d844f3394d5..f37d132743a 100644
--- a/spec/frontend/issuable/issuable_form_spec.js
+++ b/spec/frontend/issuable/issuable_form_spec.js
@@ -1,111 +1,168 @@
import $ from 'jquery';
+import Autosave from '~/autosave';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import IssuableForm from '~/issuable/issuable_form';
import setWindowLocation from 'helpers/set_window_location_helper';
+jest.mock('~/autosave');
+
+const createIssuable = (form) => {
+ return new IssuableForm(form);
+};
+
describe('IssuableForm', () => {
+ let $form;
let instance;
- const createIssuable = (form) => {
- instance = new IssuableForm(form);
- };
-
beforeEach(() => {
setHTMLFixture(`
<form>
<input name="[title]" />
+ <textarea name="[description]"></textarea>
</form>
`);
- createIssuable($('form'));
+ $form = $('form');
});
afterEach(() => {
resetHTMLFixture();
+ $form = null;
+ instance = null;
});
- describe('initAutosave', () => {
- it('creates autosave with the searchTerm included', () => {
- setWindowLocation('https://gitlab.test/foo?bar=true');
- const autosave = instance.initAutosave();
+ describe('autosave', () => {
+ let $title;
+ let $description;
+
+ beforeEach(() => {
+ $title = $form.find('input[name*="[title]"]');
+ $description = $form.find('textarea[name*="[description]"]');
+ });
+
+ afterEach(() => {
+ $title = null;
+ $description = null;
+ });
- expect(autosave.key.includes('bar=true')).toBe(true);
+ 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', () => {
+ setWindowLocation('https://gitlab.test/foo?bar=true');
+ createIssuable($form);
+
+ 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", () => {
+ setWindowLocation('https://gitlab.test/issues/new?bar=true');
+ $form.attr('data-new-issue-path', '/issues/new');
+ createIssuable($form);
+
+ expect(Autosave).toHaveBeenCalledWith(
+ $title,
+ ['/issues/new', '', 'title'],
+ 'autosave//issues/new/bar=true=title',
+ );
+ expect(Autosave).toHaveBeenCalledWith(
+ $description,
+ ['/issues/new', '', 'description'],
+ 'autosave//issues/new/bar=true=description',
+ );
+ });
});
- 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'));
+ describe('resetAutosave', () => {
+ it('calls reset on title and description', () => {
+ instance = createIssuable($form);
+
+ instance.resetAutosave();
- setWindowLocation('https://gitlab.test/issues/new?bar=true');
+ expect(instance.autosaveTitle.reset).toHaveBeenCalledTimes(1);
+ expect(instance.autosaveDescription.reset).toHaveBeenCalledTimes(1);
+ });
- const autosave = instance.initAutosave();
+ it('resets autosave when submit', () => {
+ const resetAutosave = jest.spyOn(IssuableForm.prototype, 'resetAutosave');
+ createIssuable($form);
- expect(autosave.key.includes('bar=true')).toBe(false);
+ $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');
+
+ expect(resetAutosave).toHaveBeenCalledTimes(1);
+ });
});
});
- describe('resetAutosave', () => {
- it('resets autosave on elements with the .js-reset-autosave class', () => {
- setHTMLFixture(`
- <form>
- <input name="[title]" />
- <textarea name="[description]"></textarea>
- <a class="js-reset-autosave">Cancel</a>
- </form>
- `);
- const $form = $('form');
- const resetAutosave = jest.spyOn(IssuableForm.prototype, 'resetAutosave');
- createIssuable($form);
-
- $form.find('.js-reset-autosave').trigger('click');
-
- expect(resetAutosave).toHaveBeenCalled();
+ describe('wip', () => {
+ beforeEach(() => {
+ instance = createIssuable($form);
});
- });
- describe('removeWip', () => {
- it.each`
- prefix
- ${'draFT: '}
- ${' [DRaft] '}
- ${'drAft:'}
- ${'[draFT]'}
- ${'(draft) '}
- ${' (DrafT)'}
- ${'draft: [draft] (draft)'}
- `('removes "$prefix" from the beginning of the title', ({ prefix }) => {
- instance.titleField.val(`${prefix}The Issuable's Title Value`);
-
- instance.removeWip();
-
- expect(instance.titleField.val()).toBe("The Issuable's Title Value");
+ describe('removeWip', () => {
+ it.each`
+ prefix
+ ${'draFT: '}
+ ${' [DRaft] '}
+ ${'drAft:'}
+ ${'[draFT]'}
+ ${'(draft) '}
+ ${' (DrafT)'}
+ ${'draft: [draft] (draft)'}
+ `('removes "$prefix" from the beginning of the title', ({ prefix }) => {
+ instance.titleField.val(`${prefix}The Issuable's Title Value`);
+
+ instance.removeWip();
+
+ expect(instance.titleField.val()).toBe("The Issuable's Title Value");
+ });
});
- });
- describe('addWip', () => {
- it("properly adds the work in progress prefix to the Issuable's title", () => {
- instance.titleField.val("The Issuable's Title Value");
+ describe('addWip', () => {
+ it("properly adds the work in progress prefix to the Issuable's title", () => {
+ instance.titleField.val("The Issuable's Title Value");
- instance.addWip();
+ instance.addWip();
- expect(instance.titleField.val()).toBe("Draft: The Issuable's Title Value");
+ expect(instance.titleField.val()).toBe("Draft: The Issuable's Title Value");
+ });
});
- });
- describe('workInProgress', () => {
- it.each`
- title | expected
- ${'draFT: something is happening'} | ${true}
- ${'draft something is happening'} | ${false}
- ${'something is happening to drafts'} | ${false}
- ${'something is happening'} | ${false}
- `('returns $expected with "$title"', ({ title, expected }) => {
- instance.titleField.val(title);
-
- expect(instance.workInProgress()).toBe(expected);
+ describe('workInProgress', () => {
+ it.each`
+ title | expected
+ ${'draFT: something is happening'} | ${true}
+ ${'draft something is happening'} | ${false}
+ ${'something is happening to drafts'} | ${false}
+ ${'something is happening'} | ${false}
+ `('returns $expected with "$title"', ({ title, expected }) => {
+ instance.titleField.val(title);
+
+ expect(instance.workInProgress()).toBe(expected);
+ });
});
});
});
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 2c1061d2f1b..00e620832b3 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -520,6 +520,29 @@ RSpec.describe GroupsHelper do
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
subject { helper.enabled_git_access_protocol_options_for_group }
diff --git a/spec/workers/concerns/application_worker_spec.rb b/spec/workers/concerns/application_worker_spec.rb
index 707fa0c9c78..5fde54b98f0 100644
--- a/spec/workers/concerns/application_worker_spec.rb
+++ b/spec/workers/concerns/application_worker_spec.rb
@@ -289,7 +289,6 @@ RSpec.describe ApplicationWorker do
perform_action
expect(worker.jobs.count).to eq args.count
- expect(worker.jobs).to all(include('enqueued_at'))
end
end
@@ -302,7 +301,6 @@ RSpec.describe ApplicationWorker do
perform_action
expect(worker.jobs.count).to eq args.count
- expect(worker.jobs).to all(include('enqueued_at'))
end
end