Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-02-23 18:16:59 +00:00
parent 12f988e7dc
commit 94299354d1
70 changed files with 907 additions and 147 deletions

View File

@ -71,14 +71,6 @@ GraphQL/OrderedFields:
- app/graphql/types/root_storage_statistics_type.rb
- app/graphql/types/task_completion_status.rb
- app/graphql/types/tree/blob_type.rb
- app/graphql/types/tree/submodule_type.rb
- app/graphql/types/tree/tree_entry_type.rb
- app/graphql/types/user_callout_type.rb
- app/graphql/types/user_status_type.rb
- ee/app/graphql/types/analytics/devops_adoption/snapshot_type.rb
- ee/app/graphql/types/epic_descendant_count_type.rb
- ee/app/graphql/types/epic_descendant_weight_sum_type.rb
- ee/app/graphql/types/epic_health_status_type.rb
- ee/app/graphql/types/epic_type.rb
- ee/app/graphql/types/geo/geo_node_type.rb
- ee/app/graphql/types/requirements_management/requirement_states_count_type.rb

View File

@ -2,6 +2,14 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 14.8.1 (2022-02-23)
### Fixed (3 changes)
- [Allow assigning users with private profiles with quick-actions](gitlab-org/gitlab@be36d36a5a0a3d2f0a3b507acd1ab1762e1b0f34) ([merge request](gitlab-org/gitlab!81398))
- [Stop backup files from requiring directories to exist when skipped](gitlab-org/gitlab@f8002e4fe2c83bf01557020e20be5d29f70b20c6) ([merge request](gitlab-org/gitlab!81398))
- [Fix toolbar buttons in Markdown field](gitlab-org/gitlab@74c058e49eb014662a12247ae5fc5bbfb367af0e) ([merge request](gitlab-org/gitlab!81398))
## 14.8.0 (2022-02-21)
### Added (134 changes)

View File

@ -1 +1 @@
59d695918d269f35a487c00c7b870aee124b9eaa
63abf93ad828f7a7924f3e0bb1fea8ea43d7c6af

View File

@ -4,6 +4,7 @@ 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';
@ -11,6 +12,7 @@ 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: {
@ -34,6 +36,8 @@ export default {
return NoGcpProjects;
case SCREEN_SERVICE_ACCOUNTS_FORM:
return ServiceAccountsForm;
case SCREEN_GCP_REGIONS_FORM:
return GcpRegionsForm;
default:
throw new Error(__('Unknown screen'));
}

View File

@ -0,0 +1,62 @@
<script>
import { GlButton, GlFormGroup, GlFormSelect } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: { GlButton, GlFormGroup, GlFormSelect },
props: {
availableRegions: { required: true, type: Array },
environments: { required: true, type: Array },
cancelPath: { required: true, type: String },
},
i18n: {
title: __('Configure region for environment'),
gcpRegionLabel: __('Region'),
gcpRegionDescription: __('List of suitable GCP locations'),
environmentLabel: __('Environment'),
environmentDescription: __('List of environments for this project'),
submitLabel: __('Configure region'),
cancelLabel: __('Cancel'),
},
};
</script>
<template>
<div>
<header class="gl-my-5 gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid">
<h1 class="gl-font-size-h1">{{ $options.i18n.title }}</h1>
</header>
<gl-form-group
label-for="environment"
:label="$options.i18n.environmentLabel"
:description="$options.i18n.environmentDescription"
>
<gl-form-select id="environment" name="environment" required>
<option value="*">{{ __('All') }}</option>
<option v-for="environment in environments" :key="environment.id" :value="environment.name">
{{ environment.name }}
</option>
</gl-form-select>
</gl-form-group>
<gl-form-group
label-for="gcp_region"
:label="$options.i18n.gcpRegionLabel"
:description="$options.i18n.gcpRegionDescription"
>
<gl-form-select id="gcp_region" name="gcp_region" required :list="availableRegions">
<option v-for="(region, index) in availableRegions" :key="index" :value="region">
{{ region }}
</option>
</gl-form-select>
</gl-form-group>
<div class="form-actions row">
<gl-button type="submit" category="primary" variant="confirm">
{{ $options.i18n.submitLabel }}
</gl-button>
<gl-button class="gl-ml-1" :href="cancelPath">{{ $options.i18n.cancelLabel }}</gl-button>
</div>
</div>
</template>

View File

@ -0,0 +1,56 @@
<script>
import { GlButton, GlEmptyState, GlTable } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: { GlButton, GlEmptyState, GlTable },
props: {
list: {
type: Array,
required: true,
},
createUrl: {
type: String,
required: true,
},
emptyIllustrationUrl: {
type: String,
required: true,
},
},
tableFields: [
{ key: 'environment', label: __('Environment'), sortable: true },
{ key: 'gcp_region', label: __('Region'), sortable: true },
],
i18n: {
emptyStateTitle: __('No regions configured'),
description: __('Configure your environments to be deployed to specific geographical regions'),
emptyStateAction: __('Add a GCP region'),
configureRegions: __('Configure regions'),
listTitle: __('Regions'),
},
};
</script>
<template>
<div>
<gl-empty-state
v-if="list.length === 0"
:title="$options.i18n.emptyStateTitle"
:description="$options.i18n.description"
:primary-button-link="createUrl"
:primary-button-text="$options.i18n.configureRegions"
/>
<div v-else>
<h2 class="gl-font-size-h2">{{ $options.i18n.listTitle }}</h2>
<p>{{ $options.i18n.description }}</p>
<gl-table :items="list" :fields="$options.tableFields" />
<gl-button :href="createUrl" category="primary" variant="info">
{{ $options.i18n.configureRegions }}
</gl-button>
</div>
</div>
</template>

View File

@ -2,6 +2,7 @@
import { GlTabs, GlTab } from '@gitlab/ui';
import DeploymentsServiceTable from './deployments_service_table.vue';
import ServiceAccountsList from './service_accounts_list.vue';
import GcpRegionsList from './gcp_regions_list.vue';
export default {
components: {
@ -9,6 +10,7 @@ export default {
GlTab,
DeploymentsServiceTable,
ServiceAccountsList,
GcpRegionsList,
},
props: {
serviceAccounts: {
@ -19,6 +21,10 @@ export default {
type: String,
required: true,
},
configureGcpRegionsUrl: {
type: String,
required: true,
},
emptyIllustrationUrl: {
type: String,
required: true,
@ -31,6 +37,10 @@ export default {
type: String,
required: true,
},
gcpRegions: {
type: Array,
required: true,
},
},
};
</script>
@ -44,6 +54,13 @@ export default {
:create-url="createServiceAccountUrl"
:empty-illustration-url="emptyIllustrationUrl"
/>
<hr />
<gcp-regions-list
class="gl-mx-4"
:empty-illustration-url="emptyIllustrationUrl"
:create-url="configureGcpRegionsUrl"
:list="gcpRegions"
/>
</gl-tab>
<gl-tab :title="__('Deployments')">
<deployments-service-table

View File

@ -24,9 +24,11 @@ import {
} from '~/vue_shared/components/paginated_table_with_search_and_tabs/constants';
import PaginatedTableWithSearchAndTabs from '~/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import {
I18N,
INCIDENT_STATUS_TABS,
ESCALATION_STATUSES,
TH_CREATED_AT_TEST_ID,
TH_INCIDENT_SLA_TEST_ID,
TH_SEVERITY_TEST_ID,
@ -38,7 +40,7 @@ import {
import getIncidentsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql';
import getIncidents from '../graphql/queries/get_incidents.query.graphql';
const MAX_VISIBLE_ASSIGNEES = 4;
const MAX_VISIBLE_ASSIGNEES = 3;
export default {
trackIncidentCreateNewOptions,
@ -49,7 +51,7 @@ export default {
{
key: 'severity',
label: s__('IncidentManagement|Severity'),
thClass: `${thClass} w-15p`,
thClass: `${thClass} gl-w-15p`,
tdClass: `${tdClass} sortable-cell`,
actualSortKey: 'SEVERITY',
sortable: true,
@ -61,6 +63,12 @@ export default {
thClass: `gl-pointer-events-none`,
tdClass,
},
{
key: 'escalationStatus',
label: s__('IncidentManagement|Status'),
thClass: `${thClass} gl-w-eighth gl-pointer-events-none`,
tdClass,
},
{
key: 'createdAt',
label: s__('IncidentManagement|Date created'),
@ -73,7 +81,7 @@ export default {
{
key: 'incidentSla',
label: s__('IncidentManagement|Time to SLA'),
thClass: `gl-text-right gl-w-eighth`,
thClass: `gl-text-right gl-w-10p`,
tdClass: `${tdClass} gl-text-right`,
thAttr: TH_INCIDENT_SLA_TEST_ID,
actualSortKey: 'SLA_DUE_AT',
@ -83,13 +91,13 @@ export default {
{
key: 'assignees',
label: s__('IncidentManagement|Assignees'),
thClass: 'gl-pointer-events-none w-15p',
thClass: 'gl-pointer-events-none gl-w-15',
tdClass,
},
{
key: 'published',
label: s__('IncidentManagement|Published'),
thClass: `${thClass} w-15p`,
thClass: `${thClass} gl-w-15`,
tdClass: `${tdClass} sortable-cell`,
actualSortKey: 'PUBLISHED',
sortable: true,
@ -112,6 +120,7 @@ export default {
GlEmptyState,
SeverityToken,
PaginatedTableWithSearchAndTabs,
TooltipOnTruncate,
},
directives: {
GlTooltip: GlTooltipDirective,
@ -129,6 +138,7 @@ export default {
'assigneeUsernameQuery',
'slaFeatureAvailable',
'canCreateIncident',
'incidentEscalationsAvailable',
],
apollo: {
incidents: {
@ -222,6 +232,7 @@ export default {
const isHidden = {
published: !this.publishedAvailable,
incidentSla: !this.slaFeatureAvailable,
escalationStatus: !this.incidentEscalationsAvailable,
};
return this.$options.fields.filter(({ key }) => !isHidden[key]);
@ -283,6 +294,9 @@ export default {
getSeverity(severity) {
return INCIDENT_SEVERITY[severity];
},
getEscalationStatus(escalationStatus) {
return ESCALATION_STATUSES[escalationStatus] || this.$options.i18n.noEscalationStatus;
},
pageChanged(pagination) {
this.pagination = pagination;
},
@ -370,7 +384,12 @@ export default {
<template #cell(title)="{ item }">
<div :class="{ 'gl-display-flex gl-align-items-center': item.state === 'closed' }">
<div class="gl-max-w-full text-truncate" :title="item.title">{{ item.title }}</div>
<tooltip-on-truncate
:title="item.title"
class="gl-max-w-full gl-text-truncate gl-display-block"
>
{{ item.title }}
</tooltip-on-truncate>
<gl-icon
v-if="item.state === 'closed'"
name="issue-close"
@ -381,8 +400,21 @@ export default {
</div>
</template>
<template v-if="incidentEscalationsAvailable" #cell(escalationStatus)="{ item }">
<tooltip-on-truncate
:title="getEscalationStatus(item.escalationStatus)"
data-testid="incident-escalation-status"
class="gl-display-block gl-text-truncate"
>
{{ getEscalationStatus(item.escalationStatus) }}
</tooltip-on-truncate>
</template>
<template #cell(createdAt)="{ item }">
<time-ago-tooltip :time="item.createdAt" />
<time-ago-tooltip
:time="item.createdAt"
class="gl-display-block gl-max-w-full gl-text-truncate"
/>
</template>
<template v-if="slaFeatureAvailable" #cell(incidentSla)="{ item }">
@ -392,6 +424,7 @@ export default {
:project-path="projectPath"
:sla-due-at="item.slaDueAt"
data-testid="incident-sla"
class="gl-display-block gl-max-w-full gl-text-truncate"
/>
</template>
@ -432,6 +465,7 @@ export default {
:un-published="$options.i18n.unPublished"
/>
</template>
<template #table-busy>
<gl-loading-icon size="lg" color="dark" class="mt-3" />
</template>

View File

@ -7,6 +7,7 @@ export const I18N = {
unassigned: s__('IncidentManagement|Unassigned'),
createIncidentBtnLabel: s__('IncidentManagement|Create incident'),
unPublished: s__('IncidentManagement|Unpublished'),
noEscalationStatus: s__('IncidentManagement|None'),
emptyState: {
title: s__('IncidentManagement|Display your incidents in a dedicated view'),
emptyClosedTabTitle: s__('IncidentManagement|There are no closed incidents'),
@ -37,6 +38,12 @@ export const INCIDENT_STATUS_TABS = [
},
];
export const ESCALATION_STATUSES = {
TRIGGERED: s__('AlertManagement|Triggered'),
ACKNOWLEDGED: s__('AlertManagement|Acknowledged'),
RESOLVED: s__('AlertManagement|Resolved'),
};
export const DEFAULT_PAGE_SIZE = 20;
export const TH_CREATED_AT_TEST_ID = { 'data-testid': 'incident-management-created-at-sort' };
export const TH_SEVERITY_TEST_ID = { 'data-testid': 'incident-management-severity-sort' };

View File

@ -1,4 +1,5 @@
# eslint-disable-next-line @graphql-eslint/require-id-when-available
fragment IncidentFields on Issue {
severity
escalationStatus
}

View File

@ -46,6 +46,7 @@ export default () => {
assigneeUsernameQuery,
slaFeatureAvailable: parseBoolean(slaFeatureAvailable),
canCreateIncident: parseBoolean(canCreateIncident),
incidentEscalationsAvailable: parseBoolean(gon?.features?.incidentEscalations),
},
apolloProvider,
render(createElement) {

View File

@ -120,15 +120,6 @@ const bindHowToImport = () => {
});
});
});
$('.how_to_import_link').on('click', (e) => {
e.preventDefault();
$(e.currentTarget).next('.modal').show();
});
$('.modal-header .close').on('click', () => {
$('.modal').hide();
});
};
const bindEvents = () => {

View File

@ -7,6 +7,8 @@ import getRefMixin from '../mixins/get_ref';
import DeleteBlobModal from './delete_blob_modal.vue';
import UploadBlobModal from './upload_blob_modal.vue';
const REPLACE_BLOB_MODAL_ID = 'modal-replace-blob';
export default {
i18n: {
replace: __('Replace'),
@ -76,9 +78,6 @@ export default {
},
},
computed: {
replaceModalId() {
return uniqueId('replace-modal');
},
replaceModalTitle() {
return sprintf(__('Replace %{name}'), { name: this.name });
},
@ -95,13 +94,14 @@ export default {
methods: {
showModal(modalId) {
if (this.showForkSuggestion) {
this.$emit('fork');
this.$emit('fork', 'view');
return;
}
this.$refs[modalId].show();
},
},
replaceBlobModalId: REPLACE_BLOB_MODAL_ID,
};
</script>
@ -118,7 +118,7 @@ export default {
data-testid="lock"
:data-qa-selector="lockBtnQASelector"
/>
<gl-button data-testid="replace" @click="showModal(replaceModalId)">
<gl-button data-testid="replace" @click="showModal($options.replaceBlobModalId)">
{{ $options.i18n.replace }}
</gl-button>
<gl-button data-testid="delete" @click="showModal(deleteModalId)">
@ -126,8 +126,8 @@ export default {
</gl-button>
</gl-button-group>
<upload-blob-modal
:ref="replaceModalId"
:modal-id="replaceModalId"
:ref="$options.replaceBlobModalId"
:modal-id="$options.replaceBlobModalId"
:modal-title="replaceModalTitle"
:commit-message="replaceModalTitle"
:target-branch="targetBranch || ref"

View File

@ -142,9 +142,13 @@ export default {
return this.isLoggedIn && !canModifyBlob && createMergeRequestIn && forkProject;
},
forkPath() {
return this.forkTarget === 'ide'
? this.blobInfo.ideForkAndEditPath
: this.blobInfo.forkAndEditPath;
const forkPaths = {
ide: this.blobInfo.ideForkAndEditPath,
simple: this.blobInfo.forkAndEditPath,
view: this.blobInfo.forkAndViewPath,
};
return forkPaths[this.forkTarget];
},
isUsingLfs() {
return this.blobInfo.storedExternally && this.blobInfo.externalStorage === LFS_STORAGE;
@ -249,7 +253,7 @@ export default {
:is-locked="Boolean(pathLockedByUser)"
:can-lock="canLock"
:show-fork-suggestion="showForkSuggestion"
@fork="setForkTarget('ide')"
@fork="setForkTarget('view')"
/>
</template>
</blob-header>

View File

@ -87,7 +87,7 @@ export default {
fields: {
// fields key must match case of form name for validation directive to work
commit_message: initFormField({ value: this.commitMessage }),
branch_name: initFormField({ value: this.targetBranch }),
branch_name: initFormField({ value: this.targetBranch, skipValidation: !this.canPushCode }),
},
};
return {

View File

@ -52,6 +52,7 @@ export const DEFAULT_BLOB_INFO = {
ideEditPath: '',
forkAndEditPath: '',
ideForkAndEditPath: '',
forkAndViewPath: '',
storedExternally: false,
externalStorage: '',
environmentFormattedExternalUrl: '',

View File

@ -31,6 +31,7 @@ query getBlobInfo(
ideEditPath
forkAndEditPath
ideForkAndEditPath
forkAndViewPath
environmentFormattedExternalUrl
environmentExternalUrlForRouteMap
canModifyBlob

View File

@ -1,9 +1,11 @@
<script>
import { GlIcon } from '@gitlab/ui';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
export default {
components: {
GlIcon,
TooltipOnTruncate,
},
props: {
severity: {
@ -30,13 +32,15 @@ export default {
<template>
<div
class="incident-severity gl-display-inline-flex gl-align-items-center gl-justify-content-between"
class="incident-severity gl-display-inline-flex gl-align-items-center gl-justify-content-between gl-max-w-full"
>
<gl-icon
:size="iconSize"
:name="`severity-${severity.icon}`"
:class="[`icon-${severity.icon}`, { 'gl-mr-3': !iconOnly }]"
/>
<span v-if="!iconOnly">{{ severity.label }}</span>
<tooltip-on-truncate v-if="!iconOnly" :title="severity.label" class="gl-text-truncate">{{
severity.label
}}</tooltip-on-truncate>
</div>
</template>

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
class Projects::GoogleCloud::GcpRegionsController < Projects::GoogleCloud::BaseController
# filtered list of GCP cloud run locations...
# ...that have domain mapping available
# 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
def index
@google_cloud_path = project_google_cloud_index_path(project)
@js_data = {
screen: 'gcp_regions_form',
availableRegions: AVAILABLE_REGIONS,
environments: project.environments,
cancelPath: project_google_cloud_index_path(project)
}.to_json
end
def create
permitted_params = params.permit(:environment, :gcp_region)
GoogleCloud::GcpRegionAddOrReplaceService.new(project).execute(permitted_params[:environment], permitted_params[:gcp_region])
redirect_to project_google_cloud_index_path(project), notice: _('GCP region configured')
end
end

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
class Projects::GoogleCloudController < Projects::GoogleCloud::BaseController
GCP_REGION_CI_VAR_KEY = 'GCP_REGION'
def index
@js_data = {
screen: 'home',
@ -8,7 +10,16 @@ class Projects::GoogleCloudController < Projects::GoogleCloud::BaseController
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')
emptyIllustrationUrl: ActionController::Base.helpers.image_path('illustrations/pipelines_empty.svg'),
configureGcpRegionsUrl: project_google_cloud_gcp_regions_path(project),
gcpRegions: gcp_regions
}.to_json
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
end

View File

@ -6,6 +6,9 @@ class Projects::IncidentsController < Projects::ApplicationController
before_action :authorize_read_issue!
before_action :load_incident, only: [:show]
before_action do
push_frontend_feature_flag(:incident_escalations, @project)
end
feature_category :incident_management

View File

@ -41,6 +41,9 @@ module Types
field :ide_fork_and_edit_path, GraphQL::Types::String, null: true,
description: 'Web path to edit this blob in the Web IDE using a forked project.'
field :fork_and_view_path, GraphQL::Types::String, null: true,
description: 'Web path to view this blob using a forked project.'
field :size, GraphQL::Types::Int, null: true,
description: 'Size (in bytes) of the blob.'

View File

@ -8,10 +8,10 @@ module Types
implements Types::Tree::EntryType
field :web_url, type: GraphQL::Types::String, null: true,
description: 'Web URL for the sub-module.'
field :tree_url, type: GraphQL::Types::String, null: true,
description: 'Tree URL for the sub-module.'
field :web_url, type: GraphQL::Types::String, null: true,
description: 'Web URL for the sub-module.'
end
# rubocop: enable Graphql/AuthorizeTypes
end

View File

@ -10,10 +10,10 @@ module Types
implements Types::Tree::EntryType
present_using TreeEntryPresenter
field :web_url, GraphQL::Types::String, null: true,
description: 'Web URL for the tree entry (directory).'
field :web_path, GraphQL::Types::String, null: true,
description: 'Web path for the tree entry (directory).'
field :web_url, GraphQL::Types::String, null: true,
description: 'Web URL for the tree entry (directory).'
end
# rubocop: enable Graphql/AuthorizeTypes
end

View File

@ -4,9 +4,9 @@ module Types
class UserCalloutType < BaseObject # rubocop:disable Graphql/AuthorizeTypes
graphql_name 'UserCallout'
field :feature_name, UserCalloutFeatureNameEnum, null: true,
description: 'Name of the feature that the callout is for.'
field :dismissed_at, Types::TimeType, null: true,
description: 'Date when the callout was dismissed.'
field :feature_name, UserCalloutFeatureNameEnum, null: true,
description: 'Name of the feature that the callout is for.'
end
end

View File

@ -7,11 +7,11 @@ module Types
markdown_field :message_html, null: true,
description: 'HTML of the user status message'
field :message, GraphQL::Types::String, null: true,
description: 'User status message.'
field :emoji, GraphQL::Types::String, null: true,
description: 'String representation of emoji.'
field :availability, Types::AvailabilityEnum, null: false,
description: 'User availability status.'
field :emoji, GraphQL::Types::String, null: true,
description: 'String representation of emoji.'
field :message, GraphQL::Types::String, null: true,
description: 'User status message.'
end
end

View File

@ -431,19 +431,26 @@ module ProjectsHelper
end
def import_from_bitbucket_message
link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path("integration/bitbucket") }
configure_oauth_import_message('Bitbucket', help_page_path("integration/bitbucket"))
end
str = if current_user.admin?
'ImportProjects|To enable importing projects from Bitbucket, as administrator you need to configure %{link_start}OAuth integration%{link_end}'
else
'ImportProjects|To enable importing projects from Bitbucket, ask your GitLab administrator to configure %{link_start}OAuth integration%{link_end}'
end
s_(str).html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
def import_from_gitlab_message
configure_oauth_import_message('GitLab.com', help_page_path("integration/gitlab"))
end
private
def configure_oauth_import_message(provider, help_url)
str = if current_user.admin?
'ImportProjects|To enable importing projects from %{provider}, as administrator you need to configure %{link_start}OAuth integration%{link_end}'
else
'ImportProjects|To enable importing projects from %{provider}, ask your GitLab administrator to configure %{link_start}OAuth integration%{link_end}'
end
link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_url }
s_(str).html_safe % { provider: provider, link_start: link_start, link_end: '</a>'.html_safe }
end
def tab_ability_map
{
cycle_analytics: :read_cycle_analytics,

View File

@ -3,6 +3,8 @@
module DeploymentPlatform
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def deployment_platform(environment: nil)
return if Feature.disabled?(:certificate_based_clusters, default_enabled: :yaml, type: :ops)
@deployment_platform ||= {}
@deployment_platform[environment] ||= find_deployment_platform(environment)

View File

@ -103,6 +103,10 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
fork_path_for_current_user(project, ide_edit_path)
end
def fork_and_view_path
fork_path_for_current_user(project, web_path)
end
def can_modify_blob?
super(blob, project, blob.commit_id)
end

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
module GoogleCloud
class GcpRegionAddOrReplaceService < ::BaseService
def execute(environment, region)
gcp_region_key = Projects::GoogleCloudController::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 } }
existing_variable = ::Ci::VariablesFinder.new(project, filter_params).execute.first
if existing_variable
change_params[:action] = :update
change_params[:variable] = existing_variable
else
change_params[:action] = :create
end
::Ci::ChangeVariableService.new(container: project, current_user: current_user, params: change_params).execute
end
end
end

View File

@ -1,14 +0,0 @@
#gitlab_import_modal.modal
.modal-dialog
.modal-content
.modal-header
%h3.modal-title Import projects from GitLab.com
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": "true" } &times;
.modal-body
To enable importing projects from GitLab.com,
- if current_user.admin?
as administrator you need to configure
- else
ask your GitLab administrator to configure
= link_to 'OAuth integration', help_page_path("integration/gitlab")

View File

@ -36,12 +36,11 @@
%div
- if gitlab_import_enabled?
%div
= link_to status_import_gitlab_path, class: "gl-button btn-default btn import_gitlab js-import-project-btn #{'how_to_import_link' unless gitlab_import_configured?}", data: { platform: 'gitlab_com', **tracking_attrs_data(track_label, 'click_button', 'gitlab_com') } do
= link_to status_import_gitlab_path, class: "gl-button btn-default btn import_gitlab js-import-project-btn #{'js-how-to-import-link' unless gitlab_import_configured?}",
data: { modal_title: _("Import projects from GitLab.com"), modal_message: import_from_gitlab_message, platform: 'gitlab_com', **tracking_attrs_data(track_label, 'click_button', 'gitlab_com') } do
.gl-button-icon
= sprite_icon('tanuki')
= _("GitLab.com")
- unless gitlab_import_configured?
= render 'projects/gitlab_import_modal'
- if fogbugz_import_enabled?
%div

View File

@ -0,0 +1,8 @@
- add_to_breadcrumbs _('Google Cloud'), @google_cloud_path
- breadcrumb_title _('Regions')
- page_title _('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 }

View File

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/352156
milestone: '14.8'
type: development
group: group::source code
default_enabled: false
default_enabled: true

View File

@ -0,0 +1,8 @@
---
name: certificate_based_clusters
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81054
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353410
milestone: '14.9'
type: ops
group: group::configure
default_enabled: true

View File

@ -320,6 +320,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
namespace :google_cloud do
resources :service_accounts, only: [:index, :create]
resources :gcp_regions, only: [:index, :create]
get '/deployments/cloud_run', to: 'deployments#cloud_run'
get '/deployments/cloud_storage', to: 'deployments#cloud_storage'

View File

@ -14990,6 +14990,7 @@ Returns [`Tree`](#tree).
| <a id="repositoryblobfiletype"></a>`fileType` | [`String`](#string) | Expected format of the blob based on the extension. |
| <a id="repositoryblobfindfilepath"></a>`findFilePath` | [`String`](#string) | Web path to find file. |
| <a id="repositoryblobforkandeditpath"></a>`forkAndEditPath` | [`String`](#string) | Web path to edit this blob using a forked project. |
| <a id="repositoryblobforkandviewpath"></a>`forkAndViewPath` | [`String`](#string) | Web path to view this blob using a forked project. |
| <a id="repositoryblobhistorypath"></a>`historyPath` | [`String`](#string) | Web path to blob history page. |
| <a id="repositoryblobid"></a>`id` | [`ID!`](#id) | ID of the blob. |
| <a id="repositoryblobideeditpath"></a>`ideEditPath` | [`String`](#string) | Web path to edit this blob in the Web IDE. |

View File

@ -253,6 +253,26 @@ class ValidateTextLimitMigration < Gitlab::Database::Migration[1.0]
end
```
## Increasing a text limit constraint on an existing column
Increasing text limits on existing database columns can be safely achieved by first adding the new limit (with a different name),
and then dropping the previous limit:
```ruby
class ChangeMaintainerNoteLimitInCiRunner < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
def up
add_text_limit :ci_runners, :maintainer_note, 1024, constraint_name: check_constraint_name(:ci_runners, :maintainer_note, 'max_length_1MB')
remove_text_limit :ci_runners, :maintainer_note, constraint_name: check_constraint_name(:ci_runners, :maintainer_note, 'max_length')
end
def down
# no-op: Danger of failing if there are records with length(maintainer_note) > 255
end
end
```
## Text limit constraints on large tables
If you have to clean up a text column for a really [large table](https://gitlab.com/gitlab-org/gitlab/-/blob/master/rubocop/rubocop-migrations.yml#L3)

View File

@ -166,6 +166,18 @@ end
Since Active Record is not calling the `.new` method on model classes to instantiate the objects,
you should use `expect_next_found_instance_of` or `allow_next_found_instance_of` mock helpers to setup mock on objects returned by Active Record query & finder methods._
It is also possible to set mocks and expectations for multiple instances of the same Active Record model by using the `expect_next_found_(number)_instances_of` and `allow_next_found_(number)_instances_of` helpers, like so;
```ruby
expect_next_found_2_instances_of(Project) do |project|
expect(project).to receive(:add_import_job)
end
allow_next_found_2_instances_of(Project) do |project|
allow(project).to receive(:add_import_job)
end
```
If we also want to initialize the instance with some particular arguments, we
could also pass it like:

View File

@ -518,3 +518,12 @@ Neither problem is present if we create a custom negatable matcher because the `
would be used, which would wait only as long as necessary for the job to disappear.
Lastly, negatable matchers are preferred over using matchers of the form `have_no_*` because it's a common and familiar practice to negate matchers using `not_to`. If we facilitate that practice by adding negatable matchers, we make it easier for subsequent test authors to write efficient tests.
## Use logger over puts
We currently use Rails `logger` to handle logs in both GitLab QA application and end-to-end tests.
This provides additional functionalities when compared with `puts`, such as:
- Ability to specify the logging level.
- Ability to tag similar logs.
- Auto-formatting log messages.

View File

@ -4,7 +4,7 @@ group: Runner
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Install GitLab Runner with a cluster management project
# Install GitLab Runner with a cluster management project **(FREE)**
> [Introduced](https://gitlab.com/gitlab-org/project-templates/cluster-management/-/merge_requests/5) in GitLab 14.0.

View File

@ -32,8 +32,7 @@ imported into the GitLab issue's description as plain text.
Our parser for converting text in Jira issues to GitLab Flavored Markdown is only compatible with
Jira V3 REST API.
There is an [epic](https://gitlab.com/groups/gitlab-org/-/epics/2738) tracking the addition of
items, such as issue assignees, comments, and much more. These are included in the future
There is an [epic](https://gitlab.com/groups/gitlab-org/-/epics/2738) tracking the addition of issue assignees, comments, and much more in the future
iterations of the GitLab Jira importer.
## Prerequisites
@ -60,7 +59,7 @@ Importing large projects may take several minutes depending on the size of the i
To import Jira issues to a GitLab project:
1. On the **{issues}** **Issues** page, click **Import Issues** (**{import}**) **> Import from Jira**.
1. On the **{issues}** **Issues** page, select **Import Issues** (**{import}**) **> Import from Jira**.
![Import issues from Jira button](img/jira/import_issues_from_jira_button_v12_10.png)
@ -68,20 +67,20 @@ To import Jira issues to a GitLab project:
The following form appears.
If you've previously set up the [Jira integration](../../../integration/jira/index.md), you can now see
the Jira projects that you have access to in the dropdown.
the Jira projects that you have access to in the dropdown list.
![Import issues from Jira form](img/jira/import_issues_from_jira_form_v13_2.png)
1. Click the **Import from** dropdown and select the Jira project that you wish to import issues from.
1. Click the **Import from** dropdown list and select the Jira project that you wish to import issues from.
In the **Jira-GitLab user mapping template** section, the table shows to which GitLab users your Jira
users are mapped.
When the form appears, the dropdown defaults to the user conducting the import.
When the form appears, the dropdown list defaults to the user conducting the import.
1. To change any of the mappings, click the dropdown in the **GitLab username** column and
1. To change any of the mappings, click the dropdown list in the **GitLab username** column and
select the user you want to map to each Jira user.
The dropdown may not show all the users, so use the search bar to find a specific
The dropdown list may not show all the users, so use the search bar to find a specific
user in this GitLab project.
1. Click **Continue**. You're presented with a confirmation that import has started.

View File

@ -160,7 +160,7 @@ To regenerate the email address:
### Using a URL with prefilled values
> Ability to use both `issuable_template` and `issue[description]` in the same URL [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/340529) in GitLab 14.8.
> Ability to use both `issuable_template` and `issue[description]` in the same URL [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80554) in GitLab 14.9.
To link directly to the new issue page with prefilled fields, use query
string parameters in a URL. You can embed a URL in an external

View File

@ -26,7 +26,7 @@ using the GitLab default only if no customizations are set:
1. A [project-specific](#change-the-default-branch-name-for-a-project) custom default branch name.
1. A [subgroup-level](#group-level-custom-initial-branch-name) custom default branch name.
1. A [group-level](#group-level-custom-initial-branch-name) custom default branch name.
1. An [instance-level](#instance-level-custom-initial-branch-name) custom default branch name. **(FREE SELF)**
1. An [instance-level](#instance-level-custom-initial-branch-name) custom default branch name.
1. If no custom default branch name is set at any level, GitLab defaults to:
- `main`: Projects created with GitLab 14.0 or later.
- `master`: Projects created before GitLab 14.0.

View File

@ -96,9 +96,7 @@ Prerequisite:
1. Select **Update now** (**{retry}**):
![Repository mirroring force update user interface](img/repository_mirroring_force_update.png)
## Mirror only protected branches **(PREMIUM)**
> Moved to GitLab Premium in 13.9.
## Mirror only protected branches
You can choose to mirror only the
[protected branches](../../protected_branches.md) in the mirroring project,

View File

@ -10,14 +10,12 @@ module Gitlab
transformed_for_diff(new_blob, old_blob)
Gitlab::AppLogger.info({ message: 'IPYNB_DIFF_GENERATED' })
end
rescue IpynbDiff::InvalidNotebookError => e
rescue IpynbDiff::InvalidNotebookError, IpynbDiff::InvalidTokenError => e
Gitlab::ErrorTracking.log_exception(e)
nil
end
def transformed_diff(before, after)
Gitlab::AppLogger.info({ message: 'IPYNB_DIFF_GENERATED' })
transformed_diff = IpynbDiff.diff(before, after,
raise_if_invalid_nb: true,
diffy_opts: { include_diff_info: true }).to_s(:text)

View File

@ -57,7 +57,7 @@ module Gitlab
IpynbDiff.diff(source_diff.old_blob&.data, source_diff.new_blob&.data,
raise_if_invalid_nb: true,
diffy_opts: { include_diff_info: true })
rescue IpynbDiff::InvalidNotebookError => e
rescue IpynbDiff::InvalidNotebookError, IpynbDiff::InvalidTokenError => e
Gitlab::ErrorTracking.log_exception(e)
nil
end

View File

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

View File

@ -2055,6 +2055,9 @@ msgstr ""
msgid "Add a %{type}"
msgstr ""
msgid "Add a GCP region"
msgstr ""
msgid "Add a GPG key"
msgstr ""
@ -9194,6 +9197,15 @@ msgstr ""
msgid "Configure pipelines to deploy web apps, backend services, APIs and static resources to Google Cloud"
msgstr ""
msgid "Configure region"
msgstr ""
msgid "Configure region for environment"
msgstr ""
msgid "Configure regions"
msgstr ""
msgid "Configure repository mirroring."
msgstr ""
@ -9227,6 +9239,9 @@ msgstr ""
msgid "Configure which lists are shown for anyone who visits this board"
msgstr ""
msgid "Configure your environments to be deployed to specific geographical regions"
msgstr ""
msgid "Confirm"
msgstr ""
@ -15895,6 +15910,9 @@ msgstr ""
msgid "Full name"
msgstr ""
msgid "GCP region configured"
msgstr ""
msgid "GPG Key ID:"
msgstr ""
@ -19262,6 +19280,9 @@ msgstr ""
msgid "IncidentManagement|No incidents to display."
msgstr ""
msgid "IncidentManagement|None"
msgstr ""
msgid "IncidentManagement|Open"
msgstr ""
@ -19280,6 +19301,9 @@ msgstr ""
msgid "IncidentManagement|Severity"
msgstr ""
msgid "IncidentManagement|Status"
msgstr ""
msgid "IncidentManagement|There are no closed incidents"
msgstr ""
@ -21984,6 +22008,12 @@ msgstr ""
msgid "List of all merge commits"
msgstr ""
msgid "List of environments for this project"
msgstr ""
msgid "List of suitable GCP locations"
msgstr ""
msgid "List of users allowed to exceed the rate limit."
msgstr ""
@ -24747,6 +24777,9 @@ msgstr ""
msgid "No ref selected"
msgstr ""
msgid "No regions configured"
msgstr ""
msgid "No related merge requests found."
msgstr ""
@ -30079,6 +30112,12 @@ msgstr ""
msgid "Regex pattern"
msgstr ""
msgid "Region"
msgstr ""
msgid "Regions"
msgstr ""
msgid "Register"
msgstr ""

View File

@ -32,6 +32,8 @@ module QA
def run
failures = files.flat_map do |file|
resources = read_file(file)
next if resources.nil?
filtered_resources = filter_resources(resources)
delete_resources(filtered_resources)
end
@ -45,21 +47,24 @@ module QA
private
def files
puts "Gathering JSON files...\n"
Runtime::Logger.info('Gathering JSON files...')
files = Dir.glob(@file_pattern)
abort("There is no file with this pattern #{@file_pattern}") if files.empty?
files.reject { |file| File.zero?(file) }
files.reject! { |file| File.zero?(file) }
files
end
def read_file(file)
JSON.parse(File.read(file))
rescue JSON::ParserError
Runtime::Logger.error("Failed to read #{file} - Invalid format")
nil
end
def filter_resources(resources)
puts "Filtering resources - Only keep deletable resources...\n"
Runtime::Logger.info('Filtering resources - Only keep deletable resources...')
transformed_values = resources.transform_values! do |v|
v.reject do |attributes|
@ -73,19 +78,20 @@ module QA
end
def delete_resources(resources)
Runtime::Logger.info('Nothing to delete.') && return if resources.nil?
resources.each_with_object([]) do |(key, value), failures|
value.each do |resource|
next if resource_not_found?(resource['api_path'])
msg = resource['info'] ? "#{key} - #{resource['info']}" : "#{key} at #{resource['api_path']}"
puts "\nDeleting #{msg}..."
resource_info = resource['info'] ? "#{key} - #{resource['info']}" : "#{key} at #{resource['api_path']}"
delete_response = delete(Runtime::API::Request.new(@api_client, resource['api_path']).url)
if delete_response.code == 202 || delete_response.code == 204
print "\e[32m.\e[0m"
Runtime::Logger.info("Deleting #{resource_info}... SUCCESS")
else
print "\e[31mF\e[0m"
failures << msg
Runtime::Logger.info("Deleting #{resource_info}... FAILED")
failures << resource_info
end
end
end

View File

@ -43,6 +43,7 @@ RSpec.describe Projects::IncidentsController do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index)
expect(Gon.features).to include('incidentEscalations' => true)
end
context 'when user is unauthorized' do

View File

@ -34,5 +34,28 @@ RSpec.describe 'Incident Management index', :js do
it 'alert page title' do
expect(page).to have_content('Incidents')
end
it 'has expected columns' do
table = page.find('.gl-table')
expect(table).to have_content('Severity')
expect(table).to have_content('Incident')
expect(table).to have_content('Status')
expect(table).to have_content('Date created')
expect(table).to have_content('Assignees')
end
context 'when :incident_escalations feature is disabled' do
before do
stub_feature_flags(incident_escalations: false)
end
it 'does not include the Status columns' do
visit project_incidents_path(project)
wait_for_requests
expect(page.find('.gl-table')).not_to have_content('Status')
end
end
end
end

View File

@ -15,7 +15,6 @@ RSpec.describe 'Projects > Files > User deletes files', :js do
let(:user) { create(:user) }
before do
stub_feature_flags(refactor_blob_viewer: false) # This stub will be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/349953
sign_in(user)
end

View File

@ -17,7 +17,6 @@ RSpec.describe 'Projects > Files > User replaces files', :js do
let(:user) { create(:user) }
before do
stub_feature_flags(refactor_blob_viewer: false) # This stub will be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/349953
sign_in(user)
end
@ -34,9 +33,9 @@ RSpec.describe 'Projects > Files > User replaces files', :js do
expect(page).to have_content('.gitignore')
click_on('Replace')
drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'))
find(".upload-dropzone-card").drop(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'))
page.within('#modal-upload-blob') do
page.within('#modal-replace-blob') do
fill_in(:commit_message, with: 'Replacement file commit message')
end
@ -70,9 +69,9 @@ RSpec.describe 'Projects > Files > User replaces files', :js do
expect(page).to have_content(fork_message)
click_on('Replace')
drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'))
find(".upload-dropzone-card").drop(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'))
page.within('#modal-upload-blob') do
page.within('#modal-replace-blob') do
fill_in(:commit_message, with: 'Replacement file commit message')
end

View File

@ -405,46 +405,62 @@ RSpec.describe 'New project', :js do
end
end
context 'from Bitbucket', :js do
shared_examples 'has a link to bitbucket cloud' do
context 'when bitbucket is not configured' do
before do
allow(Gitlab::Auth::OAuth::Provider).to receive(:enabled?).and_call_original
allow(Gitlab::Auth::OAuth::Provider)
.to receive(:enabled?).with(:bitbucket)
.and_return(false)
shared_examples 'has instructions to enable OAuth' do
context 'when OAuth is not configured' do
before do
sign_in(user)
visit new_project_path
click_link 'Import project'
click_link 'Bitbucket Cloud'
end
allow(Gitlab::Auth::OAuth::Provider).to receive(:enabled?).and_call_original
allow(Gitlab::Auth::OAuth::Provider)
.to receive(:enabled?).with(provider)
.and_return(false)
it 'shows import instructions' do
expect(find('.modal-body')).to have_content(bitbucket_link_content)
end
visit new_project_path
click_link 'Import project'
click_link target_link
end
it 'shows import instructions' do
expect(find('.modal-body')).to have_content(oauth_config_instructions)
end
end
end
context 'from Bitbucket', :js do
let(:target_link) { 'Bitbucket Cloud' }
let(:provider) { :bitbucket }
context 'as a user' do
let(:user) { create(:user) }
let(:bitbucket_link_content) { 'To enable importing projects from Bitbucket, ask your GitLab administrator to configure OAuth integration' }
let(:oauth_config_instructions) { 'To enable importing projects from Bitbucket, ask your GitLab administrator to configure OAuth integration' }
before do
sign_in(user)
end
it_behaves_like 'has a link to bitbucket cloud'
it_behaves_like 'has instructions to enable OAuth'
end
context 'as an admin' do
let(:user) { create(:admin) }
let(:bitbucket_link_content) { 'To enable importing projects from Bitbucket, as administrator you need to configure OAuth integration' }
let(:oauth_config_instructions) { 'To enable importing projects from Bitbucket, as administrator you need to configure OAuth integration' }
before do
sign_in(user)
end
it_behaves_like 'has instructions to enable OAuth'
end
end
it_behaves_like 'has a link to bitbucket cloud'
context 'from GitLab.com', :js do
let(:target_link) { 'GitLab.com' }
let(:provider) { :gitlab }
context 'as a user' do
let(:user) { create(:user) }
let(:oauth_config_instructions) { 'To enable importing projects from GitLab.com, ask your GitLab administrator to configure OAuth integration' }
it_behaves_like 'has instructions to enable OAuth'
end
context 'as an admin' do
let(:user) { create(:admin) }
let(:oauth_config_instructions) { 'To enable importing projects from GitLab.com, as administrator you need to configure OAuth integration' }
it_behaves_like 'has instructions to enable OAuth'
end
end
end

View File

@ -22,7 +22,9 @@ const SERVICE_ACCOUNTS_FORM_PROPS = {
};
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',

View File

@ -0,0 +1,59 @@
import { shallowMount } from '@vue/test-utils';
import { GlButton, GlFormGroup, GlFormSelect } from '@gitlab/ui';
import GcpRegionsForm from '~/google_cloud/components/gcp_regions_form.vue';
describe('GcpRegionsForm component', () => {
let wrapper;
const findHeader = () => wrapper.find('header');
const findAllFormGroups = () => wrapper.findAllComponents(GlFormGroup);
const findAllFormSelects = () => wrapper.findAllComponents(GlFormSelect);
const findAllButtons = () => wrapper.findAllComponents(GlButton);
const propsData = { availableRegions: [], environments: [], cancelPath: '#cancel-url' };
beforeEach(() => {
wrapper = shallowMount(GcpRegionsForm, { propsData });
});
afterEach(() => {
wrapper.destroy();
});
it('contains header', () => {
expect(findHeader().exists()).toBe(true);
});
it('contains Regions form group', () => {
const formGroup = findAllFormGroups().at(0);
expect(formGroup.exists()).toBe(true);
});
it('contains Regions dropdown', () => {
const select = findAllFormSelects().at(0);
expect(select.exists()).toBe(true);
});
it('contains Environments form group', () => {
const formGroup = findAllFormGroups().at(1);
expect(formGroup.exists()).toBe(true);
});
it('contains Environments dropdown', () => {
const select = findAllFormSelects().at(1);
expect(select.exists()).toBe(true);
});
it('contains Submit button', () => {
const button = findAllButtons().at(0);
expect(button.exists()).toBe(true);
expect(button.text()).toBe(GcpRegionsForm.i18n.submitLabel);
});
it('contains Cancel button', () => {
const button = findAllButtons().at(1);
expect(button.exists()).toBe(true);
expect(button.text()).toBe(GcpRegionsForm.i18n.cancelLabel);
expect(button.attributes('href')).toBe('#cancel-url');
});
});

View File

@ -0,0 +1,79 @@
import { mount } from '@vue/test-utils';
import { GlButton, GlEmptyState, GlTable } from '@gitlab/ui';
import GcpRegionsList from '~/google_cloud/components/gcp_regions_list.vue';
describe('GcpRegions component', () => {
describe('when the project does not have any configured regions', () => {
let wrapper;
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findButtonInEmptyState = () => findEmptyState().findComponent(GlButton);
beforeEach(() => {
const propsData = {
list: [],
createUrl: '#create-url',
emptyIllustrationUrl: '#empty-illustration-url',
};
wrapper = mount(GcpRegionsList, { propsData });
});
afterEach(() => {
wrapper.destroy();
});
it('shows the empty state component', () => {
expect(findEmptyState().exists()).toBe(true);
});
it('shows the link to create new service accounts', () => {
const button = findButtonInEmptyState();
expect(button.exists()).toBe(true);
expect(button.text()).toBe('Configure regions');
expect(button.attributes('href')).toBe('#create-url');
});
});
describe('when three gcp regions are passed via props', () => {
let wrapper;
const findTitle = () => wrapper.find('h2');
const findDescription = () => wrapper.find('p');
const findTable = () => wrapper.findComponent(GlTable);
const findRows = () => findTable().findAll('tr');
const findButton = () => wrapper.findComponent(GlButton);
beforeEach(() => {
const propsData = {
list: [{}, {}, {}],
createUrl: '#create-url',
emptyIllustrationUrl: '#empty-illustration-url',
};
wrapper = mount(GcpRegionsList, { propsData });
});
it('shows the title', () => {
expect(findTitle().text()).toBe('Regions');
});
it('shows the description', () => {
expect(findDescription().text()).toBe(
'Configure your environments to be deployed to specific geographical regions',
);
});
it('shows the table', () => {
expect(findTable().exists()).toBe(true);
});
it('table must have three rows + header row', () => {
expect(findRows()).toHaveLength(4);
});
it('shows the link to create new service accounts', () => {
const button = findButton();
expect(button.exists()).toBe(true);
expect(button.text()).toBe('Configure regions');
expect(button.attributes('href')).toBe('#create-url');
});
});
});

View File

@ -18,7 +18,9 @@ describe('google_cloud Home component', () => {
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',

View File

@ -48,6 +48,7 @@ describe('Incidents List', () => {
const findClosedIcon = () => wrapper.findAll("[data-testid='incident-closed']");
const findEmptyState = () => wrapper.find(GlEmptyState);
const findSeverity = () => wrapper.findAll(SeverityToken);
const findEscalationStatus = () => wrapper.findAll('[data-testid="incident-escalation-status"]');
function mountComponent({ data = {}, loading = false, provide = {} } = {}) {
wrapper = mount(IncidentsList, {
@ -80,6 +81,7 @@ describe('Incidents List', () => {
assigneeUsernameQuery: '',
slaFeatureAvailable: true,
canCreateIncident: true,
incidentEscalationsAvailable: true,
...provide,
},
stubs: {
@ -184,6 +186,34 @@ describe('Incidents List', () => {
expect(findSeverity().length).toBe(mockIncidents.length);
});
describe('Escalation status', () => {
it('renders escalation status per row', () => {
expect(findEscalationStatus().length).toBe(mockIncidents.length);
const actualStatuses = findEscalationStatus().wrappers.map((status) => status.text());
expect(actualStatuses).toEqual([
'Triggered',
'Acknowledged',
'Resolved',
I18N.noEscalationStatus,
]);
});
describe('when feature is disabled', () => {
beforeEach(() => {
mountComponent({
data: { incidents: { list: mockIncidents }, incidentsCount },
provide: { incidentEscalationsAvailable: false },
loading: false,
});
});
it('is absent if feature flag is disabled', () => {
expect(findEscalationStatus().length).toBe(0);
});
});
});
it('contains a link to the incident details page', async () => {
findTableRows().at(0).trigger('click');
expect(visitUrl).toHaveBeenCalledWith(

View File

@ -7,6 +7,7 @@
"assignees": {},
"state": "opened",
"severity": "CRITICAL",
"escalationStatus": "TRIGGERED",
"slaDueAt": "2020-06-04T12:46:08Z"
},
{
@ -26,6 +27,7 @@
},
"state": "opened",
"severity": "HIGH",
"escalationStatus": "ACKNOWLEDGED",
"slaDueAt": null
},
{
@ -35,7 +37,8 @@
"createdAt": "2020-05-19T08:53:55Z",
"assignees": {},
"state": "closed",
"severity": "LOW"
"severity": "LOW",
"escalationStatus": "RESOLVED"
},
{
"id": 4,
@ -44,6 +47,7 @@
"createdAt": "2020-05-18T17:13:35Z",
"assignees": {},
"state": "closed",
"severity": "MEDIUM"
"severity": "MEDIUM",
"escalationStatus": null
}
]

View File

@ -12,6 +12,7 @@ export const simpleViewerMock = {
ideEditPath: 'some_file.js/ide/edit',
forkAndEditPath: 'some_file.js/fork/edit',
ideForkAndEditPath: 'some_file.js/fork/ide',
forkAndViewPath: 'some_file.js/fork/view',
environmentFormattedExternalUrl: '',
environmentExternalUrlForRouteMap: '',
canModifyBlob: true,

View File

@ -30,6 +30,15 @@ export const securityTrainingProvidersResponse = {
},
};
export const disabledSecurityTrainingProvidersResponse = {
data: {
project: {
id: 1,
securityTrainingProviders: [securityTrainingProviders[0]],
},
},
};
export const dismissUserCalloutResponse = {
data: {
userCalloutCreate: {

View File

@ -42,6 +42,7 @@ RSpec.describe Types::Repository::BlobType do
:external_storage_url,
:fork_and_edit_path,
:ide_fork_and_edit_path,
:fork_and_view_path,
:language
)
end

View File

@ -1027,7 +1027,7 @@ RSpec.describe ProjectsHelper do
end
end
describe '#import_from_bitbucket_message' do
shared_examples 'configure import method modal' do
before do
allow(helper).to receive(:current_user).and_return(user)
end
@ -1036,7 +1036,7 @@ RSpec.describe ProjectsHelper do
it 'returns a link to contact an administrator' do
allow(user).to receive(:admin?).and_return(false)
expect(helper.import_from_bitbucket_message).to have_text('To enable importing projects from Bitbucket, ask your GitLab administrator to configure OAuth integration')
expect(subject).to have_text("To enable importing projects from #{import_method}, ask your GitLab administrator to configure OAuth integration")
end
end
@ -1044,8 +1044,24 @@ RSpec.describe ProjectsHelper do
it 'returns a link to configure bitbucket' do
allow(user).to receive(:admin?).and_return(true)
expect(helper.import_from_bitbucket_message).to have_text('To enable importing projects from Bitbucket, as administrator you need to configure OAuth integration')
expect(subject).to have_text("To enable importing projects from #{import_method}, as administrator you need to configure OAuth integration")
end
end
end
describe '#import_from_bitbucket_message' do
let(:import_method) { 'Bitbucket' }
subject { helper.import_from_bitbucket_message }
it_behaves_like 'configure import method modal'
end
describe '#import_from_gitlab_message' do
let(:import_method) { 'GitLab.com' }
subject { helper.import_from_gitlab_message }
it_behaves_like 'configure import method modal'
end
end

View File

@ -238,14 +238,34 @@ RSpec.describe 'Auto-DevOps.gitlab-ci.yml' do
end
it_behaves_like 'pipeline with Kubernetes jobs'
context 'when certificate_based_clusters FF is disabled' do
before do
stub_feature_flags(certificate_based_clusters: false)
end
it 'does not include production job' do
expect(build_names).not_to include('production')
end
end
end
context 'when project has an Agent is present' do
context 'when project has an Agent' do
before do
create(:cluster_agent, project: project)
end
it_behaves_like 'pipeline with Kubernetes jobs'
context 'when certificate_based_clusters FF is disabled' do
before do
stub_feature_flags(certificate_based_clusters: false)
end
it 'includes production job' do
expect(build_names).to include('production')
end
end
end
end

View File

@ -12,16 +12,28 @@ RSpec.describe DeploymentPlatform do
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
shared_examples 'certificate_based_clusters is disabled' do
before do
stub_feature_flags(certificate_based_clusters: false)
end
it { is_expected.to be_nil }
end
shared_examples 'matching environment scope' do
it 'returns environment specific cluster' do
is_expected.to eq(cluster.platform_kubernetes)
end
it_behaves_like 'certificate_based_clusters is disabled'
end
shared_examples 'not matching environment scope' do
it 'returns default cluster' do
is_expected.to eq(default_cluster.platform_kubernetes)
end
it_behaves_like 'certificate_based_clusters is disabled'
end
context 'multiple clusters use the same management project' do

View File

@ -0,0 +1,93 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::GoogleCloud::GcpRegionsController do
let_it_be(:project) { create(:project, :public) }
RSpec.shared_examples "should be not found" do
it 'returns not found' do
is_expected.to be(404)
end
end
RSpec.shared_examples "should be forbidden" do
it 'returns forbidden' do
is_expected.to be(403)
end
end
RSpec.shared_examples "public request should 404" do
it_behaves_like "should be not found"
end
RSpec.shared_examples "unauthorized access should 404" do
let(:user_guest) { create(:user) }
before do
project.add_guest(user_guest)
end
it_behaves_like "should be not found"
end
describe 'GET #index' do
subject { get project_google_cloud_gcp_regions_path(project) }
it_behaves_like "public request should 404"
it_behaves_like "unauthorized access should 404"
context 'when authorized members make requests' do
let(:user_maintainer) { create(:user) }
before do
project.add_maintainer(user_maintainer)
sign_in(user_maintainer)
end
it 'renders gcp_regions' do
is_expected.to render_template('projects/google_cloud/gcp_regions/index')
end
context 'but gitlab instance is not configured for google oauth2' do
before do
unconfigured_google_oauth2 = Struct.new(:app_id, :app_secret)
.new('', '')
allow(Gitlab::Auth::OAuth::Provider).to receive(:config_for)
.with('google_oauth2')
.and_return(unconfigured_google_oauth2)
end
it_behaves_like "should be forbidden"
end
context 'but feature flag is disabled' do
before do
stub_feature_flags(incubation_5mp_google_cloud: false)
end
it_behaves_like "should be not found"
end
end
end
describe 'POST #index' do
subject { post project_google_cloud_gcp_regions_path(project), params: { gcp_region: 'region1', environment: 'env1' } }
it_behaves_like "public request should 404"
it_behaves_like "unauthorized access should 404"
context 'when authorized members make requests' do
let(:user_maintainer) { create(:user) }
before do
project.add_maintainer(user_maintainer)
sign_in(user_maintainer)
end
it 'redirects to google cloud index' do
is_expected.to redirect_to(project_google_cloud_index_path(project))
end
end
end
end

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GoogleCloud::GcpRegionAddOrReplaceService do
it 'adds and replaces GCP region vars' do
project = create(:project, :public)
service = described_class.new(project)
service.execute('env_1', 'loc_1')
service.execute('env_2', 'loc_2')
service.execute('env_1', 'loc_3')
list = project.variables.reload.filter { |variable| variable.key == Projects::GoogleCloudController::GCP_REGION_CI_VAR_KEY }
list = list.sort_by(&:environment_scope)
aggregate_failures 'testing list of gcp regions' do
expect(list.length).to eq(2)
# asserting that the first region is replaced
expect(list.first.environment_scope).to eq('env_1')
expect(list.first.value).to eq('loc_3')
expect(list.second.environment_scope).to eq('env_2')
expect(list.second.value).to eq('loc_2')
end
end
end

View File

@ -2,19 +2,36 @@
module NextFoundInstanceOf
ERROR_MESSAGE = 'NextFoundInstanceOf mock helpers can only be used with ActiveRecord targets'
HELPER_METHOD_PATTERN = /(?:allow|expect)_next_found_(?<number>\d+)_instances_of/.freeze
def expect_next_found_instance_of(klass)
def method_missing(method_name, ...)
return super unless match_data = method_name.match(HELPER_METHOD_PATTERN)
helper_method = method_name.to_s.sub("_#{match_data[:number]}", '')
public_send(helper_method, *args, match_data[:number].to_i, &block)
end
def expect_next_found_instance_of(klass, &block)
expect_next_found_instances_of(klass, nil, &block)
end
def expect_next_found_instances_of(klass, number)
check_if_active_record!(klass)
stub_allocate(expect(klass), klass) do |expectation|
stub_allocate(expect(klass), klass, number) do |expectation|
yield(expectation)
end
end
def allow_next_found_instance_of(klass)
def allow_next_found_instance_of(klass, &block)
allow_next_found_instances_of(klass, nil, &block)
end
def allow_next_found_instances_of(klass, number)
check_if_active_record!(klass)
stub_allocate(allow(klass), klass) do |allowance|
stub_allocate(allow(klass), klass, number) do |allowance|
yield(allowance)
end
end
@ -25,8 +42,11 @@ module NextFoundInstanceOf
raise ArgumentError, ERROR_MESSAGE unless klass < ActiveRecord::Base
end
def stub_allocate(target, klass)
target.to receive(:allocate).and_wrap_original do |method|
def stub_allocate(target, klass, number)
stub = receive(:allocate)
stub.exactly(number).times if number
target.to stub.and_wrap_original do |method|
method.call.tap do |allocation|
# ActiveRecord::Core.allocate returns a frozen object:
# https://github.com/rails/rails/blob/291a3d2ef29a3842d1156ada7526f4ee60dd2b59/activerecord/lib/active_record/core.rb#L620