Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-07-18 18:08:47 +00:00
parent cc1066db64
commit 128d4d89e9
121 changed files with 2719 additions and 967 deletions

View File

@ -77,7 +77,7 @@ review-build-cng:
variables: variables:
HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}" HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}" DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}"
GITLAB_HELM_CHART_REF: "a6a609a19166f00b1a7774374041cd38a9f7e20d" GITLAB_HELM_CHART_REF: "138c146a5ba787942f66d4c7d795d224d6ba206a"
environment: environment:
name: review/${CI_COMMIT_REF_SLUG}${SCHEDULE_TYPE} # No separator for SCHEDULE_TYPE so it's compatible as before and looks nice without it name: review/${CI_COMMIT_REF_SLUG}${SCHEDULE_TYPE} # No separator for SCHEDULE_TYPE so it's compatible as before and looks nice without it
url: https://gitlab-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN} url: https://gitlab-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}

View File

@ -1,63 +0,0 @@
<script>
import { __ } from '~/locale';
import Home from './home.vue';
import IncubationBanner from './incubation_banner.vue';
import ServiceAccountsForm from './service_accounts_form.vue';
import GcpRegionsForm from './gcp_regions_form.vue';
import NoGcpProjects from './errors/no_gcp_projects.vue';
import GcpError from './errors/gcp_error.vue';
const SCREEN_GCP_ERROR = 'gcp_error';
const SCREEN_HOME = 'home';
const SCREEN_NO_GCP_PROJECTS = 'no_gcp_projects';
const SCREEN_SERVICE_ACCOUNTS_FORM = 'service_accounts_form';
const SCREEN_GCP_REGIONS_FORM = 'gcp_regions_form';
export default {
components: {
IncubationBanner,
},
inheritAttrs: false,
props: {
screen: {
required: true,
type: String,
},
},
computed: {
mainComponent() {
switch (this.screen) {
case SCREEN_HOME:
return Home;
case SCREEN_GCP_ERROR:
return GcpError;
case SCREEN_NO_GCP_PROJECTS:
return NoGcpProjects;
case SCREEN_SERVICE_ACCOUNTS_FORM:
return ServiceAccountsForm;
case SCREEN_GCP_REGIONS_FORM:
return GcpRegionsForm;
default:
throw new Error(__('Unknown screen'));
}
},
},
methods: {
feedbackUrl(template) {
return `https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/feedback/-/issues/new?issuable_template=${template}`;
},
},
};
</script>
<template>
<div>
<incubation-banner
:share-feedback-url="feedbackUrl('general_feedback')"
:report-bug-url="feedbackUrl('report_bug')"
:feature-request-url="feedbackUrl('feature_request')"
/>
<component :is="mainComponent" v-bind="$attrs" />
</div>
</template>

View File

@ -1,29 +0,0 @@
<script>
import { GlAlert } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: { GlAlert },
props: {
error: {
type: String,
required: true,
},
},
i18n: {
title: __('Google Cloud project misconfigured'),
description: __(
'GitLab and Google Cloud configuration seems to be incomplete. This probably can be fixed by your GitLab administration team. You may share these logs with them:',
),
},
};
</script>
<template>
<gl-alert :dismissible="false" variant="warning" :title="$options.i18n.title">
{{ $options.i18n.description }}
<blockquote>
<code>{{ error }}</code>
</blockquote>
</gl-alert>
</template>

View File

@ -1,26 +0,0 @@
<script>
import { GlAlert, GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: { GlAlert, GlButton },
i18n: {
title: __('Google Cloud project required'),
description: __(
'You do not have any Google Cloud projects. Please create a Google Cloud project and then reload this page.',
),
createLabel: __('Create Google Cloud project'),
},
};
</script>
<template>
<gl-alert :dismissible="false" variant="warning" :title="$options.i18n.title">
{{ $options.i18n.description }}
<template #actions>
<gl-button href="https://console.cloud.google.com/projectcreate" target="_blank">
{{ $options.i18n.createLabel }}
</gl-button>
</template>
</gl-alert>
</template>

View File

@ -0,0 +1,85 @@
<script>
import { s__ } from '~/locale';
const CONFIGURATION_KEY = 'configuration';
const DEPLOYMENTS_KEY = 'deployments';
const DATABASES_KEY = 'databases';
const i18n = {
configuration: { title: s__('CloudSeed|Configuration') },
deployments: { title: s__('CloudSeed|Deployments') },
databases: { title: s__('CloudSeed|Databases') },
};
export default {
props: {
active: {
type: String,
required: true,
},
configurationUrl: {
type: String,
required: true,
},
deploymentsUrl: {
type: String,
required: true,
},
databasesUrl: {
type: String,
required: true,
},
},
computed: {
isConfigurationActive() {
return this.active === CONFIGURATION_KEY;
},
isDeploymentsActive() {
return this.active === DEPLOYMENTS_KEY;
},
isDatabasesActive() {
return this.active === DATABASES_KEY;
},
},
i18n,
};
</script>
<template>
<div class="tabs gl-tabs">
<ul role="tablist" class="nav gl-tabs-nav">
<li role="presentation" class="nav-item">
<a
data-testid="configurationLink"
role="tab"
:href="configurationUrl"
class="nav-link gl-tab-nav-item"
:class="{ 'gl-tab-nav-item-active': isConfigurationActive }"
>
{{ $options.i18n.configuration.title }}</a
>
</li>
<li role="presentation" class="nav-item">
<a
data-testid="deploymentsLink"
role="tab"
:href="deploymentsUrl"
class="nav-link gl-tab-nav-item"
:class="{ 'gl-tab-nav-item-active': isDeploymentsActive }"
>
{{ $options.i18n.deployments.title }}
</a>
</li>
<li role="presentation" class="nav-item">
<a
data-testid="databasesLink"
role="tab"
:href="databasesUrl"
class="nav-link gl-tab-nav-item"
:class="{ 'gl-tab-nav-item-active': isDatabasesActive }"
>
{{ $options.i18n.databases.title }}
</a>
</li>
</ul>
</div>
</template>

View File

@ -1,81 +0,0 @@
<script>
import { GlTabs, GlTab } from '@gitlab/ui';
import DeploymentsServiceTable from './deployments_service_table.vue';
import RevokeOauth from './revoke_oauth.vue';
import ServiceAccountsList from './service_accounts_list.vue';
import GcpRegionsList from './gcp_regions_list.vue';
export default {
components: {
GlTabs,
GlTab,
DeploymentsServiceTable,
RevokeOauth,
ServiceAccountsList,
GcpRegionsList,
},
props: {
serviceAccounts: {
type: Array,
required: true,
},
createServiceAccountUrl: {
type: String,
required: true,
},
configureGcpRegionsUrl: {
type: String,
required: true,
},
emptyIllustrationUrl: {
type: String,
required: true,
},
enableCloudRunUrl: {
type: String,
required: true,
},
enableCloudStorageUrl: {
type: String,
required: true,
},
gcpRegions: {
type: Array,
required: true,
},
revokeOauthUrl: {
type: String,
required: true,
},
},
};
</script>
<template>
<gl-tabs>
<gl-tab :title="__('Configuration')">
<service-accounts-list
class="gl-mx-4"
:list="serviceAccounts"
:create-url="createServiceAccountUrl"
:empty-illustration-url="emptyIllustrationUrl"
/>
<hr />
<gcp-regions-list
class="gl-mx-4"
:empty-illustration-url="emptyIllustrationUrl"
:create-url="configureGcpRegionsUrl"
:list="gcpRegions"
/>
<hr v-if="revokeOauthUrl" />
<revoke-oauth v-if="revokeOauthUrl" :url="revokeOauthUrl" />
</gl-tab>
<gl-tab :title="__('Deployments')">
<deployments-service-table
:cloud-run-url="enableCloudRunUrl"
:cloud-storage-url="enableCloudStorageUrl"
/>
</gl-tab>
<gl-tab :title="__('Services')" disabled />
</gl-tabs>
</template>

View File

@ -1,22 +1,20 @@
<script> <script>
import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui'; import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
const FEATURE_REQUEST_KEY = 'feature_request';
const REPORT_BUG_KEY = 'report_bug';
const GENERAL_FEEDBACK_KEY = 'general_feedback';
export default { export default {
components: { GlAlert, GlLink, GlSprintf }, components: { GlAlert, GlLink, GlSprintf },
props: { methods: {
shareFeedbackUrl: { feedbackUrl(template) {
required: true, return `https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/feedback/-/issues/new?issuable_template=${template}`;
type: String,
},
reportBugUrl: {
required: true,
type: String,
},
featureRequestUrl: {
required: true,
type: String,
}, },
}, },
FEATURE_REQUEST_KEY,
REPORT_BUG_KEY,
GENERAL_FEEDBACK_KEY,
}; };
</script> </script>
@ -31,13 +29,13 @@ export default {
" "
> >
<template #featureLink="{ content }"> <template #featureLink="{ content }">
<gl-link :href="featureRequestUrl">{{ content }}</gl-link> <gl-link :href="feedbackUrl($options.FEATURE_REQUEST_KEY)">{{ content }}</gl-link>
</template> </template>
<template #bugLink="{ content }"> <template #bugLink="{ content }">
<gl-link :href="reportBugUrl">{{ content }}</gl-link> <gl-link :href="feedbackUrl($options.REPORT_BUG_KEY)">{{ content }}</gl-link>
</template> </template>
<template #feedbackLink="{ content }"> <template #feedbackLink="{ content }">
<gl-link :href="shareFeedbackUrl">{{ content }}</gl-link> <gl-link :href="feedbackUrl($options.GENERAL_FEEDBACK_KEY)">{{ content }}</gl-link>
</template> </template>
</gl-sprintf> </gl-sprintf>
</gl-alert> </gl-alert>

View File

@ -0,0 +1,11 @@
import Vue from 'vue';
import Panel from './panel.vue';
export default (containerId = '#js-google-cloud-configuration') => {
const element = document.querySelector(containerId);
const { ...attrs } = JSON.parse(element.getAttribute('data'));
return new Vue({
el: element,
render: (createElement) => createElement(Panel, { attrs }),
});
};

View File

@ -0,0 +1,88 @@
<script>
import GcpRegionsList from '../gcp_regions/list.vue';
import GoogleCloudMenu from '../components/google_cloud_menu.vue';
import IncubationBanner from '../components/incubation_banner.vue';
import RevokeOauth from '../components/revoke_oauth.vue';
import ServiceAccountsList from '../service_accounts/list.vue';
export default {
components: {
GcpRegionsList,
GoogleCloudMenu,
IncubationBanner,
RevokeOauth,
ServiceAccountsList,
},
props: {
configurationUrl: {
type: String,
required: true,
},
deploymentsUrl: {
type: String,
required: true,
},
databasesUrl: {
type: String,
required: true,
},
serviceAccounts: {
type: Array,
required: true,
},
createServiceAccountUrl: {
type: String,
required: true,
},
emptyIllustrationUrl: {
type: String,
required: true,
},
configureGcpRegionsUrl: {
type: String,
required: true,
},
gcpRegions: {
type: Array,
required: true,
},
revokeOauthUrl: {
type: String,
required: true,
},
},
};
</script>
<template>
<div>
<incubation-banner />
<google-cloud-menu
active="configuration"
:configuration-url="configurationUrl"
:deployments-url="deploymentsUrl"
:databases-url="databasesUrl"
/>
<service-accounts-list
class="gl-mx-4"
:list="serviceAccounts"
:create-url="createServiceAccountUrl"
:empty-illustration-url="emptyIllustrationUrl"
/>
<hr />
<gcp-regions-list
class="gl-mx-4"
:empty-illustration-url="emptyIllustrationUrl"
:create-url="configureGcpRegionsUrl"
:list="gcpRegions"
/>
<hr v-if="revokeOauthUrl" />
<revoke-oauth v-if="revokeOauthUrl" :url="revokeOauthUrl" />
</div>
</template>

View File

@ -0,0 +1,11 @@
import Vue from 'vue';
import Panel from './panel.vue';
export default (containerId = '#js-google-cloud-databases') => {
const element = document.querySelector(containerId);
const { ...attrs } = JSON.parse(element.getAttribute('data'));
return new Vue({
el: element,
render: (createElement) => createElement(Panel, { attrs }),
});
};

View File

@ -0,0 +1,38 @@
<script>
import GoogleCloudMenu from '../components/google_cloud_menu.vue';
import IncubationBanner from '../components/incubation_banner.vue';
export default {
components: {
IncubationBanner,
GoogleCloudMenu,
},
props: {
configurationUrl: {
type: String,
required: true,
},
deploymentsUrl: {
type: String,
required: true,
},
databasesUrl: {
type: String,
required: true,
},
},
};
</script>
<template>
<div>
<incubation-banner />
<google-cloud-menu
active="databases"
:configuration-url="configurationUrl"
:deployments-url="deploymentsUrl"
:databases-url="databasesUrl"
/>
</div>
</template>

View File

@ -0,0 +1,11 @@
import Vue from 'vue';
import Panel from './panel.vue';
export default (containerId = '#js-google-cloud-deployments') => {
const element = document.querySelector(containerId);
const { ...attrs } = JSON.parse(element.getAttribute('data'));
return new Vue({
el: element,
render: (createElement) => createElement(Panel, { attrs }),
});
};

View File

@ -0,0 +1,50 @@
<script>
import GoogleCloudMenu from '../components/google_cloud_menu.vue';
import IncubationBanner from '../components/incubation_banner.vue';
import ServiceTable from './service_table.vue';
export default {
components: {
ServiceTable,
IncubationBanner,
GoogleCloudMenu,
},
props: {
configurationUrl: {
type: String,
required: true,
},
deploymentsUrl: {
type: String,
required: true,
},
databasesUrl: {
type: String,
required: true,
},
enableCloudRunUrl: {
type: String,
required: true,
},
enableCloudStorageUrl: {
type: String,
required: true,
},
},
};
</script>
<template>
<div>
<incubation-banner />
<google-cloud-menu
active="deployments"
:configuration-url="configurationUrl"
:deployments-url="deploymentsUrl"
:databases-url="databasesUrl"
/>
<service-table :cloud-run-url="enableCloudRunUrl" :cloud-storage-url="enableCloudStorageUrl" />
</div>
</template>

View File

@ -0,0 +1,11 @@
import Vue from 'vue';
import Form from './form.vue';
export default (containerId = '#js-google-cloud-gcp-regions') => {
const element = document.querySelector(containerId);
const { ...attrs } = JSON.parse(element.getAttribute('data'));
return new Vue({
el: element,
render: (createElement) => createElement(Form, { attrs }),
});
};

View File

@ -1,12 +0,0 @@
import Vue from 'vue';
import App from './components/app.vue';
export default () => {
const root = '#js-google-cloud';
const element = document.querySelector(root);
const { screen, ...attrs } = JSON.parse(element.getAttribute('data'));
return new Vue({
el: element,
render: (createElement) => createElement(App, { props: { screen }, attrs }),
});
};

View File

@ -0,0 +1,11 @@
import Vue from 'vue';
import Form from './form.vue';
export default (containerId = '#js-google-cloud-service-accounts') => {
const element = document.querySelector(containerId);
const { ...attrs } = JSON.parse(element.getAttribute('data'));
return new Vue({
el: element,
render: (createElement) => createElement(Form, { attrs }),
});
};

View File

@ -5,13 +5,23 @@ import {
GlBadge, GlBadge,
GlIcon, GlIcon,
GlLabel, GlLabel,
GlButton,
GlPopover,
GlLink,
GlTooltipDirective, GlTooltipDirective,
GlSafeHtmlDirective, GlSafeHtmlDirective,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue'; import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants'; import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
import { VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE } from '../constants'; import { helpPagePath } from '~/helpers/help_page_helper';
import { __ } from '~/locale';
import {
VISIBILITY_TYPE_ICON,
GROUP_VISIBILITY_TYPE,
ITEM_TYPE,
VISIBILITY_PRIVATE,
} from '../constants';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import itemActions from './item_actions.vue'; import itemActions from './item_actions.vue';
@ -30,12 +40,16 @@ export default {
GlLoadingIcon, GlLoadingIcon,
GlIcon, GlIcon,
GlLabel, GlLabel,
GlButton,
GlPopover,
GlLink,
UserAccessRoleBadge, UserAccessRoleBadge,
itemCaret, itemCaret,
itemTypeIcon, itemTypeIcon,
itemActions, itemActions,
itemStats, itemStats,
}, },
inject: ['currentGroupVisibility'],
props: { props: {
parentGroup: { parentGroup: {
type: Object, type: Object,
@ -56,6 +70,9 @@ export default {
groupDomId() { groupDomId() {
return `group-${this.group.id}`; return `group-${this.group.id}`;
}, },
itemTestId() {
return `group-overview-item-${this.group.id}`;
},
rowClass() { rowClass() {
return { return {
'is-open': this.group.isOpen, 'is-open': this.group.isOpen,
@ -74,10 +91,10 @@ export default {
return Boolean(this.group.complianceFramework?.name); return Boolean(this.group.complianceFramework?.name);
}, },
isGroup() { isGroup() {
return this.group.type === 'group'; return this.group.type === ITEM_TYPE.GROUP;
}, },
isGroupPendingRemoval() { isGroupPendingRemoval() {
return this.group.type === 'group' && this.group.pendingRemoval; return this.group.type === ITEM_TYPE.GROUP && this.group.pendingRemoval;
}, },
visibilityIcon() { visibilityIcon() {
return VISIBILITY_TYPE_ICON[this.group.visibility]; return VISIBILITY_TYPE_ICON[this.group.visibility];
@ -94,6 +111,13 @@ export default {
showActionsMenu() { showActionsMenu() {
return this.isGroup && (this.group.canEdit || this.group.canRemove || this.group.canLeave); return this.isGroup && (this.group.canEdit || this.group.canRemove || this.group.canLeave);
}, },
shouldShowVisibilityWarning() {
return (
this.action === 'shared' &&
this.currentGroupVisibility === VISIBILITY_PRIVATE &&
this.group.visibility !== VISIBILITY_PRIVATE
);
},
}, },
methods: { methods: {
onClickRowGroup(e) { onClickRowGroup(e) {
@ -110,6 +134,17 @@ export default {
} }
}, },
}, },
i18n: {
popoverTitle: __('Less restrictive visibility'),
popoverBody: __('Project visibility level is less restrictive than the group settings.'),
learnMore: __('Learn more'),
},
shareProjectsWithGroupsHelpPagePath: helpPagePath(
'user/project/members/share_project_with_groups',
{
anchor: 'share-a-public-project-with-private-group',
},
),
safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] }, safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
AVATAR_SHAPE_OPTION_RECT, AVATAR_SHAPE_OPTION_RECT,
}; };
@ -118,6 +153,7 @@ export default {
<template> <template>
<li <li
:id="groupDomId" :id="groupDomId"
:data-testid="itemTestId"
:class="rowClass" :class="rowClass"
class="group-row" class="group-row"
:itemprop="microdata.itemprop" :itemprop="microdata.itemprop"
@ -163,7 +199,7 @@ export default {
data-testid="group-name" data-testid="group-name"
:href="group.relativePath" :href="group.relativePath"
:title="group.fullName" :title="group.fullName"
class="no-expand gl-mr-3 gl-mt-3 gl-text-gray-900!" class="no-expand gl-mr-3 gl-text-gray-900!"
:itemprop="microdata.nameItemprop" :itemprop="microdata.nameItemprop"
> >
{{ {{
@ -174,17 +210,40 @@ export default {
</a> </a>
<gl-icon <gl-icon
v-gl-tooltip.hover.bottom v-gl-tooltip.hover.bottom
class="gl-display-inline-flex gl-align-items-center gl-mr-3 gl-mt-3 gl-text-gray-500" class="gl-display-inline-flex gl-align-items-center gl-mr-3 gl-text-gray-500"
:name="visibilityIcon" :name="visibilityIcon"
:title="visibilityTooltip" :title="visibilityTooltip"
data-testid="group-visibility-icon" data-testid="group-visibility-icon"
/> />
<user-access-role-badge v-if="group.permission" class="gl-mt-3"> <template v-if="shouldShowVisibilityWarning">
<gl-button
ref="visibilityWarningButton"
class="gl-p-1! gl-bg-transparent! gl-mr-3"
category="tertiary"
icon="warning"
:aria-label="$options.i18n.popoverTitle"
@click.stop
/>
<gl-popover
:target="() => $refs.visibilityWarningButton.$el"
:title="$options.i18n.popoverTitle"
triggers="hover focus"
>
{{ $options.i18n.popoverBody }}
<div class="gl-mt-3">
<gl-link
class="gl-font-sm"
:href="$options.shareProjectsWithGroupsHelpPagePath"
>{{ $options.i18n.learnMore }}</gl-link
>
</div>
</gl-popover>
</template>
<user-access-role-badge v-if="group.permission" class="gl-mr-3">
{{ group.permission }} {{ group.permission }}
</user-access-role-badge> </user-access-role-badge>
<gl-label <gl-label
v-if="hasComplianceFramework" v-if="hasComplianceFramework"
class="gl-mt-3"
:title="complianceFramework.name" :title="complianceFramework.name"
:background-color="complianceFramework.color" :background-color="complianceFramework.color"
:description="complianceFramework.description" :description="complianceFramework.description"

View File

@ -28,28 +28,32 @@ export const ITEM_TYPE = {
GROUP: 'group', GROUP: 'group',
}; };
export const VISIBILITY_PUBLIC = 'public';
export const VISIBILITY_INTERNAL = 'internal';
export const VISIBILITY_PRIVATE = 'private';
export const GROUP_VISIBILITY_TYPE = { export const GROUP_VISIBILITY_TYPE = {
public: __( [VISIBILITY_PUBLIC]: __(
'Public - The group and any public projects can be viewed without any authentication.', 'Public - The group and any public projects can be viewed without any authentication.',
), ),
internal: __( [VISIBILITY_INTERNAL]: __(
'Internal - The group and any internal projects can be viewed by any logged in user except external users.', 'Internal - The group and any internal projects can be viewed by any logged in user except external users.',
), ),
private: __('Private - The group and its projects can only be viewed by members.'), [VISIBILITY_PRIVATE]: __('Private - The group and its projects can only be viewed by members.'),
}; };
export const PROJECT_VISIBILITY_TYPE = { export const PROJECT_VISIBILITY_TYPE = {
public: __('Public - The project can be accessed without any authentication.'), [VISIBILITY_PUBLIC]: __('Public - The project can be accessed without any authentication.'),
internal: __( [VISIBILITY_INTERNAL]: __(
'Internal - The project can be accessed by any logged in user except external users.', 'Internal - The project can be accessed by any logged in user except external users.',
), ),
private: __( [VISIBILITY_PRIVATE]: __(
'Private - Project access must be granted explicitly to each user. If this project is part of a group, access will be granted to members of the group.', 'Private - Project access must be granted explicitly to each user. If this project is part of a group, access will be granted to members of the group.',
), ),
}; };
export const VISIBILITY_TYPE_ICON = { export const VISIBILITY_TYPE_ICON = {
public: 'earth', [VISIBILITY_PUBLIC]: 'earth',
internal: 'shield', [VISIBILITY_INTERNAL]: 'shield',
private: 'lock', [VISIBILITY_PRIVATE]: 'lock',
}; };

View File

@ -55,6 +55,7 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
renderEmptyState, renderEmptyState,
canCreateSubgroups, canCreateSubgroups,
canCreateProjects, canCreateProjects,
currentGroupVisibility,
}, },
} = this.$options.el; } = this.$options.el;
@ -67,6 +68,7 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
renderEmptyState: parseBoolean(renderEmptyState), renderEmptyState: parseBoolean(renderEmptyState),
canCreateSubgroups: parseBoolean(canCreateSubgroups), canCreateSubgroups: parseBoolean(canCreateSubgroups),
canCreateProjects: parseBoolean(canCreateProjects), canCreateProjects: parseBoolean(canCreateProjects),
currentGroupVisibility,
}; };
}, },
data() { data() {

View File

@ -0,0 +1,3 @@
import init from '~/google_cloud/configuration/index';
init();

View File

@ -0,0 +1,3 @@
import init from '~/google_cloud/databases/index';
init();

View File

@ -0,0 +1,3 @@
import init from '~/google_cloud/deployments/index';
init();

View File

@ -0,0 +1,3 @@
import init from '~/google_cloud/gcp_regions/index';
init();

View File

@ -1,3 +0,0 @@
import initGoogleCloud from '~/google_cloud/index';
initGoogleCloud();

View File

@ -0,0 +1,3 @@
import init from '~/google_cloud/service_accounts/index';
init();

View File

@ -19,7 +19,7 @@ export const i18n = {
invalid: s__('Pipelines|This GitLab CI configuration is invalid.'), invalid: s__('Pipelines|This GitLab CI configuration is invalid.'),
invalidWithReason: s__('Pipelines|This GitLab CI configuration is invalid: %{reason}.'), invalidWithReason: s__('Pipelines|This GitLab CI configuration is invalid: %{reason}.'),
unavailableValidation: s__('Pipelines|Configuration validation currently not available.'), unavailableValidation: s__('Pipelines|Configuration validation currently not available.'),
valid: s__('Pipelines|This GitLab CI configuration is valid.'), valid: s__('Pipelines|Pipeline syntax is correct.'),
}; };
export default { export default {

View File

@ -52,6 +52,11 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
hideAlert: {
type: Boolean,
required: false,
default: false,
},
isValid: { isValid: {
type: Boolean, type: Boolean,
required: true, required: true,
@ -63,7 +68,8 @@ export default {
}, },
lintHelpPagePath: { lintHelpPagePath: {
type: String, type: String,
required: true, required: false,
default: '',
}, },
warnings: { warnings: {
type: Array, type: Array,
@ -96,6 +102,7 @@ export default {
<template> <template>
<div> <div>
<gl-alert <gl-alert
v-if="!hideAlert"
class="gl-mb-5" class="gl-mb-5"
:variant="status.variant" :variant="status.variant"
:title="__('Status:')" :title="__('Status:')"

View File

@ -219,8 +219,7 @@ export default {
:title="$options.i18n.tabValidate" :title="$options.i18n.tabValidate"
@click="setCurrentTab($options.tabConstants.VALIDATE_TAB)" @click="setCurrentTab($options.tabConstants.VALIDATE_TAB)"
> >
<gl-loading-icon v-if="isLoading" size="lg" class="gl-m-3" /> <ci-validate :ci-file-content="ciFileContent" />
<ci-validate v-else />
</editor-tab> </editor-tab>
<editor-tab <editor-tab
v-else v-else

View File

@ -1,10 +1,35 @@
<script> <script>
import { GlButton, GlDropdown, GlIcon, GlTooltipDirective, GlSprintf } from '@gitlab/ui'; import {
GlAlert,
GlButton,
GlDropdown,
GlIcon,
GlLoadingIcon,
GlLink,
GlTooltip,
GlTooltipDirective,
GlSprintf,
} from '@gitlab/ui';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import ValidatePipelinePopover from '../popovers/validate_pipeline_popover.vue'; import ValidatePipelinePopover from '../popovers/validate_pipeline_popover.vue';
import CiLintResults from '../lint/ci_lint_results.vue';
import getBlobContent from '../../graphql/queries/blob_content.query.graphql';
import getCurrentBranch from '../../graphql/queries/client/current_branch.query.graphql';
import lintCiMutation from '../../graphql/mutations/client/lint_ci.mutation.graphql';
export const i18n = { export const i18n = {
alertDesc: s__(
'PipelineEditor|Simulated a %{codeStart}git push%{codeEnd} event for a default branch. %{codeStart}Rules%{codeEnd}, %{codeStart}only%{codeEnd}, %{codeStart}except%{codeEnd}, and %{codeStart}needs%{codeEnd} job dependencies logic have been evaluated. %{linkStart}Learn more%{linkEnd}',
),
cancelBtn: __('Cancel'),
contentChange: s__(
'PipelineEditor|Configuration content has changed. Re-run validation for updated results.',
),
cta: s__('PipelineEditor|Validate pipeline'),
ctaDisabledTooltip: s__('PipelineEditor|Waiting for CI content to load...'),
errorAlertTitle: s__('PipelineEditor|Pipeline simulation completed with errors'),
help: __('Help'), help: __('Help'),
loading: s__('PipelineEditor|Validating pipeline... It can take up to a minute.'),
pipelineSource: s__('PipelineEditor|Pipeline Source'), pipelineSource: s__('PipelineEditor|Pipeline Source'),
pipelineSourceDefault: s__('PipelineEditor|Git push event to the default branch'), pipelineSourceDefault: s__('PipelineEditor|Git push event to the default branch'),
pipelineSourceTooltip: s__('PipelineEditor|Other pipeline sources are not available yet.'), pipelineSourceTooltip: s__('PipelineEditor|Other pipeline sources are not available yet.'),
@ -15,48 +40,179 @@ export const i18n = {
simulationNote: s__( simulationNote: s__(
'PipelineEditor|Pipeline behavior will be simulated including the %{codeStart}rules%{codeEnd} %{codeStart}only%{codeEnd} %{codeStart}except%{codeEnd} and %{codeStart}needs%{codeEnd} job dependencies.', 'PipelineEditor|Pipeline behavior will be simulated including the %{codeStart}rules%{codeEnd} %{codeStart}only%{codeEnd} %{codeStart}except%{codeEnd} and %{codeStart}needs%{codeEnd} job dependencies.',
), ),
cta: s__('PipelineEditor|Validate pipeline'), successAlertTitle: s__('PipelineEditor|Simulation completed successfully'),
}; };
export const VALIDATE_TAB_INIT = 'VALIDATE_TAB_INIT';
export const VALIDATE_TAB_RESULTS = 'VALIDATE_TAB_RESULTS';
export const VALIDATE_TAB_LOADING = 'VALIDATE_TAB_LOADING';
const BASE_CLASSES = [
'gl-display-flex',
'gl-flex-direction-column',
'gl-align-items-center',
'gl-mt-11',
];
export default { export default {
name: 'CiValidateTab', name: 'CiValidateTab',
components: { components: {
CiLintResults,
GlAlert,
GlButton, GlButton,
GlDropdown, GlDropdown,
GlIcon, GlIcon,
GlLoadingIcon,
GlLink,
GlSprintf, GlSprintf,
GlTooltip,
ValidatePipelinePopover, ValidatePipelinePopover,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
inject: ['validateTabIllustrationPath'], inject: ['ciConfigPath', 'ciLintPath', 'projectFullPath', 'validateTabIllustrationPath'],
props: {
ciFileContent: {
type: String,
required: true,
},
},
apollo: {
initialBlobContent: {
query: getBlobContent,
variables() {
return {
projectPath: this.projectFullPath,
path: this.ciConfigPath,
ref: this.currentBranch,
};
},
update(data) {
return data?.project?.repository?.blobs?.nodes[0]?.rawBlob;
},
},
currentBranch: {
query: getCurrentBranch,
update(data) {
return data.workBranches?.current?.name;
},
},
},
data() {
return {
yaml: this.ciFileContent,
state: VALIDATE_TAB_INIT,
errors: [],
hasCiContentChanged: false,
isValid: false,
jobs: [],
warnings: [],
};
},
computed: {
isInitialCiContentLoading() {
return this.$apollo.queries.initialBlobContent.loading;
},
isInitState() {
return this.state === VALIDATE_TAB_INIT;
},
isSimulationLoading() {
return this.state === VALIDATE_TAB_LOADING;
},
hasSimulationResults() {
return this.state === VALIDATE_TAB_RESULTS;
},
resultStatus() {
return {
title: this.isValid ? i18n.successAlertTitle : i18n.errorAlertTitle,
variant: this.isValid ? 'success' : 'danger',
};
},
},
watch: {
ciFileContent(value) {
this.yaml = value;
this.hasCiContentChanged = true;
},
},
methods: {
cancelSimulation() {
this.state = VALIDATE_TAB_INIT;
},
async validateYaml() {
this.state = VALIDATE_TAB_LOADING;
try {
const {
data: {
lintCI: { errors, jobs, valid, warnings },
},
} = await this.$apollo.mutate({
mutation: lintCiMutation,
variables: {
dry_run: true,
content: this.yaml,
endpoint: this.ciLintPath,
},
});
// only save the result if the user did not cancel the simulation
if (this.state === VALIDATE_TAB_LOADING) {
this.errors = errors;
this.jobs = jobs;
this.warnings = warnings;
this.isValid = valid;
this.state = VALIDATE_TAB_RESULTS;
this.hasCiContentChanged = false;
}
} catch (error) {
this.cancelSimulation();
}
},
},
i18n, i18n,
BASE_CLASSES,
}; };
</script> </script>
<template> <template>
<div> <div>
<div class="gl-mt-3"> <div class="gl-display-flex gl-justify-content-space-between gl-mt-3">
<label>{{ $options.i18n.pipelineSource }}</label> <div>
<gl-dropdown <label>{{ $options.i18n.pipelineSource }}</label>
v-gl-tooltip.hover <gl-dropdown
:title="$options.i18n.pipelineSourceTooltip" v-gl-tooltip.hover
:text="$options.i18n.pipelineSourceDefault" class="gl-ml-3"
disabled :title="$options.i18n.pipelineSourceTooltip"
data-testid="pipeline-source" :text="$options.i18n.pipelineSourceDefault"
/> disabled
<validate-pipeline-popover /> data-testid="pipeline-source"
<gl-icon />
id="validate-pipeline-help" <validate-pipeline-popover />
name="question-o" <gl-icon
class="gl-ml-1 gl-fill-blue-500" id="validate-pipeline-help"
category="secondary" name="question-o"
variant="confirm" class="gl-ml-1 gl-fill-blue-500"
:aria-label="$options.i18n.help" category="secondary"
/> variant="confirm"
:aria-label="$options.i18n.help"
/>
</div>
<div v-if="hasSimulationResults && hasCiContentChanged">
<span class="gl-text-gray-400" data-testid="content-status">
{{ $options.i18n.contentChange }}
</span>
<gl-button
variant="confirm"
class="gl-ml-2 gl-mb-2"
data-testid="resimulate-pipeline-button"
@click="validateYaml"
>
{{ $options.i18n.cta }}
</gl-button>
</div>
</div> </div>
<div class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-mt-11"> <div v-if="isInitState" :class="$options.BASE_CLASSES">
<img :src="validateTabIllustrationPath" /> <img :src="validateTabIllustrationPath" />
<h1 class="gl-font-size-h1 gl-mb-6">{{ $options.i18n.title }}</h1> <h1 class="gl-font-size-h1 gl-mb-6">{{ $options.i18n.title }}</h1>
<ul> <ul>
@ -69,9 +225,61 @@ export default {
</gl-sprintf> </gl-sprintf>
</li> </li>
</ul> </ul>
<gl-button variant="confirm" class="gl-mt-3" data-qa-selector="simulate_pipeline"> <div ref="simulatePipelineButton">
{{ $options.i18n.cta }} <gl-button
</gl-button> ref="simulatePipelineButton"
variant="confirm"
class="gl-mt-3"
:disabled="isInitialCiContentLoading"
data-testid="simulate-pipeline-button"
@click="validateYaml"
>
{{ $options.i18n.cta }}
</gl-button>
</div>
<gl-tooltip
v-if="isInitialCiContentLoading"
:target="() => $refs.simulatePipelineButton"
:title="$options.i18n.ctaDisabledTooltip"
data-testid="cta-tooltip"
/>
</div>
<div v-else-if="isSimulationLoading" :class="$options.BASE_CLASSES">
<gl-loading-icon size="lg" class="gl-m-3" />
<h1 class="gl-font-size-h1 gl-mb-6">{{ $options.i18n.loading }}</h1>
<div>
<gl-button class="gl-mt-3" data-testid="cancel-simulation" @click="cancelSimulation">
{{ $options.i18n.cancelBtn }}
</gl-button>
<gl-button class="gl-mt-3" loading data-testid="simulate-pipeline-button">
{{ $options.i18n.cta }}
</gl-button>
</div>
</div>
<div v-else-if="hasSimulationResults" class="gl-mt-5">
<gl-alert
class="gl-mb-5"
:dismissible="false"
:title="resultStatus.title"
:variant="resultStatus.variant"
>
<gl-sprintf :message="$options.i18n.alertDesc">
<template #code="{ content }">
<code>{{ content }}</code>
</template>
<template #link="{ content }">
<gl-link target="_blank" href="#">{{ content }}</gl-link>
</template>
</gl-sprintf>
</gl-alert>
<ci-lint-results
dry-run
hide-alert
:is-valid="isValid"
:jobs="jobs"
:errors="errors"
:warnings="warnings"
/>
</div> </div>
</div> </div>
</template> </template>

View File

@ -27,6 +27,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
ciConfigPath, ciConfigPath,
ciExamplesHelpPagePath, ciExamplesHelpPagePath,
ciHelpPagePath, ciHelpPagePath,
ciLintPath,
defaultBranch, defaultBranch,
emptyStateIllustrationPath, emptyStateIllustrationPath,
helpPaths, helpPaths,
@ -116,6 +117,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
ciConfigPath, ciConfigPath,
ciExamplesHelpPagePath, ciExamplesHelpPagePath,
ciHelpPagePath, ciHelpPagePath,
ciLintPath,
configurationPaths, configurationPaths,
dataMethod: 'graphql', dataMethod: 'graphql',
defaultBranch, defaultBranch,

View File

@ -1 +1 @@
export const ARTIFACTS_EXPIRED_ERROR_MESSAGE = 'Test report artifacts have expired'; export const ARTIFACTS_EXPIRED_ERROR_MESSAGE = 'Test report artifacts not found';

View File

@ -209,7 +209,6 @@ table.pipeline-project-metrics tr td {
} }
.title { .title {
margin-top: -$gl-padding-8; // negative margin required for flex-wrap
font-size: $gl-font-size; font-size: $gl-font-size;
} }

View File

@ -54,8 +54,6 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
# limit scopes when signing in with GitLab # limit scopes when signing in with GitLab
def downgrade_scopes! def downgrade_scopes!
return unless Feature.enabled?(:omniauth_login_minimal_scopes, current_user)
auth_type = params.delete('gl_auth_type') auth_type = params.delete('gl_auth_type')
return unless auth_type == 'login' return unless auth_type == 'login'

View File

@ -12,7 +12,7 @@ class Projects::GoogleCloud::BaseController < Projects::ApplicationController
def admin_project_google_cloud! def admin_project_google_cloud!
unless can?(current_user, :admin_project_google_cloud, project) unless can?(current_user, :admin_project_google_cloud, project)
track_event('admin_project_google_cloud!', 'access_denied', 'invalid_user') track_event('admin_project_google_cloud!', 'error_access_denied', 'invalid_user')
access_denied! access_denied!
end end
end end
@ -20,7 +20,11 @@ class Projects::GoogleCloud::BaseController < Projects::ApplicationController
def google_oauth2_enabled! def google_oauth2_enabled!
config = Gitlab::Auth::OAuth::Provider.config_for('google_oauth2') config = Gitlab::Auth::OAuth::Provider.config_for('google_oauth2')
if config.app_id.blank? || config.app_secret.blank? if config.app_id.blank? || config.app_secret.blank?
track_event('google_oauth2_enabled!', 'access_denied', { reason: 'google_oauth2_not_configured', config: config }) track_event(
'google_oauth2_enabled!',
'error_access_denied',
{ reason: 'google_oauth2_not_configured', config: config }
)
access_denied! 'This GitLab instance not configured for Google Oauth2.' access_denied! 'This GitLab instance not configured for Google Oauth2.'
end end
end end
@ -31,7 +35,7 @@ class Projects::GoogleCloud::BaseController < Projects::ApplicationController
enabled_for_project = Feature.enabled?(:incubation_5mp_google_cloud, project) enabled_for_project = Feature.enabled?(:incubation_5mp_google_cloud, project)
feature_is_enabled = enabled_for_user || enabled_for_group || enabled_for_project feature_is_enabled = enabled_for_user || enabled_for_group || enabled_for_project
unless feature_is_enabled unless feature_is_enabled
track_event('feature_flag_enabled!', 'access_denied', 'feature_flag_not_enabled') track_event('feature_flag_enabled!', 'error_access_denied', 'feature_flag_not_enabled')
access_denied! access_denied!
end end
end end
@ -42,7 +46,7 @@ class Projects::GoogleCloud::BaseController < Projects::ApplicationController
return if is_token_valid return if is_token_valid
return_url = project_google_cloud_index_path(project) return_url = project_google_cloud_configuration_path(project)
state = generate_session_key_redirect(request.url, return_url) state = generate_session_key_redirect(request.url, return_url)
@authorize_url = GoogleApi::CloudPlatform::Client.new(nil, @authorize_url = GoogleApi::CloudPlatform::Client.new(nil,
callback_google_api_auth_url, callback_google_api_auth_url,
@ -65,12 +69,6 @@ class Projects::GoogleCloud::BaseController < Projects::ApplicationController
session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at]
end end
def handle_gcp_error(action, error)
track_event(action, 'gcp_error', error)
@js_data = { screen: 'gcp_error', error: error.to_s }.to_json
render status: :unauthorized, template: 'projects/google_cloud/errors/gcp_error'
end
def track_event(action, label, property) def track_event(action, label, property)
options = { label: label, project: project, user: current_user } options = { label: label, project: project, user: current_user }

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
module Projects
module GoogleCloud
class ConfigurationController < Projects::GoogleCloud::BaseController
def index
@google_cloud_path = project_google_cloud_configuration_path(project)
js_data = {
configurationUrl: project_google_cloud_configuration_path(project),
deploymentsUrl: project_google_cloud_deployments_path(project),
databasesUrl: project_google_cloud_databases_path(project),
serviceAccounts: ::GoogleCloud::ServiceAccountsService.new(project).find_for_project,
createServiceAccountUrl: project_google_cloud_service_accounts_path(project),
emptyIllustrationUrl: ActionController::Base.helpers.image_path('illustrations/pipelines_empty.svg'),
configureGcpRegionsUrl: project_google_cloud_gcp_regions_path(project),
gcpRegions: gcp_regions,
revokeOauthUrl: revoke_oauth_url
}
@js_data = js_data.to_json
track_event('configuration#index', 'success', js_data)
end
private
def gcp_regions
params = { key: Projects::GoogleCloud::GcpRegionsController::GCP_REGION_CI_VAR_KEY }
list = ::Ci::VariablesFinder.new(project, params).execute
list.map { |variable| { gcp_region: variable.value, environment: variable.environment_scope } }
end
def revoke_oauth_url
google_token_valid = GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
.validate_token(expires_at_in_session)
google_token_valid ? project_google_cloud_revoke_oauth_index_path(project) : nil
end
end
end
end

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
module Projects
module GoogleCloud
class DatabasesController < Projects::GoogleCloud::BaseController
def index
@google_cloud_path = project_google_cloud_configuration_path(project)
js_data = {
configurationUrl: project_google_cloud_configuration_path(project),
deploymentsUrl: project_google_cloud_deployments_path(project),
databasesUrl: project_google_cloud_databases_path(project)
}
@js_data = js_data.to_json
track_event('databases#index', 'success', js_data)
end
end
end
end

View File

@ -3,32 +3,47 @@
class Projects::GoogleCloud::DeploymentsController < Projects::GoogleCloud::BaseController class Projects::GoogleCloud::DeploymentsController < Projects::GoogleCloud::BaseController
before_action :validate_gcp_token! before_action :validate_gcp_token!
def index
@google_cloud_path = project_google_cloud_configuration_path(project)
js_data = {
configurationUrl: project_google_cloud_configuration_path(project),
deploymentsUrl: project_google_cloud_deployments_path(project),
databasesUrl: project_google_cloud_databases_path(project),
enableCloudRunUrl: project_google_cloud_deployments_cloud_run_path(project),
enableCloudStorageUrl: project_google_cloud_deployments_cloud_storage_path(project)
}
@js_data = js_data.to_json
track_event('deployments#index', 'success', js_data)
end
def cloud_run def cloud_run
params = { google_oauth2_token: token_in_session } params = { google_oauth2_token: token_in_session }
enable_cloud_run_response = GoogleCloud::EnableCloudRunService enable_cloud_run_response = GoogleCloud::EnableCloudRunService
.new(project, current_user, params).execute .new(project, current_user, params).execute
if enable_cloud_run_response[:status] == :error if enable_cloud_run_response[:status] == :error
track_event('deployments#cloud_run', 'enable_cloud_run_error', enable_cloud_run_response) track_event('deployments#cloud_run', 'error_enable_cloud_run', enable_cloud_run_response)
flash[:error] = enable_cloud_run_response[:message] flash[:error] = enable_cloud_run_response[:message]
redirect_to project_google_cloud_index_path(project) redirect_to project_google_cloud_deployments_path(project)
else else
params = { action: GoogleCloud::GeneratePipelineService::ACTION_DEPLOY_TO_CLOUD_RUN } params = { action: GoogleCloud::GeneratePipelineService::ACTION_DEPLOY_TO_CLOUD_RUN }
generate_pipeline_response = GoogleCloud::GeneratePipelineService generate_pipeline_response = GoogleCloud::GeneratePipelineService
.new(project, current_user, params).execute .new(project, current_user, params).execute
if generate_pipeline_response[:status] == :error if generate_pipeline_response[:status] == :error
track_event('deployments#cloud_run', 'generate_pipeline_error', generate_pipeline_response) track_event('deployments#cloud_run', 'error_generate_pipeline', generate_pipeline_response)
flash[:error] = 'Failed to generate pipeline' flash[:error] = 'Failed to generate pipeline'
redirect_to project_google_cloud_index_path(project) redirect_to project_google_cloud_deployments_path(project)
else else
cloud_run_mr_params = cloud_run_mr_params(generate_pipeline_response[:branch_name]) cloud_run_mr_params = cloud_run_mr_params(generate_pipeline_response[:branch_name])
track_event('deployments#cloud_run', 'cloud_run_success', cloud_run_mr_params) track_event('deployments#cloud_run', 'success', cloud_run_mr_params)
redirect_to project_new_merge_request_path(project, merge_request: cloud_run_mr_params) redirect_to project_new_merge_request_path(project, merge_request: cloud_run_mr_params)
end end
end end
rescue Google::Apis::ClientError => error rescue Google::Apis::ClientError, Google::Apis::ServerError, Google::Apis::AuthorizationError => error
handle_gcp_error('deployments#cloud_run', error) track_event('deployments#cloud_run', 'error_gcp', error)
flash[:warning] = _('Google Cloud Error - %{error}') % { error: error }
redirect_to project_google_cloud_deployments_path(project)
end end
def cloud_storage def cloud_storage

View File

@ -6,8 +6,10 @@ class Projects::GoogleCloud::GcpRegionsController < Projects::GoogleCloud::BaseC
# Source https://cloud.google.com/run/docs/locations 2022-01-30 # Source https://cloud.google.com/run/docs/locations 2022-01-30
AVAILABLE_REGIONS = %w[asia-east1 asia-northeast1 asia-southeast1 europe-north1 europe-west1 europe-west4 us-central1 us-east1 us-east4 us-west1].freeze AVAILABLE_REGIONS = %w[asia-east1 asia-northeast1 asia-southeast1 europe-north1 europe-west1 europe-west4 us-central1 us-east1 us-east4 us-west1].freeze
GCP_REGION_CI_VAR_KEY = 'GCP_REGION'
def index def index
@google_cloud_path = project_google_cloud_index_path(project) @google_cloud_path = project_google_cloud_configuration_path(project)
params = { per_page: 50 } params = { per_page: 50 }
branches = BranchesFinder.new(project.repository, params).execute(gitaly_pagination: true) branches = BranchesFinder.new(project.repository, params).execute(gitaly_pagination: true)
tags = TagsFinder.new(project.repository, params).execute(gitaly_pagination: true) tags = TagsFinder.new(project.repository, params).execute(gitaly_pagination: true)
@ -16,16 +18,16 @@ class Projects::GoogleCloud::GcpRegionsController < Projects::GoogleCloud::BaseC
screen: 'gcp_regions_form', screen: 'gcp_regions_form',
availableRegions: AVAILABLE_REGIONS, availableRegions: AVAILABLE_REGIONS,
refs: refs, refs: refs,
cancelPath: project_google_cloud_index_path(project) cancelPath: project_google_cloud_configuration_path(project)
} }
@js_data = js_data.to_json @js_data = js_data.to_json
track_event('gcp_regions#index', 'form_render', js_data) track_event('gcp_regions#index', 'success', js_data)
end end
def create def create
permitted_params = params.permit(:ref, :gcp_region) permitted_params = params.permit(:ref, :gcp_region)
response = GoogleCloud::GcpRegionAddOrReplaceService.new(project).execute(permitted_params[:ref], permitted_params[:gcp_region]) response = GoogleCloud::GcpRegionAddOrReplaceService.new(project).execute(permitted_params[:ref], permitted_params[:gcp_region])
track_event('gcp_regions#create', 'form_submit', response) track_event('gcp_regions#create', 'success', response)
redirect_to project_google_cloud_index_path(project), notice: _('GCP region configured') redirect_to project_google_cloud_configuration_path(project), notice: _('GCP region configured')
end end
end end

View File

@ -8,16 +8,15 @@ class Projects::GoogleCloud::RevokeOauthController < Projects::GoogleCloud::Base
response = google_api_client.revoke_authorizations response = google_api_client.revoke_authorizations
if response.success? if response.success?
status = 'success'
redirect_message = { notice: s_('GoogleCloud|Google OAuth2 token revocation requested') } redirect_message = { notice: s_('GoogleCloud|Google OAuth2 token revocation requested') }
track_event('revoke_oauth#create', 'success', response.to_json)
else else
status = 'failed'
redirect_message = { alert: s_('GoogleCloud|Google OAuth2 token revocation request failed') } redirect_message = { alert: s_('GoogleCloud|Google OAuth2 token revocation request failed') }
track_event('revoke_oauth#create', 'error', response.to_json)
end end
session.delete(GoogleApi::CloudPlatform::Client.session_key_for_token) session.delete(GoogleApi::CloudPlatform::Client.session_key_for_token)
track_event('revoke_oauth#create', 'create', status)
redirect_to project_google_cloud_index_path(project), redirect_message redirect_to project_google_cloud_configuration_path(project), redirect_message
end end
end end

View File

@ -4,14 +4,15 @@ class Projects::GoogleCloud::ServiceAccountsController < Projects::GoogleCloud::
before_action :validate_gcp_token! before_action :validate_gcp_token!
def index def index
@google_cloud_path = project_google_cloud_index_path(project) @google_cloud_path = project_google_cloud_configuration_path(project)
google_api_client = GoogleApi::CloudPlatform::Client.new(token_in_session, nil) google_api_client = GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
gcp_projects = google_api_client.list_projects gcp_projects = google_api_client.list_projects
if gcp_projects.empty? if gcp_projects.empty?
@js_data = { screen: 'no_gcp_projects' }.to_json @js_data = { screen: 'no_gcp_projects' }.to_json
track_event('service_accounts#index', 'form_error', 'no_gcp_projects') track_event('service_accounts#index', 'error_form', 'no_gcp_projects')
render status: :unauthorized, template: 'projects/google_cloud/errors/no_gcp_projects' flash[:warning] = _('No Google Cloud projects - You need at least one Google Cloud project')
redirect_to project_google_cloud_configuration_path(project)
else else
params = { per_page: 50 } params = { per_page: 50 }
branches = BranchesFinder.new(project.repository, params).execute(gitaly_pagination: true) branches = BranchesFinder.new(project.repository, params).execute(gitaly_pagination: true)
@ -21,14 +22,16 @@ class Projects::GoogleCloud::ServiceAccountsController < Projects::GoogleCloud::
screen: 'service_accounts_form', screen: 'service_accounts_form',
gcpProjects: gcp_projects, gcpProjects: gcp_projects,
refs: refs, refs: refs,
cancelPath: project_google_cloud_index_path(project) cancelPath: project_google_cloud_configuration_path(project)
} }
@js_data = js_data.to_json @js_data = js_data.to_json
track_event('service_accounts#index', 'form_success', js_data) track_event('service_accounts#index', 'success', js_data)
end end
rescue Google::Apis::ClientError => error rescue Google::Apis::ClientError, Google::Apis::ServerError, Google::Apis::AuthorizationError => error
handle_gcp_error('service_accounts#index', error) track_event('service_accounts#index', 'error_gcp', error)
flash[:warning] = _('Google Cloud Error - %{error}') % { error: error }
redirect_to project_google_cloud_configuration_path(project)
end end
def create def create
@ -42,9 +45,11 @@ class Projects::GoogleCloud::ServiceAccountsController < Projects::GoogleCloud::
environment_name: permitted_params[:ref] environment_name: permitted_params[:ref]
).execute ).execute
track_event('service_accounts#create', 'form_submit', response) track_event('service_accounts#create', 'success', response)
redirect_to project_google_cloud_index_path(project), notice: response.message redirect_to project_google_cloud_configuration_path(project), notice: response.message
rescue Google::Apis::ClientError, Google::Apis::ServerError, Google::Apis::AuthorizationError => error rescue Google::Apis::ClientError, Google::Apis::ServerError, Google::Apis::AuthorizationError => error
handle_gcp_error('service_accounts#create', error) track_event('service_accounts#create', 'error_gcp', error)
flash[:warning] = _('Google Cloud Error - %{error}') % { error: error }
redirect_to project_google_cloud_configuration_path(project)
end end
end end

View File

@ -1,34 +0,0 @@
# frozen_string_literal: true
class Projects::GoogleCloudController < Projects::GoogleCloud::BaseController
GCP_REGION_CI_VAR_KEY = 'GCP_REGION'
def index
js_data = {
screen: 'home',
serviceAccounts: GoogleCloud::ServiceAccountsService.new(project).find_for_project,
createServiceAccountUrl: project_google_cloud_service_accounts_path(project),
enableCloudRunUrl: project_google_cloud_deployments_cloud_run_path(project),
enableCloudStorageUrl: project_google_cloud_deployments_cloud_storage_path(project),
emptyIllustrationUrl: ActionController::Base.helpers.image_path('illustrations/pipelines_empty.svg'),
configureGcpRegionsUrl: project_google_cloud_gcp_regions_path(project),
gcpRegions: gcp_regions,
revokeOauthUrl: revoke_oauth_url
}
@js_data = js_data.to_json
track_event('google_cloud#index', 'index', js_data)
end
private
def gcp_regions
list = ::Ci::VariablesFinder.new(project, { key: GCP_REGION_CI_VAR_KEY }).execute
list.map { |variable| { gcp_region: variable.value, environment: variable.environment_scope } }
end
def revoke_oauth_url
google_token_valid = GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
.validate_token(expires_at_in_session)
google_token_valid ? project_google_cloud_revoke_oauth_index_path(project) : nil
end
end

View File

@ -35,7 +35,7 @@ module Projects
def validate_test_reports! def validate_test_reports!
unless pipeline.has_test_reports? unless pipeline.has_test_reports?
render json: { errors: 'Test report artifacts have expired' }, status: :not_found render json: { errors: 'Test report artifacts not found' }, status: :not_found
end end
end end

View File

@ -18,6 +18,7 @@ module Ci
"ci-config-path": project.ci_config_path_or_default, "ci-config-path": project.ci_config_path_or_default,
"ci-examples-help-page-path" => help_page_path('ci/examples/index'), "ci-examples-help-page-path" => help_page_path('ci/examples/index'),
"ci-help-page-path" => help_page_path('ci/index'), "ci-help-page-path" => help_page_path('ci/index'),
"ci-lint-path" => project_ci_lint_path(project),
"default-branch" => project.default_branch_or_main, "default-branch" => project.default_branch_or_main,
"empty-state-illustration-path" => image_path('illustrations/empty-state/empty-dag-md.svg'), "empty-state-illustration-path" => image_path('illustrations/empty-state/empty-dag-md.svg'),
"initial-branch-name" => initial_branch, "initial-branch-name" => initial_branch,

View File

@ -57,7 +57,22 @@ class CustomerRelations::Contact < ApplicationRecord
end end
def self.sort_by_name def self.sort_by_name
order("last_name ASC, first_name ASC") order(Gitlab::Pagination::Keyset::Order.build([
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'last_name',
order_expression: arel_table[:last_name].asc,
distinct: false
),
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'first_name',
order_expression: arel_table[:first_name].asc,
distinct: false
),
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'id',
order_expression: arel_table[:id].asc
)
]))
end end
def self.find_ids_by_emails(group, emails) def self.find_ids_by_emails(group, emails)

View File

@ -124,8 +124,24 @@ class Issue < ApplicationRecord
scope :order_due_date_desc, -> { reorder(arel_table[:due_date].desc.nulls_last) } scope :order_due_date_desc, -> { reorder(arel_table[:due_date].desc.nulls_last) }
scope :order_closest_future_date, -> { reorder(Arel.sql('CASE WHEN issues.due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - issues.due_date) ASC')) } scope :order_closest_future_date, -> { reorder(Arel.sql('CASE WHEN issues.due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - issues.due_date) ASC')) }
scope :order_created_at_desc, -> { reorder(created_at: :desc) } scope :order_created_at_desc, -> { reorder(created_at: :desc) }
scope :order_severity_asc, -> { includes(:issuable_severity).order('issuable_severities.severity ASC NULLS FIRST') } scope :order_severity_asc, -> do
scope :order_severity_desc, -> { includes(:issuable_severity).order('issuable_severities.severity DESC NULLS LAST') } build_keyset_order_on_joined_column(
scope: includes(:issuable_severity),
attribute_name: 'issuable_severities_severity',
column: IssuableSeverity.arel_table[:severity],
direction: :asc,
nullable: :nulls_first
)
end
scope :order_severity_desc, -> do
build_keyset_order_on_joined_column(
scope: includes(:issuable_severity),
attribute_name: 'issuable_severities_severity',
column: IssuableSeverity.arel_table[:severity],
direction: :desc,
nullable: :nulls_last
)
end
scope :order_escalation_status_asc, -> { includes(:incident_management_issuable_escalation_status).order(IncidentManagement::IssuableEscalationStatus.arel_table[:status].asc.nulls_last).references(:incident_management_issuable_escalation_status) } scope :order_escalation_status_asc, -> { includes(:incident_management_issuable_escalation_status).order(IncidentManagement::IssuableEscalationStatus.arel_table[:status].asc.nulls_last).references(:incident_management_issuable_escalation_status) }
scope :order_escalation_status_desc, -> { includes(:incident_management_issuable_escalation_status).order(IncidentManagement::IssuableEscalationStatus.arel_table[:status].desc.nulls_last).references(:incident_management_issuable_escalation_status) } scope :order_escalation_status_desc, -> { includes(:incident_management_issuable_escalation_status).order(IncidentManagement::IssuableEscalationStatus.arel_table[:status].desc.nulls_last).references(:incident_management_issuable_escalation_status) }
scope :order_closed_at_asc, -> { reorder(arel_table[:closed_at].asc.nulls_last) } scope :order_closed_at_asc, -> { reorder(arel_table[:closed_at].asc.nulls_last) }
@ -234,6 +250,31 @@ class Issue < ApplicationRecord
alias_method :with_state, :with_state_id alias_method :with_state, :with_state_id
alias_method :with_states, :with_state_ids alias_method :with_states, :with_state_ids
def build_keyset_order_on_joined_column(scope:, attribute_name:, column:, direction:, nullable:)
reversed_direction = direction == :asc ? :desc : :asc
# rubocop: disable GitlabSecurity/PublicSend
order = ::Gitlab::Pagination::Keyset::Order.build([
::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: attribute_name,
column_expression: column,
order_expression: column.send(direction).send(nullable),
reversed_order_expression: column.send(reversed_direction).send(nullable),
order_direction: direction,
distinct: false,
add_to_projections: true,
nullable: nullable
),
::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'id',
order_expression: arel_table['id'].desc
)
])
# rubocop: enable GitlabSecurity/PublicSend
order.apply_cursor_conditions(scope).order(order)
end
override :order_upvotes_desc override :order_upvotes_desc
def order_upvotes_desc def order_upvotes_desc
reorder(upvotes_count: :desc) reorder(upvotes_count: :desc)
@ -331,10 +372,10 @@ class Issue < ApplicationRecord
when 'due_date', 'due_date_asc' then order_due_date_asc.with_order_id_desc when 'due_date', 'due_date_asc' then order_due_date_asc.with_order_id_desc
when 'due_date_desc' then order_due_date_desc.with_order_id_desc when 'due_date_desc' then order_due_date_desc.with_order_id_desc
when 'relative_position', 'relative_position_asc' then order_by_relative_position when 'relative_position', 'relative_position_asc' then order_by_relative_position
when 'severity_asc' then order_severity_asc.with_order_id_desc when 'severity_asc' then order_severity_asc
when 'severity_desc' then order_severity_desc.with_order_id_desc when 'severity_desc' then order_severity_desc
when 'escalation_status_asc' then order_escalation_status_asc.with_order_id_desc when 'escalation_status_asc' then order_escalation_status_asc
when 'escalation_status_desc' then order_escalation_status_desc.with_order_id_desc when 'escalation_status_desc' then order_escalation_status_desc
when 'closed_at', 'closed_at_asc' then order_closed_at_asc when 'closed_at', 'closed_at_asc' then order_closed_at_asc
when 'closed_at_desc' then order_closed_at_desc when 'closed_at_desc' then order_closed_at_desc
else else

View File

@ -5,6 +5,8 @@ class ProjectSetting < ApplicationRecord
belongs_to :project, inverse_of: :project_setting belongs_to :project, inverse_of: :project_setting
scope :for_projects, ->(projects) { where(project_id: projects) }
enum squash_option: { enum squash_option: {
never: 0, never: 0,
always: 1, always: 1,

View File

@ -3,7 +3,7 @@
module GoogleCloud module GoogleCloud
class GcpRegionAddOrReplaceService < ::GoogleCloud::BaseService class GcpRegionAddOrReplaceService < ::GoogleCloud::BaseService
def execute(environment, region) def execute(environment, region)
gcp_region_key = Projects::GoogleCloudController::GCP_REGION_CI_VAR_KEY gcp_region_key = Projects::GoogleCloud::GcpRegionsController::GCP_REGION_CI_VAR_KEY
change_params = { variable_params: { key: gcp_region_key, value: region, environment_scope: environment } } change_params = { variable_params: { key: gcp_region_key, value: region, environment_scope: environment } }
filter_params = { key: gcp_region_key, filter: { environment_scope: environment } } filter_params = { key: gcp_region_key, filter: { environment_scope: environment } }

View File

@ -162,6 +162,12 @@ module Groups
projects_to_update projects_to_update
.update_all(visibility_level: @new_parent_group.visibility_level) .update_all(visibility_level: @new_parent_group.visibility_level)
update_project_settings(@updated_project_ids)
end
# Overridden in EE
def update_project_settings(updated_project_ids)
end end
def update_two_factor_authentication def update_two_factor_authentication

View File

@ -3,5 +3,5 @@
%p= _("There are no projects shared with this group yet") %p= _("There are no projects shared with this group yet")
%ul.content-list{ data: { hide_projects: 'false', group_id: group.id, path: group_path(group) } } %ul.content-list{ data: { hide_projects: 'false', group_id: group.id, path: group_path(group) } }
.js-groups-list-holder .js-groups-list-holder{ data: { current_group_visibility: group.visibility } }
= gl_loading_icon = gl_loading_icon

View File

@ -0,0 +1,7 @@
- add_to_breadcrumbs _('Google Cloud'), @google_cloud_path
- breadcrumb_title s_('CloudSeed|Configuration')
- page_title s_('CloudSeed|Configuration')
- @content_class = "limit-container-width" unless fluid_layout
#js-google-cloud-configuration{ data: @js_data }

View File

@ -0,0 +1,7 @@
- add_to_breadcrumbs _('Google Cloud'), @google_cloud_path
- breadcrumb_title s_('CloudSeed|Databases')
- page_title s_('CloudSeed|Databases')
- @content_class = "limit-container-width" unless fluid_layout
#js-google-cloud-databases{ data: @js_data }

View File

@ -0,0 +1,7 @@
- add_to_breadcrumbs _('Google Cloud'), @google_cloud_path
- breadcrumb_title s_('CloudSeed|Deployments')
- page_title s_('CloudSeed|Deployments')
- @content_class = "limit-container-width" unless fluid_layout
#js-google-cloud-deployments{ data: @js_data }

View File

@ -1,6 +0,0 @@
- breadcrumb_title _('Google Cloud')
- page_title _('Google Cloud')
- @content_class = "limit-container-width" unless fluid_layout
#js-google-cloud{ data: @js_data }

View File

@ -1,6 +0,0 @@
- breadcrumb_title _('Google Cloud')
- page_title _('Google Cloud')
- @content_class = "limit-container-width" unless fluid_layout
#js-google-cloud{ data: @js_data }

View File

@ -1,8 +1,8 @@
- add_to_breadcrumbs _('Google Cloud'), @google_cloud_path - add_to_breadcrumbs _('Google Cloud'), @google_cloud_path
- breadcrumb_title _('Regions') - breadcrumb_title _('CloudSeed|Regions')
- page_title _('Regions') - page_title s_('CloudSeed|Regions')
- @content_class = "limit-container-width" unless fluid_layout - @content_class = "limit-container-width" unless fluid_layout
= form_tag project_google_cloud_gcp_regions_path(@project), method: 'post' do = form_tag project_google_cloud_gcp_regions_path(@project), method: 'post' do
#js-google-cloud{ data: @js_data } #js-google-cloud-gcp-regions{ data: @js_data }

View File

@ -1,6 +0,0 @@
- breadcrumb_title _('Google Cloud')
- page_title _('Google Cloud')
- @content_class = "limit-container-width" unless fluid_layout
#js-google-cloud{ data: @js_data }

View File

@ -1,8 +1,8 @@
- add_to_breadcrumbs _('Google Cloud'), @google_cloud_path - add_to_breadcrumbs _('Google Cloud'), @google_cloud_path
- breadcrumb_title _('Service Account') - breadcrumb_title s_('CloudSeed|Service Account')
- page_title _('Service Account') - page_title s_('CloudSeed|Service Account')
- @content_class = "limit-container-width" unless fluid_layout - @content_class = "limit-container-width" unless fluid_layout
= form_tag project_google_cloud_service_accounts_path(@project), method: 'post' do = form_tag project_google_cloud_service_accounts_path(@project), method: 'post' do
#js-google-cloud{ data: @js_data } #js-google-cloud-service-accounts{ data: @js_data }

View File

@ -1,8 +1,8 @@
--- ---
name: omniauth_login_minimal_scopes name: enforce_memory_watchdog
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78556 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91910
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/351331 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/367534
milestone: '14.8' milestone: '15.2'
type: development type: ops
group: 'group::authentication and authorization' group: group::memory
default_enabled: false default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: gitlab_memory_watchdog
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91910
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/367534
milestone: '15.2'
type: ops
group: group::memory
default_enabled: false

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
return unless Gitlab::Runtime.application?
return unless Gitlab::Utils.to_boolean(ENV['GITLAB_MEMORY_WATCHDOG_ENABLED'])
Gitlab::Cluster::LifecycleEvents.on_worker_start do
handler =
if Gitlab::Runtime.puma?
Gitlab::Memory::Watchdog::PumaHandler.new
elsif Gitlab::Runtime.sidekiq?
Gitlab::Memory::Watchdog::TermProcessHandler.new
else
Gitlab::Memory::Watchdog::NullHandler.instance
end
Gitlab::Memory::Watchdog.new(
handler: handler, logger: Gitlab::AppLogger
).start
end

View File

@ -298,15 +298,18 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resources :terraform, only: [:index] resources :terraform, only: [:index]
resources :google_cloud, only: [:index]
namespace :google_cloud do namespace :google_cloud do
get '/configuration', to: 'configuration#index'
resources :revoke_oauth, only: [:create] resources :revoke_oauth, only: [:create]
resources :service_accounts, only: [:index, :create] resources :service_accounts, only: [:index, :create]
resources :gcp_regions, only: [:index, :create] resources :gcp_regions, only: [:index, :create]
get '/deployments', to: 'deployments#index'
get '/deployments/cloud_run', to: 'deployments#cloud_run' get '/deployments/cloud_run', to: 'deployments#cloud_run'
get '/deployments/cloud_storage', to: 'deployments#cloud_storage' get '/deployments/cloud_storage', to: 'deployments#cloud_storage'
get '/databases', to: 'databases#index'
end end
resources :environments, except: [:destroy] do resources :environments, except: [:destroy] do

View File

@ -117,10 +117,9 @@ signed in.
## Reduce access privileges on sign in ## Reduce access privileges on sign in
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/337663) in GitLab 14.8 [with a flag](../administration/feature_flags.md) named `omniauth_login_minimal_scopes`. Disabled by default. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/337663) in GitLab 14.8 [with a flag](../administration/feature_flags.md) named `omniauth_login_minimal_scopes`. Disabled by default.
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/351331) in GitLab 14.9.
FLAG: > - [Feature flag `omniauth_login_minimal_scopes`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/83453) removed in GitLab 15.2
On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `omniauth_login_minimal_scopes`. On GitLab.com, this feature is not available.
If you use a GitLab instance for authentication, you can reduce access rights when an OAuth application is used for sign in. If you use a GitLab instance for authentication, you can reduce access rights when an OAuth application is used for sign in.

View File

@ -64,7 +64,7 @@ Use CI/CD environment variables to configure your project.
1. On the left sidebar, select **Settings > CI/CD**. 1. On the left sidebar, select **Settings > CI/CD**.
1. Expand **Variables**. 1. Expand **Variables**.
1. Set the variable `BASE64_CIVO_CREDENTIALS` to the [token](https://www.civo.com/account/security) from your Civo account. 1. Set the variable `BASE64_CIVO_TOKEN` to the [token](https://www.civo.com/account/security) from your Civo account.
1. Set the variable `TF_VAR_agent_token` to the agent token you received in the previous task. 1. Set the variable `TF_VAR_agent_token` to the agent token you received in the previous task.
1. Set the variable `TF_VAR_kas_address` to the agent server address in the previous task. 1. Set the variable `TF_VAR_kas_address` to the agent server address in the previous task.
@ -78,8 +78,8 @@ contains other variables that you can override according to your needs:
- `TF_VAR_civo_region`: Set your cluster's region. - `TF_VAR_civo_region`: Set your cluster's region.
- `TF_VAR_cluster_name`: Set your cluster's name. - `TF_VAR_cluster_name`: Set your cluster's name.
- `TF_VAR_cluster_description`: Set a description for the cluster. To create a reference to your GitLab project on your Civo cluster detail page, set this value to `$CI_PROJECT_URL`. This value helps you determine which project was responsible for provisioning the cluster you see on the Civo dashboard. - `TF_VAR_cluster_description`: Set a description for the cluster. To create a reference to your GitLab project on your Civo cluster detail page, set this value to `$CI_PROJECT_URL`. This value helps you determine which project was responsible for provisioning the cluster you see on the Civo dashboard.
- `TF_VAR_machine_type`: Set the machine type for the Kubernetes nodes. - `TF_VAR_target_nodes_size`: Set the size of the nodes to use for the cluster
- `TF_VAR_node_count`: Set the number of Kubernetes nodes. - `TF_VAR_num_target_nodes`: Set the number of Kubernetes nodes.
- `TF_VAR_agent_version`: Set the version of the GitLab agent. - `TF_VAR_agent_version`: Set the version of the GitLab agent.
- `TF_VAR_agent_namespace`: Set the Kubernetes namespace for the GitLab agent. - `TF_VAR_agent_namespace`: Set the Kubernetes namespace for the GitLab agent.

View File

@ -10,11 +10,57 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/13294) in GitLab 12.0. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/13294) in GitLab 12.0.
> - Moved to GitLab Free. > - Moved to GitLab Free.
NOTE: ## Namespace storage limit
Free tier namespaces on GitLab SaaS have a 5GB storage limit. This limit is not visible on the storage quota page nor currently enforced for users who exceed the limit. To learn more, visit our [pricing page](https://about.gitlab.com/pricing/).
A project's repository has a free storage quota of 10 GB. When a project's repository reaches Namespaces on a GitLab SaaS Free tier have a 5 GB storage limit. For more information, see our [pricing page](https://about.gitlab.com/pricing/).
the quota it is locked. You cannot push changes to a locked project. To monitor the size of each This limit is not visible on the storage quota page, but we plan to make it visible and enforced starting October 19, 2022.
Storage types that add to the total namespace storage are:
- Git repository
- Git LFS
- Artifacts
- Container registry
- Package registry
- Dependecy proxy
- Wiki
- Snippets
If your total namespace storage exceeds the available namespace storage quota, all projects under the namespace are locked. A locked project will not be able to push to the repository, run pipelines and jobs, or build and push packages.
To prevent exceeding the namespace storage quota, you can:
1. [Purchase more storage](../subscriptions/gitlab_com/index.md#purchase-more-storage-and-transfer).
1. [Upgrade to a paid tier](../subscriptions/gitlab_com/#upgrade-your-gitlab-saas-subscription-tier).
1. [Reduce storage usage](#manage-your-storage-usage).
### Namespace storage limit enforcement schedule
Starting October 19, 2022, a storage limit will be enforced on all GitLab Free namespaces.
We will start with a large limit enforcement and eventually reduce it to 5 GB.
The following table describes the enforcement schedule, which is subject to change.
| Target enforcement date | Limit | Expected Impact | Status |
| ------ | ------ | ------ | ------ |
| October 19, 2022 | 45,000 GB | LOW | Pending (**{hourglass}**)|
| October 20, 2022 | 7,500 GB | LOW | Pending (**{hourglass}**)|
| October 24, 2022 | 500 GB | MEDIUM | Pending (**{hourglass}**)|
| October 27, 2022 | 75 GB | MEDIUM HIGH | Pending (**{hourglass}**)|
| November 2, 2022 | 10 GB | HIGH | Pending (**{hourglass}**)|
| November 9, 2022 | 5 GB | VERY HIGH | Pending (**{hourglass}**)|
Namespaces that reach the enforced limit will have their projects locked. To unlock your project, you will have to [manage its storage](#manage-your-storage-usage).
### Project storage limit
Namespaces on a GitLab SaaS **paid** tier (Premium and Ultimate) have a storage limit on their project repositories.
A project's repository has a storage quota of 10 GB. A namespace has either a namespace-level storage limit or a project-level storage limit, but not both.
- Paid tier namespaces have project-level storage limits enforced.
- Free tier namespaces have namespace-level storage limits.
When a project's repository reaches the quota, the project is locked. You cannot push changes to a locked project. To monitor the size of each
repository in a namespace, including a breakdown for each project, you can repository in a namespace, including a breakdown for each project, you can
[view storage usage](#view-storage-usage). To allow a project's repository to exceed the free quota [view storage usage](#view-storage-usage). To allow a project's repository to exceed the free quota
you must purchase additional storage. For more details, see [Excess storage usage](#excess-storage-usage). you must purchase additional storage. For more details, see [Excess storage usage](#excess-storage-usage).

View File

@ -0,0 +1,192 @@
# frozen_string_literal: true
module Gitlab
module Memory
# A background thread that observes Ruby heap fragmentation and calls
# into a handler when the Ruby heap has been fragmented for an extended
# period of time.
#
# See Gitlab::Metrics::Memory for how heap fragmentation is defined.
#
# To decide whether a given fragmentation level is being exceeded,
# the watchdog regularly polls the GC. Whenever a violation occurs
# a strike is issued. If the maximum number of strikes are reached,
# a handler is invoked to deal with the situation.
#
# The duration for which a process may be above a given fragmentation
# threshold is computed as `max_strikes * sleep_time_seconds`.
class Watchdog < Daemon
DEFAULT_SLEEP_TIME_SECONDS = 60
DEFAULT_HEAP_FRAG_THRESHOLD = 0.5
DEFAULT_MAX_STRIKES = 5
# This handler does nothing. It returns `false` to indicate to the
# caller that the situation has not been dealt with so it will
# receive calls repeatedly if fragmentation remains high.
#
# This is useful for "dress rehearsals" in production since it allows
# us to observe how frequently the handler is invoked before taking action.
class NullHandler
include Singleton
def on_high_heap_fragmentation(value)
# NOP
false
end
end
# This handler sends SIGTERM and considers the situation handled.
class TermProcessHandler
def initialize(pid = $$)
@pid = pid
end
def on_high_heap_fragmentation(value)
Process.kill(:TERM, @pid)
true
end
end
# This handler invokes Puma's graceful termination handler, which takes
# into account a configurable grace period during which a process may
# remain unresponsive to a SIGTERM.
class PumaHandler
def initialize(puma_options = ::Puma.cli_config.options)
@worker = ::Puma::Cluster::WorkerHandle.new(0, $$, 0, puma_options)
end
def on_high_heap_fragmentation(value)
@worker.term
true
end
end
# max_heap_fragmentation:
# The degree to which the Ruby heap is allowed to be fragmented. Range [0,1].
# max_strikes:
# How many times the process is allowed to be above max_heap_fragmentation before
# a handler is invoked.
# sleep_time_seconds:
# Used to control the frequency with which the watchdog will wake up and poll the GC.
def initialize(
handler: NullHandler.instance,
logger: Logger.new($stdout),
max_heap_fragmentation: ENV['GITLAB_MEMWD_MAX_HEAP_FRAG']&.to_f || DEFAULT_HEAP_FRAG_THRESHOLD,
max_strikes: ENV['GITLAB_MEMWD_MAX_STRIKES']&.to_i || DEFAULT_MAX_STRIKES,
sleep_time_seconds: ENV['GITLAB_MEMWD_SLEEP_TIME_SEC']&.to_i || DEFAULT_SLEEP_TIME_SECONDS,
**options)
super(**options)
@handler = handler
@logger = logger
@max_heap_fragmentation = max_heap_fragmentation
@sleep_time_seconds = sleep_time_seconds
@max_strikes = max_strikes
@alive = true
@strikes = 0
init_prometheus_metrics(max_heap_fragmentation)
end
attr_reader :strikes, :max_heap_fragmentation, :max_strikes, :sleep_time_seconds
def run_thread
@logger.info(log_labels.merge(message: 'started'))
while @alive
sleep(@sleep_time_seconds)
monitor_heap_fragmentation if Feature.enabled?(:gitlab_memory_watchdog, type: :ops)
end
@logger.info(log_labels.merge(message: 'stopped'))
end
private
def monitor_heap_fragmentation
heap_fragmentation = Gitlab::Metrics::Memory.gc_heap_fragmentation
if heap_fragmentation > @max_heap_fragmentation
@strikes += 1
@heap_frag_violations.increment
else
@strikes = 0
end
if @strikes > @max_strikes
# If the handler returns true, it means the event is handled and we can shut down.
@alive = !handle_heap_fragmentation_limit_exceeded(heap_fragmentation)
@strikes = 0
end
end
def handle_heap_fragmentation_limit_exceeded(value)
@logger.warn(
log_labels.merge(
message: 'heap fragmentation limit exceeded',
memwd_cur_heap_frag: value
))
@heap_frag_violations_handled.increment
handler.on_high_heap_fragmentation(value)
end
def handler
# This allows us to keep the watchdog running but turn it into "friendly mode" where
# all that happens is we collect logs and Prometheus events for fragmentation violations.
return NullHandler.instance unless Feature.enabled?(:enforce_memory_watchdog, type: :ops)
@handler
end
def stop_working
@alive = false
end
def log_labels
{
pid: $$,
worker_id: worker_id,
memwd_handler_class: handler.class.name,
memwd_sleep_time_s: @sleep_time_seconds,
memwd_max_heap_frag: @max_heap_fragmentation,
memwd_max_strikes: @max_strikes,
memwd_cur_strikes: @strikes,
memwd_rss_bytes: process_rss_bytes
}
end
def worker_id
::Prometheus::PidProvider.worker_id
end
def process_rss_bytes
Gitlab::Metrics::System.memory_usage_rss
end
def init_prometheus_metrics(max_heap_fragmentation)
default_labels = { pid: worker_id }
@heap_frag_limit = Gitlab::Metrics.gauge(
:gitlab_memwd_heap_frag_limit,
'The configured limit for how fragmented the Ruby heap is allowed to be',
default_labels
)
@heap_frag_limit.set({}, max_heap_fragmentation)
@heap_frag_violations = Gitlab::Metrics.counter(
:gitlab_memwd_heap_frag_violations_total,
'Total number of times heap fragmentation in a Ruby process exceeded its allowed maximum',
default_labels
)
@heap_frag_violations_handled = Gitlab::Metrics.counter(
:gitlab_memwd_heap_frag_violations_handled_total,
'Total number of times heap fragmentation violations in a Ruby process were handled',
default_labels
)
end
end
end
end

View File

@ -88,8 +88,14 @@ module Sidebars
::Sidebars::MenuItem.new( ::Sidebars::MenuItem.new(
title: _('Google Cloud'), title: _('Google Cloud'),
link: project_google_cloud_index_path(context.project), link: project_google_cloud_configuration_path(context.project),
active_routes: { controller: [:google_cloud, :service_accounts, :deployments, :gcp_regions] }, active_routes: { controller: [
:configuration,
:service_accounts,
:databases,
:deployments,
:gcp_regions
] },
item_id: :google_cloud item_id: :google_cloud
) )
end end

View File

@ -8318,6 +8318,9 @@ msgstr ""
msgid "CloudSeed|CloudSQL Instance" msgid "CloudSeed|CloudSQL Instance"
msgstr "" msgstr ""
msgid "CloudSeed|Configuration"
msgstr ""
msgid "CloudSeed|Create cluster" msgid "CloudSeed|Create cluster"
msgstr "" msgstr ""
@ -8336,6 +8339,12 @@ msgstr ""
msgid "CloudSeed|Database version" msgid "CloudSeed|Database version"
msgstr "" msgstr ""
msgid "CloudSeed|Databases"
msgstr ""
msgid "CloudSeed|Deployments"
msgstr ""
msgid "CloudSeed|Description" msgid "CloudSeed|Description"
msgstr "" msgstr ""
@ -8393,12 +8402,18 @@ msgstr ""
msgid "CloudSeed|Refs" msgid "CloudSeed|Refs"
msgstr "" msgstr ""
msgid "CloudSeed|Regions"
msgstr ""
msgid "CloudSeed|Scalable, secure, and highly available in-memory service for Redis" msgid "CloudSeed|Scalable, secure, and highly available in-memory service for Redis"
msgstr "" msgstr ""
msgid "CloudSeed|Service" msgid "CloudSeed|Service"
msgstr "" msgstr ""
msgid "CloudSeed|Service Account"
msgstr ""
msgid "CloudSeed|Services" msgid "CloudSeed|Services"
msgstr "" msgstr ""
@ -10662,9 +10677,6 @@ msgstr ""
msgid "Create %{workspace} label" msgid "Create %{workspace} label"
msgstr "" msgstr ""
msgid "Create Google Cloud project"
msgstr ""
msgid "Create New Directory" msgid "Create New Directory"
msgstr "" msgstr ""
@ -17494,9 +17506,6 @@ msgstr ""
msgid "GitLab account request rejected" msgid "GitLab account request rejected"
msgstr "" msgstr ""
msgid "GitLab and Google Cloud configuration seems to be incomplete. This probably can be fixed by your GitLab administration team. You may share these logs with them:"
msgstr ""
msgid "GitLab commit" msgid "GitLab commit"
msgstr "" msgstr ""
@ -17983,18 +17992,15 @@ msgstr ""
msgid "Google Cloud" msgid "Google Cloud"
msgstr "" msgstr ""
msgid "Google Cloud Error - %{error}"
msgstr ""
msgid "Google Cloud Project" msgid "Google Cloud Project"
msgstr "" msgstr ""
msgid "Google Cloud authorizations required" msgid "Google Cloud authorizations required"
msgstr "" msgstr ""
msgid "Google Cloud project misconfigured"
msgstr ""
msgid "Google Cloud project required"
msgstr ""
msgid "GoogleCloud|Cancel" msgid "GoogleCloud|Cancel"
msgstr "" msgstr ""
@ -23204,6 +23210,9 @@ msgstr ""
msgid "Less Details" msgid "Less Details"
msgstr "" msgstr ""
msgid "Less restrictive visibility"
msgstr ""
msgid "Let's Encrypt does not accept emails on example.com" msgid "Let's Encrypt does not accept emails on example.com"
msgstr "" msgstr ""
@ -25982,6 +25991,9 @@ msgstr ""
msgid "No Epic" msgid "No Epic"
msgstr "" msgstr ""
msgid "No Google Cloud projects - You need at least one Google Cloud project"
msgstr ""
msgid "No Matching Results" msgid "No Matching Results"
msgstr "" msgstr ""
@ -28378,6 +28390,9 @@ msgstr ""
msgid "PipelineEditorTutorial|🚀 Run your first pipeline" msgid "PipelineEditorTutorial|🚀 Run your first pipeline"
msgstr "" msgstr ""
msgid "PipelineEditor|Configuration content has changed. Re-run validation for updated results."
msgstr ""
msgid "PipelineEditor|Current content in the Edit tab will be used for the simulation." msgid "PipelineEditor|Current content in the Edit tab will be used for the simulation."
msgstr "" msgstr ""
@ -28396,6 +28411,15 @@ msgstr ""
msgid "PipelineEditor|Pipeline behavior will be simulated including the %{codeStart}rules%{codeEnd} %{codeStart}only%{codeEnd} %{codeStart}except%{codeEnd} and %{codeStart}needs%{codeEnd} job dependencies. %{linkStart}Learn more%{linkEnd}" msgid "PipelineEditor|Pipeline behavior will be simulated including the %{codeStart}rules%{codeEnd} %{codeStart}only%{codeEnd} %{codeStart}except%{codeEnd} and %{codeStart}needs%{codeEnd} job dependencies. %{linkStart}Learn more%{linkEnd}"
msgstr "" msgstr ""
msgid "PipelineEditor|Pipeline simulation completed with errors"
msgstr ""
msgid "PipelineEditor|Simulated a %{codeStart}git push%{codeEnd} event for a default branch. %{codeStart}Rules%{codeEnd}, %{codeStart}only%{codeEnd}, %{codeStart}except%{codeEnd}, and %{codeStart}needs%{codeEnd} job dependencies logic have been evaluated. %{linkStart}Learn more%{linkEnd}"
msgstr ""
msgid "PipelineEditor|Simulation completed successfully"
msgstr ""
msgid "PipelineEditor|The CI/CD configuration is continuously validated. Errors and warnings are displayed when the CI/CD configuration file is not empty." msgid "PipelineEditor|The CI/CD configuration is continuously validated. Errors and warnings are displayed when the CI/CD configuration file is not empty."
msgstr "" msgstr ""
@ -28417,6 +28441,12 @@ msgstr ""
msgid "PipelineEditor|Validate pipeline under simulated conditions" msgid "PipelineEditor|Validate pipeline under simulated conditions"
msgstr "" msgstr ""
msgid "PipelineEditor|Validating pipeline... It can take up to a minute."
msgstr ""
msgid "PipelineEditor|Waiting for CI content to load..."
msgstr ""
msgid "PipelineScheduleIntervalPattern|Custom (%{linkStart}Cron syntax%{linkEnd})" msgid "PipelineScheduleIntervalPattern|Custom (%{linkStart}Cron syntax%{linkEnd})"
msgstr "" msgstr ""
@ -28696,6 +28726,9 @@ msgstr ""
msgid "Pipelines|Pipeline Editor" msgid "Pipelines|Pipeline Editor"
msgstr "" msgstr ""
msgid "Pipelines|Pipeline syntax is correct."
msgstr ""
msgid "Pipelines|Project cache successfully reset." msgid "Pipelines|Project cache successfully reset."
msgstr "" msgstr ""
@ -30151,6 +30184,9 @@ msgstr ""
msgid "Project uploads" msgid "Project uploads"
msgstr "" msgstr ""
msgid "Project visibility level is less restrictive than the group settings."
msgstr ""
msgid "Project visibility level will be changed to match namespace rules when transferring to a group." msgid "Project visibility level will be changed to match namespace rules when transferring to a group."
msgstr "" msgstr ""
@ -35588,9 +35624,6 @@ msgstr ""
msgid "ServicePing|Turn on service ping to review instance-level analytics." msgid "ServicePing|Turn on service ping to review instance-level analytics."
msgstr "" msgstr ""
msgid "Services"
msgstr ""
msgid "Session ID" msgid "Session ID"
msgstr "" msgstr ""
@ -41343,9 +41376,6 @@ msgstr ""
msgid "Unknown response text" msgid "Unknown response text"
msgstr "" msgstr ""
msgid "Unknown screen"
msgstr ""
msgid "Unknown user" msgid "Unknown user"
msgstr "" msgstr ""
@ -44417,9 +44447,6 @@ msgstr ""
msgid "You currently have more than %{free_limit} members across all your personal projects. From June 22, 2022, the %{free_limit} most recently active members will remain active, and the remaining members will get a %{link_start}status of Over limit%{link_end} and lose access. To view and manage members, check the members page for each project in your namespace. We recommend you %{move_link_start}move your project to a group%{move_link_end} so you can easily manage users and features." msgid "You currently have more than %{free_limit} members across all your personal projects. From June 22, 2022, the %{free_limit} most recently active members will remain active, and the remaining members will get a %{link_start}status of Over limit%{link_end} and lose access. To view and manage members, check the members page for each project in your namespace. We recommend you %{move_link_start}move your project to a group%{move_link_end} so you can easily manage users and features."
msgstr "" msgstr ""
msgid "You do not have any Google Cloud projects. Please create a Google Cloud project and then reload this page."
msgstr ""
msgid "You do not have any subscriptions yet" msgid "You do not have any subscriptions yet"
msgstr "" msgstr ""

View File

@ -50,7 +50,7 @@ module QA
it 'shows valid validations', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/349128' do it 'shows valid validations', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/349128' do
Page::Project::PipelineEditor::Show.perform do |show| Page::Project::PipelineEditor::Show.perform do |show|
aggregate_failures do aggregate_failures do
expect(show.ci_syntax_validate_message).to have_content('CI configuration is valid') expect(show.ci_syntax_validate_message).to have_content('Pipeline syntax is correct')
show.go_to_visualize_tab show.go_to_visualize_tab
{ stage1: 'job1', stage2: 'job2' }.each_pair do |stage, job| { stage1: 'job1', stage2: 'job2' }.each_pair do |stage, job|

View File

@ -248,6 +248,7 @@ function download_chart() {
helm repo add gitlab https://charts.gitlab.io helm repo add gitlab https://charts.gitlab.io
echoinfo "Building the gitlab chart's dependencies..." echoinfo "Building the gitlab chart's dependencies..."
helm dependency build "gitlab-${GITLAB_HELM_CHART_REF}"
} }
function base_config_changed() { function base_config_changed() {

View File

@ -64,7 +64,7 @@ RSpec.describe Projects::Pipelines::TestsController do
get_tests_show_json(build_ids) get_tests_show_json(build_ids)
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['errors']).to eq('Test report artifacts have expired') expect(json_response['errors']).to eq('Test report artifacts not found')
end end
end end

View File

@ -97,6 +97,31 @@ RSpec.describe 'Group show page' do
end end
end end
context 'when a public project is shared with a private group' do
let_it_be(:private_group) { create(:group, :private) }
let_it_be(:public_project) { create(:project, :public) }
let_it_be(:project_group_link) { create(:project_group_link, group: private_group, project: public_project) }
before do
private_group.add_owner(user)
sign_in(user)
end
it 'shows warning popover', :js do
visit group_path(private_group)
click_link _('Shared projects')
wait_for_requests
page.within("[data-testid=\"group-overview-item-#{public_project.id}\"]") do
click_button _('Less restrictive visibility')
end
expect(page).to have_content _('Project visibility level is less restrictive than the group settings.')
end
end
context 'when user does not have permissions to create new subgroups or projects', :js do context 'when user does not have permissions to create new subgroups or projects', :js do
before do before do
group.add_reporter(user) group.add_reporter(user)

View File

@ -1,5 +1,16 @@
import { mockJobs } from 'jest/pipeline_editor/mock_data'; import { mockJobs } from 'jest/pipeline_editor/mock_data';
export const mockLintDataError = {
data: {
lintCI: {
errors: ['Error message'],
warnings: ['Warning message'],
valid: false,
jobs: mockJobs,
},
},
};
export const mockLintDataValid = { export const mockLintDataValid = {
data: { data: {
lintCI: { lintCI: {

View File

@ -1,77 +0,0 @@
import { shallowMount } from '@vue/test-utils';
import { mapValues } from 'lodash';
import App from '~/google_cloud/components/app.vue';
import Home from '~/google_cloud/components/home.vue';
import IncubationBanner from '~/google_cloud/components/incubation_banner.vue';
import ServiceAccountsForm from '~/google_cloud/components/service_accounts_form.vue';
import GcpError from '~/google_cloud/components/errors/gcp_error.vue';
import NoGcpProjects from '~/google_cloud/components/errors/no_gcp_projects.vue';
const BASE_FEEDBACK_URL =
'https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/feedback/-/issues/new';
const SCREEN_COMPONENTS = {
Home,
ServiceAccountsForm,
GcpError,
NoGcpProjects,
};
const SERVICE_ACCOUNTS_FORM_PROPS = {
gcpProjects: [1, 2, 3],
refs: [4, 5, 6],
cancelPath: '',
};
const HOME_PROPS = {
serviceAccounts: [{}, {}],
gcpRegions: [{}, {}],
createServiceAccountUrl: '#url-create-service-account',
configureGcpRegionsUrl: '#url-configure-gcp-regions',
emptyIllustrationUrl: '#url-empty-illustration',
enableCloudRunUrl: '#url-enable-cloud-run',
enableCloudStorageUrl: '#enableCloudStorageUrl',
revokeOauthUrl: '#revokeOauthUrl',
};
describe('google_cloud App component', () => {
let wrapper;
const findIncubationBanner = () => wrapper.findComponent(IncubationBanner);
afterEach(() => {
wrapper.destroy();
});
describe.each`
screen | extraProps | componentName
${'gcp_error'} | ${{ error: 'mock_gcp_client_error' }} | ${'GcpError'}
${'no_gcp_projects'} | ${{}} | ${'NoGcpProjects'}
${'service_accounts_form'} | ${SERVICE_ACCOUNTS_FORM_PROPS} | ${'ServiceAccountsForm'}
${'home'} | ${HOME_PROPS} | ${'Home'}
`('for screen=$screen', ({ screen, extraProps, componentName }) => {
const component = SCREEN_COMPONENTS[componentName];
beforeEach(() => {
wrapper = shallowMount(App, { propsData: { screen, ...extraProps } });
});
it(`renders only ${componentName}`, () => {
const existences = mapValues(SCREEN_COMPONENTS, (x) => wrapper.findComponent(x).exists());
expect(existences).toEqual({
...mapValues(SCREEN_COMPONENTS, () => false),
[componentName]: true,
});
});
it(`renders the ${componentName} with props`, () => {
expect(wrapper.findComponent(component).props()).toEqual(extraProps);
});
it('renders incubation banner', () => {
expect(findIncubationBanner().props()).toEqual({
shareFeedbackUrl: `${BASE_FEEDBACK_URL}?issuable_template=general_feedback`,
reportBugUrl: `${BASE_FEEDBACK_URL}?issuable_template=report_bug`,
featureRequestUrl: `${BASE_FEEDBACK_URL}?issuable_template=feature_request`,
});
});
});
});

View File

@ -1,34 +0,0 @@
import { shallowMount } from '@vue/test-utils';
import { GlAlert } from '@gitlab/ui';
import GcpError from '~/google_cloud/components/errors/gcp_error.vue';
describe('GcpError component', () => {
let wrapper;
const findAlert = () => wrapper.findComponent(GlAlert);
const findBlockquote = () => wrapper.find('blockquote');
const propsData = { error: 'IAM and CloudResourceManager API disabled' };
beforeEach(() => {
wrapper = shallowMount(GcpError, { propsData });
});
afterEach(() => {
wrapper.destroy();
});
it('contains alert', () => {
expect(findAlert().exists()).toBe(true);
});
it('contains relevant text', () => {
const alertText = findAlert().text();
expect(findAlert().props('title')).toBe(GcpError.i18n.title);
expect(alertText).toContain(GcpError.i18n.description);
});
it('contains error stacktrace', () => {
expect(findBlockquote().text()).toBe(propsData.error);
});
});

View File

@ -1,33 +0,0 @@
import { mount } from '@vue/test-utils';
import { GlAlert, GlButton } from '@gitlab/ui';
import NoGcpProjects from '~/google_cloud/components/errors/no_gcp_projects.vue';
describe('NoGcpProjects component', () => {
let wrapper;
const findAlert = () => wrapper.findComponent(GlAlert);
const findButton = () => wrapper.findComponent(GlButton);
beforeEach(() => {
wrapper = mount(NoGcpProjects);
});
afterEach(() => {
wrapper.destroy();
});
it('contains alert', () => {
expect(findAlert().exists()).toBe(true);
});
it('contains relevant text', () => {
expect(findAlert().props('title')).toBe(NoGcpProjects.i18n.title);
expect(findAlert().text()).toContain(NoGcpProjects.i18n.description);
});
it('contains create gcp project button', () => {
const button = findButton();
expect(button.text()).toBe(NoGcpProjects.i18n.createLabel);
expect(button.attributes('href')).toBe('https://console.cloud.google.com/projectcreate');
});
});

View File

@ -0,0 +1,40 @@
import { mountExtended } from 'helpers/vue_test_utils_helper';
import GoogleCloudMenu from '~/google_cloud/components/google_cloud_menu.vue';
describe('google_cloud/components/google_cloud_menu', () => {
let wrapper;
const props = {
active: 'configuration',
configurationUrl: 'configuration-url',
deploymentsUrl: 'deployments-url',
databasesUrl: 'databases-url',
};
beforeEach(() => {
wrapper = mountExtended(GoogleCloudMenu, { propsData: props });
});
afterEach(() => {
wrapper.destroy();
});
it('contains active configuration link', () => {
const link = wrapper.findByTestId('configurationLink');
expect(link.text()).toBe(GoogleCloudMenu.i18n.configuration.title);
expect(link.attributes('href')).toBe(props.configurationUrl);
expect(link.element.classList.contains('gl-tab-nav-item-active')).toBe(true);
});
it('contains deployments link', () => {
const link = wrapper.findByTestId('deploymentsLink');
expect(link.text()).toBe(GoogleCloudMenu.i18n.deployments.title);
expect(link.attributes('href')).toBe(props.deploymentsUrl);
});
it('contains databases link', () => {
const link = wrapper.findByTestId('databasesLink');
expect(link.text()).toBe(GoogleCloudMenu.i18n.databases.title);
expect(link.attributes('href')).toBe(props.databasesUrl);
});
});

View File

@ -1,66 +0,0 @@
import { shallowMount } from '@vue/test-utils';
import { GlTab, GlTabs } from '@gitlab/ui';
import Home from '~/google_cloud/components/home.vue';
import ServiceAccountsList from '~/google_cloud/components/service_accounts_list.vue';
describe('google_cloud Home component', () => {
let wrapper;
const findTabs = () => wrapper.findComponent(GlTabs);
const findTabItems = () => findTabs().findAllComponents(GlTab);
const findTabItemsModel = () =>
findTabs()
.findAllComponents(GlTab)
.wrappers.map((x) => ({
title: x.attributes('title'),
disabled: x.attributes('disabled'),
}));
const TEST_HOME_PROPS = {
serviceAccounts: [{}, {}],
gcpRegions: [{}, {}],
createServiceAccountUrl: '#url-create-service-account',
configureGcpRegionsUrl: '#url-configure-gcp-regions',
emptyIllustrationUrl: '#url-empty-illustration',
enableCloudRunUrl: '#url-enable-cloud-run',
enableCloudStorageUrl: '#enableCloudStorageUrl',
revokeOauthUrl: '#revokeOauthUrl',
};
beforeEach(() => {
const propsData = {
screen: 'home',
...TEST_HOME_PROPS,
};
wrapper = shallowMount(Home, { propsData });
});
afterEach(() => {
wrapper.destroy();
});
describe('google_cloud App tabs', () => {
it('should contain tabs', () => {
expect(findTabs().exists()).toBe(true);
});
it('should contain three tab items', () => {
expect(findTabItemsModel()).toEqual([
{ title: 'Configuration', disabled: undefined },
{ title: 'Deployments', disabled: undefined },
{ title: 'Services', disabled: '' },
]);
});
describe('configuration tab', () => {
it('should contain service accounts component', () => {
const serviceAccounts = findTabItems().at(0).findComponent(ServiceAccountsList);
expect(serviceAccounts.props()).toEqual({
list: TEST_HOME_PROPS.serviceAccounts,
createUrl: TEST_HOME_PROPS.createServiceAccountUrl,
emptyIllustrationUrl: TEST_HOME_PROPS.emptyIllustrationUrl,
});
});
});
});
});

View File

@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils';
import { GlAlert, GlLink } from '@gitlab/ui'; import { GlAlert, GlLink } from '@gitlab/ui';
import IncubationBanner from '~/google_cloud/components/incubation_banner.vue'; import IncubationBanner from '~/google_cloud/components/incubation_banner.vue';
describe('IncubationBanner component', () => { describe('google_cloud/components/incubation_banner', () => {
let wrapper; let wrapper;
const findAlert = () => wrapper.findComponent(GlAlert); const findAlert = () => wrapper.findComponent(GlAlert);
@ -12,12 +12,7 @@ describe('IncubationBanner component', () => {
const findShareFeedbackLink = () => findLinks().at(2); const findShareFeedbackLink = () => findLinks().at(2);
beforeEach(() => { beforeEach(() => {
const propsData = { wrapper = mount(IncubationBanner);
shareFeedbackUrl: 'url_general_feedback',
reportBugUrl: 'url_report_bug',
featureRequestUrl: 'url_feature_request',
};
wrapper = mount(IncubationBanner, { propsData });
}); });
afterEach(() => { afterEach(() => {
@ -41,20 +36,26 @@ describe('IncubationBanner component', () => {
it('contains feature request link', () => { it('contains feature request link', () => {
const link = findFeatureRequestLink(); const link = findFeatureRequestLink();
const expected =
'https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/feedback/-/issues/new?issuable_template=feature_request';
expect(link.text()).toBe('request a feature'); expect(link.text()).toBe('request a feature');
expect(link.attributes('href')).toBe('url_feature_request'); expect(link.attributes('href')).toBe(expected);
}); });
it('contains report bug link', () => { it('contains report bug link', () => {
const link = findReportBugLink(); const link = findReportBugLink();
const expected =
'https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/feedback/-/issues/new?issuable_template=report_bug';
expect(link.text()).toBe('report a bug'); expect(link.text()).toBe('report a bug');
expect(link.attributes('href')).toBe('url_report_bug'); expect(link.attributes('href')).toBe(expected);
}); });
it('contains share feedback link', () => { it('contains share feedback link', () => {
const link = findShareFeedbackLink(); const link = findShareFeedbackLink();
const expected =
'https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/feedback/-/issues/new?issuable_template=general_feedback';
expect(link.text()).toBe('share feedback'); expect(link.text()).toBe('share feedback');
expect(link.attributes('href')).toBe('url_general_feedback'); expect(link.attributes('href')).toBe(expected);
}); });
}); });
}); });

View File

@ -5,7 +5,7 @@ import RevokeOauth, {
GOOGLE_CLOUD_REVOKE_DESCRIPTION, GOOGLE_CLOUD_REVOKE_DESCRIPTION,
} from '~/google_cloud/components/revoke_oauth.vue'; } from '~/google_cloud/components/revoke_oauth.vue';
describe('RevokeOauth component', () => { describe('google_cloud/components/revoke_oauth', () => {
let wrapper; let wrapper;
const findTitle = () => wrapper.find('h2'); const findTitle = () => wrapper.find('h2');

View File

@ -0,0 +1,65 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import Panel from '~/google_cloud/configuration/panel.vue';
import IncubationBanner from '~/google_cloud/components/incubation_banner.vue';
import GoogleCloudMenu from '~/google_cloud/components/google_cloud_menu.vue';
import ServiceAccountsList from '~/google_cloud/service_accounts/list.vue';
import GcpRegionsList from '~/google_cloud/gcp_regions/list.vue';
import RevokeOauth from '~/google_cloud/components/revoke_oauth.vue';
describe('google_cloud/configuration/panel', () => {
let wrapper;
const props = {
configurationUrl: 'configuration-url',
deploymentsUrl: 'deployments-url',
databasesUrl: 'databases-url',
serviceAccounts: [],
createServiceAccountUrl: 'create-service-account-url',
emptyIllustrationUrl: 'empty-illustration-url',
gcpRegions: [],
configureGcpRegionsUrl: 'configure-gcp-regions-url',
revokeOauthUrl: 'revoke-oauth-url',
};
beforeEach(() => {
wrapper = shallowMountExtended(Panel, { propsData: props });
});
afterEach(() => {
wrapper.destroy();
});
it('contains incubation banner', () => {
const target = wrapper.findComponent(IncubationBanner);
expect(target.exists()).toBe(true);
});
it('contains google cloud menu with `configuration` active', () => {
const target = wrapper.findComponent(GoogleCloudMenu);
expect(target.exists()).toBe(true);
expect(target.props('active')).toBe('configuration');
expect(target.props('configurationUrl')).toBe(props.configurationUrl);
expect(target.props('deploymentsUrl')).toBe(props.deploymentsUrl);
expect(target.props('databasesUrl')).toBe(props.databasesUrl);
});
it('contains service accounts list', () => {
const target = wrapper.findComponent(ServiceAccountsList);
expect(target.exists()).toBe(true);
expect(target.props('list')).toBe(props.serviceAccounts);
expect(target.props('createUrl')).toBe(props.createServiceAccountUrl);
expect(target.props('emptyIllustrationUrl')).toBe(props.emptyIllustrationUrl);
});
it('contains gcp regions list', () => {
const target = wrapper.findComponent(GcpRegionsList);
expect(target.props('list')).toBe(props.gcpRegions);
expect(target.props('createUrl')).toBe(props.configureGcpRegionsUrl);
expect(target.props('emptyIllustrationUrl')).toBe(props.emptyIllustrationUrl);
});
it('contains revoke oauth', () => {
const target = wrapper.findComponent(RevokeOauth);
expect(target.props('url')).toBe(props.revokeOauthUrl);
});
});

View File

@ -1,8 +1,8 @@
import { GlFormCheckbox } from '@gitlab/ui'; import { GlFormCheckbox } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import InstanceForm from '~/google_cloud/components/cloudsql/create_instance_form.vue'; import InstanceForm from '~/google_cloud/databases/cloudsql/create_instance_form.vue';
describe('google_cloud::cloudsql::create_instance_form component', () => { describe('google_cloud/databases/cloudsql/create_instance_form', () => {
let wrapper; let wrapper;
const findByTestId = (id) => wrapper.findByTestId(id); const findByTestId = (id) => wrapper.findByTestId(id);

View File

@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlEmptyState, GlTable } from '@gitlab/ui'; import { GlEmptyState, GlTable } from '@gitlab/ui';
import InstanceTable from '~/google_cloud/components/cloudsql/instance_table.vue'; import InstanceTable from '~/google_cloud/databases/cloudsql/instance_table.vue';
describe('google_cloud::databases::service_table component', () => { describe('google_cloud/databases/cloudsql/instance_table', () => {
let wrapper; let wrapper;
const findEmptyState = () => wrapper.findComponent(GlEmptyState); const findEmptyState = () => wrapper.findComponent(GlEmptyState);

View File

@ -0,0 +1,36 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import Panel from '~/google_cloud/databases/panel.vue';
import IncubationBanner from '~/google_cloud/components/incubation_banner.vue';
import GoogleCloudMenu from '~/google_cloud/components/google_cloud_menu.vue';
describe('google_cloud/databases/panel', () => {
let wrapper;
const props = {
configurationUrl: 'configuration-url',
deploymentsUrl: 'deployments-url',
databasesUrl: 'databases-url',
};
beforeEach(() => {
wrapper = shallowMountExtended(Panel, { propsData: props });
});
afterEach(() => {
wrapper.destroy();
});
it('contains incubation banner', () => {
const target = wrapper.findComponent(IncubationBanner);
expect(target.exists()).toBe(true);
});
it('contains google cloud menu with `databases` active', () => {
const target = wrapper.findComponent(GoogleCloudMenu);
expect(target.exists()).toBe(true);
expect(target.props('active')).toBe('databases');
expect(target.props('configurationUrl')).toBe(props.configurationUrl);
expect(target.props('deploymentsUrl')).toBe(props.deploymentsUrl);
expect(target.props('databasesUrl')).toBe(props.databasesUrl);
});
});

View File

@ -1,8 +1,8 @@
import { GlTable } from '@gitlab/ui'; import { GlTable } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper';
import ServiceTable from '~/google_cloud/components/databases/service_table.vue'; import ServiceTable from '~/google_cloud/databases/service_table.vue';
describe('google_cloud::databases::service_table component', () => { describe('google_cloud/databases/service_table', () => {
let wrapper; let wrapper;
const findTable = () => wrapper.findComponent(GlTable); const findTable = () => wrapper.findComponent(GlTable);

View File

@ -0,0 +1,46 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import Panel from '~/google_cloud/deployments/panel.vue';
import IncubationBanner from '~/google_cloud/components/incubation_banner.vue';
import GoogleCloudMenu from '~/google_cloud/components/google_cloud_menu.vue';
import ServiceTable from '~/google_cloud/deployments/service_table.vue';
describe('google_cloud/deployments/panel', () => {
let wrapper;
const props = {
configurationUrl: 'configuration-url',
deploymentsUrl: 'deployments-url',
databasesUrl: 'databases-url',
enableCloudRunUrl: 'cloud-run-url',
enableCloudStorageUrl: 'cloud-storage-url',
};
beforeEach(() => {
wrapper = shallowMountExtended(Panel, { propsData: props });
});
afterEach(() => {
wrapper.destroy();
});
it('contains incubation banner', () => {
const target = wrapper.findComponent(IncubationBanner);
expect(target.exists()).toBe(true);
});
it('contains google cloud menu with `deployments` active', () => {
const target = wrapper.findComponent(GoogleCloudMenu);
expect(target.exists()).toBe(true);
expect(target.props('active')).toBe('deployments');
expect(target.props('configurationUrl')).toBe(props.configurationUrl);
expect(target.props('deploymentsUrl')).toBe(props.deploymentsUrl);
expect(target.props('databasesUrl')).toBe(props.databasesUrl);
});
it('contains service-table', () => {
const target = wrapper.findComponent(ServiceTable);
expect(target.exists()).toBe(true);
expect(target.props('cloudRunUrl')).toBe(props.enableCloudRunUrl);
expect(target.props('cloudStorageUrl')).toBe(props.enableCloudStorageUrl);
});
});

View File

@ -1,8 +1,8 @@
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { GlButton, GlTable } from '@gitlab/ui'; import { GlButton, GlTable } from '@gitlab/ui';
import DeploymentsServiceTable from '~/google_cloud/components/deployments_service_table.vue'; import DeploymentsServiceTable from '~/google_cloud/deployments/service_table.vue';
describe('google_cloud DeploymentsServiceTable component', () => { describe('google_cloud/deployments/service_table', () => {
let wrapper; let wrapper;
const findTable = () => wrapper.findComponent(GlTable); const findTable = () => wrapper.findComponent(GlTable);

View File

@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlButton, GlFormGroup, GlFormSelect } from '@gitlab/ui'; import { GlButton, GlFormGroup, GlFormSelect } from '@gitlab/ui';
import GcpRegionsForm from '~/google_cloud/components/gcp_regions_form.vue'; import GcpRegionsForm from '~/google_cloud/gcp_regions/form.vue';
describe('GcpRegionsForm component', () => { describe('google_cloud/gcp_regions/form', () => {
let wrapper; let wrapper;
const findHeader = () => wrapper.find('header'); const findHeader = () => wrapper.find('header');

View File

@ -1,8 +1,8 @@
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { GlButton, GlEmptyState, GlTable } from '@gitlab/ui'; import { GlButton, GlEmptyState, GlTable } from '@gitlab/ui';
import GcpRegionsList from '~/google_cloud/components/gcp_regions_list.vue'; import GcpRegionsList from '~/google_cloud/gcp_regions/list.vue';
describe('GcpRegions component', () => { describe('google_cloud/gcp_regions/list', () => {
describe('when the project does not have any configured regions', () => { describe('when the project does not have any configured regions', () => {
let wrapper; let wrapper;

View File

@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlButton, GlFormGroup, GlFormSelect, GlFormCheckbox } from '@gitlab/ui'; import { GlButton, GlFormGroup, GlFormSelect, GlFormCheckbox } from '@gitlab/ui';
import ServiceAccountsForm from '~/google_cloud/components/service_accounts_form.vue'; import ServiceAccountsForm from '~/google_cloud/service_accounts/form.vue';
describe('ServiceAccountsForm component', () => { describe('google_cloud/service_accounts/form', () => {
let wrapper; let wrapper;
const findHeader = () => wrapper.find('header'); const findHeader = () => wrapper.find('header');

View File

@ -1,8 +1,8 @@
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { GlAlert, GlButton, GlEmptyState, GlTable } from '@gitlab/ui'; import { GlAlert, GlButton, GlEmptyState, GlTable } from '@gitlab/ui';
import ServiceAccountsList from '~/google_cloud/components/service_accounts_list.vue'; import ServiceAccountsList from '~/google_cloud/service_accounts/list.vue';
describe('ServiceAccounts component', () => { describe('google_cloud/service_accounts/list', () => {
describe('when the project does not have any service accounts', () => { describe('when the project does not have any service accounts', () => {
let wrapper; let wrapper;

View File

@ -1,4 +1,4 @@
import { mount } from '@vue/test-utils'; import { GlPopover } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import GroupFolder from '~/groups/components/group_folder.vue'; import GroupFolder from '~/groups/components/group_folder.vue';
import GroupItem from '~/groups/components/group_item.vue'; import GroupItem from '~/groups/components/group_item.vue';
@ -6,14 +6,25 @@ import ItemActions from '~/groups/components/item_actions.vue';
import eventHub from '~/groups/event_hub'; import eventHub from '~/groups/event_hub';
import { getGroupItemMicrodata } from '~/groups/store/utils'; import { getGroupItemMicrodata } from '~/groups/store/utils';
import * as urlUtilities from '~/lib/utils/url_utility'; import * as urlUtilities from '~/lib/utils/url_utility';
import {
ITEM_TYPE,
VISIBILITY_INTERNAL,
VISIBILITY_PRIVATE,
VISIBILITY_PUBLIC,
} from '~/groups/constants';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { mockParentGroupItem, mockChildren } from '../mock_data'; import { mockParentGroupItem, mockChildren } from '../mock_data';
const createComponent = ( const createComponent = (
propsData = { group: mockParentGroupItem, parentGroup: mockChildren[0] }, propsData = { group: mockParentGroupItem, parentGroup: mockChildren[0] },
provide = {
currentGroupVisibility: VISIBILITY_PRIVATE,
},
) => { ) => {
return mount(GroupItem, { return mountExtended(GroupItem, {
propsData, propsData,
components: { GroupFolder }, components: { GroupFolder },
provide,
}); });
}; };
@ -276,4 +287,90 @@ describe('GroupItemComponent', () => {
}); });
}); });
}); });
describe('visibility warning popover', () => {
const findPopover = () => wrapper.findComponent(GlPopover);
const itDoesNotRenderVisibilityWarningPopover = () => {
it('does not render visibility warning popover', () => {
expect(findPopover().exists()).toBe(false);
});
};
describe('when showing groups', () => {
beforeEach(() => {
wrapper = createComponent();
});
itDoesNotRenderVisibilityWarningPopover();
});
describe('when `action` prop is not `shared`', () => {
beforeEach(() => {
wrapper = createComponent({
group: mockParentGroupItem,
parentGroup: mockChildren[0],
action: 'subgroups_and_projects',
});
});
itDoesNotRenderVisibilityWarningPopover();
});
describe('when showing projects', () => {
describe.each`
itemVisibility | currentGroupVisibility | isPopoverShown
${VISIBILITY_PRIVATE} | ${VISIBILITY_PUBLIC} | ${false}
${VISIBILITY_INTERNAL} | ${VISIBILITY_PUBLIC} | ${false}
${VISIBILITY_PUBLIC} | ${VISIBILITY_PUBLIC} | ${false}
${VISIBILITY_PRIVATE} | ${VISIBILITY_PRIVATE} | ${false}
${VISIBILITY_INTERNAL} | ${VISIBILITY_PRIVATE} | ${true}
${VISIBILITY_PUBLIC} | ${VISIBILITY_PRIVATE} | ${true}
`(
'when item visibility is $itemVisibility and parent group visibility is $currentGroupVisibility',
({ itemVisibility, currentGroupVisibility, isPopoverShown }) => {
beforeEach(() => {
wrapper = createComponent(
{
group: {
...mockParentGroupItem,
visibility: itemVisibility,
type: ITEM_TYPE.PROJECT,
},
parentGroup: mockChildren[0],
action: 'shared',
},
{
currentGroupVisibility,
},
);
});
if (isPopoverShown) {
it('renders visibility warning popover', () => {
expect(findPopover().exists()).toBe(true);
});
} else {
itDoesNotRenderVisibilityWarningPopover();
}
},
);
});
it('sets up popover `target` prop correctly', () => {
wrapper = createComponent({
group: {
...mockParentGroupItem,
visibility: VISIBILITY_PUBLIC,
type: ITEM_TYPE.PROJECT,
},
parentGroup: mockChildren[0],
action: 'shared',
});
expect(findPopover().props('target')()).toEqual(
wrapper.findByRole('button', { name: GroupItem.i18n.popoverTitle }).element,
);
});
});
}); });

View File

@ -1,45 +1,55 @@
import Vue, { nextTick } from 'vue'; import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper';
import groupFolderComponent from '~/groups/components/group_folder.vue'; import GroupFolderComponent from '~/groups/components/group_folder.vue';
import groupItemComponent from '~/groups/components/group_item.vue'; import GroupItemComponent from '~/groups/components/group_item.vue';
import groupsComponent from '~/groups/components/groups.vue'; import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
import GroupsComponent from '~/groups/components/groups.vue';
import eventHub from '~/groups/event_hub'; import eventHub from '~/groups/event_hub';
import { VISIBILITY_PRIVATE } from '~/groups/constants';
import { mockGroups, mockPageInfo } from '../mock_data'; import { mockGroups, mockPageInfo } from '../mock_data';
const createComponent = (searchEmpty = false) => { describe('GroupsComponent', () => {
const Component = Vue.extend(groupsComponent); let wrapper;
return mountComponent(Component, { const defaultPropsData = {
groups: mockGroups, groups: mockGroups,
pageInfo: mockPageInfo, pageInfo: mockPageInfo,
searchEmptyMessage: 'No matching results', searchEmptyMessage: 'No matching results',
searchEmpty, searchEmpty: false,
}); };
};
describe('GroupsComponent', () => { const createComponent = ({ propsData } = {}) => {
let vm; wrapper = mountExtended(GroupsComponent, {
propsData: {
...defaultPropsData,
...propsData,
},
provide: {
currentGroupVisibility: VISIBILITY_PRIVATE,
},
});
};
const findPaginationLinks = () => wrapper.findComponent(PaginationLinks);
beforeEach(async () => { beforeEach(async () => {
Vue.component('GroupFolder', groupFolderComponent); Vue.component('GroupFolder', GroupFolderComponent);
Vue.component('GroupItem', groupItemComponent); Vue.component('GroupItem', GroupItemComponent);
vm = createComponent();
await nextTick();
}); });
afterEach(() => { afterEach(() => {
vm.$destroy(); wrapper.destroy();
}); });
describe('methods', () => { describe('methods', () => {
describe('change', () => { describe('change', () => {
it('should emit `fetchPage` event when page is changed via pagination', () => { it('should emit `fetchPage` event when page is changed via pagination', () => {
createComponent();
jest.spyOn(eventHub, '$emit').mockImplementation(); jest.spyOn(eventHub, '$emit').mockImplementation();
vm.change(2); findPaginationLinks().props('change')(2);
expect(eventHub.$emit).toHaveBeenCalledWith('fetchPage', { expect(eventHub.$emit).toHaveBeenCalledWith('fetchPage', {
page: 2, page: 2,
@ -52,18 +62,18 @@ describe('GroupsComponent', () => {
}); });
describe('template', () => { describe('template', () => {
it('should render component template correctly', async () => { it('should render component template correctly', () => {
await nextTick(); createComponent();
expect(vm.$el.querySelector('.groups-list-tree-container')).toBeDefined();
expect(vm.$el.querySelector('.group-list-tree')).toBeDefined(); expect(wrapper.findComponent(GroupFolderComponent).exists()).toBe(true);
expect(vm.$el.querySelector('.gl-pagination')).toBeDefined(); expect(findPaginationLinks().exists()).toBe(true);
expect(vm.$el.querySelectorAll('.has-no-search-results').length).toBe(0); expect(wrapper.findByText(defaultPropsData.searchEmptyMessage).exists()).toBe(false);
}); });
it('should render empty search message when `searchEmpty` is `true`', async () => { it('should render empty search message when `searchEmpty` is `true`', () => {
vm.searchEmpty = true; createComponent({ propsData: { searchEmpty: true } });
await nextTick();
expect(vm.$el.querySelector('.has-no-search-results')).toBeDefined(); expect(wrapper.findByText(defaultPropsData.searchEmptyMessage).exists()).toBe(true);
}); });
}); });
}); });

Some files were not shown because too many files have changed in this diff Show More