Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
17ef30f3df
commit
3c86701bc8
|
@ -38,7 +38,7 @@
|
|||
needs: ["setup-test-env", "retrieve-tests-metadata", "compile-test-assets", "detect-tests"]
|
||||
script:
|
||||
- !reference [.base-script, script]
|
||||
- rspec_paralellized_job "--tag ~quarantine --tag ~geo --tag ~level:migration"
|
||||
- rspec_paralellized_job "--tag ~quarantine --tag ~level:migration"
|
||||
|
||||
.base-artifacts:
|
||||
artifacts:
|
||||
|
@ -61,7 +61,7 @@
|
|||
- .rails:rules:ee-and-foss-migration
|
||||
script:
|
||||
- !reference [.base-script, script]
|
||||
- rspec_paralellized_job "--tag ~quarantine --tag ~geo --tag level:migration"
|
||||
- rspec_paralellized_job "--tag ~quarantine --tag level:migration"
|
||||
|
||||
.rspec-base-pg11:
|
||||
extends:
|
||||
|
@ -123,33 +123,6 @@
|
|||
- .rspec-base
|
||||
- .use-pg13-ee
|
||||
|
||||
.rspec-ee-base-geo:
|
||||
extends: .rspec-base
|
||||
script:
|
||||
- !reference [.base-script, script]
|
||||
- rspec_paralellized_job "--tag ~quarantine --tag geo"
|
||||
|
||||
.rspec-ee-base-geo-pg11:
|
||||
extends:
|
||||
- .rspec-ee-base-geo
|
||||
- .use-pg11-ee
|
||||
|
||||
.rspec-ee-base-geo-pg12:
|
||||
extends:
|
||||
- .rspec-ee-base-geo
|
||||
- .use-pg12-ee
|
||||
|
||||
.rspec-jh-base-geo-pg12:
|
||||
extends:
|
||||
- .rspec-jh-base-pg12
|
||||
script:
|
||||
- !reference [.rspec-ee-base-geo, script]
|
||||
|
||||
.rspec-ee-base-geo-pg13:
|
||||
extends:
|
||||
- .rspec-ee-base-geo
|
||||
- .use-pg13-ee
|
||||
|
||||
.db-job-base:
|
||||
extends:
|
||||
- .rails-job-base
|
||||
|
@ -172,10 +145,7 @@
|
|||
parallel: 22
|
||||
|
||||
.rspec-ee-unit-parallel:
|
||||
parallel: 14
|
||||
|
||||
.rspec-ee-unit-geo-parallel:
|
||||
parallel: 2
|
||||
parallel: 16
|
||||
|
||||
.rspec-integration-parallel:
|
||||
parallel: 10
|
||||
|
@ -522,9 +492,6 @@ rspec:deprecations:
|
|||
- rspec-ee unit pg12
|
||||
- rspec-ee integration pg12
|
||||
- rspec-ee system pg12
|
||||
- rspec-ee unit pg12 geo
|
||||
- rspec-ee integration pg12 geo
|
||||
- rspec-ee system pg12 geo
|
||||
variables:
|
||||
SETUP_DB: "false"
|
||||
script:
|
||||
|
@ -576,14 +543,6 @@ rspec:coverage:
|
|||
- rspec-ee unit pg12 single-db
|
||||
- rspec-ee integration pg12 single-db
|
||||
- rspec-ee system pg12 single-db
|
||||
# Geo jobs
|
||||
- rspec-ee unit pg12 geo
|
||||
- rspec-ee integration pg12 geo
|
||||
- rspec-ee system pg12 geo
|
||||
# Geo minimal jobs
|
||||
- rspec-ee unit pg12 geo minimal
|
||||
- rspec-ee integration pg12 geo minimal
|
||||
- rspec-ee system pg12 geo minimal
|
||||
# Memory jobs
|
||||
- memory-on-boot
|
||||
# As-if-FOSS jobs
|
||||
|
@ -878,40 +837,6 @@ rspec-ee system pg12 single-db:
|
|||
- .single-db-rspec
|
||||
- .rails:rules:single-db
|
||||
|
||||
rspec-ee unit pg12 geo:
|
||||
extends:
|
||||
- .rspec-ee-base-geo-pg12
|
||||
- .rails:rules:ee-only-unit
|
||||
- .rspec-ee-unit-geo-parallel
|
||||
|
||||
rspec-ee unit pg12 geo minimal:
|
||||
extends:
|
||||
- rspec-ee unit pg12 geo
|
||||
- .minimal-rspec-tests
|
||||
- .rails:rules:ee-only-unit:minimal
|
||||
|
||||
rspec-ee integration pg12 geo:
|
||||
extends:
|
||||
- .rspec-ee-base-geo-pg12
|
||||
- .rails:rules:ee-only-integration
|
||||
|
||||
rspec-ee integration pg12 geo minimal:
|
||||
extends:
|
||||
- rspec-ee integration pg12 geo
|
||||
- .minimal-rspec-tests
|
||||
- .rails:rules:ee-only-integration:minimal
|
||||
|
||||
rspec-ee system pg12 geo:
|
||||
extends:
|
||||
- .rspec-ee-base-geo-pg12
|
||||
- .rails:rules:ee-only-system
|
||||
|
||||
rspec-ee system pg12 geo minimal:
|
||||
extends:
|
||||
- rspec-ee system pg12 geo
|
||||
- .minimal-rspec-tests
|
||||
- .rails:rules:ee-only-system:minimal
|
||||
|
||||
rspec-ee migration pg12-as-if-jh:
|
||||
extends:
|
||||
- .rspec-jh-base-pg12
|
||||
|
@ -937,22 +862,6 @@ rspec-ee system pg12-as-if-jh:
|
|||
- .rails:rules:as-if-jh-rspec
|
||||
- .rspec-ee-system-parallel
|
||||
|
||||
rspec-ee unit pg12-as-if-jh geo:
|
||||
extends:
|
||||
- .rspec-jh-base-geo-pg12
|
||||
- .rails:rules:as-if-jh-rspec
|
||||
- .rspec-ee-unit-geo-parallel
|
||||
|
||||
rspec-ee integration pg12-as-if-jh geo:
|
||||
extends:
|
||||
- .rspec-jh-base-geo-pg12
|
||||
- .rails:rules:as-if-jh-rspec
|
||||
|
||||
rspec-ee system pg12-as-if-jh geo:
|
||||
extends:
|
||||
- .rspec-jh-base-geo-pg12
|
||||
- .rails:rules:as-if-jh-rspec
|
||||
|
||||
rspec-jh migration pg12-as-if-jh:
|
||||
extends:
|
||||
- .rspec-jh-base-pg12
|
||||
|
@ -974,21 +883,6 @@ rspec-jh system pg12-as-if-jh:
|
|||
- .rspec-jh-base-pg12
|
||||
- .rails:rules:as-if-jh-rspec
|
||||
|
||||
rspec-jh unit pg12-as-if-jh geo:
|
||||
extends:
|
||||
- .rspec-jh-base-geo-pg12
|
||||
- .rails:rules:as-if-jh-rspec
|
||||
|
||||
rspec-jh integration pg12-as-if-jh geo:
|
||||
extends:
|
||||
- .rspec-jh-base-geo-pg12
|
||||
- .rails:rules:as-if-jh-rspec
|
||||
|
||||
rspec-jh system pg12-as-if-jh geo:
|
||||
extends:
|
||||
- .rspec-jh-base-geo-pg12
|
||||
- .rails:rules:as-if-jh-rspec
|
||||
|
||||
db:rollback geo:
|
||||
extends:
|
||||
- db:rollback
|
||||
|
@ -1086,22 +980,6 @@ rspec-ee system pg11:
|
|||
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
|
||||
- .rspec-ee-system-parallel
|
||||
|
||||
rspec-ee unit pg11 geo:
|
||||
extends:
|
||||
- .rspec-ee-base-geo-pg11
|
||||
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
|
||||
- .rspec-ee-unit-geo-parallel
|
||||
|
||||
rspec-ee integration pg11 geo:
|
||||
extends:
|
||||
- .rspec-ee-base-geo-pg11
|
||||
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
|
||||
|
||||
rspec-ee system pg11 geo:
|
||||
extends:
|
||||
- .rspec-ee-base-geo-pg11
|
||||
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
|
||||
|
||||
# PG13
|
||||
rspec-ee migration pg13:
|
||||
extends:
|
||||
|
@ -1127,22 +1005,6 @@ rspec-ee system pg13:
|
|||
- .rspec-ee-base-pg13
|
||||
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
|
||||
- .rspec-ee-system-parallel
|
||||
|
||||
rspec-ee unit pg13 geo:
|
||||
extends:
|
||||
- .rspec-ee-base-geo-pg13
|
||||
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
|
||||
- .rspec-ee-unit-geo-parallel
|
||||
|
||||
rspec-ee integration pg13 geo:
|
||||
extends:
|
||||
- .rspec-ee-base-geo-pg13
|
||||
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
|
||||
|
||||
rspec-ee system pg13 geo:
|
||||
extends:
|
||||
- .rspec-ee-base-geo-pg13
|
||||
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
|
||||
# EE: default branch nightly scheduled jobs #
|
||||
#####################################
|
||||
|
||||
|
|
|
@ -152,4 +152,6 @@ knapsack-report:
|
|||
- .generate-knapsack-report-base
|
||||
stage: post-qa
|
||||
variables:
|
||||
QA_KNAPSACK_REPORT_FILE_PATTERN: $CI_PROJECT_DIR/tmp/knapsack/*/*.json
|
||||
# knapsack report upload uses gitlab-qa image with code already there
|
||||
GIT_STRATEGY: none
|
||||
QA_KNAPSACK_REPORT_FILE_PATTERN: $CI_PROJECT_DIR/qa/tmp/knapsack/*/*.json
|
||||
|
|
|
@ -38,9 +38,6 @@ update-tests-metadata:
|
|||
- rspec-ee unit pg12
|
||||
- rspec-ee integration pg12
|
||||
- rspec-ee system pg12
|
||||
- rspec-ee unit pg12 geo
|
||||
- rspec-ee integration pg12 geo
|
||||
- rspec-ee system pg12 geo
|
||||
script:
|
||||
- run_timed_command "retry gem install fog-aws mime-types activesupport rspec_profiling postgres-copy --no-document"
|
||||
- source ./scripts/rspec_helpers.sh
|
||||
|
|
|
@ -164,7 +164,6 @@ RSpec/AnyInstanceOf:
|
|||
- spec/controllers/snippets/notes_controller_spec.rb
|
||||
- spec/controllers/snippets_controller_spec.rb
|
||||
- spec/features/admin/admin_mode/login_spec.rb
|
||||
- spec/features/groups/clusters/eks_spec.rb
|
||||
- spec/features/groups/members/tabs_spec.rb
|
||||
- spec/features/ide/static_object_external_storage_csp_spec.rb
|
||||
- spec/features/issuables/issuable_list_spec.rb
|
||||
|
|
|
@ -1 +1 @@
|
|||
372599313791cb92e579e0ff02279f33cbcd71b5
|
||||
1f98d5a94c880e3e556ae3ace095f83e44f002fb
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { GlLink, GlSprintf } from '@gitlab/ui';
|
||||
import { mapState } from 'vuex';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
import { s__ } from '~/locale';
|
||||
|
||||
export default {
|
||||
|
@ -10,13 +10,11 @@ export default {
|
|||
'ClusterIntegration|Enter details about your cluster. %{linkStart}How do I use a certificate to connect to my cluster?%{linkEnd}',
|
||||
),
|
||||
},
|
||||
clusterConnectHelpPath: helpPagePath('user/project/clusters/add_existing_cluster'),
|
||||
components: {
|
||||
GlLink,
|
||||
GlSprintf,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['clusterConnectHelpPath']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -26,7 +24,7 @@ export default {
|
|||
<p>
|
||||
<gl-sprintf :message="$options.i18n.information">
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="clusterConnectHelpPath" target="_blank">{{ content }}</gl-link>
|
||||
<gl-link :href="$options.clusterConnectHelpPath">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
|
|
|
@ -159,7 +159,7 @@ export default {
|
|||
)
|
||||
}}</span>
|
||||
<template #modal-footer>
|
||||
<gl-button variant="secondary" @click="handleCancel">{{ __('Cancel') }}</gl-button>
|
||||
<gl-button @click="handleCancel">{{ __('Cancel') }}</gl-button>
|
||||
<template v-if="confirmCleanup">
|
||||
<gl-button
|
||||
:disabled="!canSubmit"
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
import Vue from 'vue';
|
||||
import NewCluster from './components/new_cluster.vue';
|
||||
import { createStore } from './stores/new_cluster';
|
||||
|
||||
export default () => {
|
||||
const entryPoint = document.querySelector('#js-cluster-new');
|
||||
const el = document.querySelector('#js-cluster-new');
|
||||
|
||||
if (!entryPoint) {
|
||||
if (!el) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Vue({
|
||||
el: '#js-cluster-new',
|
||||
store: createStore(entryPoint.dataset),
|
||||
el,
|
||||
render(createElement) {
|
||||
return createElement(NewCluster);
|
||||
},
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import state from './state';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export const createStore = (initialState) =>
|
||||
new Vuex.Store({
|
||||
state: state(initialState),
|
||||
});
|
||||
|
||||
export default createStore;
|
|
@ -1,3 +0,0 @@
|
|||
export default (initialState = {}) => ({
|
||||
clusterConnectHelpPath: initialState.clusterConnectHelpPath,
|
||||
});
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlDropdown, GlDropdownItem, GlModalDirective, GlTooltip } from '@gitlab/ui';
|
||||
import { GlButton, GlDropdown, GlDropdownItem, GlModalDirective, GlTooltip } from '@gitlab/ui';
|
||||
|
||||
import { INSTALL_AGENT_MODAL_ID, CLUSTERS_ACTIONS } from '../constants';
|
||||
|
||||
|
@ -7,6 +7,7 @@ export default {
|
|||
i18n: CLUSTERS_ACTIONS,
|
||||
INSTALL_AGENT_MODAL_ID,
|
||||
components: {
|
||||
GlButton,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlTooltip,
|
||||
|
@ -15,7 +16,6 @@ export default {
|
|||
GlModalDirective,
|
||||
},
|
||||
inject: [
|
||||
'newClusterPath',
|
||||
'addClusterPath',
|
||||
'newClusterDocsPath',
|
||||
'canAddCluster',
|
||||
|
@ -42,20 +42,59 @@ export default {
|
|||
}
|
||||
return this.addClusterPath;
|
||||
},
|
||||
actionItems() {
|
||||
const createCluster = {
|
||||
href: this.newClusterDocsPath,
|
||||
title: this.$options.i18n.createCluster,
|
||||
testid: 'create-cluster-link',
|
||||
};
|
||||
const connectCluster = {
|
||||
href: this.addClusterPath,
|
||||
title: this.$options.i18n.connectClusterCertificate,
|
||||
testid: 'connect-cluster-link',
|
||||
};
|
||||
const actions = [];
|
||||
|
||||
if (this.displayClusterAgents) {
|
||||
actions.push(createCluster);
|
||||
}
|
||||
if (this.displayClusterAgents && this.certificateBasedClustersEnabled) {
|
||||
actions.push(connectCluster);
|
||||
}
|
||||
|
||||
return actions;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getTooltipTarget() {
|
||||
return this.actionItems.length ? this.$refs.actions.$el : this.$refs.actionsContainer;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="nav-controls gl-ml-auto">
|
||||
<div ref="actionsContainer" class="nav-controls gl-ml-auto">
|
||||
<gl-tooltip
|
||||
v-if="!canAddCluster"
|
||||
:target="() => $refs.dropdown.$el"
|
||||
:title="$options.i18n.dropdownDisabledHint"
|
||||
:target="() => getTooltipTarget()"
|
||||
:title="$options.i18n.actionsDisabledHint"
|
||||
/>
|
||||
|
||||
<gl-button
|
||||
v-if="!actionItems.length"
|
||||
data-qa-selector="clusters_actions_button"
|
||||
category="primary"
|
||||
variant="confirm"
|
||||
:disabled="!canAddCluster"
|
||||
:href="defaultActionUrl"
|
||||
>
|
||||
{{ defaultActionText }}
|
||||
</gl-button>
|
||||
|
||||
<gl-dropdown
|
||||
ref="dropdown"
|
||||
v-else
|
||||
ref="actions"
|
||||
v-gl-modal-directive="shouldTriggerModal && $options.INSTALL_AGENT_MODAL_ID"
|
||||
data-qa-selector="clusters_actions_button"
|
||||
category="primary"
|
||||
|
@ -67,31 +106,13 @@ export default {
|
|||
right
|
||||
>
|
||||
<gl-dropdown-item
|
||||
v-if="displayClusterAgents"
|
||||
:href="newClusterDocsPath"
|
||||
data-testid="create-cluster-link"
|
||||
v-for="action in actionItems"
|
||||
:key="action.title"
|
||||
:href="action.href"
|
||||
:data-testid="action.testid"
|
||||
@click.stop
|
||||
>
|
||||
{{ $options.i18n.createCluster }}
|
||||
</gl-dropdown-item>
|
||||
|
||||
<template v-if="displayClusterAgents && certificateBasedClustersEnabled">
|
||||
<gl-dropdown-item :href="newClusterPath" data-testid="new-cluster-link" @click.stop>
|
||||
{{ $options.i18n.createClusterCertificate }}
|
||||
</gl-dropdown-item>
|
||||
|
||||
<gl-dropdown-item :href="addClusterPath" data-testid="connect-cluster-link" @click.stop>
|
||||
{{ $options.i18n.connectClusterCertificate }}
|
||||
</gl-dropdown-item>
|
||||
</template>
|
||||
|
||||
<gl-dropdown-item
|
||||
v-if="certificateBasedClustersEnabled && !displayClusterAgents"
|
||||
:href="newClusterPath"
|
||||
data-testid="new-cluster-link"
|
||||
@click.stop
|
||||
>
|
||||
{{ $options.i18n.createClusterDeprecated }}
|
||||
{{ action.title }}
|
||||
</gl-dropdown-item>
|
||||
</gl-dropdown>
|
||||
</div>
|
||||
|
|
|
@ -234,11 +234,9 @@ export const CLUSTERS_ACTIONS = {
|
|||
connectCluster: s__('ClusterAgents|Connect a cluster'),
|
||||
connectWithAgent: s__('ClusterAgents|Connect a cluster (agent)'),
|
||||
connectClusterDeprecated: s__('ClusterAgents|Connect a cluster (deprecated)'),
|
||||
createClusterDeprecated: s__('ClusterAgents|Create a cluster (deprecated)'),
|
||||
createCluster: s__('ClusterAgents|Create a cluster'),
|
||||
createClusterCertificate: s__('ClusterAgents|Create a cluster (certificate - deprecated)'),
|
||||
connectClusterCertificate: s__('ClusterAgents|Connect a cluster (certificate - deprecated)'),
|
||||
dropdownDisabledHint: s__(
|
||||
actionsDisabledHint: s__(
|
||||
'ClusterAgents|Requires a Maintainer or greater role to perform these actions',
|
||||
),
|
||||
};
|
||||
|
|
|
@ -23,7 +23,6 @@ export default () => {
|
|||
defaultBranchName,
|
||||
projectPath,
|
||||
kasAddress,
|
||||
newClusterPath,
|
||||
addClusterPath,
|
||||
newClusterDocsPath,
|
||||
emptyStateHelpText,
|
||||
|
@ -42,7 +41,6 @@ export default () => {
|
|||
emptyStateImage,
|
||||
projectPath,
|
||||
kasAddress,
|
||||
newClusterPath,
|
||||
addClusterPath,
|
||||
newClusterDocsPath,
|
||||
emptyStateHelpText,
|
||||
|
|
|
@ -1,246 +0,0 @@
|
|||
<script>
|
||||
import { GlIcon } from '@gitlab/ui';
|
||||
import $ from 'jquery';
|
||||
import { isNil } from 'lodash';
|
||||
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
|
||||
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
|
||||
import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
|
||||
|
||||
const toArray = (value) => (isNil(value) ? [] : [].concat(value));
|
||||
const itemsProp = (items, prop) => items.map((item) => item[prop]);
|
||||
const defaultSearchFn = (searchQuery, labelProp) => (item) =>
|
||||
item[labelProp].toLowerCase().indexOf(searchQuery) > -1;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DropdownButton,
|
||||
DropdownSearchInput,
|
||||
DropdownHiddenInput,
|
||||
GlIcon,
|
||||
},
|
||||
props: {
|
||||
fieldName: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
defaultValue: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
value: {
|
||||
type: [Object, Array, String],
|
||||
required: false,
|
||||
default: () => null,
|
||||
},
|
||||
labelProperty: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'name',
|
||||
},
|
||||
valueProperty: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'value',
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
loadingText: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
disabledText: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
hasErrors: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
errorMessage: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
searchFieldPlaceholder: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
emptyText: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
searchFn: {
|
||||
type: Function,
|
||||
required: false,
|
||||
default: defaultSearchFn,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchQuery: '',
|
||||
focusOnSearch: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
toggleText() {
|
||||
if (this.loading && this.loadingText) {
|
||||
return this.loadingText;
|
||||
}
|
||||
|
||||
if (this.disabled && this.disabledText) {
|
||||
return this.disabledText;
|
||||
}
|
||||
|
||||
if (!this.selectedItems.length) {
|
||||
return this.placeholder;
|
||||
}
|
||||
|
||||
return this.selectedItemsLabels;
|
||||
},
|
||||
results() {
|
||||
return this.getItemsOrEmptyList().filter(this.searchFn(this.searchQuery, this.labelProperty));
|
||||
},
|
||||
selectedItems() {
|
||||
const valueProp = this.valueProperty;
|
||||
const valueList = toArray(this.value);
|
||||
const items = this.getItemsOrEmptyList();
|
||||
|
||||
return items.filter((item) => valueList.some((value) => item[valueProp] === value));
|
||||
},
|
||||
selectedItemsLabels() {
|
||||
return itemsProp(this.selectedItems, this.labelProperty).join(', ');
|
||||
},
|
||||
selectedItemsValues() {
|
||||
return itemsProp(this.selectedItems, this.valueProperty).join(', ');
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
$(this.$refs.dropdown)
|
||||
.on('shown.bs.dropdown', () => {
|
||||
this.focusOnSearch = true;
|
||||
})
|
||||
.on('hidden.bs.dropdown', () => {
|
||||
this.focusOnSearch = false;
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
// eslint-disable-next-line @gitlab/no-global-event-off
|
||||
$(this.$refs.dropdown).off();
|
||||
},
|
||||
methods: {
|
||||
getItemsOrEmptyList() {
|
||||
return this.items || [];
|
||||
},
|
||||
selectSingle(item) {
|
||||
this.$emit('input', item[this.valueProperty]);
|
||||
},
|
||||
selectMultiple(item) {
|
||||
const value = toArray(this.value);
|
||||
const itemValue = item[this.valueProperty];
|
||||
const itemValueIndex = value.indexOf(itemValue);
|
||||
|
||||
if (itemValueIndex > -1) {
|
||||
value.splice(itemValueIndex, 1);
|
||||
} else {
|
||||
value.push(itemValue);
|
||||
}
|
||||
|
||||
this.$emit('input', value);
|
||||
},
|
||||
isSelected(item) {
|
||||
return this.selectedItems.includes(item);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div ref="dropdown" class="dropdown">
|
||||
<dropdown-hidden-input :name="fieldName" :value="selectedItemsValues" />
|
||||
<dropdown-button
|
||||
:class="{ 'border-danger': hasErrors }"
|
||||
:is-disabled="disabled"
|
||||
:is-loading="loading"
|
||||
:toggle-text="toggleText"
|
||||
/>
|
||||
<div class="dropdown-menu dropdown-select">
|
||||
<dropdown-search-input
|
||||
v-model="searchQuery"
|
||||
:focused="focusOnSearch"
|
||||
:placeholder-text="searchFieldPlaceholder"
|
||||
/>
|
||||
<div class="dropdown-content">
|
||||
<ul>
|
||||
<li v-if="!results.length">
|
||||
<span class="js-empty-text menu-item">{{ emptyText }}</span>
|
||||
</li>
|
||||
<li v-for="item in results" :key="item.id">
|
||||
<button
|
||||
v-if="multiple"
|
||||
class="js-dropdown-item d-flex align-items-center"
|
||||
type="button"
|
||||
@click.stop.prevent="selectMultiple(item)"
|
||||
>
|
||||
<gl-icon
|
||||
:class="[{ invisible: !isSelected(item) }, 'mr-1']"
|
||||
name="mobile-issue-close"
|
||||
/>
|
||||
<slot name="item" :item="item">{{ item.name }}</slot>
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="js-dropdown-item"
|
||||
type="button"
|
||||
@click.prevent="selectSingle(item)"
|
||||
>
|
||||
<slot name="item" :item="item">{{ item.name }}</slot>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
v-if="hasErrors && errorMessage"
|
||||
:class="[
|
||||
'form-text js-eks-dropdown-error-message',
|
||||
{
|
||||
'text-danger': hasErrors,
|
||||
'text-muted': !hasErrors,
|
||||
},
|
||||
]"
|
||||
>{{ errorMessage }}</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
|
@ -1,58 +0,0 @@
|
|||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import EksClusterConfigurationForm from './eks_cluster_configuration_form.vue';
|
||||
import ServiceCredentialsForm from './service_credentials_form.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ServiceCredentialsForm,
|
||||
EksClusterConfigurationForm,
|
||||
},
|
||||
props: {
|
||||
gitlabManagedClusterHelpPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
namespacePerEnvironmentHelpPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
kubernetesIntegrationHelpPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
accountAndExternalIdsHelpPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
createRoleArnHelpPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
externalLinkIcon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['hasCredentials']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="js-create-eks-cluster">
|
||||
<eks-cluster-configuration-form
|
||||
v-if="hasCredentials"
|
||||
:gitlab-managed-cluster-help-path="gitlabManagedClusterHelpPath"
|
||||
:namespace-per-environment-help-path="namespacePerEnvironmentHelpPath"
|
||||
:kubernetes-integration-help-path="kubernetesIntegrationHelpPath"
|
||||
:external-link-icon="externalLinkIcon"
|
||||
/>
|
||||
<service-credentials-form
|
||||
v-else
|
||||
:create-role-arn-help-path="createRoleArnHelpPath"
|
||||
:account-and-external-ids-help-path="accountAndExternalIdsHelpPath"
|
||||
:external-link-icon="externalLinkIcon"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -1,530 +0,0 @@
|
|||
<script>
|
||||
import {
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
GlFormCheckbox,
|
||||
GlIcon,
|
||||
GlLink,
|
||||
GlSprintf,
|
||||
GlButton,
|
||||
} from '@gitlab/ui';
|
||||
import { createNamespacedHelpers, mapState, mapActions, mapGetters } from 'vuex';
|
||||
import ClusterFormDropdown from '~/create_cluster/components/cluster_form_dropdown.vue';
|
||||
import { s__ } from '~/locale';
|
||||
import { KUBERNETES_VERSIONS } from '../constants';
|
||||
|
||||
const { mapState: mapRolesState, mapActions: mapRolesActions } = createNamespacedHelpers('roles');
|
||||
const { mapState: mapKeyPairsState, mapActions: mapKeyPairsActions } = createNamespacedHelpers(
|
||||
'keyPairs',
|
||||
);
|
||||
const { mapState: mapVpcsState, mapActions: mapVpcActions } = createNamespacedHelpers('vpcs');
|
||||
const { mapState: mapSubnetsState, mapActions: mapSubnetActions } = createNamespacedHelpers(
|
||||
'subnets',
|
||||
);
|
||||
const {
|
||||
mapState: mapSecurityGroupsState,
|
||||
mapActions: mapSecurityGroupsActions,
|
||||
} = createNamespacedHelpers('securityGroups');
|
||||
const { mapState: mapInstanceTypesState } = createNamespacedHelpers('instanceTypes');
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ClusterFormDropdown,
|
||||
GlFormCheckbox,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
GlIcon,
|
||||
GlLink,
|
||||
GlSprintf,
|
||||
GlButton,
|
||||
},
|
||||
props: {
|
||||
gitlabManagedClusterHelpPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
namespacePerEnvironmentHelpPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
kubernetesIntegrationHelpPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
externalLinkIcon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
kubernetesIntegrationHelpText: s__(
|
||||
'ClusterIntegration|Read our %{linkStart}help page%{linkEnd} on Kubernetes cluster integration.',
|
||||
),
|
||||
roleDropdownHelpText: s__(
|
||||
'ClusterIntegration|Your service role is distinct from the provision role used when authenticating. It will allow Amazon EKS and the Kubernetes control plane to manage AWS resources on your behalf. To use a new role, first create one on %{linkStart}Amazon Web Services%{linkEnd}.',
|
||||
),
|
||||
roleDropdownHelpPath:
|
||||
'https://docs.aws.amazon.com/eks/latest/userguide/service_IAM_role.html#create-service-role',
|
||||
regionInputLabel: s__('ClusterIntegration|Cluster Region'),
|
||||
regionHelpText: s__(
|
||||
'ClusterIntegration|The region the new cluster will be created in. You must reauthenticate to change regions.',
|
||||
),
|
||||
keyPairDropdownHelpText: s__(
|
||||
'ClusterIntegration|Select the key pair name that will be used to create EC2 nodes. To use a new key pair name, first create one on %{linkStart}Amazon Web Services%{linkEnd}.',
|
||||
),
|
||||
keyPairDropdownHelpPath:
|
||||
'https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html#having-ec2-create-your-key-pair',
|
||||
vpcDropdownHelpText: s__(
|
||||
'ClusterIntegration|Select a VPC to use for your EKS Cluster resources. To use a new VPC, first create one on %{linkStart}Amazon Web Services %{linkEnd}.',
|
||||
),
|
||||
vpcDropdownHelpPath:
|
||||
'https://docs.aws.amazon.com/eks/latest/userguide/getting-started-console.html#vpc-create',
|
||||
subnetDropdownHelpText: s__(
|
||||
'ClusterIntegration|Choose the %{linkStart}subnets %{linkEnd} in your VPC where your worker nodes will run.',
|
||||
),
|
||||
subnetDropdownHelpPath: 'https://console.aws.amazon.com/vpc/home?#subnets',
|
||||
securityGroupDropdownHelpText: s__(
|
||||
'ClusterIntegration|Choose the %{linkStart}security group%{linkEnd} to apply to the EKS-managed Elastic Network Interfaces that are created in your worker node subnets.',
|
||||
),
|
||||
securityGroupDropdownHelpPath: 'https://console.aws.amazon.com/vpc/home?#securityGroups',
|
||||
instanceTypesDropdownHelpText: s__(
|
||||
'ClusterIntegration|Choose the worker node %{linkStart}instance type%{linkEnd}.',
|
||||
),
|
||||
instanceTypesDropdownHelpPath: 'https://aws.amazon.com/ec2/instance-types',
|
||||
gitlabManagedClusterHelpText: s__(
|
||||
'ClusterIntegration|Allow GitLab to manage namespace and service accounts for this cluster. %{linkStart}More information%{linkEnd}',
|
||||
),
|
||||
namespacePerEnvironmentHelpText: s__(
|
||||
'ClusterIntegration|Deploy each environment to its own namespace. Otherwise, environments within a project share a project-wide namespace. Note that anyone who can trigger a deployment of a namespace can read its secrets. If modified, existing environments will use their current namespaces until the cluster cache is cleared. %{linkStart}More information%{linkEnd}',
|
||||
),
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'clusterName',
|
||||
'environmentScope',
|
||||
'kubernetesVersion',
|
||||
'selectedRegion',
|
||||
'selectedKeyPair',
|
||||
'selectedVpc',
|
||||
'selectedSubnet',
|
||||
'selectedRole',
|
||||
'selectedSecurityGroup',
|
||||
'selectedInstanceType',
|
||||
'nodeCount',
|
||||
'gitlabManagedCluster',
|
||||
'namespacePerEnvironment',
|
||||
'isCreatingCluster',
|
||||
]),
|
||||
...mapGetters(['subnetValid']),
|
||||
...mapRolesState({
|
||||
roles: 'items',
|
||||
isLoadingRoles: 'isLoadingItems',
|
||||
loadingRolesError: 'loadingItemsError',
|
||||
}),
|
||||
...mapKeyPairsState({
|
||||
keyPairs: 'items',
|
||||
isLoadingKeyPairs: 'isLoadingItems',
|
||||
loadingKeyPairsError: 'loadingItemsError',
|
||||
}),
|
||||
...mapVpcsState({
|
||||
vpcs: 'items',
|
||||
isLoadingVpcs: 'isLoadingItems',
|
||||
loadingVpcsError: 'loadingItemsError',
|
||||
}),
|
||||
...mapSubnetsState({
|
||||
subnets: 'items',
|
||||
isLoadingSubnets: 'isLoadingItems',
|
||||
loadingSubnetsError: 'loadingItemsError',
|
||||
}),
|
||||
...mapSecurityGroupsState({
|
||||
securityGroups: 'items',
|
||||
isLoadingSecurityGroups: 'isLoadingItems',
|
||||
loadingSecurityGroupsError: 'loadingItemsError',
|
||||
}),
|
||||
...mapInstanceTypesState({
|
||||
instanceTypes: 'items',
|
||||
isLoadingInstanceTypes: 'isLoadingItems',
|
||||
loadingInstanceTypesError: 'loadingItemsError',
|
||||
}),
|
||||
kubernetesVersions() {
|
||||
return KUBERNETES_VERSIONS;
|
||||
},
|
||||
vpcDropdownDisabled() {
|
||||
return !this.selectedRegion;
|
||||
},
|
||||
keyPairDropdownDisabled() {
|
||||
return !this.selectedRegion;
|
||||
},
|
||||
subnetDropdownDisabled() {
|
||||
return !this.selectedVpc;
|
||||
},
|
||||
securityGroupDropdownDisabled() {
|
||||
return !this.selectedVpc;
|
||||
},
|
||||
createClusterButtonDisabled() {
|
||||
return (
|
||||
!this.clusterName ||
|
||||
!this.environmentScope ||
|
||||
!this.kubernetesVersion ||
|
||||
!this.selectedRegion ||
|
||||
!this.selectedKeyPair ||
|
||||
!this.selectedVpc ||
|
||||
!this.subnetValid ||
|
||||
!this.selectedRole ||
|
||||
!this.selectedSecurityGroup ||
|
||||
!this.selectedInstanceType ||
|
||||
!this.nodeCount ||
|
||||
this.isCreatingCluster
|
||||
);
|
||||
},
|
||||
displaySubnetError() {
|
||||
return Boolean(this.loadingSubnetsError) || this.selectedSubnet?.length === 1;
|
||||
},
|
||||
createClusterButtonLabel() {
|
||||
return this.isCreatingCluster
|
||||
? s__('ClusterIntegration|Creating Kubernetes cluster')
|
||||
: s__('ClusterIntegration|Create Kubernetes cluster');
|
||||
},
|
||||
subnetValidationErrorText() {
|
||||
if (this.loadingSubnetsError) {
|
||||
return s__('ClusterIntegration|Could not load subnets for the selected VPC');
|
||||
}
|
||||
|
||||
return s__('ClusterIntegration|You should select at least two subnets');
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchRoles();
|
||||
this.setRegionAndFetchVpcsAndKeyPairs();
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
'createCluster',
|
||||
'setClusterName',
|
||||
'setEnvironmentScope',
|
||||
'setKubernetesVersion',
|
||||
'setRegion',
|
||||
'setVpc',
|
||||
'setSubnet',
|
||||
'setRole',
|
||||
'setKeyPair',
|
||||
'setSecurityGroup',
|
||||
'setInstanceType',
|
||||
'setNodeCount',
|
||||
'setGitlabManagedCluster',
|
||||
'setNamespacePerEnvironment',
|
||||
]),
|
||||
...mapVpcActions({ fetchVpcs: 'fetchItems' }),
|
||||
...mapSubnetActions({ fetchSubnets: 'fetchItems' }),
|
||||
...mapRolesActions({ fetchRoles: 'fetchItems' }),
|
||||
...mapKeyPairsActions({ fetchKeyPairs: 'fetchItems' }),
|
||||
...mapSecurityGroupsActions({ fetchSecurityGroups: 'fetchItems' }),
|
||||
setRegionAndFetchVpcsAndKeyPairs() {
|
||||
this.setVpc({ vpc: null });
|
||||
this.setKeyPair({ keyPair: null });
|
||||
this.setSubnet({ subnet: [] });
|
||||
this.setSecurityGroup({ securityGroup: null });
|
||||
this.fetchVpcs({ region: this.selectedRegion });
|
||||
this.fetchKeyPairs({ region: this.selectedRegion });
|
||||
},
|
||||
setVpcAndFetchSubnets(vpc) {
|
||||
this.setVpc({ vpc });
|
||||
this.setSubnet({ subnet: [] });
|
||||
this.setSecurityGroup({ securityGroup: null });
|
||||
this.fetchSubnets({ vpc, region: this.selectedRegion });
|
||||
this.fetchSecurityGroups({ vpc, region: this.selectedRegion });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<form name="eks-cluster-configuration-form">
|
||||
<h4>
|
||||
{{ s__('ClusterIntegration|Enter the details for your Amazon EKS Kubernetes cluster') }}
|
||||
</h4>
|
||||
<div class="mb-3">
|
||||
<gl-sprintf :message="$options.i18n.kubernetesIntegrationHelpText">
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="kubernetesIntegrationHelpPath">
|
||||
{{ content }}
|
||||
</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="label-bold" for="eks-cluster-name">{{
|
||||
s__('ClusterIntegration|Kubernetes cluster name')
|
||||
}}</label>
|
||||
<gl-form-input
|
||||
id="eks-cluster-name"
|
||||
:value="clusterName"
|
||||
@input="setClusterName({ clusterName: $event })"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="label-bold" for="eks-environment-scope">{{
|
||||
s__('ClusterIntegration|Environment scope')
|
||||
}}</label>
|
||||
<gl-form-input
|
||||
id="eks-environment-scope"
|
||||
:value="environmentScope"
|
||||
@input="setEnvironmentScope({ environmentScope: $event })"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="label-bold" for="eks-kubernetes-version">{{
|
||||
s__('ClusterIntegration|Kubernetes version')
|
||||
}}</label>
|
||||
<cluster-form-dropdown
|
||||
field-id="eks-kubernetes-version"
|
||||
field-name="eks-kubernetes-version"
|
||||
:value="kubernetesVersion"
|
||||
:items="kubernetesVersions"
|
||||
:empty-text="s__('ClusterIntegration|Kubernetes version not found')"
|
||||
@input="setKubernetesVersion({ kubernetesVersion: $event })"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="label-bold" for="eks-role">{{ s__('ClusterIntegration|Service role') }}</label>
|
||||
<cluster-form-dropdown
|
||||
field-id="eks-role"
|
||||
field-name="eks-role"
|
||||
:value="selectedRole"
|
||||
:items="roles"
|
||||
:loading="isLoadingRoles"
|
||||
:loading-text="s__('ClusterIntegration|Loading IAM Roles')"
|
||||
:placeholder="s__('ClusterIntegration|Select service role')"
|
||||
:search-field-placeholder="s__('ClusterIntegration|Search IAM Roles')"
|
||||
:empty-text="s__('ClusterIntegration|No IAM Roles found')"
|
||||
:has-errors="Boolean(loadingRolesError)"
|
||||
:error-message="s__('ClusterIntegration|Could not load IAM roles')"
|
||||
@input="setRole({ role: $event })"
|
||||
/>
|
||||
<p class="form-text text-muted">
|
||||
<gl-sprintf :message="$options.i18n.roleDropdownHelpText">
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="$options.i18n.roleDropdownHelpPath" target="_blank">
|
||||
{{ content }}
|
||||
<gl-icon name="external-link" class="gl-vertical-align-middle" />
|
||||
</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
</div>
|
||||
<gl-form-group
|
||||
:label="$options.i18n.regionInputLabel"
|
||||
:description="$options.i18n.regionHelpText"
|
||||
>
|
||||
<gl-form-input id="eks-region" :value="selectedRegion" type="text" readonly />
|
||||
</gl-form-group>
|
||||
<div class="form-group">
|
||||
<label class="label-bold" for="eks-key-pair">{{
|
||||
s__('ClusterIntegration|Key pair name')
|
||||
}}</label>
|
||||
<cluster-form-dropdown
|
||||
field-id="eks-key-pair"
|
||||
field-name="eks-key-pair"
|
||||
:value="selectedKeyPair"
|
||||
:items="keyPairs"
|
||||
:disabled="keyPairDropdownDisabled"
|
||||
:disabled-text="s__('ClusterIntegration|Select a region to choose a Key Pair')"
|
||||
:loading="isLoadingKeyPairs"
|
||||
:loading-text="s__('ClusterIntegration|Loading Key Pairs')"
|
||||
:placeholder="s__('ClusterIntegration|Select key pair')"
|
||||
:search-field-placeholder="s__('ClusterIntegration|Search Key Pairs')"
|
||||
:empty-text="s__('ClusterIntegration|No Key Pairs found')"
|
||||
:has-errors="Boolean(loadingKeyPairsError)"
|
||||
:error-message="s__('ClusterIntegration|Could not load Key Pairs')"
|
||||
@input="setKeyPair({ keyPair: $event })"
|
||||
/>
|
||||
<p class="form-text text-muted">
|
||||
<gl-sprintf :message="$options.i18n.keyPairDropdownHelpText">
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="$options.i18n.keyPairDropdownHelpPath" target="_blank">
|
||||
{{ content }}
|
||||
<gl-icon name="external-link" class="gl-vertical-align-middle" />
|
||||
</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="label-bold" for="eks-vpc">{{ s__('ClusterIntegration|VPC') }}</label>
|
||||
<cluster-form-dropdown
|
||||
field-id="eks-vpc"
|
||||
field-name="eks-vpc"
|
||||
:value="selectedVpc"
|
||||
:items="vpcs"
|
||||
:loading="isLoadingVpcs"
|
||||
:disabled="vpcDropdownDisabled"
|
||||
:disabled-text="s__('ClusterIntegration|Select a region to choose a VPC')"
|
||||
:loading-text="s__('ClusterIntegration|Loading VPCs')"
|
||||
:placeholder="s__('ClusterIntegration|Select a VPC')"
|
||||
:search-field-placeholder="s__('ClusterIntegration|Search VPCs')"
|
||||
:empty-text="s__('ClusterIntegration|No VPCs found')"
|
||||
:has-errors="Boolean(loadingVpcsError)"
|
||||
:error-message="s__('ClusterIntegration|Could not load VPCs for the selected region')"
|
||||
@input="setVpcAndFetchSubnets($event)"
|
||||
/>
|
||||
<p class="form-text text-muted">
|
||||
<gl-sprintf :message="$options.i18n.vpcDropdownHelpText">
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="$options.i18n.vpcDropdownHelpPath" target="_blank">
|
||||
{{ content }}
|
||||
<gl-icon name="external-link" class="gl-vertical-align-middle" />
|
||||
</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="label-bold" for="eks-role">{{ s__('ClusterIntegration|Subnets') }}</label>
|
||||
<cluster-form-dropdown
|
||||
field-id="eks-subnet"
|
||||
field-name="eks-subnet"
|
||||
multiple
|
||||
:value="selectedSubnet"
|
||||
:items="subnets"
|
||||
:loading="isLoadingSubnets"
|
||||
:disabled="subnetDropdownDisabled"
|
||||
:disabled-text="s__('ClusterIntegration|Select a VPC to choose a subnet')"
|
||||
:loading-text="s__('ClusterIntegration|Loading subnets')"
|
||||
:placeholder="s__('ClusterIntegration|Select a subnet')"
|
||||
:search-field-placeholder="s__('ClusterIntegration|Search subnets')"
|
||||
:empty-text="s__('ClusterIntegration|No subnet found')"
|
||||
:has-errors="displaySubnetError"
|
||||
:error-message="subnetValidationErrorText"
|
||||
@input="setSubnet({ subnet: $event })"
|
||||
/>
|
||||
<p class="form-text text-muted">
|
||||
<gl-sprintf :message="$options.i18n.subnetDropdownHelpText">
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="$options.i18n.subnetDropdownHelpPath" target="_blank">
|
||||
{{ content }}
|
||||
<gl-icon name="external-link" class="gl-vertical-align-middle" />
|
||||
</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="label-bold" for="eks-security-group">{{
|
||||
s__('ClusterIntegration|Security group')
|
||||
}}</label>
|
||||
<cluster-form-dropdown
|
||||
field-id="eks-security-group"
|
||||
field-name="eks-security-group"
|
||||
:value="selectedSecurityGroup"
|
||||
:items="securityGroups"
|
||||
:loading="isLoadingSecurityGroups"
|
||||
:disabled="securityGroupDropdownDisabled"
|
||||
:disabled-text="s__('ClusterIntegration|Select a VPC to choose a security group')"
|
||||
:loading-text="s__('ClusterIntegration|Loading security groups')"
|
||||
:placeholder="s__('ClusterIntegration|Select a security group')"
|
||||
:search-field-placeholder="s__('ClusterIntegration|Search security groups')"
|
||||
:empty-text="s__('ClusterIntegration|No security group found')"
|
||||
:has-errors="Boolean(loadingSecurityGroupsError)"
|
||||
:error-message="
|
||||
s__('ClusterIntegration|Could not load security groups for the selected VPC')
|
||||
"
|
||||
@input="setSecurityGroup({ securityGroup: $event })"
|
||||
/>
|
||||
<p class="form-text text-muted">
|
||||
<gl-sprintf :message="$options.i18n.securityGroupDropdownHelpText">
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="$options.i18n.securityGroupDropdownHelpPath" target="_blank">
|
||||
{{ content }}
|
||||
<gl-icon name="external-link" class="gl-vertical-align-middle" />
|
||||
</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="label-bold" for="eks-instance-type">{{
|
||||
s__('ClusterIntegration|Instance type')
|
||||
}}</label>
|
||||
<cluster-form-dropdown
|
||||
field-id="eks-instance-type"
|
||||
field-name="eks-instance-type"
|
||||
:value="selectedInstanceType"
|
||||
:items="instanceTypes"
|
||||
:loading="isLoadingInstanceTypes"
|
||||
:loading-text="s__('ClusterIntegration|Loading instance types')"
|
||||
:placeholder="s__('ClusterIntegration|Select an instance type')"
|
||||
:search-field-placeholder="s__('ClusterIntegration|Search instance types')"
|
||||
:empty-text="s__('ClusterIntegration|No instance type found')"
|
||||
:has-errors="Boolean(loadingInstanceTypesError)"
|
||||
:error-message="s__('ClusterIntegration|Could not load instance types')"
|
||||
@input="setInstanceType({ instanceType: $event })"
|
||||
/>
|
||||
<p class="form-text text-muted">
|
||||
<gl-sprintf :message="$options.i18n.instanceTypesDropdownHelpText">
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="$options.i18n.instanceTypesDropdownHelpPath" target="_blank">
|
||||
{{ content }}
|
||||
<gl-icon name="external-link" class="gl-vertical-align-middle" />
|
||||
</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="label-bold" for="eks-node-count">{{
|
||||
s__('ClusterIntegration|Number of nodes')
|
||||
}}</label>
|
||||
<gl-form-input
|
||||
id="eks-node-count"
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
:value="nodeCount"
|
||||
@input="setNodeCount({ nodeCount: $event })"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<gl-form-checkbox
|
||||
:checked="gitlabManagedCluster"
|
||||
@input="setGitlabManagedCluster({ gitlabManagedCluster: $event })"
|
||||
>{{ s__('ClusterIntegration|GitLab-managed cluster') }}</gl-form-checkbox
|
||||
>
|
||||
<p class="form text text-muted">
|
||||
<gl-sprintf :message="$options.i18n.gitlabManagedClusterHelpText">
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="gitlabManagedClusterHelpPath" target="_blank">
|
||||
{{ content }}
|
||||
</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<gl-form-checkbox
|
||||
:checked="namespacePerEnvironment"
|
||||
@input="setNamespacePerEnvironment({ namespacePerEnvironment: $event })"
|
||||
>{{ s__('ClusterIntegration|Namespace per environment') }}</gl-form-checkbox
|
||||
>
|
||||
<p class="form text text-muted">
|
||||
<gl-sprintf :message="$options.i18n.namespacePerEnvironmentHelpText">
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="namespacePerEnvironmentHelpPath" target="_blank">
|
||||
{{ content }}
|
||||
</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<gl-button
|
||||
variant="success"
|
||||
category="primary"
|
||||
class="js-create-cluster"
|
||||
:disabled="createClusterButtonDisabled"
|
||||
:loading="isCreatingCluster"
|
||||
@click="createCluster()"
|
||||
>
|
||||
{{ createClusterButtonLabel }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
|
@ -1,182 +0,0 @@
|
|||
<script>
|
||||
import { GlButton, GlFormGroup, GlFormInput, GlIcon, GlLink, GlSprintf, GlAlert } from '@gitlab/ui';
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import { s__, __ } from '~/locale';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import { DEFAULT_REGION } from '../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlButton,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
GlIcon,
|
||||
GlLink,
|
||||
GlSprintf,
|
||||
ClipboardButton,
|
||||
GlAlert,
|
||||
},
|
||||
props: {
|
||||
accountAndExternalIdsHelpPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
createRoleArnHelpPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
externalLinkIcon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
regionInputLabel: s__('ClusterIntegration|Cluster Region'),
|
||||
regionHelpPath: 'https://aws.amazon.com/about-aws/global-infrastructure/regions_az/',
|
||||
regionHelpText: s__(
|
||||
'ClusterIntegration|Select the region you want to create the new cluster in. Make sure you have access to this region for your role to be able to authenticate. If no region is selected, we will use %{codeStart}DEFAULT_REGION%{codeEnd}. Learn more about %{linkStart}Regions%{linkEnd}.',
|
||||
),
|
||||
accountAndExternalIdsHelpText: s__(
|
||||
'ClusterIntegration|The Amazon Resource Name (ARN) associated with your role. If you do not have a provisioned role, first create one on %{awsLinkStart}Amazon Web Services %{awsLinkEnd} using the above account and external IDs. %{moreInfoStart}More information%{moreInfoEnd}',
|
||||
),
|
||||
regionHelpTextDefaultRegion: DEFAULT_REGION,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
roleArn: this.$store.state.roleArn,
|
||||
selectedRegion: this.$store.state.selectedRegion,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['accountId', 'externalId', 'isCreatingRole', 'createRoleError']),
|
||||
submitButtonDisabled() {
|
||||
return this.isCreatingRole || !this.roleArn;
|
||||
},
|
||||
submitButtonLabel() {
|
||||
return this.isCreatingRole
|
||||
? __('Authenticating')
|
||||
: s__('ClusterIntegration|Authenticate with AWS');
|
||||
},
|
||||
awsHelpLink() {
|
||||
return 'https://console.aws.amazon.com/iam/home?#roles';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['createRole']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<form name="service-credentials-form">
|
||||
<h4>{{ s__('ClusterIntegration|Authenticate with Amazon Web Services') }}</h4>
|
||||
<p>
|
||||
{{
|
||||
s__(
|
||||
'ClusterIntegration|You must grant access to your organization’s AWS resources in order to create a new EKS cluster. To grant access, create a provision role using the account and external ID below and provide us the ARN.',
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<gl-alert
|
||||
v-if="createRoleError"
|
||||
class="js-invalid-credentials gl-mb-5"
|
||||
variant="danger"
|
||||
:dismissible="false"
|
||||
>
|
||||
{{ createRoleError }}
|
||||
</gl-alert>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="gitlab-account-id">{{ __('Account ID') }}</label>
|
||||
<div class="input-group">
|
||||
<gl-form-input id="gitlab-account-id" type="text" readonly :value="accountId" />
|
||||
<div class="input-group-append">
|
||||
<clipboard-button
|
||||
:text="accountId"
|
||||
:title="__('Copy Account ID to clipboard')"
|
||||
class="input-group-text js-copy-account-id-button"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="eks-external-id">{{ __('External ID') }}</label>
|
||||
<div class="input-group">
|
||||
<gl-form-input id="eks-external-id" type="text" readonly :value="externalId" />
|
||||
<div class="input-group-append">
|
||||
<clipboard-button
|
||||
:text="externalId"
|
||||
:title="__('Copy External ID to clipboard')"
|
||||
class="input-group-text js-copy-external-id-button"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 mb-3 mt-n3">
|
||||
<p class="form-text text-muted">
|
||||
<gl-sprintf :message="$options.i18n.accountAndExternalIdsHelpText">
|
||||
<template #awsLink="{ content }">
|
||||
<gl-link :href="awsHelpLink" target="_blank">
|
||||
{{ content }}
|
||||
<gl-icon name="external-link" class="gl-vertical-align-middle" />
|
||||
</gl-link>
|
||||
</template>
|
||||
<template #moreInfo="{ content }">
|
||||
<gl-link :href="accountAndExternalIdsHelpPath" target="_blank">
|
||||
{{ content }}
|
||||
</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="eks-provision-role-arn">{{ s__('ClusterIntegration|Provision Role ARN') }}</label>
|
||||
<gl-form-input id="eks-provision-role-arn" v-model="roleArn" />
|
||||
<p class="form-text text-muted">
|
||||
<gl-sprintf :message="$options.i18n.accountAndExternalIdsHelpText">
|
||||
<template #awsLink="{ content }">
|
||||
<gl-link :href="awsHelpLink" target="_blank">
|
||||
{{ content }}
|
||||
<gl-icon name="external-link" class="gl-vertical-align-middle" />
|
||||
</gl-link>
|
||||
</template>
|
||||
<template #moreInfo="{ content }">
|
||||
<gl-link :href="accountAndExternalIdsHelpPath" target="_blank">
|
||||
{{ content }}
|
||||
</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<gl-form-group :label="$options.i18n.regionInputLabel">
|
||||
<gl-form-input id="eks-region" v-model="selectedRegion" type="text" />
|
||||
|
||||
<template #description>
|
||||
<gl-sprintf :message="$options.i18n.regionHelpText">
|
||||
<template #code>
|
||||
<code>{{ $options.i18n.regionHelpTextDefaultRegion }}</code>
|
||||
</template>
|
||||
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="$options.i18n.regionHelpPath" target="_blank">
|
||||
{{ content }}
|
||||
<gl-icon name="external-link" />
|
||||
</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</template>
|
||||
</gl-form-group>
|
||||
|
||||
<gl-button
|
||||
variant="success"
|
||||
category="primary"
|
||||
type="submit"
|
||||
:disabled="submitButtonDisabled"
|
||||
:loading="isCreatingRole"
|
||||
@click.prevent="createRole({ roleArn, selectedRegion, externalId })"
|
||||
>
|
||||
{{ submitButtonLabel }}
|
||||
</gl-button>
|
||||
</form>
|
||||
</template>
|
|
@ -1,9 +0,0 @@
|
|||
export const DEFAULT_REGION = 'us-east-2';
|
||||
|
||||
export const KUBERNETES_VERSIONS = [
|
||||
{ name: '1.16', value: '1.16' },
|
||||
{ name: '1.17', value: '1.17' },
|
||||
{ name: '1.18', value: '1.18' },
|
||||
{ name: '1.19', value: '1.19' },
|
||||
{ name: '1.20', value: '1.20', default: true },
|
||||
];
|
|
@ -1,55 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import CreateEksCluster from './components/create_eks_cluster.vue';
|
||||
import createStore from './store';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export default (el) => {
|
||||
const {
|
||||
gitlabManagedClusterHelpPath,
|
||||
namespacePerEnvironmentHelpPath,
|
||||
kubernetesIntegrationHelpPath,
|
||||
accountAndExternalIdsHelpPath,
|
||||
createRoleArnHelpPath,
|
||||
externalId,
|
||||
accountId,
|
||||
instanceTypes,
|
||||
hasCredentials,
|
||||
createRolePath,
|
||||
createClusterPath,
|
||||
externalLinkIcon,
|
||||
roleArn,
|
||||
} = el.dataset;
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
store: createStore({
|
||||
initialState: {
|
||||
hasCredentials: parseBoolean(hasCredentials),
|
||||
externalId,
|
||||
accountId,
|
||||
instanceTypes: JSON.parse(instanceTypes),
|
||||
createRolePath,
|
||||
createClusterPath,
|
||||
roleArn,
|
||||
},
|
||||
}),
|
||||
components: {
|
||||
CreateEksCluster,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement('create-eks-cluster', {
|
||||
props: {
|
||||
gitlabManagedClusterHelpPath,
|
||||
namespacePerEnvironmentHelpPath,
|
||||
kubernetesIntegrationHelpPath,
|
||||
accountAndExternalIdsHelpPath,
|
||||
createRoleArnHelpPath,
|
||||
externalLinkIcon,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
|
@ -1,79 +0,0 @@
|
|||
import EC2 from 'aws-sdk/clients/ec2';
|
||||
import IAM from 'aws-sdk/clients/iam';
|
||||
import AWS from 'aws-sdk/global';
|
||||
|
||||
const lookupVpcName = ({ Tags: tags, VpcId: id }) => {
|
||||
const nameTag = tags.find(({ Key: key }) => key === 'Name');
|
||||
|
||||
return nameTag ? nameTag.Value : id;
|
||||
};
|
||||
|
||||
export const setAWSConfig = ({ awsCredentials }) => {
|
||||
AWS.config = awsCredentials;
|
||||
};
|
||||
|
||||
export const fetchRoles = () => {
|
||||
const iam = new IAM();
|
||||
|
||||
return iam
|
||||
.listRoles()
|
||||
.promise()
|
||||
.then(({ Roles: roles }) => roles.map(({ RoleName: name, Arn: value }) => ({ name, value })));
|
||||
};
|
||||
|
||||
export const fetchKeyPairs = ({ region }) => {
|
||||
const ec2 = new EC2({ region });
|
||||
|
||||
return ec2
|
||||
.describeKeyPairs()
|
||||
.promise()
|
||||
.then(({ KeyPairs: keyPairs }) => keyPairs.map(({ KeyName: name }) => ({ name, value: name })));
|
||||
};
|
||||
|
||||
export const fetchVpcs = ({ region }) => {
|
||||
const ec2 = new EC2({ region });
|
||||
|
||||
return ec2
|
||||
.describeVpcs()
|
||||
.promise()
|
||||
.then(({ Vpcs: vpcs }) =>
|
||||
vpcs.map((vpc) => ({
|
||||
value: vpc.VpcId,
|
||||
name: lookupVpcName(vpc),
|
||||
})),
|
||||
);
|
||||
};
|
||||
|
||||
export const fetchSubnets = ({ vpc, region }) => {
|
||||
const ec2 = new EC2({ region });
|
||||
|
||||
return ec2
|
||||
.describeSubnets({
|
||||
Filters: [
|
||||
{
|
||||
Name: 'vpc-id',
|
||||
Values: [vpc],
|
||||
},
|
||||
],
|
||||
})
|
||||
.promise()
|
||||
.then(({ Subnets: subnets }) => subnets.map(({ SubnetId: id }) => ({ value: id, name: id })));
|
||||
};
|
||||
|
||||
export const fetchSecurityGroups = ({ region, vpc }) => {
|
||||
const ec2 = new EC2({ region });
|
||||
|
||||
return ec2
|
||||
.describeSecurityGroups({
|
||||
Filters: [
|
||||
{
|
||||
Name: 'vpc-id',
|
||||
Values: [vpc],
|
||||
},
|
||||
],
|
||||
})
|
||||
.promise()
|
||||
.then(({ SecurityGroups: securityGroups }) =>
|
||||
securityGroups.map(({ GroupName: name, GroupId: value }) => ({ name, value })),
|
||||
);
|
||||
};
|
|
@ -1,148 +0,0 @@
|
|||
import createFlash from '~/flash';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
import { DEFAULT_REGION } from '../constants';
|
||||
import { setAWSConfig } from '../services/aws_services_facade';
|
||||
import * as types from './mutation_types';
|
||||
|
||||
const getErrorMessage = (data) => {
|
||||
const errorKey = Object.keys(data)[0];
|
||||
|
||||
return data[errorKey][0];
|
||||
};
|
||||
|
||||
export const setClusterName = ({ commit }, payload) => {
|
||||
commit(types.SET_CLUSTER_NAME, payload);
|
||||
};
|
||||
|
||||
export const setEnvironmentScope = ({ commit }, payload) => {
|
||||
commit(types.SET_ENVIRONMENT_SCOPE, payload);
|
||||
};
|
||||
|
||||
export const setKubernetesVersion = ({ commit }, payload) => {
|
||||
commit(types.SET_KUBERNETES_VERSION, payload);
|
||||
};
|
||||
|
||||
export const createRole = ({ dispatch, state: { createRolePath } }, payload) => {
|
||||
dispatch('requestCreateRole');
|
||||
|
||||
const region = payload.selectedRegion || DEFAULT_REGION;
|
||||
|
||||
return axios
|
||||
.post(createRolePath, {
|
||||
role_arn: payload.roleArn,
|
||||
role_external_id: payload.externalId,
|
||||
region,
|
||||
})
|
||||
.then(({ data }) => {
|
||||
const awsData = {
|
||||
...convertObjectPropsToCamelCase(data),
|
||||
region,
|
||||
};
|
||||
|
||||
dispatch('createRoleSuccess', awsData);
|
||||
})
|
||||
.catch((error) => {
|
||||
let message = error;
|
||||
if (error?.response?.data?.message) {
|
||||
message = error.response.data.message;
|
||||
}
|
||||
dispatch('createRoleError', { error: message });
|
||||
});
|
||||
};
|
||||
|
||||
export const requestCreateRole = ({ commit }) => {
|
||||
commit(types.REQUEST_CREATE_ROLE);
|
||||
};
|
||||
|
||||
export const createRoleSuccess = ({ dispatch, commit }, awsCredentials) => {
|
||||
dispatch('setRegion', { region: awsCredentials.region });
|
||||
setAWSConfig({ awsCredentials });
|
||||
commit(types.CREATE_ROLE_SUCCESS);
|
||||
};
|
||||
|
||||
export const createRoleError = ({ commit }, payload) => {
|
||||
commit(types.CREATE_ROLE_ERROR, payload);
|
||||
};
|
||||
|
||||
export const createCluster = ({ dispatch, state }) => {
|
||||
dispatch('requestCreateCluster');
|
||||
|
||||
return axios
|
||||
.post(state.createClusterPath, {
|
||||
name: state.clusterName,
|
||||
environment_scope: state.environmentScope,
|
||||
managed: state.gitlabManagedCluster,
|
||||
namespace_per_environment: state.namespacePerEnvironment,
|
||||
provider_aws_attributes: {
|
||||
kubernetes_version: state.kubernetesVersion,
|
||||
region: state.selectedRegion,
|
||||
vpc_id: state.selectedVpc,
|
||||
subnet_ids: state.selectedSubnet,
|
||||
role_arn: state.selectedRole,
|
||||
key_name: state.selectedKeyPair,
|
||||
security_group_id: state.selectedSecurityGroup,
|
||||
instance_type: state.selectedInstanceType,
|
||||
num_nodes: state.nodeCount,
|
||||
},
|
||||
})
|
||||
.then(({ headers: { location } }) => dispatch('createClusterSuccess', location))
|
||||
.catch(({ response: { data } }) => {
|
||||
dispatch('createClusterError', data);
|
||||
});
|
||||
};
|
||||
|
||||
export const requestCreateCluster = ({ commit }) => {
|
||||
commit(types.REQUEST_CREATE_CLUSTER);
|
||||
};
|
||||
|
||||
export const createClusterSuccess = (_, location) => {
|
||||
window.location.assign(location);
|
||||
};
|
||||
|
||||
export const createClusterError = ({ commit }, error) => {
|
||||
commit(types.CREATE_CLUSTER_ERROR, error);
|
||||
createFlash({
|
||||
message: getErrorMessage(error),
|
||||
});
|
||||
};
|
||||
|
||||
export const setRegion = ({ commit }, payload) => {
|
||||
commit(types.SET_REGION, payload);
|
||||
};
|
||||
|
||||
export const setKeyPair = ({ commit }, payload) => {
|
||||
commit(types.SET_KEY_PAIR, payload);
|
||||
};
|
||||
|
||||
export const setVpc = ({ commit }, payload) => {
|
||||
commit(types.SET_VPC, payload);
|
||||
};
|
||||
|
||||
export const setSubnet = ({ commit }, payload) => {
|
||||
commit(types.SET_SUBNET, payload);
|
||||
};
|
||||
|
||||
export const setRole = ({ commit }, payload) => {
|
||||
commit(types.SET_ROLE, payload);
|
||||
};
|
||||
|
||||
export const setSecurityGroup = ({ commit }, payload) => {
|
||||
commit(types.SET_SECURITY_GROUP, payload);
|
||||
};
|
||||
|
||||
export const setGitlabManagedCluster = ({ commit }, payload) => {
|
||||
commit(types.SET_GITLAB_MANAGED_CLUSTER, payload);
|
||||
};
|
||||
|
||||
export const setNamespacePerEnvironment = ({ commit }, payload) => {
|
||||
commit(types.SET_NAMESPACE_PER_ENVIRONMENT, payload);
|
||||
};
|
||||
|
||||
export const setInstanceType = ({ commit }, payload) => {
|
||||
commit(types.SET_INSTANCE_TYPE, payload);
|
||||
};
|
||||
|
||||
export const setNodeCount = ({ commit }, payload) => {
|
||||
commit(types.SET_NODE_COUNT, payload);
|
||||
};
|
|
@ -1,2 +0,0 @@
|
|||
export const subnetValid = ({ selectedSubnet }) =>
|
||||
Array.isArray(selectedSubnet) && selectedSubnet.length >= 2;
|
|
@ -1,49 +0,0 @@
|
|||
import Vuex from 'vuex';
|
||||
import clusterDropdownStore from '~/create_cluster/store/cluster_dropdown';
|
||||
import {
|
||||
fetchRoles,
|
||||
fetchKeyPairs,
|
||||
fetchVpcs,
|
||||
fetchSubnets,
|
||||
fetchSecurityGroups,
|
||||
} from '../services/aws_services_facade';
|
||||
import * as actions from './actions';
|
||||
import * as getters from './getters';
|
||||
import mutations from './mutations';
|
||||
import state from './state';
|
||||
|
||||
const createStore = ({ initialState }) =>
|
||||
new Vuex.Store({
|
||||
actions,
|
||||
getters,
|
||||
mutations,
|
||||
state: Object.assign(state(), initialState),
|
||||
modules: {
|
||||
roles: {
|
||||
namespaced: true,
|
||||
...clusterDropdownStore({ fetchFn: fetchRoles }),
|
||||
},
|
||||
keyPairs: {
|
||||
namespaced: true,
|
||||
...clusterDropdownStore({ fetchFn: fetchKeyPairs }),
|
||||
},
|
||||
vpcs: {
|
||||
namespaced: true,
|
||||
...clusterDropdownStore({ fetchFn: fetchVpcs }),
|
||||
},
|
||||
subnets: {
|
||||
namespaced: true,
|
||||
...clusterDropdownStore({ fetchFn: fetchSubnets }),
|
||||
},
|
||||
securityGroups: {
|
||||
namespaced: true,
|
||||
...clusterDropdownStore({ fetchFn: fetchSecurityGroups }),
|
||||
},
|
||||
instanceTypes: {
|
||||
namespaced: true,
|
||||
...clusterDropdownStore({ initialState: { items: initialState.instanceTypes } }),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default createStore;
|
|
@ -1,19 +0,0 @@
|
|||
export const SET_CLUSTER_NAME = 'SET_CLUSTER_NAME';
|
||||
export const SET_ENVIRONMENT_SCOPE = 'SET_ENVIRONMENT_SCOPE';
|
||||
export const SET_KUBERNETES_VERSION = 'SET_KUBERNETES_VERSION';
|
||||
export const SET_REGION = 'SET_REGION';
|
||||
export const SET_VPC = 'SET_VPC';
|
||||
export const SET_KEY_PAIR = 'SET_KEY_PAIR';
|
||||
export const SET_SUBNET = 'SET_SUBNET';
|
||||
export const SET_ROLE = 'SET_ROLE';
|
||||
export const SET_SECURITY_GROUP = 'SET_SECURITY_GROUP';
|
||||
export const SET_INSTANCE_TYPE = 'SET_INSTANCE_TYPE';
|
||||
export const SET_NODE_COUNT = 'SET_NODE_COUNT';
|
||||
export const SET_GITLAB_MANAGED_CLUSTER = 'SET_GITLAB_MANAGED_CLUSTER';
|
||||
export const SET_NAMESPACE_PER_ENVIRONMENT = 'SET_NAMESPACE_PER_ENVIRONMENT';
|
||||
export const REQUEST_CREATE_ROLE = 'REQUEST_CREATE_ROLE';
|
||||
export const CREATE_ROLE_SUCCESS = 'CREATE_ROLE_SUCCESS';
|
||||
export const CREATE_ROLE_ERROR = 'CREATE_ROLE_ERROR';
|
||||
export const REQUEST_CREATE_CLUSTER = 'REQUEST_CREATE_CLUSTER';
|
||||
export const CREATE_CLUSTER_SUCCESS = 'CREATE_CLUSTER_SUCCESS';
|
||||
export const CREATE_CLUSTER_ERROR = 'CREATE_CLUSTER_ERROR';
|
|
@ -1,66 +0,0 @@
|
|||
import * as types from './mutation_types';
|
||||
|
||||
export default {
|
||||
[types.SET_CLUSTER_NAME](state, { clusterName }) {
|
||||
state.clusterName = clusterName;
|
||||
},
|
||||
[types.SET_ENVIRONMENT_SCOPE](state, { environmentScope }) {
|
||||
state.environmentScope = environmentScope;
|
||||
},
|
||||
[types.SET_KUBERNETES_VERSION](state, { kubernetesVersion }) {
|
||||
state.kubernetesVersion = kubernetesVersion;
|
||||
},
|
||||
[types.SET_REGION](state, { region }) {
|
||||
state.selectedRegion = region;
|
||||
},
|
||||
[types.SET_KEY_PAIR](state, { keyPair }) {
|
||||
state.selectedKeyPair = keyPair;
|
||||
},
|
||||
[types.SET_VPC](state, { vpc }) {
|
||||
state.selectedVpc = vpc;
|
||||
},
|
||||
[types.SET_SUBNET](state, { subnet }) {
|
||||
state.selectedSubnet = subnet;
|
||||
},
|
||||
[types.SET_ROLE](state, { role }) {
|
||||
state.selectedRole = role;
|
||||
},
|
||||
[types.SET_SECURITY_GROUP](state, { securityGroup }) {
|
||||
state.selectedSecurityGroup = securityGroup;
|
||||
},
|
||||
[types.SET_INSTANCE_TYPE](state, { instanceType }) {
|
||||
state.selectedInstanceType = instanceType;
|
||||
},
|
||||
[types.SET_NODE_COUNT](state, { nodeCount }) {
|
||||
state.nodeCount = nodeCount;
|
||||
},
|
||||
[types.SET_GITLAB_MANAGED_CLUSTER](state, { gitlabManagedCluster }) {
|
||||
state.gitlabManagedCluster = gitlabManagedCluster;
|
||||
},
|
||||
[types.SET_NAMESPACE_PER_ENVIRONMENT](state, { namespacePerEnvironment }) {
|
||||
state.namespacePerEnvironment = namespacePerEnvironment;
|
||||
},
|
||||
[types.REQUEST_CREATE_ROLE](state) {
|
||||
state.isCreatingRole = true;
|
||||
state.createRoleError = null;
|
||||
state.hasCredentials = false;
|
||||
},
|
||||
[types.CREATE_ROLE_SUCCESS](state) {
|
||||
state.isCreatingRole = false;
|
||||
state.createRoleError = null;
|
||||
state.hasCredentials = true;
|
||||
},
|
||||
[types.CREATE_ROLE_ERROR](state, { error }) {
|
||||
state.isCreatingRole = false;
|
||||
state.createRoleError = error;
|
||||
state.hasCredentials = false;
|
||||
},
|
||||
[types.REQUEST_CREATE_CLUSTER](state) {
|
||||
state.isCreatingCluster = true;
|
||||
state.createClusterError = null;
|
||||
},
|
||||
[types.CREATE_CLUSTER_ERROR](state, { error }) {
|
||||
state.isCreatingCluster = false;
|
||||
state.createClusterError = error;
|
||||
},
|
||||
};
|
|
@ -1,34 +0,0 @@
|
|||
import { KUBERNETES_VERSIONS } from '../constants';
|
||||
|
||||
const kubernetesVersion = KUBERNETES_VERSIONS.find((version) => version.default).value;
|
||||
|
||||
export default () => ({
|
||||
createRolePath: null,
|
||||
|
||||
isCreatingRole: false,
|
||||
roleCreated: false,
|
||||
createRoleError: false,
|
||||
|
||||
accountId: '',
|
||||
externalId: '',
|
||||
|
||||
roleArn: '',
|
||||
|
||||
clusterName: '',
|
||||
environmentScope: '*',
|
||||
kubernetesVersion,
|
||||
selectedRegion: '',
|
||||
selectedRole: '',
|
||||
selectedKeyPair: '',
|
||||
selectedVpc: '',
|
||||
selectedSubnet: [],
|
||||
selectedSecurityGroup: '',
|
||||
selectedInstanceType: 'm5.large',
|
||||
nodeCount: '3',
|
||||
|
||||
isCreatingCluster: false,
|
||||
createClusterError: false,
|
||||
|
||||
gitlabManagedCluster: true,
|
||||
namespacePerEnvironment: true,
|
||||
});
|
|
@ -1,70 +0,0 @@
|
|||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
|
||||
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
|
||||
import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
|
||||
|
||||
import store from '../store';
|
||||
|
||||
export default {
|
||||
store,
|
||||
components: {
|
||||
DropdownButton,
|
||||
DropdownSearchInput,
|
||||
DropdownHiddenInput,
|
||||
GlLoadingIcon,
|
||||
},
|
||||
props: {
|
||||
fieldId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
fieldName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
defaultValue: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
hasErrors: false,
|
||||
searchQuery: '',
|
||||
gapiError: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
results() {
|
||||
if (!this.items) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.items.filter((item) => item.name.toLowerCase().indexOf(this.searchQuery) > -1);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
fetchSuccessHandler() {
|
||||
if (this.defaultValue) {
|
||||
const itemToSelect = this.items.find((item) => item.name === this.defaultValue);
|
||||
|
||||
if (itemToSelect) {
|
||||
this.setItem(itemToSelect.name);
|
||||
}
|
||||
}
|
||||
|
||||
this.isLoading = false;
|
||||
this.hasErrors = false;
|
||||
},
|
||||
fetchFailureHandler(resp) {
|
||||
this.isLoading = false;
|
||||
this.hasErrors = true;
|
||||
|
||||
if (resp.result && resp.result.error) {
|
||||
this.gapiError = resp.result.error.message;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
|
@ -1,112 +0,0 @@
|
|||
<script>
|
||||
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||
import { sprintf, s__ } from '~/locale';
|
||||
|
||||
import gkeDropdownMixin from './gke_dropdown_mixin';
|
||||
|
||||
export default {
|
||||
name: 'GkeMachineTypeDropdown',
|
||||
mixins: [gkeDropdownMixin],
|
||||
computed: {
|
||||
...mapState([
|
||||
'isValidatingProjectBilling',
|
||||
'projectHasBillingEnabled',
|
||||
'selectedZone',
|
||||
'selectedMachineType',
|
||||
]),
|
||||
...mapState({ items: 'machineTypes' }),
|
||||
...mapGetters(['hasZone', 'hasMachineType']),
|
||||
isDisabled() {
|
||||
return (
|
||||
this.isLoading ||
|
||||
this.isValidatingProjectBilling ||
|
||||
!this.projectHasBillingEnabled ||
|
||||
!this.hasZone
|
||||
);
|
||||
},
|
||||
toggleText() {
|
||||
if (this.isLoading) {
|
||||
return s__('ClusterIntegration|Fetching machine types');
|
||||
}
|
||||
|
||||
if (this.selectedMachineType) {
|
||||
return this.selectedMachineType;
|
||||
}
|
||||
|
||||
if (!this.projectHasBillingEnabled && !this.hasZone) {
|
||||
return s__('ClusterIntegration|Select project and zone to choose machine type');
|
||||
}
|
||||
|
||||
return !this.hasZone
|
||||
? s__('ClusterIntegration|Select zone to choose machine type')
|
||||
: s__('ClusterIntegration|Select machine type');
|
||||
},
|
||||
errorMessage() {
|
||||
return sprintf(
|
||||
s__(
|
||||
'ClusterIntegration|An error occurred while trying to fetch zone machine types: %{error}',
|
||||
),
|
||||
{ error: this.gapiError },
|
||||
);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
selectedZone() {
|
||||
this.hasErrors = false;
|
||||
|
||||
if (this.hasZone) {
|
||||
this.isLoading = true;
|
||||
|
||||
this.fetchMachineTypes().then(this.fetchSuccessHandler).catch(this.fetchFailureHandler);
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['fetchMachineTypes']),
|
||||
...mapActions({ setItem: 'setMachineType' }),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="js-gcp-machine-type-dropdown dropdown">
|
||||
<dropdown-hidden-input :name="fieldName" :value="selectedMachineType" />
|
||||
<dropdown-button
|
||||
:class="{ 'border-danger': hasErrors }"
|
||||
:is-disabled="isDisabled"
|
||||
:is-loading="isLoading"
|
||||
:toggle-text="toggleText"
|
||||
/>
|
||||
<div class="dropdown-menu dropdown-select">
|
||||
<dropdown-search-input
|
||||
v-model="searchQuery"
|
||||
:placeholder-text="s__('ClusterIntegration|Search machine types')"
|
||||
/>
|
||||
<div class="dropdown-content">
|
||||
<ul>
|
||||
<li v-show="!results.length">
|
||||
<span class="menu-item">
|
||||
{{ s__('ClusterIntegration|No machine types matched your search') }}
|
||||
</span>
|
||||
</li>
|
||||
<li v-for="result in results" :key="result.id">
|
||||
<button type="button" @click.prevent="setItem(result.name)">{{ result.name }}</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="dropdown-loading"><gl-loading-icon size="sm" /></div>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
v-if="hasErrors"
|
||||
:class="{
|
||||
'text-danger': hasErrors,
|
||||
'text-muted': !hasErrors,
|
||||
}"
|
||||
class="form-text"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
|
@ -1,53 +0,0 @@
|
|||
<script>
|
||||
import { createNamespacedHelpers, mapState, mapGetters, mapActions } from 'vuex';
|
||||
|
||||
import ClusterFormDropdown from '~/create_cluster/components/cluster_form_dropdown.vue';
|
||||
|
||||
const { mapState: mapDropdownState } = createNamespacedHelpers('networks');
|
||||
const { mapActions: mapSubnetworkActions } = createNamespacedHelpers('subnetworks');
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ClusterFormDropdown,
|
||||
},
|
||||
props: {
|
||||
fieldName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['selectedNetwork']),
|
||||
...mapDropdownState(['items', 'isLoadingItems', 'loadingItemsError']),
|
||||
...mapGetters(['hasZone', 'projectId', 'region']),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setNetwork', 'setSubnetwork']),
|
||||
...mapSubnetworkActions({ fetchSubnetworks: 'fetchItems' }),
|
||||
setNetworkAndFetchSubnetworks(network) {
|
||||
const { projectId: project, region } = this;
|
||||
|
||||
this.setSubnetwork('');
|
||||
this.setNetwork(network);
|
||||
this.fetchSubnetworks({ project, region, network: network.selfLink });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<cluster-form-dropdown
|
||||
:field-name="fieldName"
|
||||
:value="selectedNetwork"
|
||||
:items="items"
|
||||
:disabled="!hasZone"
|
||||
:loading="isLoadingItems"
|
||||
:has-errors="Boolean(loadingItemsError)"
|
||||
:loading-text="s__('ClusterIntegration|Loading networks')"
|
||||
:placeholder="s__('ClusterIntegration|Select a network')"
|
||||
:search-field-placeholder="s__('ClusterIntegration|Search networks')"
|
||||
:empty-text="s__('ClusterIntegration|No networks found')"
|
||||
:error-message="s__('ClusterIntegration|Could not load networks')"
|
||||
:disabled-text="s__('ClusterIntegration|Select a zone to choose a network')"
|
||||
@input="setNetworkAndFetchSubnetworks"
|
||||
/>
|
||||
</template>
|
|
@ -1,194 +0,0 @@
|
|||
<script>
|
||||
import { GlSprintf, GlLink, GlIcon } from '@gitlab/ui';
|
||||
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||
import { s__ } from '~/locale';
|
||||
|
||||
import gkeDropdownMixin from './gke_dropdown_mixin';
|
||||
|
||||
export default {
|
||||
name: 'GkeProjectIdDropdown',
|
||||
components: {
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
GlIcon,
|
||||
},
|
||||
mixins: [gkeDropdownMixin],
|
||||
props: {
|
||||
docsUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['selectedProject', 'isValidatingProjectBilling', 'projectHasBillingEnabled']),
|
||||
...mapState({ items: 'projects' }),
|
||||
...mapGetters(['hasProject']),
|
||||
hasOneProject() {
|
||||
return this.items && this.items.length === 1;
|
||||
},
|
||||
isDisabled() {
|
||||
return (
|
||||
this.isLoading || this.isValidatingProjectBilling || (this.items && this.items.length < 2)
|
||||
);
|
||||
},
|
||||
toggleText() {
|
||||
if (this.isValidatingProjectBilling) {
|
||||
return s__('ClusterIntegration|Validating project billing status');
|
||||
}
|
||||
|
||||
if (this.isLoading) {
|
||||
return s__('ClusterIntegration|Fetching projects');
|
||||
}
|
||||
|
||||
if (this.hasProject) {
|
||||
return this.selectedProject.name;
|
||||
}
|
||||
|
||||
if (!this.items) {
|
||||
return s__('ClusterIntegration|No projects found');
|
||||
}
|
||||
|
||||
return s__('ClusterIntegration|Select project');
|
||||
},
|
||||
helpText() {
|
||||
if (this.hasErrors) {
|
||||
return this.errorMessage;
|
||||
}
|
||||
|
||||
if (!this.items) {
|
||||
return s__(
|
||||
'ClusterIntegration|We were unable to fetch any projects. Ensure that you have a project on %{docsLinkStart}Google Cloud Platform%{docsLinkEnd}.',
|
||||
);
|
||||
}
|
||||
|
||||
return this.items.length
|
||||
? s__(
|
||||
'ClusterIntegration|To use a new project, first create one on %{docsLinkStart}Google Cloud Platform%{docsLinkEnd}.',
|
||||
)
|
||||
: s__(
|
||||
'ClusterIntegration|To create a cluster, first create a project on %{docsLinkStart}Google Cloud Platform%{docsLinkEnd}.',
|
||||
);
|
||||
},
|
||||
errorMessage() {
|
||||
if (!this.projectHasBillingEnabled) {
|
||||
if (this.gapiError) {
|
||||
return s__(
|
||||
'ClusterIntegration|We could not verify that one of your projects on GCP has billing enabled. Please try again.',
|
||||
);
|
||||
}
|
||||
|
||||
return s__(
|
||||
'ClusterIntegration|This project does not have billing enabled. To create a cluster, %{linkToBillingStart}enable billing%{linkToBillingEnd} and try again.',
|
||||
);
|
||||
}
|
||||
|
||||
return s__(
|
||||
'ClusterIntegration|An error occurred while trying to fetch your projects: %{error}',
|
||||
);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
selectedProject() {
|
||||
this.setIsValidatingProjectBilling(true);
|
||||
|
||||
this.validateProjectBilling()
|
||||
.then(this.validateProjectBillingSuccessHandler)
|
||||
.catch(this.validateProjectBillingFailureHandler);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.isLoading = true;
|
||||
|
||||
this.fetchProjects().then(this.fetchSuccessHandler).catch(this.fetchFailureHandler);
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['fetchProjects', 'setIsValidatingProjectBilling', 'validateProjectBilling']),
|
||||
...mapActions({ setItem: 'setProject' }),
|
||||
fetchSuccessHandler() {
|
||||
if (this.defaultValue) {
|
||||
const projectToSelect = this.items.find((item) => item.projectId === this.defaultValue);
|
||||
|
||||
if (projectToSelect) {
|
||||
this.setItem(projectToSelect);
|
||||
}
|
||||
} else if (this.items.length === 1) {
|
||||
this.setItem(this.items[0]);
|
||||
}
|
||||
|
||||
this.isLoading = false;
|
||||
this.hasErrors = false;
|
||||
},
|
||||
validateProjectBillingSuccessHandler() {
|
||||
this.hasErrors = !this.projectHasBillingEnabled;
|
||||
},
|
||||
validateProjectBillingFailureHandler(resp) {
|
||||
this.hasErrors = true;
|
||||
|
||||
this.gapiError = resp.result ? resp.result.error.message : resp;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="js-gcp-project-id-dropdown dropdown">
|
||||
<dropdown-hidden-input :name="fieldName" :value="selectedProject.projectId" />
|
||||
<dropdown-button
|
||||
:class="{
|
||||
'border-danger': hasErrors,
|
||||
'read-only': hasOneProject,
|
||||
}"
|
||||
:is-disabled="isDisabled"
|
||||
:is-loading="isLoading"
|
||||
:toggle-text="toggleText"
|
||||
/>
|
||||
<div class="dropdown-menu dropdown-select">
|
||||
<dropdown-search-input
|
||||
v-model="searchQuery"
|
||||
:placeholder-text="s__('ClusterIntegration|Search projects')"
|
||||
/>
|
||||
<div class="dropdown-content">
|
||||
<ul>
|
||||
<li v-show="!results.length">
|
||||
<span class="menu-item">
|
||||
{{ s__('ClusterIntegration|No projects matched your search') }}
|
||||
</span>
|
||||
</li>
|
||||
<li v-for="result in results" :key="result.project_number">
|
||||
<button type="button" @click.prevent="setItem(result)">{{ result.name }}</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="dropdown-loading"><gl-loading-icon size="sm" /></div>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
:class="{
|
||||
'text-danger': hasErrors,
|
||||
'text-muted': !hasErrors,
|
||||
}"
|
||||
class="form-text"
|
||||
>
|
||||
<gl-sprintf :message="helpText">
|
||||
<template #linkToBilling="{ content }">
|
||||
<gl-link
|
||||
:href="'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral'"
|
||||
target="_blank"
|
||||
>{{ content }} <gl-icon name="external-link"
|
||||
/></gl-link>
|
||||
</template>
|
||||
|
||||
<template #docsLink="{ content }">
|
||||
<gl-link :href="docsUrl" target="_blank"
|
||||
>{{ content }} <gl-icon name="external-link"
|
||||
/></gl-link>
|
||||
</template>
|
||||
|
||||
<template #error>
|
||||
{{ gapiError }}
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
|
@ -1,18 +0,0 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapGetters(['hasValidData']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="!hasValidData"
|
||||
class="js-gke-cluster-creation-submit btn btn-success"
|
||||
>
|
||||
{{ s__('ClusterIntegration|Create Kubernetes cluster') }}
|
||||
</button>
|
||||
</template>
|
|
@ -1,44 +0,0 @@
|
|||
<script>
|
||||
import { createNamespacedHelpers, mapState, mapGetters, mapActions } from 'vuex';
|
||||
|
||||
import ClusterFormDropdown from '~/create_cluster/components/cluster_form_dropdown.vue';
|
||||
|
||||
const { mapState: mapDropdownState } = createNamespacedHelpers('subnetworks');
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ClusterFormDropdown,
|
||||
},
|
||||
props: {
|
||||
fieldName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['selectedSubnetwork']),
|
||||
...mapDropdownState(['items', 'isLoadingItems', 'loadingItemsError']),
|
||||
...mapGetters(['hasNetwork']),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setSubnetwork']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<cluster-form-dropdown
|
||||
:field-name="fieldName"
|
||||
:value="selectedSubnetwork"
|
||||
:items="items"
|
||||
:disabled="!hasNetwork"
|
||||
:loading="isLoadingItems"
|
||||
:has-errors="Boolean(loadingItemsError)"
|
||||
:loading-text="s__('ClusterIntegration|Loading subnetworks')"
|
||||
:placeholder="s__('ClusterIntegration|Select a subnetwork')"
|
||||
:search-field-placeholder="s__('ClusterIntegration|Search subnetworks')"
|
||||
:empty-text="s__('ClusterIntegration|No subnetworks found')"
|
||||
:error-message="s__('ClusterIntegration|Could not load subnetworks')"
|
||||
:disabled-text="s__('ClusterIntegration|Select a network to choose a subnetwork')"
|
||||
@input="setSubnetwork"
|
||||
/>
|
||||
</template>
|
|
@ -1,101 +0,0 @@
|
|||
<script>
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import { sprintf, s__ } from '~/locale';
|
||||
|
||||
import gkeDropdownMixin from './gke_dropdown_mixin';
|
||||
|
||||
export default {
|
||||
name: 'GkeZoneDropdown',
|
||||
mixins: [gkeDropdownMixin],
|
||||
computed: {
|
||||
...mapState([
|
||||
'selectedProject',
|
||||
'selectedZone',
|
||||
'projects',
|
||||
'isValidatingProjectBilling',
|
||||
'projectHasBillingEnabled',
|
||||
]),
|
||||
...mapState({ items: 'zones' }),
|
||||
isDisabled() {
|
||||
return this.isLoading || this.isValidatingProjectBilling || !this.projectHasBillingEnabled;
|
||||
},
|
||||
toggleText() {
|
||||
if (this.isLoading) {
|
||||
return s__('ClusterIntegration|Fetching zones');
|
||||
}
|
||||
|
||||
if (this.selectedZone) {
|
||||
return this.selectedZone;
|
||||
}
|
||||
|
||||
return !this.projectHasBillingEnabled
|
||||
? s__('ClusterIntegration|Select project to choose zone')
|
||||
: s__('ClusterIntegration|Select zone');
|
||||
},
|
||||
errorMessage() {
|
||||
return sprintf(
|
||||
s__('ClusterIntegration|An error occurred while trying to fetch project zones: %{error}'),
|
||||
{ error: this.gapiError },
|
||||
);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
isValidatingProjectBilling(isValidating) {
|
||||
this.hasErrors = false;
|
||||
|
||||
if (!isValidating && this.projectHasBillingEnabled) {
|
||||
this.isLoading = true;
|
||||
|
||||
this.fetchZones().then(this.fetchSuccessHandler).catch(this.fetchFailureHandler);
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['fetchZones']),
|
||||
...mapActions({ setItem: 'setZone' }),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="js-gcp-zone-dropdown dropdown">
|
||||
<dropdown-hidden-input :name="fieldName" :value="selectedZone" />
|
||||
<dropdown-button
|
||||
:class="{ 'border-danger': hasErrors }"
|
||||
:is-disabled="isDisabled"
|
||||
:is-loading="isLoading"
|
||||
:toggle-text="toggleText"
|
||||
/>
|
||||
<div class="dropdown-menu dropdown-select">
|
||||
<dropdown-search-input
|
||||
v-model="searchQuery"
|
||||
:placeholder-text="s__('ClusterIntegration|Search zones')"
|
||||
/>
|
||||
<div class="dropdown-content">
|
||||
<ul>
|
||||
<li v-show="!results.length">
|
||||
<span class="menu-item">
|
||||
{{ s__('ClusterIntegration|No zones matched your search') }}
|
||||
</span>
|
||||
</li>
|
||||
<li v-for="result in results" :key="result.id">
|
||||
<button type="button" @click.prevent="setItem(result.name)">{{ result.name }}</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="dropdown-loading"><gl-loading-icon size="sm" /></div>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
v-if="hasErrors"
|
||||
:class="{
|
||||
'text-danger': hasErrors,
|
||||
'text-muted': !hasErrors,
|
||||
}"
|
||||
class="form-text"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
|
@ -1,11 +0,0 @@
|
|||
import { s__ } from '~/locale';
|
||||
|
||||
export const GCP_API_ERROR = s__(
|
||||
'ClusterIntegration|An error occurred when trying to contact the Google Cloud API. Please try again later.',
|
||||
);
|
||||
export const GCP_API_CLOUD_BILLING_ENDPOINT =
|
||||
'https://www.googleapis.com/discovery/v1/apis/cloudbilling/v1/rest';
|
||||
export const GCP_API_CLOUD_RESOURCE_MANAGER_ENDPOINT =
|
||||
'https://www.googleapis.com/discovery/v1/apis/cloudresourcemanager/v1/rest';
|
||||
export const GCP_API_COMPUTE_ENDPOINT =
|
||||
'https://www.googleapis.com/discovery/v1/apis/compute/v1/rest';
|
|
@ -1,24 +0,0 @@
|
|||
// This is a helper module to lazily import the google APIs for the GKE cluster
|
||||
// integration without introducing an indirect global dependency on an
|
||||
// initialized window.gapi object.
|
||||
export default () => {
|
||||
if (window.gapiPromise === undefined) {
|
||||
// first time loading the module
|
||||
window.gapiPromise = new Promise((resolve, reject) => {
|
||||
// this callback is set as a query param to script.src URL
|
||||
window.onGapiLoad = () => {
|
||||
resolve(window.gapi);
|
||||
};
|
||||
|
||||
const script = document.createElement('script');
|
||||
// do not use script.onload, because gapi continues to load after the initial script load
|
||||
script.type = 'text/javascript';
|
||||
script.async = true;
|
||||
script.src = 'https://apis.google.com/js/api.js?onload=onGapiLoad';
|
||||
script.onerror = reject;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
return window.gapiPromise;
|
||||
};
|
|
@ -1,95 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import createFlash from '~/flash';
|
||||
import GkeMachineTypeDropdown from './components/gke_machine_type_dropdown.vue';
|
||||
import GkeProjectIdDropdown from './components/gke_project_id_dropdown.vue';
|
||||
import GkeSubmitButton from './components/gke_submit_button.vue';
|
||||
import GkeZoneDropdown from './components/gke_zone_dropdown.vue';
|
||||
import * as CONSTANTS from './constants';
|
||||
import gapiLoader from './gapi_loader';
|
||||
|
||||
import store from './store';
|
||||
|
||||
const mountComponent = (entryPoint, component, componentName, extraProps = {}) => {
|
||||
const el = document.querySelector(entryPoint);
|
||||
if (!el) return false;
|
||||
|
||||
const hiddenInput = el.querySelector('input');
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
store,
|
||||
components: {
|
||||
[componentName]: component,
|
||||
},
|
||||
render: (createElement) =>
|
||||
createElement(componentName, {
|
||||
props: {
|
||||
fieldName: hiddenInput.getAttribute('name'),
|
||||
fieldId: hiddenInput.getAttribute('id'),
|
||||
defaultValue: hiddenInput.value,
|
||||
...extraProps,
|
||||
},
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const mountGkeProjectIdDropdown = () => {
|
||||
const entryPoint = '.js-gcp-project-id-dropdown-entry-point';
|
||||
const el = document.querySelector(entryPoint);
|
||||
|
||||
mountComponent(entryPoint, GkeProjectIdDropdown, 'gke-project-id-dropdown', {
|
||||
docsUrl: el.dataset.docsurl,
|
||||
});
|
||||
};
|
||||
|
||||
const mountGkeZoneDropdown = () => {
|
||||
mountComponent('.js-gcp-zone-dropdown-entry-point', GkeZoneDropdown, 'gke-zone-dropdown');
|
||||
};
|
||||
|
||||
const mountGkeMachineTypeDropdown = () => {
|
||||
mountComponent(
|
||||
'.js-gcp-machine-type-dropdown-entry-point',
|
||||
GkeMachineTypeDropdown,
|
||||
'gke-machine-type-dropdown',
|
||||
);
|
||||
};
|
||||
|
||||
const mountGkeSubmitButton = () => {
|
||||
mountComponent('.js-gke-cluster-creation-submit-container', GkeSubmitButton, 'gke-submit-button');
|
||||
};
|
||||
|
||||
const gkeDropdownErrorHandler = () => {
|
||||
createFlash({
|
||||
message: CONSTANTS.GCP_API_ERROR,
|
||||
});
|
||||
};
|
||||
|
||||
const initializeGapiClient = (gapi) => () => {
|
||||
const el = document.querySelector('.js-gke-cluster-creation');
|
||||
if (!el) return false;
|
||||
|
||||
return gapi.client
|
||||
.init({
|
||||
discoveryDocs: [
|
||||
CONSTANTS.GCP_API_CLOUD_BILLING_ENDPOINT,
|
||||
CONSTANTS.GCP_API_CLOUD_RESOURCE_MANAGER_ENDPOINT,
|
||||
CONSTANTS.GCP_API_COMPUTE_ENDPOINT,
|
||||
],
|
||||
})
|
||||
.then(() => {
|
||||
gapi.client.setToken({ access_token: el.dataset.token });
|
||||
|
||||
mountGkeProjectIdDropdown();
|
||||
mountGkeZoneDropdown();
|
||||
mountGkeMachineTypeDropdown();
|
||||
mountGkeSubmitButton();
|
||||
})
|
||||
.catch(gkeDropdownErrorHandler);
|
||||
};
|
||||
|
||||
const initGkeDropdowns = () =>
|
||||
gapiLoader()
|
||||
.then((gapi) => gapi.load('client', initializeGapiClient(gapi)))
|
||||
.catch(gkeDropdownErrorHandler);
|
||||
|
||||
export default initGkeDropdowns;
|
|
@ -1,99 +0,0 @@
|
|||
import gapiLoader from '../gapi_loader';
|
||||
import * as types from './mutation_types';
|
||||
|
||||
const gapiResourceListRequest = ({ resource, params, commit, mutation, payloadKey }) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const request = resource.list(params);
|
||||
|
||||
return request.then(
|
||||
(resp) => {
|
||||
const { result } = resp;
|
||||
|
||||
commit(mutation, result[payloadKey]);
|
||||
|
||||
resolve();
|
||||
},
|
||||
(resp) => {
|
||||
reject(resp);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
export const setProject = ({ commit }, selectedProject) => {
|
||||
commit(types.SET_PROJECT, selectedProject);
|
||||
};
|
||||
|
||||
export const setZone = ({ commit }, selectedZone) => {
|
||||
commit(types.SET_ZONE, selectedZone);
|
||||
};
|
||||
|
||||
export const setMachineType = ({ commit }, selectedMachineType) => {
|
||||
commit(types.SET_MACHINE_TYPE, selectedMachineType);
|
||||
};
|
||||
|
||||
export const setIsValidatingProjectBilling = ({ commit }, isValidatingProjectBilling) => {
|
||||
commit(types.SET_IS_VALIDATING_PROJECT_BILLING, isValidatingProjectBilling);
|
||||
};
|
||||
|
||||
export const fetchProjects = ({ commit }) =>
|
||||
gapiLoader().then((gapi) =>
|
||||
gapiResourceListRequest({
|
||||
resource: gapi.client.cloudresourcemanager.projects,
|
||||
params: {},
|
||||
commit,
|
||||
mutation: types.SET_PROJECTS,
|
||||
payloadKey: 'projects',
|
||||
}),
|
||||
);
|
||||
|
||||
export const validateProjectBilling = ({ dispatch, commit, state }) =>
|
||||
gapiLoader()
|
||||
.then((gapi) => {
|
||||
const request = gapi.client.cloudbilling.projects.getBillingInfo({
|
||||
name: `projects/${state.selectedProject.projectId}`,
|
||||
});
|
||||
|
||||
commit(types.SET_ZONE, '');
|
||||
commit(types.SET_MACHINE_TYPE, '');
|
||||
|
||||
return request;
|
||||
})
|
||||
.then(
|
||||
(resp) => {
|
||||
const { billingEnabled } = resp.result;
|
||||
|
||||
commit(types.SET_PROJECT_BILLING_STATUS, Boolean(billingEnabled));
|
||||
dispatch('setIsValidatingProjectBilling', false);
|
||||
},
|
||||
(errorResp) => {
|
||||
dispatch('setIsValidatingProjectBilling', false);
|
||||
return errorResp;
|
||||
},
|
||||
);
|
||||
|
||||
export const fetchZones = ({ commit, state }) =>
|
||||
gapiLoader().then((gapi) =>
|
||||
gapiResourceListRequest({
|
||||
resource: gapi.client.compute.zones,
|
||||
params: {
|
||||
project: state.selectedProject.projectId,
|
||||
},
|
||||
commit,
|
||||
mutation: types.SET_ZONES,
|
||||
payloadKey: 'items',
|
||||
}),
|
||||
);
|
||||
|
||||
export const fetchMachineTypes = ({ commit, state }) =>
|
||||
gapiLoader().then((gapi) =>
|
||||
gapiResourceListRequest({
|
||||
resource: gapi.client.compute.machineTypes,
|
||||
params: {
|
||||
project: state.selectedProject.projectId,
|
||||
zone: state.selectedZone,
|
||||
},
|
||||
commit,
|
||||
mutation: types.SET_MACHINE_TYPES,
|
||||
payloadKey: 'items',
|
||||
}),
|
||||
);
|
|
@ -1,5 +0,0 @@
|
|||
export const hasProject = (state) => Boolean(state.selectedProject.projectId);
|
||||
export const hasZone = (state) => Boolean(state.selectedZone);
|
||||
export const hasMachineType = (state) => Boolean(state.selectedMachineType);
|
||||
export const hasValidData = (state, getters) =>
|
||||
Boolean(state.projectHasBillingEnabled) && getters.hasZone && getters.hasMachineType;
|
|
@ -1,18 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import * as actions from './actions';
|
||||
import * as getters from './getters';
|
||||
import mutations from './mutations';
|
||||
import createState from './state';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export const createStore = () =>
|
||||
new Vuex.Store({
|
||||
actions,
|
||||
getters,
|
||||
mutations,
|
||||
state: createState(),
|
||||
});
|
||||
|
||||
export default createStore();
|
|
@ -1,8 +0,0 @@
|
|||
export const SET_PROJECT = 'SET_PROJECT';
|
||||
export const SET_PROJECT_BILLING_STATUS = 'SET_PROJECT_BILLING_STATUS';
|
||||
export const SET_IS_VALIDATING_PROJECT_BILLING = 'SET_IS_VALIDATING_PROJECT_BILLING';
|
||||
export const SET_ZONE = 'SET_ZONE';
|
||||
export const SET_MACHINE_TYPE = 'SET_MACHINE_TYPE';
|
||||
export const SET_PROJECTS = 'SET_PROJECTS';
|
||||
export const SET_ZONES = 'SET_ZONES';
|
||||
export const SET_MACHINE_TYPES = 'SET_MACHINE_TYPES';
|
|
@ -1,28 +0,0 @@
|
|||
import * as types from './mutation_types';
|
||||
|
||||
export default {
|
||||
[types.SET_PROJECT](state, selectedProject) {
|
||||
Object.assign(state, { selectedProject });
|
||||
},
|
||||
[types.SET_IS_VALIDATING_PROJECT_BILLING](state, isValidatingProjectBilling) {
|
||||
Object.assign(state, { isValidatingProjectBilling });
|
||||
},
|
||||
[types.SET_PROJECT_BILLING_STATUS](state, projectHasBillingEnabled) {
|
||||
Object.assign(state, { projectHasBillingEnabled });
|
||||
},
|
||||
[types.SET_ZONE](state, selectedZone) {
|
||||
Object.assign(state, { selectedZone });
|
||||
},
|
||||
[types.SET_MACHINE_TYPE](state, selectedMachineType) {
|
||||
Object.assign(state, { selectedMachineType });
|
||||
},
|
||||
[types.SET_PROJECTS](state, projects) {
|
||||
Object.assign(state, { projects });
|
||||
},
|
||||
[types.SET_ZONES](state, zones) {
|
||||
Object.assign(state, { zones });
|
||||
},
|
||||
[types.SET_MACHINE_TYPES](state, machineTypes) {
|
||||
Object.assign(state, { machineTypes });
|
||||
},
|
||||
};
|
|
@ -1,13 +0,0 @@
|
|||
export default () => ({
|
||||
selectedProject: {
|
||||
projectId: '',
|
||||
name: '',
|
||||
},
|
||||
selectedZone: '',
|
||||
selectedMachineType: '',
|
||||
isValidatingProjectBilling: null,
|
||||
projectHasBillingEnabled: null,
|
||||
projects: [],
|
||||
zones: [],
|
||||
machineTypes: [],
|
||||
});
|
|
@ -1,35 +0,0 @@
|
|||
import PersistentUserCallout from '~/persistent_user_callout';
|
||||
import initGkeDropdowns from './gke_cluster';
|
||||
import initGkeNamespace from './gke_cluster_namespace';
|
||||
|
||||
const newClusterViews = [':clusters:new', ':clusters:create_gcp', ':clusters:create_user'];
|
||||
|
||||
const isProjectLevelCluster = (page) => page.startsWith('project:clusters');
|
||||
|
||||
export default (document) => {
|
||||
const { page } = document.body.dataset;
|
||||
const isNewClusterView = newClusterViews.some((view) => page.endsWith(view));
|
||||
|
||||
if (!isNewClusterView) {
|
||||
return;
|
||||
}
|
||||
|
||||
const callout = document.querySelector('.gcp-signup-offer');
|
||||
PersistentUserCallout.factory(callout);
|
||||
|
||||
initGkeDropdowns();
|
||||
|
||||
if (isProjectLevelCluster(page)) {
|
||||
initGkeNamespace();
|
||||
}
|
||||
|
||||
import(/* webpackChunkName: 'eks_cluster' */ '~/create_cluster/eks_cluster')
|
||||
.then(({ default: initCreateEKSCluster }) => {
|
||||
const el = document.querySelector('.js-create-eks-cluster-form-container');
|
||||
|
||||
if (el) {
|
||||
initCreateEKSCluster(el);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
|
@ -1,14 +0,0 @@
|
|||
import * as types from './mutation_types';
|
||||
|
||||
export default (fetchItems) => ({
|
||||
requestItems: ({ commit }) => commit(types.REQUEST_ITEMS),
|
||||
receiveItemsSuccess: ({ commit }, payload) => commit(types.RECEIVE_ITEMS_SUCCESS, payload),
|
||||
receiveItemsError: ({ commit }, payload) => commit(types.RECEIVE_ITEMS_ERROR, payload),
|
||||
fetchItems: ({ dispatch }, payload) => {
|
||||
dispatch('requestItems');
|
||||
|
||||
return fetchItems(payload)
|
||||
.then((items) => dispatch('receiveItemsSuccess', { items }))
|
||||
.catch((error) => dispatch('receiveItemsError', { error }));
|
||||
},
|
||||
});
|
|
@ -1,13 +0,0 @@
|
|||
import actions from './actions';
|
||||
import * as getters from './getters';
|
||||
import mutations from './mutations';
|
||||
import state from './state';
|
||||
|
||||
const createStore = ({ fetchFn, initialState }) => ({
|
||||
actions: actions(fetchFn),
|
||||
getters,
|
||||
mutations,
|
||||
state: Object.assign(state(), initialState || {}),
|
||||
});
|
||||
|
||||
export default createStore;
|
|
@ -1,3 +0,0 @@
|
|||
export const REQUEST_ITEMS = 'REQUEST_ITEMS';
|
||||
export const RECEIVE_ITEMS_SUCCESS = 'REQUEST_ITEMS_SUCCESS';
|
||||
export const RECEIVE_ITEMS_ERROR = 'RECEIVE_ITEMS_ERROR';
|
|
@ -1,16 +0,0 @@
|
|||
import * as types from './mutation_types';
|
||||
|
||||
export default {
|
||||
[types.REQUEST_ITEMS](state) {
|
||||
state.isLoadingItems = true;
|
||||
state.loadingItemsError = null;
|
||||
},
|
||||
[types.RECEIVE_ITEMS_SUCCESS](state, { items }) {
|
||||
state.isLoadingItems = false;
|
||||
state.items = items;
|
||||
},
|
||||
[types.RECEIVE_ITEMS_ERROR](state, { error }) {
|
||||
state.isLoadingItems = false;
|
||||
state.loadingItemsError = error;
|
||||
},
|
||||
};
|
|
@ -1,5 +0,0 @@
|
|||
export default () => ({
|
||||
isLoadingItems: false,
|
||||
items: [],
|
||||
loadingItemsError: null,
|
||||
});
|
|
@ -45,6 +45,8 @@ export default {
|
|||
copyToClipboardText: s__('EnableReviewApp|Copy snippet text'),
|
||||
title: s__('ReviewApp|Enable Review App'),
|
||||
},
|
||||
visualReviewsDocs: helpPagePath('ci/review_apps/index.md', { anchor: 'visual-reviews' }),
|
||||
connectClusterDocs: helpPagePath('user/clusters/agent/index'),
|
||||
data() {
|
||||
const modalInfoCopyId = uniqueId('enable-review-app-copy-string-');
|
||||
|
||||
|
@ -64,9 +66,6 @@ export default {
|
|||
except:
|
||||
- ${this.defaultBranchName}`;
|
||||
},
|
||||
visualReviewsDocs() {
|
||||
return helpPagePath('ci/review_apps/index.md', { anchor: 'visual-reviews' });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -88,11 +87,7 @@ export default {
|
|||
<strong>{{ content }}</strong>
|
||||
</template>
|
||||
<template #link="{ content }">
|
||||
<gl-link
|
||||
href="https://docs.gitlab.com/ee/user/project/clusters/add_remove_clusters.html"
|
||||
target="_blank"
|
||||
>{{ content }}</gl-link
|
||||
>
|
||||
<gl-link :href="$options.connectClusterDocs" target="_blank">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
|
@ -134,7 +129,7 @@ export default {
|
|||
<strong>{{ content }}</strong>
|
||||
</template>
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="visualReviewsDocs" target="_blank">{{ content }}</gl-link>
|
||||
<gl-link :href="$options.visualReviewsDocs" target="_blank">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
|
|
|
@ -44,6 +44,11 @@ const STATUS_MAP = {
|
|||
text: __('Failed'),
|
||||
variant: 'danger',
|
||||
},
|
||||
[STATUSES.TIMEOUT]: {
|
||||
icon: 'status-failed',
|
||||
text: __('Timeout'),
|
||||
variant: 'danger',
|
||||
},
|
||||
[STATUSES.CANCELLED]: {
|
||||
icon: 'status-stopped',
|
||||
text: __('Cancelled'),
|
||||
|
|
|
@ -10,4 +10,5 @@ export const STATUSES = {
|
|||
NONE: 'none',
|
||||
SCHEDULING: 'scheduling',
|
||||
CANCELLED: 'cancelled',
|
||||
TIMEOUT: 'timeout',
|
||||
};
|
||||
|
|
|
@ -10,7 +10,7 @@ export function getInvalidNameValidationMessage(importTarget) {
|
|||
}
|
||||
|
||||
export function isFinished(group) {
|
||||
return [STATUSES.FINISHED, STATUSES.FAILED].includes(group.progress?.status);
|
||||
return [STATUSES.FINISHED, STATUSES.FAILED, STATUSES.TIMEOUT].includes(group.progress?.status);
|
||||
}
|
||||
|
||||
export function isAvailableForImport(group) {
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
import initCreateCluster from '~/create_cluster/init_create_cluster';
|
||||
|
||||
initCreateCluster(document, gon);
|
|
@ -1,5 +1,3 @@
|
|||
import initIntegrationForm from '~/clusters/forms/show/index';
|
||||
import initCreateCluster from '~/create_cluster/init_create_cluster';
|
||||
|
||||
initCreateCluster(document, gon);
|
||||
initIntegrationForm();
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
import initCreateCluster from '~/create_cluster/init_create_cluster';
|
||||
|
||||
initCreateCluster(document, gon);
|
|
@ -1,6 +1,6 @@
|
|||
import ClustersBundle from '~/clusters/clusters_bundle';
|
||||
import initIntegrationForm from '~/clusters/forms/show';
|
||||
import initGkeNamespace from '~/create_cluster/gke_cluster_namespace';
|
||||
import initGkeNamespace from '~/clusters/gke_cluster_namespace';
|
||||
import initClusterHealth from './cluster_health';
|
||||
|
||||
new ClustersBundle(); // eslint-disable-line no-new
|
||||
|
|
|
@ -19,7 +19,7 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
runnerUrl: {
|
||||
runnerPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
|
@ -66,7 +66,7 @@ export default {
|
|||
<runner-update-form
|
||||
:loading="loading"
|
||||
:runner="runner"
|
||||
:runner-url="runnerUrl"
|
||||
:runner-path="runnerPath"
|
||||
class="gl-my-5"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -12,7 +12,7 @@ export const initAdminRunnerEdit = (selector = '#js-admin-runner-edit') => {
|
|||
return null;
|
||||
}
|
||||
|
||||
const { runnerId, runnerUrl } = el.dataset;
|
||||
const { runnerId, runnerPath } = el.dataset;
|
||||
|
||||
const apolloProvider = new VueApollo({
|
||||
defaultClient: createDefaultClient(),
|
||||
|
@ -25,7 +25,7 @@ export const initAdminRunnerEdit = (selector = '#js-admin-runner-edit') => {
|
|||
return h(AdminRunnerEditApp, {
|
||||
props: {
|
||||
runnerId,
|
||||
runnerUrl,
|
||||
runnerPath,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
import { showAlertFromLocalStorage } from '../local_storage_alert/show_alert_from_local_storage';
|
||||
import AdminRunnerShowApp from './admin_runner_show_app.vue';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
export const initAdminRunnerShow = (selector = '#js-admin-runner-show') => {
|
||||
showAlertFromLocalStorage();
|
||||
|
||||
const el = document.querySelector(selector);
|
||||
|
||||
if (!el) {
|
||||
|
|
|
@ -14,10 +14,12 @@ import {
|
|||
runnerToModel,
|
||||
} from 'ee_else_ce/runner/runner_update_form_utils';
|
||||
import { createAlert, VARIANT_SUCCESS } from '~/flash';
|
||||
import { redirectTo } from '~/lib/utils/url_utility';
|
||||
import { __ } from '~/locale';
|
||||
import { captureException } from '~/runner/sentry_utils';
|
||||
import { ACCESS_LEVEL_NOT_PROTECTED, ACCESS_LEVEL_REF_PROTECTED, PROJECT_TYPE } from '../constants';
|
||||
import runnerUpdateMutation from '../graphql/details/runner_update.mutation.graphql';
|
||||
import { saveAlertToLocalStorage } from '../local_storage_alert/save_alert_to_local_storage';
|
||||
|
||||
export default {
|
||||
name: 'RunnerUpdateForm',
|
||||
|
@ -46,7 +48,7 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
runnerUrl: {
|
||||
runnerPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
|
@ -85,24 +87,23 @@ export default {
|
|||
});
|
||||
|
||||
if (errors?.length) {
|
||||
// Validation errors need not be thrown
|
||||
createAlert({ message: errors[0] });
|
||||
return;
|
||||
this.onError(errors[0]);
|
||||
} else {
|
||||
this.onSuccess();
|
||||
}
|
||||
|
||||
this.onSuccess();
|
||||
} catch (error) {
|
||||
const { message } = error;
|
||||
|
||||
createAlert({ message });
|
||||
this.onError(message);
|
||||
captureException({ error, component: this.$options.name });
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
onSuccess() {
|
||||
createAlert({ message: __('Changes saved.'), variant: VARIANT_SUCCESS });
|
||||
this.model = runnerToModel(this.runner);
|
||||
saveAlertToLocalStorage({ message: __('Changes saved.'), variant: VARIANT_SUCCESS });
|
||||
redirectTo(this.runnerPath);
|
||||
},
|
||||
onError(message) {
|
||||
this.saving = false;
|
||||
createAlert({ message });
|
||||
},
|
||||
},
|
||||
ACCESS_LEVEL_NOT_PROTECTED,
|
||||
|
@ -210,7 +211,7 @@ export default {
|
|||
>
|
||||
{{ __('Save changes') }}
|
||||
</gl-button>
|
||||
<gl-button :href="runnerUrl">
|
||||
<gl-button :href="runnerPath">
|
||||
{{ __('Cancel') }}
|
||||
</gl-button>
|
||||
</div>
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
<script>
|
||||
export default {
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: [Number, String],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input :name="name" :value="value" type="hidden" />
|
||||
</template>
|
|
@ -1,49 +0,0 @@
|
|||
<script>
|
||||
import { GlIcon } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
},
|
||||
props: {
|
||||
placeholderText: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: __('Search'),
|
||||
},
|
||||
focused: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return { searchQuery: this.value };
|
||||
},
|
||||
watch: {
|
||||
searchQuery(query) {
|
||||
this.$emit('input', query);
|
||||
},
|
||||
focused(val) {
|
||||
if (val) {
|
||||
this.$refs.searchInput.focus();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dropdown-input">
|
||||
<input
|
||||
ref="searchInput"
|
||||
v-model="searchQuery"
|
||||
:placeholder="placeholderText"
|
||||
class="dropdown-input-field"
|
||||
type="search"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<gl-icon name="search" class="dropdown-input-search" data-hidden="true" />
|
||||
</div>
|
||||
</template>
|
|
@ -6,12 +6,9 @@ class Clusters::ClustersController < Clusters::BaseController
|
|||
include MetricsDashboard
|
||||
|
||||
before_action :cluster, only: [:cluster_status, :show, :update, :destroy, :clear_cache]
|
||||
before_action :generate_gcp_authorize_url, only: [:new]
|
||||
before_action :validate_gcp_token, only: [:new]
|
||||
before_action :gcp_cluster, only: [:new]
|
||||
before_action :user_cluster, only: [:new, :connect]
|
||||
before_action :user_cluster, only: [:connect]
|
||||
before_action :authorize_read_cluster!, only: [:show, :index]
|
||||
before_action :authorize_create_cluster!, only: [:new, :connect, :authorize_aws_role]
|
||||
before_action :authorize_create_cluster!, only: [:connect, :authorize_aws_role]
|
||||
before_action :authorize_update_cluster!, only: [:update]
|
||||
before_action :update_applications_status, only: [:cluster_status]
|
||||
before_action :ensure_feature_enabled!, except: [:index, :new_cluster_docs]
|
||||
|
@ -46,16 +43,6 @@ class Clusters::ClustersController < Clusters::BaseController
|
|||
end
|
||||
end
|
||||
|
||||
def new
|
||||
if params[:provider] == 'aws'
|
||||
@aws_role = Aws::Role.create_or_find_by!(user: current_user)
|
||||
@instance_types = load_instance_types.to_json
|
||||
|
||||
elsif params[:provider] == 'gcp'
|
||||
redirect_to @authorize_url if @authorize_url && !@valid_gcp_token
|
||||
end
|
||||
end
|
||||
|
||||
# Overridding ActionController::Metal#status is NOT a good idea
|
||||
def cluster_status
|
||||
respond_to do |format|
|
||||
|
@ -108,24 +95,6 @@ class Clusters::ClustersController < Clusters::BaseController
|
|||
redirect_to clusterable.index_path, status: :found
|
||||
end
|
||||
|
||||
def create_gcp
|
||||
@gcp_cluster = ::Clusters::CreateService
|
||||
.new(current_user, create_gcp_cluster_params)
|
||||
.execute(access_token: token_in_session)
|
||||
.present(current_user: current_user)
|
||||
|
||||
if @gcp_cluster.persisted?
|
||||
redirect_to @gcp_cluster.show_path
|
||||
else
|
||||
generate_gcp_authorize_url
|
||||
validate_gcp_token
|
||||
user_cluster
|
||||
params[:provider] = 'gcp'
|
||||
|
||||
render :new, locals: { active_tab: 'create' }
|
||||
end
|
||||
end
|
||||
|
||||
def create_aws
|
||||
@aws_cluster = ::Clusters::CreateService
|
||||
.new(current_user, create_aws_cluster_params)
|
||||
|
@ -235,24 +204,6 @@ class Clusters::ClustersController < Clusters::BaseController
|
|||
end
|
||||
end
|
||||
|
||||
def create_gcp_cluster_params
|
||||
params.require(:cluster).permit(
|
||||
*base_permitted_cluster_params,
|
||||
:name,
|
||||
provider_gcp_attributes: [
|
||||
:gcp_project_id,
|
||||
:zone,
|
||||
:num_nodes,
|
||||
:machine_type,
|
||||
:cloud_run,
|
||||
:legacy_abac
|
||||
]).merge(
|
||||
provider_type: :gcp,
|
||||
platform_type: :kubernetes,
|
||||
clusterable: clusterable.__subject__
|
||||
)
|
||||
end
|
||||
|
||||
def create_aws_cluster_params
|
||||
params.require(:cluster).permit(
|
||||
*base_permitted_cluster_params,
|
||||
|
@ -296,10 +247,10 @@ class Clusters::ClustersController < Clusters::BaseController
|
|||
end
|
||||
|
||||
def generate_gcp_authorize_url
|
||||
new_path = clusterable.new_path(provider: :gcp).to_s
|
||||
error_path = @project ? project_clusters_path(@project) : new_path
|
||||
connect_path = clusterable.connect_path().to_s
|
||||
error_path = @project ? project_clusters_path(@project) : connect_path
|
||||
|
||||
state = generate_session_key_redirect(new_path, error_path)
|
||||
state = generate_session_key_redirect(connect_path, error_path)
|
||||
|
||||
@authorize_url = GoogleApi::CloudPlatform::Client.new(
|
||||
nil, callback_google_api_auth_url,
|
||||
|
|
|
@ -17,7 +17,6 @@ module ClustersHelper
|
|||
clusters_empty_state_image: image_path('illustrations/empty-state/empty-state-clusters.svg'),
|
||||
empty_state_image: image_path('illustrations/empty-state/empty-state-agents.svg'),
|
||||
empty_state_help_text: clusterable.empty_state_help_text,
|
||||
new_cluster_path: clusterable.new_path,
|
||||
add_cluster_path: clusterable.connect_path,
|
||||
new_cluster_docs_path: clusterable.new_cluster_docs_path,
|
||||
can_add_cluster: clusterable.can_add_cluster?.to_s,
|
||||
|
@ -43,12 +42,6 @@ module ClustersHelper
|
|||
}
|
||||
end
|
||||
|
||||
def js_cluster_new
|
||||
{
|
||||
cluster_connect_help_path: help_page_path('user/project/clusters/add_remove_clusters', anchor: 'add-existing-cluster')
|
||||
}
|
||||
end
|
||||
|
||||
def render_gcp_signup_offer
|
||||
return if Gitlab::CurrentSettings.current_application_settings.hide_third_party_offers?
|
||||
return unless show_gcp_signup_offer?
|
||||
|
|
|
@ -155,6 +155,20 @@ module Emails
|
|||
end
|
||||
end
|
||||
|
||||
def approved_merge_request_email(recipient_id, merge_request_id, approved_by_user_id, reason = nil)
|
||||
setup_merge_request_mail(merge_request_id, recipient_id)
|
||||
|
||||
@approved_by = User.find(approved_by_user_id)
|
||||
mail_answer_thread(@merge_request, merge_request_thread_options(approved_by_user_id, reason))
|
||||
end
|
||||
|
||||
def unapproved_merge_request_email(recipient_id, merge_request_id, unapproved_by_user_id, reason = nil)
|
||||
setup_merge_request_mail(merge_request_id, recipient_id)
|
||||
|
||||
@unapproved_by = User.find(unapproved_by_user_id)
|
||||
mail_answer_thread(@merge_request, merge_request_thread_options(unapproved_by_user_id, reason))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def setup_merge_request_mail(merge_request_id, recipient_id, present: false)
|
||||
|
|
|
@ -14,9 +14,16 @@ module Integrations
|
|||
logger.error(message)
|
||||
end
|
||||
|
||||
def log_exception(error, params = {})
|
||||
Gitlab::ExceptionLogFormatter.format!(error, params)
|
||||
|
||||
log_error(params[:message] || error.message, params)
|
||||
end
|
||||
|
||||
def build_message(message, params = {})
|
||||
{
|
||||
integration_class: self.class.name,
|
||||
integration_id: id,
|
||||
project_id: project&.id,
|
||||
project_path: project&.full_path,
|
||||
message: message
|
||||
|
|
|
@ -352,16 +352,7 @@ module Integrations
|
|||
|
||||
true
|
||||
rescue StandardError => error
|
||||
log_error(
|
||||
"Issue transition failed",
|
||||
error: {
|
||||
exception_class: error.class.name,
|
||||
exception_message: error.message,
|
||||
exception_backtrace: Gitlab::BacktraceCleaner.clean_backtrace(error.backtrace)
|
||||
},
|
||||
client_url: client_url
|
||||
)
|
||||
|
||||
log_exception(error, message: 'Issue transition failed', client_url: client_url)
|
||||
false
|
||||
end
|
||||
|
||||
|
@ -538,9 +529,7 @@ module Integrations
|
|||
yield
|
||||
rescue StandardError => error
|
||||
@error = error
|
||||
payload = { client_url: client_url }
|
||||
Gitlab::ExceptionLogFormatter.format!(error, payload)
|
||||
log_error("Error sending message", payload)
|
||||
log_exception(error, message: 'Error sending message', client_url: client_url)
|
||||
nil
|
||||
end
|
||||
|
||||
|
|
|
@ -28,10 +28,6 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated
|
|||
polymorphic_path([clusterable, :clusters], options)
|
||||
end
|
||||
|
||||
def new_path(options = {})
|
||||
new_polymorphic_path([clusterable, :cluster], options)
|
||||
end
|
||||
|
||||
def connect_path
|
||||
polymorphic_path([clusterable, :clusters], action: :connect)
|
||||
end
|
||||
|
@ -40,22 +36,10 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated
|
|||
polymorphic_path([clusterable, :clusters], action: :new_cluster_docs)
|
||||
end
|
||||
|
||||
def authorize_aws_role_path
|
||||
polymorphic_path([clusterable, :clusters], action: :authorize_aws_role)
|
||||
end
|
||||
|
||||
def create_user_clusters_path
|
||||
polymorphic_path([clusterable, :clusters], action: :create_user)
|
||||
end
|
||||
|
||||
def create_gcp_clusters_path
|
||||
polymorphic_path([clusterable, :clusters], action: :create_gcp)
|
||||
end
|
||||
|
||||
def create_aws_clusters_path
|
||||
polymorphic_path([clusterable, :clusters], action: :create_aws)
|
||||
end
|
||||
|
||||
def cluster_status_cluster_path(cluster, params = {})
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
|
|
@ -18,11 +18,6 @@ class InstanceClusterablePresenter < ClusterablePresenter
|
|||
admin_clusters_path(options)
|
||||
end
|
||||
|
||||
override :new_path
|
||||
def new_path(options = {})
|
||||
new_admin_cluster_path(options)
|
||||
end
|
||||
|
||||
override :cluster_status_cluster_path
|
||||
def cluster_status_cluster_path(cluster, params = {})
|
||||
cluster_status_admin_cluster_path(cluster, params)
|
||||
|
@ -53,21 +48,6 @@ class InstanceClusterablePresenter < ClusterablePresenter
|
|||
create_user_admin_clusters_path
|
||||
end
|
||||
|
||||
override :create_gcp_clusters_path
|
||||
def create_gcp_clusters_path
|
||||
create_gcp_admin_clusters_path
|
||||
end
|
||||
|
||||
override :create_aws_clusters_path
|
||||
def create_aws_clusters_path
|
||||
create_aws_admin_clusters_path
|
||||
end
|
||||
|
||||
override :authorize_aws_role_path
|
||||
def authorize_aws_role_path
|
||||
authorize_aws_role_admin_clusters_path
|
||||
end
|
||||
|
||||
override :empty_state_help_text
|
||||
def empty_state_help_text
|
||||
s_('ClusterIntegration|Adding an integration will share the cluster across all projects.')
|
||||
|
|
|
@ -3,8 +3,6 @@
|
|||
module Jira
|
||||
module Requests
|
||||
class Base
|
||||
include Integrations::Loggable
|
||||
|
||||
JIRA_API_VERSION = 2
|
||||
# Limit the size of the JSON error message we will attempt to parse, as the JSON is external input.
|
||||
JIRA_ERROR_JSON_SIZE_LIMIT = 5_000
|
||||
|
@ -54,17 +52,13 @@ module Jira
|
|||
def request
|
||||
response = client.get(url)
|
||||
build_service_response(response)
|
||||
rescue *ALL_ERRORS => e
|
||||
log_error('Error sending message',
|
||||
client_url: client.options[:site],
|
||||
error: {
|
||||
exception_class: e.class.name,
|
||||
exception_message: e.message,
|
||||
exception_backtrace: Gitlab::BacktraceCleaner.clean_backtrace(e.backtrace)
|
||||
}
|
||||
rescue *ALL_ERRORS => error
|
||||
jira_integration.log_exception(error,
|
||||
message: 'Error sending message',
|
||||
client_url: client.options[:site]
|
||||
)
|
||||
|
||||
ServiceResponse.error(message: error_message(e))
|
||||
ServiceResponse.error(message: error_message(error))
|
||||
end
|
||||
|
||||
def auth_docs_link_start
|
||||
|
|
|
@ -33,6 +33,8 @@ module MergeRequests
|
|||
|
||||
def execute_approval_hooks(merge_request, current_user)
|
||||
# Only one approval is required for a merge request to be approved
|
||||
notification_service.async.approve_mr(merge_request, current_user)
|
||||
|
||||
execute_hooks(merge_request, 'approved')
|
||||
end
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ module MergeRequests
|
|||
def trigger_approval_hooks(merge_request)
|
||||
yield
|
||||
|
||||
notification_service.async.unapprove_mr(merge_request, current_user)
|
||||
execute_hooks(merge_request, 'unapproved')
|
||||
end
|
||||
|
||||
|
|
|
@ -761,6 +761,14 @@ class NotificationService
|
|||
mailer.in_product_marketing_email(user_id, group_id, track, series).deliver_later
|
||||
end
|
||||
|
||||
def approve_mr(merge_request, current_user)
|
||||
approve_mr_email(merge_request, merge_request.target_project, current_user)
|
||||
end
|
||||
|
||||
def unapprove_mr(merge_request, current_user)
|
||||
unapprove_mr_email(merge_request, merge_request.target_project, current_user)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def new_resource_email(target, current_user, method)
|
||||
|
@ -866,6 +874,22 @@ class NotificationService
|
|||
|
||||
private
|
||||
|
||||
def approve_mr_email(merge_request, project, current_user)
|
||||
recipients = ::NotificationRecipients::BuildService.build_recipients(merge_request, current_user, action: 'approve')
|
||||
|
||||
recipients.each do |recipient|
|
||||
mailer.approved_merge_request_email(recipient.user.id, merge_request.id, current_user.id).deliver_later
|
||||
end
|
||||
end
|
||||
|
||||
def unapprove_mr_email(merge_request, project, current_user)
|
||||
recipients = ::NotificationRecipients::BuildService.build_recipients(merge_request, current_user, action: 'unapprove')
|
||||
|
||||
recipients.each do |recipient|
|
||||
mailer.unapproved_merge_request_email(recipient.user.id, merge_request.id, current_user.id).deliver_later
|
||||
end
|
||||
end
|
||||
|
||||
def pipeline_notification_status(ref_status, pipeline)
|
||||
if Ci::Ref.failing_state?(ref_status)
|
||||
'failed'
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
- breadcrumb_title runner_name
|
||||
- page_title runner_name
|
||||
|
||||
#js-admin-runner-edit{ data: {runner_id: @runner.id, runner_url: admin_runner_path(@runner) } }
|
||||
#js-admin-runner-edit{ data: {runner_id: @runner.id, runner_path: admin_runner_path(@runner) } }
|
||||
|
||||
- if @runner.project_type?
|
||||
.gl-overflow-auto
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
- is_connect_page = local_assigns.fetch(:is_connect_page, false)
|
||||
- docs_mode = local_assigns.fetch(:docs_mode, false)
|
||||
- title = is_connect_page ? s_('ClusterIntegration|Connect a Kubernetes cluster') : s_('ClusterIntegration|Create a Kubernetes cluster')
|
||||
|
||||
%h3
|
||||
|
@ -7,7 +6,7 @@
|
|||
%p
|
||||
= clusterable.sidebar_text
|
||||
|
||||
- if !docs_mode
|
||||
- if is_connect_page
|
||||
%p
|
||||
= clusterable.learn_more_link
|
||||
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
- if !Gitlab::CurrentSettings.eks_integration_enabled?
|
||||
- documentation_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/clusters/add_eks_clusters.md',
|
||||
anchor: 'additional-requirements-for-self-managed-instances') }
|
||||
= s_('Amazon authentication is not %{link_start}correctly configured%{link_end}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_start: documentation_link_start, link_end: '<a/>'.html_safe }
|
||||
- else
|
||||
.js-create-eks-cluster-form-container{ data: { 'gitlab-managed-cluster-help-path' => help_page_path('user/project/clusters/gitlab_managed_clusters.md'),
|
||||
'namespace-per-environment-help-path' => help_page_path('user/project/clusters/deploy_to_cluster.md', anchor: 'custom-namespace'),
|
||||
'create-role-path' => clusterable.authorize_aws_role_path,
|
||||
'create-cluster-path' => clusterable.create_aws_clusters_path,
|
||||
'account-id' => Gitlab::CurrentSettings.eks_account_id,
|
||||
'external-id' => @aws_role.role_external_id,
|
||||
'role-arn' => @aws_role.role_arn,
|
||||
'instance-types' => @instance_types,
|
||||
'kubernetes-integration-help-path' => help_page_path('user/infrastructure/clusters/index.md'),
|
||||
'account-and-external-ids-help-path' => help_page_path('user/project/clusters/add_eks_clusters.md', anchor: 'how-to-create-a-new-cluster-on-eks-through-cluster-certificates-deprecated'),
|
||||
'create-role-arn-help-path' => help_page_path('user/project/clusters/add_eks_clusters.md', anchor: 'how-to-create-a-new-cluster-on-eks-through-cluster-certificates-deprecated'),
|
||||
'external-link-icon' => sprite_icon('external-link') } }
|
|
@ -1,15 +1,11 @@
|
|||
- provider = local_assigns.fetch(:provider)
|
||||
- is_current_provider = provider == params[:provider]
|
||||
- logo_path = local_assigns.fetch(:logo_path)
|
||||
- help_path = local_assigns.fetch(:help_path)
|
||||
- label = local_assigns.fetch(:label)
|
||||
- last = local_assigns.fetch(:last, false)
|
||||
- docs_mode = local_assigns.fetch(:docs_mode, false)
|
||||
- classes = ["btn btn-confirm gl-button btn-confirm-secondary gl-flex-direction-column gl-w-half"]
|
||||
- conditional_classes = [("gl-mr-5" unless last), ("active" if is_current_provider && !docs_mode), ("js-create-#{provider}-cluster-button" if !docs_mode)]
|
||||
- link = docs_mode ? help_path : clusterable.new_path(provider: provider)
|
||||
- conditional_classes = [("gl-mr-5" unless last)]
|
||||
|
||||
= link_to link, class: classes + conditional_classes do
|
||||
= link_to help_path, class: classes + conditional_classes do
|
||||
.svg-content.gl-p-3= image_tag logo_path, alt: label, class: "gl-w-64 gl-h-64"
|
||||
%span
|
||||
= label
|
||||
|
|
|
@ -3,13 +3,12 @@
|
|||
- create_cluster_label = s_('ClusterIntegration|Where do you want to create a cluster?')
|
||||
- eks_help_path = help_page_path('user/infrastructure/clusters/connect/new_eks_cluster')
|
||||
- gke_help_path = help_page_path('user/infrastructure/clusters/connect/new_gke_cluster')
|
||||
- docs_mode = local_assigns.fetch(:docs_mode, false)
|
||||
|
||||
.gl-p-5
|
||||
%h4.gl-mb-5
|
||||
= create_cluster_label
|
||||
.gl-display-flex
|
||||
= render partial: 'clusters/clusters/cloud_providers/cloud_provider_button',
|
||||
locals: { provider: 'aws', label: eks_label, logo_path: 'illustrations/logos/amazon_eks.svg', help_path: eks_help_path, docs_mode: docs_mode }
|
||||
locals: { label: eks_label, logo_path: 'illustrations/logos/amazon_eks.svg', help_path: eks_help_path }
|
||||
= render partial: 'clusters/clusters/cloud_providers/cloud_provider_button',
|
||||
locals: { provider: 'gcp', label: gke_label, logo_path: 'illustrations/logos/google_gke.svg', help_path: gke_help_path, docs_mode: docs_mode, last: true }
|
||||
locals: { label: gke_label, logo_path: 'illustrations/logos/google_gke.svg', help_path: gke_help_path, last: true }
|
||||
|
|
|
@ -9,5 +9,5 @@
|
|||
.gl-w-quarter.gl-xs-w-full.gl-flex-shrink-0.gl-md-mr-5
|
||||
= render 'sidebar', is_connect_page: true
|
||||
.gl-w-full
|
||||
#js-cluster-new{ data: js_cluster_new }
|
||||
#js-cluster-new
|
||||
= render 'clusters/clusters/user/form'
|
||||
|
|
|
@ -1,87 +0,0 @@
|
|||
- external_link_icon = sprite_icon('external-link')
|
||||
- zones_link_url = 'https://cloud.google.com/compute/docs/regions-zones/regions-zones'
|
||||
- machine_type_link_url = 'https://cloud.google.com/compute/docs/machine-types'
|
||||
- pricing_link_url = 'https://cloud.google.com/compute/pricing#machinetype'
|
||||
- kubernetes_integration_url = help_page_path('user/infrastructure/clusters/index.md')
|
||||
- help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe
|
||||
- help_link_end = ' %{external_link_icon}</a>'.html_safe % { external_link_icon: external_link_icon }
|
||||
|
||||
%p
|
||||
= s_('ClusterIntegration|Read our %{link_start}help page%{link_end} on Kubernetes cluster integration.').html_safe % { link_start: help_link_start % { url: kubernetes_integration_url }, link_end: '</a>'.html_safe }
|
||||
|
||||
%p= link_to('Select a different Google account', @authorize_url)
|
||||
|
||||
= bootstrap_form_for @gcp_cluster, html: { class: 'gl-show-field-errors js-gke-cluster-creation prepend-top-20',
|
||||
data: { token: token_in_session } }, url: clusterable.create_gcp_clusters_path, as: :cluster do |field|
|
||||
= field.text_field :name, required: true, title: s_('ClusterIntegration|Cluster name is required.'),
|
||||
label: s_('ClusterIntegration|Kubernetes cluster name'), label_class: 'label-bold'
|
||||
= field.form_group :environment_scope, label: { text: s_('ClusterIntegration|Environment scope'),
|
||||
class: 'label-bold' } do
|
||||
= field.text_field :environment_scope, required: true, class: 'form-control',
|
||||
title: 'Environment scope is required.', wrapper: false
|
||||
.form-text.text-muted= s_("ClusterIntegration|Choose which of your environments will use this cluster.")
|
||||
|
||||
= field.fields_for :provider_gcp, @gcp_cluster.provider_gcp do |provider_gcp_field|
|
||||
.form-group
|
||||
= provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project'),
|
||||
class: 'label-bold'
|
||||
.js-gcp-project-id-dropdown-entry-point{ data: { docsUrl: 'https://console.cloud.google.com/home/dashboard' } }
|
||||
= provider_gcp_field.hidden_field :gcp_project_id
|
||||
.dropdown
|
||||
%button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', disabled: true }
|
||||
%span.dropdown-toggle-text
|
||||
= _('Select project')
|
||||
= sprite_icon('chevron-down', css_class: 'dropdown-menu-toggle-icon gl-top-3')
|
||||
%span.form-text.text-muted
|
||||
|
||||
.form-group
|
||||
= provider_gcp_field.label :zone, s_('ClusterIntegration|Zone'), class: 'label-bold'
|
||||
.js-gcp-zone-dropdown-entry-point
|
||||
= provider_gcp_field.hidden_field :zone
|
||||
.dropdown
|
||||
%button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', disabled: true }
|
||||
%span.dropdown-toggle-text
|
||||
= _('Select project to choose zone')
|
||||
= sprite_icon('chevron-down', css_class: 'dropdown-menu-toggle-icon gl-top-3')
|
||||
%p.form-text.text-muted
|
||||
= s_('ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}.').html_safe % { help_link_start: help_link_start % { url: zones_link_url }, help_link_end: help_link_end }
|
||||
|
||||
= provider_gcp_field.number_field :num_nodes, required: true, placeholder: '3',
|
||||
title: s_('ClusterIntegration|Number of nodes must be a numerical value.'),
|
||||
label: s_('ClusterIntegration|Number of nodes'), label_class: 'label-bold'
|
||||
|
||||
.form-group
|
||||
= provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type'), class: 'label-bold'
|
||||
.js-gcp-machine-type-dropdown-entry-point
|
||||
= provider_gcp_field.hidden_field :machine_type
|
||||
.dropdown
|
||||
%button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', disabled: true }
|
||||
%span.dropdown-toggle-text
|
||||
= _('Select project and zone to choose machine type')
|
||||
= sprite_icon('chevron-down', css_class: 'dropdown-menu-toggle-icon gl-top-3')
|
||||
%p.form-text.text-muted
|
||||
= s_('ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}.').html_safe % { help_link_start_machine_type: help_link_start % { url: machine_type_link_url }, help_link_start_pricing: help_link_start % { url: pricing_link_url }, help_link_end: help_link_end }
|
||||
|
||||
.form-group
|
||||
= provider_gcp_field.check_box :cloud_run, { label: s_('ClusterIntegration|Enable Cloud Run for Anthos'),
|
||||
label_class: 'label-bold' }
|
||||
.form-text.text-muted
|
||||
= s_('ClusterIntegration|Uses the Cloud Run, Istio, and HTTP Load Balancing addons for this cluster.')
|
||||
= link_to _('Learn more.'), help_page_path('user/project/clusters/add_gke_clusters.md', anchor: 'cloud-run-for-anthos'), target: '_blank', rel: 'noopener noreferrer'
|
||||
|
||||
.form-group
|
||||
= field.check_box :managed, { label: s_('ClusterIntegration|GitLab-managed cluster'),
|
||||
label_class: 'label-bold' }
|
||||
.form-text.text-muted
|
||||
= s_('ClusterIntegration|Allow GitLab to manage namespaces and service accounts for this cluster.')
|
||||
= link_to _('Learn more.'), help_page_path('user/project/clusters/gitlab_managed_clusters.md'), target: '_blank', rel: 'noopener noreferrer'
|
||||
|
||||
.form-group
|
||||
= field.check_box :namespace_per_environment, { label: s_('ClusterIntegration|Namespace per environment'), label_class: 'label-bold' }
|
||||
.form-text.text-muted
|
||||
= s_('ClusterIntegration|Deploy each environment to its own namespace. Otherwise, environments within a project share a project-wide namespace. Note that anyone who can trigger a deployment of a namespace can read its secrets. If modified, existing environments will use their current namespaces until the cluster cache is cleared.')
|
||||
= link_to _('Learn more.'), help_page_path('user/project/clusters/deploy_to_cluster.md', anchor: 'custom-namespace'), target: '_blank', rel: 'noopener noreferrer'
|
||||
|
||||
.form-group.js-gke-cluster-creation-submit-container
|
||||
= field.submit s_('ClusterIntegration|Create Kubernetes cluster'),
|
||||
class: 'js-gke-cluster-creation-submit gl-button btn btn-confirm', disabled: true
|
|
@ -1,3 +0,0 @@
|
|||
- documentation_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path("integration/google") }
|
||||
- link_end = '<a/>'.html_safe
|
||||
= s_('Google authentication is not %{link_start}properly configured%{link_end}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_start: documentation_link_start, link_end: link_end }
|
|
@ -1,14 +0,0 @@
|
|||
%h4
|
||||
= s_('ClusterIntegration|Enter the details for your Kubernetes cluster')
|
||||
%p
|
||||
= s_('ClusterIntegration|Please make sure that your Google account meets the following requirements:')
|
||||
%ul
|
||||
%li
|
||||
- link_to_kubernetes_engine = link_to(s_('ClusterIntegration|access to Google Kubernetes Engine'), 'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer')
|
||||
= s_('ClusterIntegration|Your account must have %{link_to_kubernetes_engine}').html_safe % { link_to_kubernetes_engine: link_to_kubernetes_engine }
|
||||
%li
|
||||
- link_to_requirements = link_to(s_('ClusterIntegration|meets the requirements'), 'https://cloud.google.com/kubernetes-engine/docs/quickstart?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer')
|
||||
= s_('ClusterIntegration|Make sure your account %{link_to_requirements} to create Kubernetes clusters').html_safe % { link_to_requirements: link_to_requirements }
|
||||
%li
|
||||
- link_to_container_project = link_to(s_('ClusterIntegration|Google Kubernetes Engine project'), 'https://console.cloud.google.com/home/dashboard?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer')
|
||||
= s_('ClusterIntegration|This account must have permissions to create a Kubernetes cluster in the %{link_to_container_project} specified below').html_safe % { link_to_container_project: link_to_container_project }
|
|
@ -1,5 +0,0 @@
|
|||
= render 'clusters/clusters/gcp/header'
|
||||
- if @valid_gcp_token
|
||||
= render 'clusters/clusters/gcp/form'
|
||||
- else
|
||||
= render 'clusters/clusters/gcp/gcp_not_configured'
|
|
@ -1,19 +0,0 @@
|
|||
- @content_class = 'limit-container-width' unless fluid_layout
|
||||
- add_to_breadcrumbs _('Kubernetes Clusters'), clusterable.index_path
|
||||
- breadcrumb_title _('Create a cluster')
|
||||
- page_title _('Create a Kubernetes cluster')
|
||||
- provider = params[:provider]
|
||||
|
||||
= render 'deprecation_alert'
|
||||
|
||||
= render_gcp_signup_offer
|
||||
|
||||
.gl-md-display-flex.gl-mt-3
|
||||
.gl-w-quarter.gl-xs-w-full.gl-flex-shrink-0.gl-md-mr-5
|
||||
= render 'sidebar', is_connect_page: false
|
||||
.gl-w-full
|
||||
= render 'clusters/clusters/cloud_providers/cloud_provider_selector'
|
||||
|
||||
- if ['aws', 'gcp'].include?(provider)
|
||||
.gl-p-5.gl-border-1.gl-border-t-solid.gl-border-gray-100
|
||||
= render "clusters/clusters/#{provider}/new"
|
|
@ -2,12 +2,11 @@
|
|||
- add_to_breadcrumbs _('Kubernetes Clusters'), clusterable.index_path
|
||||
- breadcrumb_title _('Create a cluster')
|
||||
- page_title _('Create a Kubernetes cluster')
|
||||
- docs_mode = true
|
||||
|
||||
= render_gcp_signup_offer
|
||||
|
||||
.gl-md-display-flex.gl-mt-3
|
||||
.gl-w-quarter.gl-xs-w-full.gl-flex-shrink-0.gl-md-mr-5
|
||||
= render 'sidebar', docs_mode: docs_mode, is_connect_page: false
|
||||
= render 'sidebar', is_connect_page: false
|
||||
.gl-w-full
|
||||
= render 'clusters/clusters/cloud_providers/cloud_provider_selector', docs_mode: docs_mode
|
||||
= render 'clusters/clusters/cloud_providers/cloud_provider_selector'
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
- more_info_link = link_to _('Learn more.'), help_page_path('user/project/clusters/add_remove_clusters.md',
|
||||
anchor: 'add-existing-cluster'), target: '_blank', rel: 'noopener noreferrer'
|
||||
- rbac_help_link = link_to _('Learn more.'), help_page_path('user/project/clusters/add_remove_clusters.md',
|
||||
anchor: 'access-controls'), target: '_blank', rel: 'noopener noreferrer'
|
||||
- more_info_link = link_to _('Learn more.'), help_page_path('user/project/clusters/add_existing_cluster'), target: '_blank', rel: 'noopener noreferrer'
|
||||
- rbac_help_link = link_to _('Learn more.'), help_page_path('user/project/clusters/cluster_access'), target: '_blank', rel: 'noopener noreferrer'
|
||||
|
||||
- api_url_help_text = s_('ClusterIntegration|The URL used to access the Kubernetes API.')
|
||||
- ca_cert_help_text = s_('ClusterIntegration|The Kubernetes certificate used to authenticate to the cluster.')
|
||||
|
|
|
@ -0,0 +1,157 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
%html{ lang: "en" }
|
||||
%head
|
||||
%meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/
|
||||
%meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/
|
||||
%meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/
|
||||
%title= message.subject
|
||||
:css
|
||||
/* CLIENT-SPECIFIC STYLES */
|
||||
body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
|
||||
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
|
||||
img { -ms-interpolation-mode: bicubic; }
|
||||
|
||||
/* iOS BLUE LINKS */
|
||||
a[x-apple-data-detectors] {
|
||||
color: inherit !important;
|
||||
text-decoration: none !important;
|
||||
font-size: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
}
|
||||
|
||||
/* ANDROID MARGIN HACK */
|
||||
body { margin:0 !important; }
|
||||
div[style*="margin: 16px 0"] { margin:0 !important; }
|
||||
|
||||
@media only screen and (max-width: 639px) {
|
||||
body, #body {
|
||||
min-width: 320px !important;
|
||||
}
|
||||
table.wrapper {
|
||||
width: 100% !important;
|
||||
min-width: 320px !important;
|
||||
}
|
||||
table.wrapper > tbody > tr > td {
|
||||
border-left: 0 !important;
|
||||
border-right: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
padding-left: 10px !important;
|
||||
padding-right: 10px !important;
|
||||
}
|
||||
}
|
||||
|
||||
ul.users-list {
|
||||
list-style: none;
|
||||
padding: 0px;
|
||||
display: block;
|
||||
margin-top: 0px;
|
||||
}
|
||||
ul.users-list li {
|
||||
display: inline-block;
|
||||
padding-right: 12px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
%body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
|
||||
%table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" }
|
||||
%tbody
|
||||
%tr.line
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" }
|
||||
%tr.header
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
|
||||
%img{ alt: "GitLab", height: "55", src: image_url('mailers/gitlab_logo.png'), width: "55" }/
|
||||
%tr
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
|
||||
%table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" }
|
||||
%tbody
|
||||
%tr
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
|
||||
%table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" }
|
||||
%tbody
|
||||
%tr.success
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#31af64;" }
|
||||
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
|
||||
%tbody
|
||||
%tr
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" }
|
||||
%img{ alt: "✓", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif'), style: "display:block;", width: "13" }/
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" }
|
||||
- if @merge_request.respond_to? :approvals_required
|
||||
%span Merge request was approved (#{@merge_request.approvals.count}/#{@merge_request.approvals_required})
|
||||
- else
|
||||
%span Merge request was approved
|
||||
%tr.spacer
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
|
||||
|
||||
%tr.section
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;line-height:1.4;text-align:center;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
|
||||
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;width:100%;" }
|
||||
%tbody
|
||||
%tr{ style: 'width:100%;' }
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;text-align:center;" }
|
||||
%img{ src: image_url('mailers/approval/icon-merge-request-gray.gif'), style: "height:18px;width:18px;margin-bottom:-4px;", alt: "Merge request icon" }
|
||||
%span{ style: "font-weight: 600;color:#333333;" } Merge request
|
||||
%a{ href: merge_request_url(@merge_request), style: "font-weight: 600;color:#3777b0;text-decoration:none" }= @merge_request.to_reference
|
||||
%span was approved by
|
||||
%img.avatar{ height: "24", src: avatar_icon_for_user(@approved_by, 24, only_path: false), style: "border-radius:12px;margin:-7px 0 -7px 3px;", width: "24", alt: "Avatar" }/
|
||||
%a.muted{ href: user_url(@approved_by), style: "color:#333333;text-decoration:none;" }
|
||||
= @approved_by.name
|
||||
%tr.spacer
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
|
||||
|
||||
%tr.section
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
|
||||
%table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" }
|
||||
%tbody
|
||||
%tr
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" }
|
||||
- namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name
|
||||
- namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner)
|
||||
%a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" }
|
||||
= namespace_name
|
||||
\/
|
||||
%a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" }
|
||||
= @project.name
|
||||
%tr
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
|
||||
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
|
||||
%tbody
|
||||
%tr
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
|
||||
%img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
|
||||
%span.muted{ style: "color:#333333;text-decoration:none;" }
|
||||
= @merge_request.source_branch
|
||||
%tr
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
|
||||
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
|
||||
%tbody
|
||||
%tr
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
|
||||
%img.avatar{ height: "24", src: avatar_icon_for_user(@merge_request.author, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
|
||||
%a.muted{ href: user_url(@merge_request.author), style: "color:#333333;text-decoration:none;" }
|
||||
= @merge_request.author.name
|
||||
|
||||
- if @merge_request.assignees.any?
|
||||
= render 'users_list', users: @merge_request.assignees, user_label: assignees_label(@merge_request, include_value: false)
|
||||
|
||||
- if @merge_request.reviewers.any?
|
||||
= render 'users_list', users: @merge_request.reviewers, user_label: reviewers_label(@merge_request, include_value: false)
|
||||
- if Gitlab.ee?
|
||||
-# EE-specific start
|
||||
= render 'layouts/mailer/additional_text'
|
||||
-# EE-specific end
|
||||
|
||||
%tr.footer
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
|
||||
%img{ alt: "GitLab", src: image_url('mailers/gitlab_logo_black_text.png'), style: "display:block;margin:0 auto 1em;", width: "90" }/
|
||||
%div
|
||||
- manage_notifications_link = link_to(_("Manage all notifications"), profile_notifications_url, style: "color:#3777b0;text-decoration:none;")
|
||||
- help_link = link_to(_("Help"), help_url, style: "color:#3777b0;text-decoration:none;")
|
||||
= _("You're receiving this email because of your account on %{host}. %{manage_notifications_link} · %{help_link}").html_safe % { host: Gitlab.config.gitlab.host, manage_notifications_link: manage_notifications_link, help_link: help_link }
|
|
@ -0,0 +1,9 @@
|
|||
Merge request #{@merge_request.to_reference} was approved by #{sanitize_name(@approved_by.name)}
|
||||
|
||||
Merge request URL: #{project_merge_request_url(@merge_request.target_project, @merge_request)}
|
||||
|
||||
= merge_path_description(@merge_request, 'to')
|
||||
|
||||
Author: #{sanitize_name(@merge_request.author_name)}
|
||||
= assignees_label(@merge_request)
|
||||
= reviewers_label(@merge_request)
|
|
@ -0,0 +1,156 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
%html{ lang: "en" }
|
||||
%head
|
||||
%meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/
|
||||
%meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/
|
||||
%meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/
|
||||
%title= message.subject
|
||||
:css
|
||||
/* CLIENT-SPECIFIC STYLES */
|
||||
body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
|
||||
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
|
||||
img { -ms-interpolation-mode: bicubic; }
|
||||
|
||||
/* iOS BLUE LINKS */
|
||||
a[x-apple-data-detectors] {
|
||||
color: inherit !important;
|
||||
text-decoration: none !important;
|
||||
font-size: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
}
|
||||
|
||||
/* ANDROID MARGIN HACK */
|
||||
body { margin:0 !important; }
|
||||
div[style*="margin: 16px 0"] { margin:0 !important; }
|
||||
|
||||
@media only screen and (max-width: 639px) {
|
||||
body, #body {
|
||||
min-width: 320px !important;
|
||||
}
|
||||
table.wrapper {
|
||||
width: 100% !important;
|
||||
min-width: 320px !important;
|
||||
}
|
||||
table.wrapper > tbody > tr > td {
|
||||
border-left: 0 !important;
|
||||
border-right: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
padding-left: 10px !important;
|
||||
padding-right: 10px !important;
|
||||
}
|
||||
}
|
||||
|
||||
ul.users-list {
|
||||
list-style: none;
|
||||
padding: 0px;
|
||||
display: block;
|
||||
margin-top: 0px;
|
||||
}
|
||||
ul.users-list li {
|
||||
display: inline-block;
|
||||
padding-right: 12px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
%body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
|
||||
%table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" }
|
||||
%tbody
|
||||
%tr.line
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" }
|
||||
%tr.header
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
|
||||
%img{ alt: "GitLab", height: "55", src: image_url('mailers/gitlab_logo.png'), width: "55" }/
|
||||
%tr
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
|
||||
%table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" }
|
||||
%tbody
|
||||
%tr
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
|
||||
%table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" }
|
||||
%tbody
|
||||
%tr.success
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#FC6D26;" }
|
||||
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
|
||||
%tbody
|
||||
%tr
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" }
|
||||
%img{ alt: "✗", height: "13", src: image_url('mailers/approval/icon-x-orange-inverted.gif'), style: "display:block;", width: "13" }/
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" }
|
||||
- if @merge_request.respond_to? :approvals_required
|
||||
%span Merge request was unapproved (#{@merge_request.approvals.count}/#{@merge_request.approvals_required})
|
||||
- else
|
||||
%span Merge request was unapproved
|
||||
%tr.spacer
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
|
||||
|
||||
%tr.section
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;line-height:1.4;text-align:center;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
|
||||
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;width:100%;" }
|
||||
%tbody
|
||||
%tr{ style: 'width:100%;' }
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;text-align:center;" }
|
||||
%img{ src: image_url('mailers/approval/icon-merge-request-gray.gif'), style: "height:18px;width:18px;margin-bottom:-4px;", alt: "Merge request icon" }
|
||||
%span{ style: "font-weight: 600;color:#333333;" } Merge request
|
||||
%a{ href: merge_request_url(@merge_request), style: "font-weight: 600;color:#3777b0;text-decoration:none" }= @merge_request.to_reference
|
||||
%span was unapproved by
|
||||
%img.avatar{ height: "24", src: avatar_icon_for_user(@unapproved_by, 24), style: "border-radius:12px;margin:-7px 0 -7px 3px;", width: "24", alt: "Avatar" }/
|
||||
%a.muted{ href: user_url(@unapproved_by), style: "color:#333333;text-decoration:none;" }
|
||||
= @unapproved_by.name
|
||||
%tr.spacer
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
|
||||
|
||||
%tr.section
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
|
||||
%table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" }
|
||||
%tbody
|
||||
%tr
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" }
|
||||
- namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name
|
||||
- namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner)
|
||||
%a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" }
|
||||
= namespace_name
|
||||
\/
|
||||
%a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" }
|
||||
= @project.name
|
||||
%tr
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
|
||||
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
|
||||
%tbody
|
||||
%tr
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
|
||||
%img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
|
||||
%span.muted{ style: "color:#333333;text-decoration:none;" }
|
||||
= @merge_request.source_branch
|
||||
%tr
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
|
||||
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
|
||||
%tbody
|
||||
%tr
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
|
||||
%img.avatar{ height: "24", src: avatar_icon_for_user(@merge_request.author, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
|
||||
%a.muted{ href: user_url(@merge_request.author), style: "color:#333333;text-decoration:none;" }
|
||||
= @merge_request.author.name
|
||||
- if @merge_request.assignees.any?
|
||||
= render 'users_list', users: @merge_request.assignees, user_label: assignees_label(@merge_request, include_value: false)
|
||||
|
||||
- if @merge_request.reviewers.any?
|
||||
= render 'users_list', users: @merge_request.reviewers, user_label: reviewers_label(@merge_request, include_value: false)
|
||||
- if Gitlab.ee?
|
||||
-# EE-specific start
|
||||
= render 'layouts/mailer/additional_text'
|
||||
-# EE-specific end
|
||||
|
||||
%tr.footer
|
||||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
|
||||
%img{ alt: "GitLab", src: image_url('mailers/gitlab_logo_black_text.png'), style: "display:block;margin:0 auto 1em;", width: "90" }/
|
||||
%div
|
||||
- manage_notifications_link = link_to(_("Manage all notifications"), profile_notifications_url, style: "color:#3777b0;text-decoration:none;")
|
||||
- help_link = link_to(_("Help"), help_url, style: "color:#3777b0;text-decoration:none;")
|
||||
= _("You're receiving this email because of your account on %{host}. %{manage_notifications_link} · %{help_link}").html_safe % { host: Gitlab.config.gitlab.host, manage_notifications_link: manage_notifications_link, help_link: help_link }
|
|
@ -0,0 +1,9 @@
|
|||
Merge request #{@merge_request.to_reference} was unapproved by #{@unapproved_by.name}
|
||||
|
||||
Merge request URL: #{project_merge_request_url(@merge_request.target_project, @merge_request)}
|
||||
|
||||
= merge_path_description(@merge_request, 'to')
|
||||
|
||||
Author: #{sanitize_name(@merge_request.author_name)}
|
||||
= assignees_label(@merge_request)
|
||||
= reviewers_label(@merge_request)
|
|
@ -13,10 +13,13 @@ class ProjectServiceWorker # rubocop:disable Scalability/IdempotentWorker
|
|||
|
||||
def perform(hook_id, data)
|
||||
data = data.with_indifferent_access
|
||||
integration = Integration.find(hook_id)
|
||||
integration.execute(data)
|
||||
rescue StandardError => error
|
||||
integration_class = integration&.class&.name || "Not Found"
|
||||
Gitlab::ErrorTracking.log_exception(error, integration_class: integration_class)
|
||||
integration = Integration.find_by_id(hook_id)
|
||||
return unless integration
|
||||
|
||||
begin
|
||||
integration.execute(data)
|
||||
rescue StandardError => error
|
||||
integration.log_exception(error)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -234,12 +234,11 @@ Rails.application.routes.draw do
|
|||
# End of the /-/ scope.
|
||||
|
||||
concern :clusterable do
|
||||
resources :clusters, only: [:index, :new, :show, :update, :destroy] do
|
||||
resources :clusters, only: [:index, :show, :update, :destroy] do
|
||||
collection do
|
||||
get :connect
|
||||
get :new_cluster_docs
|
||||
post :create_user
|
||||
post :create_gcp
|
||||
post :create_aws
|
||||
post :authorize_aws_role
|
||||
end
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveRequirementsManagementTestReportsRequirementId < Gitlab::Database::Migration[2.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
TARGET_TABLE = :requirements_management_test_reports
|
||||
CONSTRAINT_NAME = 'fk_rails_fb3308ad55'
|
||||
|
||||
def up
|
||||
with_lock_retries do
|
||||
remove_column TARGET_TABLE, :requirement_id
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
unless column_exists?(TARGET_TABLE, :requirement_id)
|
||||
with_lock_retries do
|
||||
add_column TARGET_TABLE, :requirement_id, :bigint, after: :created_at
|
||||
end
|
||||
end
|
||||
|
||||
add_concurrent_index TARGET_TABLE, :requirement_id,
|
||||
name: :index_requirements_management_test_reports_on_requirement_id
|
||||
|
||||
add_concurrent_foreign_key TARGET_TABLE, :requirements,
|
||||
column: :requirement_id, name: CONSTRAINT_NAME, on_delete: :cascade
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
0e38608a14abd18ab257531f11e31e0a5d7d3801d9725ae02731b6b5ce881db7
|
|
@ -20111,7 +20111,6 @@ ALTER SEQUENCE requirements_id_seq OWNED BY requirements.id;
|
|||
CREATE TABLE requirements_management_test_reports (
|
||||
id bigint NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
requirement_id bigint,
|
||||
author_id bigint,
|
||||
state smallint NOT NULL,
|
||||
build_id bigint,
|
||||
|
@ -29021,8 +29020,6 @@ CREATE INDEX index_requirements_management_test_reports_on_build_id ON requireme
|
|||
|
||||
CREATE INDEX index_requirements_management_test_reports_on_issue_id ON requirements_management_test_reports USING btree (issue_id);
|
||||
|
||||
CREATE INDEX index_requirements_management_test_reports_on_requirement_id ON requirements_management_test_reports USING btree (requirement_id);
|
||||
|
||||
CREATE INDEX index_requirements_on_author_id ON requirements USING btree (author_id);
|
||||
|
||||
CREATE INDEX index_requirements_on_created_at ON requirements USING btree (created_at);
|
||||
|
@ -33519,9 +33516,6 @@ ALTER TABLE ONLY merge_requests_closing_issues
|
|||
ALTER TABLE ONLY banned_users
|
||||
ADD CONSTRAINT fk_rails_fa5bb598e5 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY requirements_management_test_reports
|
||||
ADD CONSTRAINT fk_rails_fb3308ad55 FOREIGN KEY (requirement_id) REFERENCES requirements(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY operations_feature_flags_issues
|
||||
ADD CONSTRAINT fk_rails_fb4d2a7cb1 FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
@ -463,6 +463,9 @@ gitlab_rails['incoming_email_host'] = "exchange.example.com"
|
|||
gitlab_rails['incoming_email_port'] = 993
|
||||
# Whether the IMAP server uses SSL
|
||||
gitlab_rails['incoming_email_ssl'] = true
|
||||
|
||||
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
|
||||
gitlab_rails['incoming_email_expunge_deleted'] = true
|
||||
```
|
||||
|
||||
Example for source installs:
|
||||
|
@ -491,6 +494,9 @@ incoming_email:
|
|||
port: 993
|
||||
# Whether the IMAP server uses SSL
|
||||
ssl: true
|
||||
|
||||
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
|
||||
expunge_deleted: true
|
||||
```
|
||||
|
||||
##### Dedicated email address
|
||||
|
@ -521,6 +527,9 @@ gitlab_rails['incoming_email_host'] = "exchange.example.com"
|
|||
gitlab_rails['incoming_email_port'] = 993
|
||||
# Whether the IMAP server uses SSL
|
||||
gitlab_rails['incoming_email_ssl'] = true
|
||||
|
||||
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
|
||||
gitlab_rails['incoming_email_expunge_deleted'] = true
|
||||
```
|
||||
|
||||
Example for source installs:
|
||||
|
@ -545,6 +554,9 @@ incoming_email:
|
|||
port: 993
|
||||
# Whether the IMAP server uses SSL
|
||||
ssl: true
|
||||
|
||||
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
|
||||
expunge_deleted: true
|
||||
```
|
||||
|
||||
#### Microsoft Office 365
|
||||
|
@ -599,6 +611,9 @@ gitlab_rails['incoming_email_host'] = "outlook.office365.com"
|
|||
gitlab_rails['incoming_email_port'] = 993
|
||||
# Whether the IMAP server uses SSL
|
||||
gitlab_rails['incoming_email_ssl'] = true
|
||||
|
||||
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
|
||||
gitlab_rails['incoming_email_expunge_deleted'] = true
|
||||
```
|
||||
|
||||
This example for source installs assumes the mailbox `incoming@office365.example.com`:
|
||||
|
@ -626,6 +641,9 @@ incoming_email:
|
|||
port: 993
|
||||
# Whether the IMAP server uses SSL
|
||||
ssl: true
|
||||
|
||||
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
|
||||
expunge_deleted: true
|
||||
```
|
||||
|
||||
##### Catch-all mailbox
|
||||
|
@ -654,6 +672,9 @@ gitlab_rails['incoming_email_host'] = "outlook.office365.com"
|
|||
gitlab_rails['incoming_email_port'] = 993
|
||||
# Whether the IMAP server uses SSL
|
||||
gitlab_rails['incoming_email_ssl'] = true
|
||||
|
||||
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
|
||||
gitlab_rails['incoming_email_expunge_deleted'] = true
|
||||
```
|
||||
|
||||
This example for source installs assumes the catch-all mailbox `incoming@office365.example.com`:
|
||||
|
@ -681,6 +702,9 @@ incoming_email:
|
|||
port: 993
|
||||
# Whether the IMAP server uses SSL
|
||||
ssl: true
|
||||
|
||||
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
|
||||
expunge_deleted: true
|
||||
```
|
||||
|
||||
##### Dedicated email address
|
||||
|
@ -708,6 +732,9 @@ gitlab_rails['incoming_email_host'] = "outlook.office365.com"
|
|||
gitlab_rails['incoming_email_port'] = 993
|
||||
# Whether the IMAP server uses SSL
|
||||
gitlab_rails['incoming_email_ssl'] = true
|
||||
|
||||
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
|
||||
gitlab_rails['incoming_email_expunge_deleted'] = true
|
||||
```
|
||||
|
||||
This example for source installs assumes the dedicated email address `incoming@office365.example.com`:
|
||||
|
@ -730,6 +757,9 @@ incoming_email:
|
|||
port: 993
|
||||
# Whether the IMAP server uses SSL
|
||||
ssl: true
|
||||
|
||||
# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
|
||||
expunge_deleted: true
|
||||
```
|
||||
|
||||
#### Microsoft Graph
|
||||
|
|
|
@ -4359,7 +4359,7 @@ Input type: `SecurityPolicyProjectAssignInput`
|
|||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationsecuritypolicyprojectassignclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationsecuritypolicyprojectassignfullpath"></a>`fullPath` | [`String`](#string) | Full path of the project. |
|
||||
| <a id="mutationsecuritypolicyprojectassignfullpath"></a>`fullPath` | [`String`](#string) | Full path of the project or group. |
|
||||
| <a id="mutationsecuritypolicyprojectassignprojectpath"></a>`projectPath` **{warning-solid}** | [`ID`](#id) | **Deprecated:** Use `fullPath`. Deprecated in 14.10. |
|
||||
| <a id="mutationsecuritypolicyprojectassignsecuritypolicyprojectid"></a>`securityPolicyProjectId` | [`ProjectID!`](#projectid) | ID of the security policy project. |
|
||||
|
||||
|
@ -4381,7 +4381,7 @@ Input type: `SecurityPolicyProjectCreateInput`
|
|||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationsecuritypolicyprojectcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationsecuritypolicyprojectcreatefullpath"></a>`fullPath` | [`String`](#string) | Full path of the project. |
|
||||
| <a id="mutationsecuritypolicyprojectcreatefullpath"></a>`fullPath` | [`String`](#string) | Full path of the project or group. |
|
||||
| <a id="mutationsecuritypolicyprojectcreateprojectpath"></a>`projectPath` **{warning-solid}** | [`ID`](#id) | **Deprecated:** Use `fullPath`. Deprecated in 14.10. |
|
||||
|
||||
#### Fields
|
||||
|
@ -4403,7 +4403,7 @@ Input type: `SecurityPolicyProjectUnassignInput`
|
|||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationsecuritypolicyprojectunassignclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationsecuritypolicyprojectunassignfullpath"></a>`fullPath` | [`String`](#string) | Full path of the project. |
|
||||
| <a id="mutationsecuritypolicyprojectunassignfullpath"></a>`fullPath` | [`String`](#string) | Full path of the project or group. |
|
||||
| <a id="mutationsecuritypolicyprojectunassignprojectpath"></a>`projectPath` **{warning-solid}** | [`ID`](#id) | **Deprecated:** Use `fullPath`. Deprecated in 14.10. |
|
||||
|
||||
#### Fields
|
||||
|
|
|
@ -302,7 +302,7 @@ Parameters:
|
|||
|
||||
NOTE:
|
||||
`name`, `api_url`, `ca_cert` and `token` can only be updated if the cluster was added
|
||||
through the ["Add existing Kubernetes cluster"](../user/project/clusters/add_remove_clusters.md#add-existing-cluster) option or
|
||||
through the ["Add existing Kubernetes cluster"](../user/project/clusters/add_existing_cluster.md) option or
|
||||
through the ["Add existing cluster to project"](#add-existing-cluster-to-project) endpoint.
|
||||
|
||||
Example request:
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue