Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
12f988e7dc
commit
94299354d1
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1 +1 @@
|
|||
59d695918d269f35a487c00c7b870aee124b9eaa
|
||||
63abf93ad828f7a7924f3e0bb1fea8ea43d7c6af
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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' };
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
# eslint-disable-next-line @graphql-eslint/require-id-when-available
|
||||
fragment IncidentFields on Issue {
|
||||
severity
|
||||
escalationStatus
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ export default () => {
|
|||
assigneeUsernameQuery,
|
||||
slaFeatureAvailable: parseBoolean(slaFeatureAvailable),
|
||||
canCreateIncident: parseBoolean(canCreateIncident),
|
||||
incidentEscalationsAvailable: parseBoolean(gon?.features?.incidentEscalations),
|
||||
},
|
||||
apolloProvider,
|
||||
render(createElement) {
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -52,6 +52,7 @@ export const DEFAULT_BLOB_INFO = {
|
|||
ideEditPath: '',
|
||||
forkAndEditPath: '',
|
||||
ideForkAndEditPath: '',
|
||||
forkAndViewPath: '',
|
||||
storedExternally: false,
|
||||
externalStorage: '',
|
||||
environmentFormattedExternalUrl: '',
|
||||
|
|
|
@ -31,6 +31,7 @@ query getBlobInfo(
|
|||
ideEditPath
|
||||
forkAndEditPath
|
||||
ideForkAndEditPath
|
||||
forkAndViewPath
|
||||
environmentFormattedExternalUrl
|
||||
environmentExternalUrlForRouteMap
|
||||
canModifyBlob
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.'
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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" } ×
|
||||
.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")
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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'
|
||||
|
|
|
@ -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. |
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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',
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -30,6 +30,15 @@ export const securityTrainingProvidersResponse = {
|
|||
},
|
||||
};
|
||||
|
||||
export const disabledSecurityTrainingProvidersResponse = {
|
||||
data: {
|
||||
project: {
|
||||
id: 1,
|
||||
securityTrainingProviders: [securityTrainingProviders[0]],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const dismissUserCalloutResponse = {
|
||||
data: {
|
||||
userCalloutCreate: {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue