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/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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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,
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
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() {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { initGroupOverviewTabs } from '~/groups/init_overview_tabs';
|
||||
import initGroupDetails from '../shared/group_details';
|
||||
|
||||
initGroupDetails('details');
|
||||
initGroupOverviewTabs();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@import 'page_bundles/mixins_and_variables_and_functions';
|
||||
|
||||
.application-theme {
|
||||
$ui-gray-bg: #303030;
|
||||
$ui-light-gray-bg: #f0f0f0;
|
|
@ -16,6 +16,10 @@
|
|||
line-height: 20px;
|
||||
}
|
||||
|
||||
.report-block-child-icon {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.report-block-list {
|
||||
list-style: none;
|
||||
padding: 0 1px;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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, ""
|
||||
|
|
|
@ -33,7 +33,10 @@
|
|||
|
||||
= 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) } } }
|
||||
- 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)
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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'
|
||||
|
||||
if Rails.env.development?
|
||||
Sidekiq.default_worker_options[:backtrace] = true
|
||||
Sidekiq.default_job_options[:backtrace] = true
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
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
|
||||
|
|
|
@ -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
|
||||
{
|
||||
id: project.id,
|
||||
name: name
|
||||
}
|
||||
errors
|
||||
}
|
||||
}
|
||||
GQL
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
{
|
||||
id: agent.project.id,
|
||||
agent_id: agent.id,
|
||||
name: agent.name
|
||||
}
|
||||
errors
|
||||
}
|
||||
}
|
||||
GQL
|
||||
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
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,51 +1,52 @@
|
|||
# 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
|
||||
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-project'
|
||||
project.name = 'autodevops-app-project'
|
||||
project.template_name = 'express'
|
||||
project.auto_devops_enabled = true
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
set_kube_ingress_base_domain(project)
|
||||
disable_optional_jobs(project)
|
||||
let!(:cluster) { Service::KubernetesCluster.new(provider_class: Service::ClusterProvider::Gcloud).create! }
|
||||
|
||||
let!(:kubernetes_agent) do
|
||||
Resource::Clusters::Agent.fabricate_via_api! do |agent|
|
||||
agent.name = 'agent1'
|
||||
agent.project = app_project
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Auto DevOps support' do
|
||||
context 'when rbac is enabled' do
|
||||
let(:cluster) { Service::KubernetesCluster.new.create! }
|
||||
let!(:agent_token) do
|
||||
Resource::Clusters::AgentToken.fabricate_via_api! do |token|
|
||||
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
|
||||
cluster&.remove!
|
||||
project.remove_via_api!
|
||||
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
|
||||
app_project.visit!
|
||||
|
||||
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::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')
|
||||
|
@ -56,23 +57,11 @@ module QA
|
|||
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
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
||||
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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 Autosave from '~/autosave';
|
||||
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
|
||||
import IssuableForm from '~/issuable/issuable_form';
|
||||
import setWindowLocation from 'helpers/set_window_location_helper';
|
||||
|
||||
describe('IssuableForm', () => {
|
||||
let instance;
|
||||
jest.mock('~/autosave');
|
||||
|
||||
const createIssuable = (form) => {
|
||||
instance = new IssuableForm(form);
|
||||
};
|
||||
const createIssuable = (form) => {
|
||||
return new IssuableForm(form);
|
||||
};
|
||||
|
||||
describe('IssuableForm', () => {
|
||||
let $form;
|
||||
let instance;
|
||||
|
||||
beforeEach(() => {
|
||||
setHTMLFixture(`
|
||||
<form>
|
||||
<input name="[title]" />
|
||||
<textarea name="[description]"></textarea>
|
||||
</form>
|
||||
`);
|
||||
createIssuable($('form'));
|
||||
$form = $('form');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
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', () => {
|
||||
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');
|
||||
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", () => {
|
||||
setHTMLFixture(`
|
||||
<form data-new-issue-path="/issues/new">
|
||||
<input name="[title]" />
|
||||
</form>
|
||||
`);
|
||||
createIssuable($('form'));
|
||||
|
||||
setWindowLocation('https://gitlab.test/issues/new?bar=true');
|
||||
$form.attr('data-new-issue-path', '/issues/new');
|
||||
createIssuable($form);
|
||||
|
||||
const autosave = instance.initAutosave();
|
||||
|
||||
expect(autosave.key.includes('bar=true')).toBe(false);
|
||||
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',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
it('calls reset on title and description', () => {
|
||||
instance = createIssuable($form);
|
||||
|
||||
instance.resetAutosave();
|
||||
|
||||
expect(instance.autosaveTitle.reset).toHaveBeenCalledTimes(1);
|
||||
expect(instance.autosaveDescription.reset).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('resets autosave when submit', () => {
|
||||
const resetAutosave = jest.spyOn(IssuableForm.prototype, 'resetAutosave');
|
||||
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');
|
||||
|
||||
expect(resetAutosave).toHaveBeenCalled();
|
||||
expect(resetAutosave).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('wip', () => {
|
||||
beforeEach(() => {
|
||||
instance = createIssuable($form);
|
||||
});
|
||||
|
||||
describe('removeWip', () => {
|
||||
it.each`
|
||||
|
@ -108,4 +164,5 @@ describe('IssuableForm', () => {
|
|||
expect(instance.workInProgress()).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue