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:
HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}"
GITLAB_HELM_CHART_REF: "a6a609a19166f00b1a7774374041cd38a9f7e20d"
GITLAB_HELM_CHART_REF: "138c146a5ba787942f66d4c7d795d224d6ba206a"
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
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>
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 {
components: { GlAlert, GlLink, GlSprintf },
props: {
shareFeedbackUrl: {
required: true,
type: String,
},
reportBugUrl: {
required: true,
type: String,
},
featureRequestUrl: {
required: true,
type: String,
methods: {
feedbackUrl(template) {
return `https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/feedback/-/issues/new?issuable_template=${template}`;
},
},
FEATURE_REQUEST_KEY,
REPORT_BUG_KEY,
GENERAL_FEEDBACK_KEY,
};
</script>
@ -31,13 +29,13 @@ export default {
"
>
<template #featureLink="{ content }">
<gl-link :href="featureRequestUrl">{{ content }}</gl-link>
<gl-link :href="feedbackUrl($options.FEATURE_REQUEST_KEY)">{{ content }}</gl-link>
</template>
<template #bugLink="{ content }">
<gl-link :href="reportBugUrl">{{ content }}</gl-link>
<gl-link :href="feedbackUrl($options.REPORT_BUG_KEY)">{{ content }}</gl-link>
</template>
<template #feedbackLink="{ content }">
<gl-link :href="shareFeedbackUrl">{{ content }}</gl-link>
<gl-link :href="feedbackUrl($options.GENERAL_FEEDBACK_KEY)">{{ content }}</gl-link>
</template>
</gl-sprintf>
</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,
GlIcon,
GlLabel,
GlButton,
GlPopover,
GlLink,
GlTooltipDirective,
GlSafeHtmlDirective,
} from '@gitlab/ui';
import { visitUrl } from '~/lib/utils/url_utility';
import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
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 itemActions from './item_actions.vue';
@ -30,12 +40,16 @@ export default {
GlLoadingIcon,
GlIcon,
GlLabel,
GlButton,
GlPopover,
GlLink,
UserAccessRoleBadge,
itemCaret,
itemTypeIcon,
itemActions,
itemStats,
},
inject: ['currentGroupVisibility'],
props: {
parentGroup: {
type: Object,
@ -56,6 +70,9 @@ export default {
groupDomId() {
return `group-${this.group.id}`;
},
itemTestId() {
return `group-overview-item-${this.group.id}`;
},
rowClass() {
return {
'is-open': this.group.isOpen,
@ -74,10 +91,10 @@ export default {
return Boolean(this.group.complianceFramework?.name);
},
isGroup() {
return this.group.type === 'group';
return this.group.type === ITEM_TYPE.GROUP;
},
isGroupPendingRemoval() {
return this.group.type === 'group' && this.group.pendingRemoval;
return this.group.type === ITEM_TYPE.GROUP && this.group.pendingRemoval;
},
visibilityIcon() {
return VISIBILITY_TYPE_ICON[this.group.visibility];
@ -94,6 +111,13 @@ export default {
showActionsMenu() {
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: {
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'] },
AVATAR_SHAPE_OPTION_RECT,
};
@ -118,6 +153,7 @@ export default {
<template>
<li
:id="groupDomId"
:data-testid="itemTestId"
:class="rowClass"
class="group-row"
:itemprop="microdata.itemprop"
@ -163,7 +199,7 @@ export default {
data-testid="group-name"
:href="group.relativePath"
: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"
>
{{
@ -174,17 +210,40 @@ export default {
</a>
<gl-icon
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"
:title="visibilityTooltip"
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 }}
</user-access-role-badge>
<gl-label
v-if="hasComplianceFramework"
class="gl-mt-3"
:title="complianceFramework.name"
:background-color="complianceFramework.color"
:description="complianceFramework.description"

View File

@ -28,28 +28,32 @@ export const ITEM_TYPE = {
GROUP: 'group',
};
export const VISIBILITY_PUBLIC = 'public';
export const VISIBILITY_INTERNAL = 'internal';
export const VISIBILITY_PRIVATE = 'private';
export const GROUP_VISIBILITY_TYPE = {
public: __(
[VISIBILITY_PUBLIC]: __(
'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.',
),
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 = {
public: __('Public - The project can be accessed without any authentication.'),
internal: __(
[VISIBILITY_PUBLIC]: __('Public - The project can be accessed without any authentication.'),
[VISIBILITY_INTERNAL]: __(
'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.',
),
};
export const VISIBILITY_TYPE_ICON = {
public: 'earth',
internal: 'shield',
private: 'lock',
[VISIBILITY_PUBLIC]: 'earth',
[VISIBILITY_INTERNAL]: 'shield',
[VISIBILITY_PRIVATE]: 'lock',
};

View File

@ -55,6 +55,7 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
renderEmptyState,
canCreateSubgroups,
canCreateProjects,
currentGroupVisibility,
},
} = this.$options.el;
@ -67,6 +68,7 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
renderEmptyState: parseBoolean(renderEmptyState),
canCreateSubgroups: parseBoolean(canCreateSubgroups),
canCreateProjects: parseBoolean(canCreateProjects),
currentGroupVisibility,
};
},
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.'),
invalidWithReason: s__('Pipelines|This GitLab CI configuration is invalid: %{reason}.'),
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 {

View File

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

View File

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

View File

@ -1,10 +1,35 @@
<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 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 = {
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'),
loading: s__('PipelineEditor|Validating pipeline... It can take up to a minute.'),
pipelineSource: s__('PipelineEditor|Pipeline Source'),
pipelineSourceDefault: s__('PipelineEditor|Git push event to the default branch'),
pipelineSourceTooltip: s__('PipelineEditor|Other pipeline sources are not available yet.'),
@ -15,48 +40,179 @@ export const i18n = {
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.',
),
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 {
name: 'CiValidateTab',
components: {
CiLintResults,
GlAlert,
GlButton,
GlDropdown,
GlIcon,
GlLoadingIcon,
GlLink,
GlSprintf,
GlTooltip,
ValidatePipelinePopover,
},
directives: {
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,
BASE_CLASSES,
};
</script>
<template>
<div>
<div class="gl-mt-3">
<label>{{ $options.i18n.pipelineSource }}</label>
<gl-dropdown
v-gl-tooltip.hover
:title="$options.i18n.pipelineSourceTooltip"
:text="$options.i18n.pipelineSourceDefault"
disabled
data-testid="pipeline-source"
/>
<validate-pipeline-popover />
<gl-icon
id="validate-pipeline-help"
name="question-o"
class="gl-ml-1 gl-fill-blue-500"
category="secondary"
variant="confirm"
:aria-label="$options.i18n.help"
/>
<div class="gl-display-flex gl-justify-content-space-between gl-mt-3">
<div>
<label>{{ $options.i18n.pipelineSource }}</label>
<gl-dropdown
v-gl-tooltip.hover
class="gl-ml-3"
:title="$options.i18n.pipelineSourceTooltip"
:text="$options.i18n.pipelineSourceDefault"
disabled
data-testid="pipeline-source"
/>
<validate-pipeline-popover />
<gl-icon
id="validate-pipeline-help"
name="question-o"
class="gl-ml-1 gl-fill-blue-500"
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 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" />
<h1 class="gl-font-size-h1 gl-mb-6">{{ $options.i18n.title }}</h1>
<ul>
@ -69,9 +225,61 @@ export default {
</gl-sprintf>
</li>
</ul>
<gl-button variant="confirm" class="gl-mt-3" data-qa-selector="simulate_pipeline">
{{ $options.i18n.cta }}
</gl-button>
<div ref="simulatePipelineButton">
<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>
</template>

View File

@ -27,6 +27,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
ciConfigPath,
ciExamplesHelpPagePath,
ciHelpPagePath,
ciLintPath,
defaultBranch,
emptyStateIllustrationPath,
helpPaths,
@ -116,6 +117,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
ciConfigPath,
ciExamplesHelpPagePath,
ciHelpPagePath,
ciLintPath,
configurationPaths,
dataMethod: 'graphql',
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 {
margin-top: -$gl-padding-8; // negative margin required for flex-wrap
font-size: $gl-font-size;
}

View File

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

View File

@ -12,7 +12,7 @@ class Projects::GoogleCloud::BaseController < Projects::ApplicationController
def admin_project_google_cloud!
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!
end
end
@ -20,7 +20,11 @@ class Projects::GoogleCloud::BaseController < Projects::ApplicationController
def google_oauth2_enabled!
config = Gitlab::Auth::OAuth::Provider.config_for('google_oauth2')
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.'
end
end
@ -31,7 +35,7 @@ class Projects::GoogleCloud::BaseController < Projects::ApplicationController
enabled_for_project = Feature.enabled?(:incubation_5mp_google_cloud, project)
feature_is_enabled = enabled_for_user || enabled_for_group || enabled_for_project
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!
end
end
@ -42,7 +46,7 @@ class Projects::GoogleCloud::BaseController < Projects::ApplicationController
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)
@authorize_url = GoogleApi::CloudPlatform::Client.new(nil,
callback_google_api_auth_url,
@ -65,12 +69,6 @@ class Projects::GoogleCloud::BaseController < Projects::ApplicationController
session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at]
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)
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
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
params = { google_oauth2_token: token_in_session }
enable_cloud_run_response = GoogleCloud::EnableCloudRunService
.new(project, current_user, params).execute
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]
redirect_to project_google_cloud_index_path(project)
redirect_to project_google_cloud_deployments_path(project)
else
params = { action: GoogleCloud::GeneratePipelineService::ACTION_DEPLOY_TO_CLOUD_RUN }
generate_pipeline_response = GoogleCloud::GeneratePipelineService
.new(project, current_user, params).execute
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'
redirect_to project_google_cloud_index_path(project)
redirect_to project_google_cloud_deployments_path(project)
else
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)
end
end
rescue Google::Apis::ClientError => error
handle_gcp_error('deployments#cloud_run', error)
rescue Google::Apis::ClientError, Google::Apis::ServerError, Google::Apis::AuthorizationError => 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
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
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
@google_cloud_path = project_google_cloud_index_path(project)
@google_cloud_path = project_google_cloud_configuration_path(project)
params = { per_page: 50 }
branches = BranchesFinder.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',
availableRegions: AVAILABLE_REGIONS,
refs: refs,
cancelPath: project_google_cloud_index_path(project)
cancelPath: project_google_cloud_configuration_path(project)
}
@js_data = js_data.to_json
track_event('gcp_regions#index', 'form_render', js_data)
track_event('gcp_regions#index', 'success', js_data)
end
def create
permitted_params = params.permit(:ref, :gcp_region)
response = GoogleCloud::GcpRegionAddOrReplaceService.new(project).execute(permitted_params[:ref], permitted_params[:gcp_region])
track_event('gcp_regions#create', 'form_submit', response)
redirect_to project_google_cloud_index_path(project), notice: _('GCP region configured')
track_event('gcp_regions#create', 'success', response)
redirect_to project_google_cloud_configuration_path(project), notice: _('GCP region configured')
end
end

View File

@ -8,16 +8,15 @@ class Projects::GoogleCloud::RevokeOauthController < Projects::GoogleCloud::Base
response = google_api_client.revoke_authorizations
if response.success?
status = 'success'
redirect_message = { notice: s_('GoogleCloud|Google OAuth2 token revocation requested') }
track_event('revoke_oauth#create', 'success', response.to_json)
else
status = 'failed'
redirect_message = { alert: s_('GoogleCloud|Google OAuth2 token revocation request failed') }
track_event('revoke_oauth#create', 'error', response.to_json)
end
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

View File

@ -4,14 +4,15 @@ class Projects::GoogleCloud::ServiceAccountsController < Projects::GoogleCloud::
before_action :validate_gcp_token!
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)
gcp_projects = google_api_client.list_projects
if gcp_projects.empty?
@js_data = { screen: 'no_gcp_projects' }.to_json
track_event('service_accounts#index', 'form_error', 'no_gcp_projects')
render status: :unauthorized, template: 'projects/google_cloud/errors/no_gcp_projects'
track_event('service_accounts#index', 'error_form', '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
params = { per_page: 50 }
branches = BranchesFinder.new(project.repository, params).execute(gitaly_pagination: true)
@ -21,14 +22,16 @@ class Projects::GoogleCloud::ServiceAccountsController < Projects::GoogleCloud::
screen: 'service_accounts_form',
gcpProjects: gcp_projects,
refs: refs,
cancelPath: project_google_cloud_index_path(project)
cancelPath: project_google_cloud_configuration_path(project)
}
@js_data = js_data.to_json
track_event('service_accounts#index', 'form_success', js_data)
track_event('service_accounts#index', 'success', js_data)
end
rescue Google::Apis::ClientError => error
handle_gcp_error('service_accounts#index', error)
rescue Google::Apis::ClientError, Google::Apis::ServerError, Google::Apis::AuthorizationError => 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
def create
@ -42,9 +45,11 @@ class Projects::GoogleCloud::ServiceAccountsController < Projects::GoogleCloud::
environment_name: permitted_params[:ref]
).execute
track_event('service_accounts#create', 'form_submit', response)
redirect_to project_google_cloud_index_path(project), notice: response.message
track_event('service_accounts#create', 'success', response)
redirect_to project_google_cloud_configuration_path(project), notice: response.message
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

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!
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

View File

@ -18,6 +18,7 @@ module Ci
"ci-config-path": project.ci_config_path_or_default,
"ci-examples-help-page-path" => help_page_path('ci/examples/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,
"empty-state-illustration-path" => image_path('illustrations/empty-state/empty-dag-md.svg'),
"initial-branch-name" => initial_branch,

View File

@ -57,7 +57,22 @@ class CustomerRelations::Contact < ApplicationRecord
end
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
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_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_severity_asc, -> { includes(:issuable_severity).order('issuable_severities.severity ASC NULLS FIRST') }
scope :order_severity_desc, -> { includes(:issuable_severity).order('issuable_severities.severity DESC NULLS LAST') }
scope :order_severity_asc, -> do
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_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) }
@ -234,6 +250,31 @@ class Issue < ApplicationRecord
alias_method :with_state, :with_state_id
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
def order_upvotes_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_desc' then order_due_date_desc.with_order_id_desc
when 'relative_position', 'relative_position_asc' then order_by_relative_position
when 'severity_asc' then order_severity_asc.with_order_id_desc
when 'severity_desc' then order_severity_desc.with_order_id_desc
when 'escalation_status_asc' then order_escalation_status_asc.with_order_id_desc
when 'escalation_status_desc' then order_escalation_status_desc.with_order_id_desc
when 'severity_asc' then order_severity_asc
when 'severity_desc' then order_severity_desc
when 'escalation_status_asc' then order_escalation_status_asc
when 'escalation_status_desc' then order_escalation_status_desc
when 'closed_at', 'closed_at_asc' then order_closed_at_asc
when 'closed_at_desc' then order_closed_at_desc
else

View File

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

View File

@ -3,7 +3,7 @@
module GoogleCloud
class GcpRegionAddOrReplaceService < ::GoogleCloud::BaseService
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 } }
filter_params = { key: gcp_region_key, filter: { environment_scope: environment } }

View File

@ -162,6 +162,12 @@ module Groups
projects_to_update
.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
def update_two_factor_authentication

View File

@ -3,5 +3,5 @@
%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) } }
.js-groups-list-holder
.js-groups-list-holder{ data: { current_group_visibility: group.visibility } }
= 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
- breadcrumb_title _('Regions')
- page_title _('Regions')
- breadcrumb_title _('CloudSeed|Regions')
- page_title s_('CloudSeed|Regions')
- @content_class = "limit-container-width" unless fluid_layout
= 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
- breadcrumb_title _('Service Account')
- page_title _('Service Account')
- breadcrumb_title s_('CloudSeed|Service Account')
- page_title s_('CloudSeed|Service Account')
- @content_class = "limit-container-width" unless fluid_layout
= 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
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78556
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/351331
milestone: '14.8'
type: development
group: 'group::authentication and authorization'
name: enforce_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,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 :google_cloud, only: [:index]
namespace :google_cloud do
get '/configuration', to: 'configuration#index'
resources :revoke_oauth, only: [:create]
resources :service_accounts, 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_storage', to: 'deployments#cloud_storage'
get '/databases', to: 'databases#index'
end
resources :environments, except: [:destroy] do

View File

@ -117,10 +117,9 @@ signed 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.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `omniauth_login_minimal_scopes`. On GitLab.com, this feature is not available.
> - [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.
> - [Feature flag `omniauth_login_minimal_scopes`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/83453) removed in GitLab 15.2
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. 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_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_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_machine_type`: Set the machine type for the Kubernetes nodes.
- `TF_VAR_node_count`: Set the number of Kubernetes nodes.
- `TF_VAR_target_nodes_size`: Set the size of the nodes to use for the cluster
- `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_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.
> - Moved to GitLab Free.
NOTE:
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/).
## Namespace storage limit
A project's repository has a free storage quota of 10 GB. When a project's repository reaches
the quota it is locked. You cannot push changes to a locked project. To monitor the size of each
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/).
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
[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).

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(
title: _('Google Cloud'),
link: project_google_cloud_index_path(context.project),
active_routes: { controller: [:google_cloud, :service_accounts, :deployments, :gcp_regions] },
link: project_google_cloud_configuration_path(context.project),
active_routes: { controller: [
:configuration,
:service_accounts,
:databases,
:deployments,
:gcp_regions
] },
item_id: :google_cloud
)
end

View File

@ -8318,6 +8318,9 @@ msgstr ""
msgid "CloudSeed|CloudSQL Instance"
msgstr ""
msgid "CloudSeed|Configuration"
msgstr ""
msgid "CloudSeed|Create cluster"
msgstr ""
@ -8336,6 +8339,12 @@ msgstr ""
msgid "CloudSeed|Database version"
msgstr ""
msgid "CloudSeed|Databases"
msgstr ""
msgid "CloudSeed|Deployments"
msgstr ""
msgid "CloudSeed|Description"
msgstr ""
@ -8393,12 +8402,18 @@ msgstr ""
msgid "CloudSeed|Refs"
msgstr ""
msgid "CloudSeed|Regions"
msgstr ""
msgid "CloudSeed|Scalable, secure, and highly available in-memory service for Redis"
msgstr ""
msgid "CloudSeed|Service"
msgstr ""
msgid "CloudSeed|Service Account"
msgstr ""
msgid "CloudSeed|Services"
msgstr ""
@ -10662,9 +10677,6 @@ msgstr ""
msgid "Create %{workspace} label"
msgstr ""
msgid "Create Google Cloud project"
msgstr ""
msgid "Create New Directory"
msgstr ""
@ -17494,9 +17506,6 @@ msgstr ""
msgid "GitLab account request rejected"
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"
msgstr ""
@ -17983,18 +17992,15 @@ msgstr ""
msgid "Google Cloud"
msgstr ""
msgid "Google Cloud Error - %{error}"
msgstr ""
msgid "Google Cloud Project"
msgstr ""
msgid "Google Cloud authorizations required"
msgstr ""
msgid "Google Cloud project misconfigured"
msgstr ""
msgid "Google Cloud project required"
msgstr ""
msgid "GoogleCloud|Cancel"
msgstr ""
@ -23204,6 +23210,9 @@ msgstr ""
msgid "Less Details"
msgstr ""
msgid "Less restrictive visibility"
msgstr ""
msgid "Let's Encrypt does not accept emails on example.com"
msgstr ""
@ -25982,6 +25991,9 @@ msgstr ""
msgid "No Epic"
msgstr ""
msgid "No Google Cloud projects - You need at least one Google Cloud project"
msgstr ""
msgid "No Matching Results"
msgstr ""
@ -28378,6 +28390,9 @@ msgstr ""
msgid "PipelineEditorTutorial|🚀 Run your first pipeline"
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."
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}"
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."
msgstr ""
@ -28417,6 +28441,12 @@ msgstr ""
msgid "PipelineEditor|Validate pipeline under simulated conditions"
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})"
msgstr ""
@ -28696,6 +28726,9 @@ msgstr ""
msgid "Pipelines|Pipeline Editor"
msgstr ""
msgid "Pipelines|Pipeline syntax is correct."
msgstr ""
msgid "Pipelines|Project cache successfully reset."
msgstr ""
@ -30151,6 +30184,9 @@ msgstr ""
msgid "Project uploads"
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."
msgstr ""
@ -35588,9 +35624,6 @@ msgstr ""
msgid "ServicePing|Turn on service ping to review instance-level analytics."
msgstr ""
msgid "Services"
msgstr ""
msgid "Session ID"
msgstr ""
@ -41343,9 +41376,6 @@ msgstr ""
msgid "Unknown response text"
msgstr ""
msgid "Unknown screen"
msgstr ""
msgid "Unknown user"
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."
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"
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
Page::Project::PipelineEditor::Show.perform do |show|
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
{ 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
echoinfo "Building the gitlab chart's dependencies..."
helm dependency build "gitlab-${GITLAB_HELM_CHART_REF}"
}
function base_config_changed() {

View File

@ -64,7 +64,7 @@ RSpec.describe Projects::Pipelines::TestsController do
get_tests_show_json(build_ids)
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

View File

@ -97,6 +97,31 @@ RSpec.describe 'Group show page' do
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
before do
group.add_reporter(user)

View File

@ -1,5 +1,16 @@
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 = {
data: {
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 IncubationBanner from '~/google_cloud/components/incubation_banner.vue';
describe('IncubationBanner component', () => {
describe('google_cloud/components/incubation_banner', () => {
let wrapper;
const findAlert = () => wrapper.findComponent(GlAlert);
@ -12,12 +12,7 @@ describe('IncubationBanner component', () => {
const findShareFeedbackLink = () => findLinks().at(2);
beforeEach(() => {
const propsData = {
shareFeedbackUrl: 'url_general_feedback',
reportBugUrl: 'url_report_bug',
featureRequestUrl: 'url_feature_request',
};
wrapper = mount(IncubationBanner, { propsData });
wrapper = mount(IncubationBanner);
});
afterEach(() => {
@ -41,20 +36,26 @@ describe('IncubationBanner component', () => {
it('contains feature request link', () => {
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.attributes('href')).toBe('url_feature_request');
expect(link.attributes('href')).toBe(expected);
});
it('contains report bug link', () => {
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.attributes('href')).toBe('url_report_bug');
expect(link.attributes('href')).toBe(expected);
});
it('contains share feedback link', () => {
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.attributes('href')).toBe('url_general_feedback');
expect(link.attributes('href')).toBe(expected);
});
});
});

View File

@ -5,7 +5,7 @@ import RevokeOauth, {
GOOGLE_CLOUD_REVOKE_DESCRIPTION,
} from '~/google_cloud/components/revoke_oauth.vue';
describe('RevokeOauth component', () => {
describe('google_cloud/components/revoke_oauth', () => {
let wrapper;
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 { 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;
const findByTestId = (id) => wrapper.findByTestId(id);

View File

@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
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;
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 { 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;
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 { 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;
const findTable = () => wrapper.findComponent(GlTable);

View File

@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
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;
const findHeader = () => wrapper.find('header');

View File

@ -1,8 +1,8 @@
import { mount } from '@vue/test-utils';
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', () => {
let wrapper;

View File

@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
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;
const findHeader = () => wrapper.find('header');

View File

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

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