Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
cc1066db64
commit
128d4d89e9
|
@ -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}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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 }),
|
||||
});
|
||||
};
|
|
@ -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>
|
|
@ -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 }),
|
||||
});
|
||||
};
|
|
@ -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>
|
|
@ -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 }),
|
||||
});
|
||||
};
|
|
@ -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>
|
|
@ -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 }),
|
||||
});
|
||||
};
|
|
@ -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 }),
|
||||
});
|
||||
};
|
|
@ -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 }),
|
||||
});
|
||||
};
|
|
@ -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"
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
import init from '~/google_cloud/configuration/index';
|
||||
|
||||
init();
|
|
@ -0,0 +1,3 @@
|
|||
import init from '~/google_cloud/databases/index';
|
||||
|
||||
init();
|
|
@ -0,0 +1,3 @@
|
|||
import init from '~/google_cloud/deployments/index';
|
||||
|
||||
init();
|
|
@ -0,0 +1,3 @@
|
|||
import init from '~/google_cloud/gcp_regions/index';
|
||||
|
||||
init();
|
|
@ -1,3 +0,0 @@
|
|||
import initGoogleCloud from '~/google_cloud/index';
|
||||
|
||||
initGoogleCloud();
|
|
@ -0,0 +1,3 @@
|
|||
import init from '~/google_cloud/service_accounts/index';
|
||||
|
||||
init();
|
|
@ -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 {
|
||||
|
|
|
@ -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:')"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1 +1 @@
|
|||
export const ARTIFACTS_EXPIRED_ERROR_MESSAGE = 'Test report artifacts have expired';
|
||||
export const ARTIFACTS_EXPIRED_ERROR_MESSAGE = 'Test report artifacts not found';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 } }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
|
@ -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 }
|
|
@ -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 }
|
|
@ -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 }
|
|
@ -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 }
|
|
@ -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 }
|
||||
|
|
|
@ -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 }
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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|
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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`,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
|
@ -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');
|
|
@ -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;
|
||||
|
|
@ -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');
|
|
@ -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;
|
||||
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue