summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-08-22 18:10:26 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-08-22 18:10:26 +0000
commita3ffaa242bb3b9b4c95a8223ca800126887e3a82 (patch)
treebaf3d90b38dd8c93873498c838e8b0894c74d3ea
parentadaa4599f18149e0fb8f8e120a7ed8e7d05e36be (diff)
downloadgitlab-ce-a3ffaa242bb3b9b4c95a8223ca800126887e3a82.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop.yml3
-rw-r--r--.rubocop_todo/rspec/missing_example_group_argument.yml16
-rw-r--r--app/assets/javascripts/blob/notebook/notebook_viewer.vue2
-rw-r--r--app/assets/javascripts/google_cloud/databases/index.js11
-rw-r--r--app/assets/javascripts/google_cloud/databases/init_index.js11
-rw-r--r--app/assets/javascripts/google_cloud/databases/init_new.js11
-rw-r--r--app/assets/javascripts/google_cloud/databases/panel.vue38
-rw-r--r--app/assets/javascripts/notebook/cells/code.vue13
-rw-r--r--app/assets/javascripts/notebook/cells/code/index.vue25
-rw-r--r--app/assets/javascripts/notebook/cells/output/index.vue6
-rw-r--r--app/assets/javascripts/notebook/index.vue6
-rw-r--r--app/assets/javascripts/notebook/lib/highlight.js5
-rw-r--r--app/assets/javascripts/pages/projects/google_cloud/databases/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/google_cloud/databases/index/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/google_cloud/databases/new/index.js3
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue32
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/index.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue32
-rw-r--r--app/assets/javascripts/vue_shared/components/code_block.stories.js18
-rw-r--r--app/assets/javascripts/vue_shared/components/code_block.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/code_block_highlighted.stories.js18
-rw-r--r--app/assets/javascripts/vue_shared/components/code_block_highlighted.vue72
-rw-r--r--app/assets/stylesheets/framework/files.scss5
-rw-r--r--app/controllers/projects/google_cloud/databases_controller.rb128
-rw-r--r--app/finders/deployments_finder.rb46
-rw-r--r--app/graphql/resolvers/deployment_resolver.rb20
-rw-r--r--app/graphql/resolvers/deployments_resolver.rb39
-rw-r--r--app/graphql/types/deployment_details_type.rb9
-rw-r--r--app/graphql/types/deployment_status_enum.rb14
-rw-r--r--app/graphql/types/deployment_type.rb54
-rw-r--r--app/graphql/types/deployments_order_by_input_type.rb24
-rw-r--r--app/graphql/types/environment_type.rb9
-rw-r--r--app/graphql/types/project_type.rb6
-rw-r--r--app/graphql/types/sort_direction_enum.rb11
-rw-r--r--app/helpers/projects/google_cloud/cloudsql_helper.rb55
-rw-r--r--app/models/deployment.rb2
-rw-r--r--app/presenters/deployments/deployment_presenter.rb7
-rw-r--r--app/services/google_cloud/create_cloudsql_instance_service.rb2
-rw-r--r--app/services/google_cloud/enable_cloudsql_service.rb2
-rw-r--r--app/views/projects/_service_desk_settings.html.haml3
-rw-r--r--app/views/projects/google_cloud/databases/cloudsql_form.html.haml9
-rw-r--r--app/views/projects/google_cloud/gcp_regions/index.html.haml2
-rw-r--r--app/views/shared/groups/_group.html.haml2
-rw-r--r--app/workers/google_cloud/create_cloudsql_instance_worker.rb23
-rw-r--r--config/feature_flags/development/ci_forked_source_public_cost_factor.yml8
-rw-r--r--config/feature_flags/development/incubation_5mp_google_cloud.yml2
-rw-r--r--config/routes/project.rb2
-rw-r--r--data/whats_new/2022082200001_15_03.yml70
-rw-r--r--db/migrate/20220803235114_add_auto_ban_user_to_application_settings.rb8
-rw-r--r--db/schema_migrations/202208032351141
-rw-r--r--db/structure.sql1
-rw-r--r--doc/api/graphql/reference/index.md122
-rw-r--r--doc/api/jobs.md2
-rw-r--r--doc/api/settings.md1
-rw-r--r--doc/ci/pipelines/downstream_pipelines.md65
-rw-r--r--doc/ci/pipelines/index.md53
-rw-r--r--doc/ci/pipelines/multi_project_pipelines.md2
-rw-r--r--doc/ci/pipelines/parent_child_pipelines.md2
-rw-r--r--doc/user/clusters/management_project_template.md2
-rw-r--r--locale/gitlab.pot24
-rw-r--r--package.json5
-rw-r--r--spec/finders/deployments_finder_spec.rb42
-rw-r--r--spec/frontend/google_cloud/databases/panel_spec.js17
-rw-r--r--spec/frontend/notebook/index_spec.js10
-rw-r--r--spec/frontend/notebook/lib/highlight_spec.js15
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js28
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js38
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/code_block_spec.js.snap26
-rw-r--r--spec/frontend/vue_shared/components/code_block_highlighted_spec.js65
-rw-r--r--spec/frontend/vue_shared/components/code_block_spec.js82
-rw-r--r--spec/graphql/resolvers/deployment_resolver_spec.rb28
-rw-r--r--spec/graphql/resolvers/deployments_resolver_spec.rb41
-rw-r--r--spec/graphql/types/deployment_details_type_spec.rb17
-rw-r--r--spec/graphql/types/deployment_type_spec.rb17
-rw-r--r--spec/graphql/types/environment_type_spec.rb2
-rw-r--r--spec/helpers/projects/google_cloud/cloudsql_helper_spec.rb25
-rw-r--r--spec/models/deployment_spec.rb21
-rw-r--r--spec/requests/api/graphql/environments/deployments_query_spec.rb345
-rw-r--r--spec/requests/api/graphql/project/deployment_spec.rb51
-rw-r--r--spec/requests/projects/google_cloud/configuration_controller_spec.rb5
-rw-r--r--spec/requests/projects/google_cloud/databases_controller_spec.rb222
-rw-r--r--spec/services/google_cloud/enable_cloudsql_service_spec.rb22
-rw-r--r--spec/support/shared_examples/requests/projects/google_cloud/google_cloud_ff_examples.rb18
-rw-r--r--spec/support/shared_examples/requests/projects/google_cloud/google_cloud_role_examples.rb55
-rw-r--r--spec/support/shared_examples/requests/projects/google_cloud/google_oauth2_config_examples.rb22
-rw-r--r--spec/support/shared_examples/requests/projects/google_cloud/google_oauth2_token_examples.rb47
-rw-r--r--storybook/config/preview.js7
-rw-r--r--yarn.lock35
88 files changed, 2111 insertions, 369 deletions
diff --git a/.rubocop.yml b/.rubocop.yml
index aa36d3f03ff..db607ec36ec 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -569,9 +569,6 @@ RSpec/ImplicitSubject:
RSpec/ReceiveNever:
Enabled: false
-RSpec/MissingExampleGroupArgument:
- Enabled: false
-
RSpec/UnspecifiedException:
Enabled: false
diff --git a/.rubocop_todo/rspec/missing_example_group_argument.yml b/.rubocop_todo/rspec/missing_example_group_argument.yml
new file mode 100644
index 00000000000..1506c9536a0
--- /dev/null
+++ b/.rubocop_todo/rspec/missing_example_group_argument.yml
@@ -0,0 +1,16 @@
+---
+RSpec/MissingExampleGroupArgument:
+ Exclude:
+ - 'ee/spec/controllers/groups/audit_events_controller_spec.rb'
+ - 'ee/spec/services/ee/notification_service_spec.rb'
+ - 'ee/spec/support/shared_examples/controllers/concerns/description_diff_actions_shared_examples.rb'
+ - 'spec/controllers/projects/issues_controller_spec.rb'
+ - 'spec/controllers/projects/merge_requests_controller_spec.rb'
+ - 'spec/factories/projects/ci_feature_usages.rb'
+ - 'spec/lib/gitlab/background_migration/backfill_jira_tracker_deployment_type2_spec.rb'
+ - 'spec/lib/gitlab/git_access_spec.rb'
+ - 'spec/policies/award_emoji_policy_spec.rb'
+ - 'spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb'
+ - 'spec/requests/api/graphql/mutations/notes/create/image_diff_note_spec.rb'
+ - 'spec/services/notification_service_spec.rb'
+ - 'spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb'
diff --git a/app/assets/javascripts/blob/notebook/notebook_viewer.vue b/app/assets/javascripts/blob/notebook/notebook_viewer.vue
index d2a841c88f1..79b8d273883 100644
--- a/app/assets/javascripts/blob/notebook/notebook_viewer.vue
+++ b/app/assets/javascripts/blob/notebook/notebook_viewer.vue
@@ -66,7 +66,7 @@ export default {
<div v-if="loading && !error" class="text-center loading">
<gl-loading-icon class="mt-5" size="lg" />
</div>
- <notebook-lab v-if="!loading && !error" :notebook="json" code-css-class="code white" />
+ <notebook-lab v-if="!loading && !error" :notebook="json" />
<p v-if="error" class="text-center">
<span v-if="loadError" ref="loadErrorMessage">{{
__('An error occurred while loading the file. Please try again later.')
diff --git a/app/assets/javascripts/google_cloud/databases/index.js b/app/assets/javascripts/google_cloud/databases/index.js
deleted file mode 100644
index e240a1116e8..00000000000
--- a/app/assets/javascripts/google_cloud/databases/index.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import Vue from 'vue';
-import Panel from './panel.vue';
-
-export default (containerId = '#js-google-cloud-databases') => {
- const element = document.querySelector(containerId);
- const { ...attrs } = JSON.parse(element.getAttribute('data'));
- return new Vue({
- el: element,
- render: (createElement) => createElement(Panel, { attrs }),
- });
-};
diff --git a/app/assets/javascripts/google_cloud/databases/init_index.js b/app/assets/javascripts/google_cloud/databases/init_index.js
new file mode 100644
index 00000000000..931143833cb
--- /dev/null
+++ b/app/assets/javascripts/google_cloud/databases/init_index.js
@@ -0,0 +1,11 @@
+import Vue from 'vue';
+import Panel from './panel.vue';
+
+export default () => {
+ const element = document.querySelector('#js-google-cloud-databases');
+ const attrs = JSON.parse(element.getAttribute('data'));
+ return new Vue({
+ el: element,
+ render: (createElement) => createElement(Panel, { attrs }),
+ });
+};
diff --git a/app/assets/javascripts/google_cloud/databases/init_new.js b/app/assets/javascripts/google_cloud/databases/init_new.js
new file mode 100644
index 00000000000..3feb2dc2f98
--- /dev/null
+++ b/app/assets/javascripts/google_cloud/databases/init_new.js
@@ -0,0 +1,11 @@
+import Vue from 'vue';
+import Form from './cloudsql/create_instance_form.vue';
+
+export default () => {
+ const element = document.querySelector('#js-google-cloud-databases-cloudsql-form');
+ const attrs = JSON.parse(element.getAttribute('data'));
+ return new Vue({
+ el: element,
+ render: (createElement) => createElement(Form, { attrs }),
+ });
+};
diff --git a/app/assets/javascripts/google_cloud/databases/panel.vue b/app/assets/javascripts/google_cloud/databases/panel.vue
index e2f18c286a5..8b91c508871 100644
--- a/app/assets/javascripts/google_cloud/databases/panel.vue
+++ b/app/assets/javascripts/google_cloud/databases/panel.vue
@@ -1,11 +1,15 @@
<script>
import GoogleCloudMenu from '../components/google_cloud_menu.vue';
import IncubationBanner from '../components/incubation_banner.vue';
+import InstanceTable from './cloudsql/instance_table.vue';
+import ServiceTable from './service_table.vue';
export default {
components: {
IncubationBanner,
+ InstanceTable,
GoogleCloudMenu,
+ ServiceTable,
},
props: {
configurationUrl: {
@@ -20,6 +24,26 @@ export default {
type: String,
required: true,
},
+ cloudsqlPostgresUrl: {
+ type: String,
+ required: true,
+ },
+ cloudsqlMysqlUrl: {
+ type: String,
+ required: true,
+ },
+ cloudsqlSqlserverUrl: {
+ type: String,
+ required: true,
+ },
+ cloudsqlInstances: {
+ type: Array,
+ required: true,
+ },
+ emptyIllustrationUrl: {
+ type: String,
+ required: true,
+ },
},
};
</script>
@@ -34,5 +58,19 @@ export default {
:deployments-url="deploymentsUrl"
:databases-url="databasesUrl"
/>
+
+ <service-table
+ alloydb-postgres-url="#"
+ :cloudsql-mysql-url="cloudsqlMysqlUrl"
+ :cloudsql-postgres-url="cloudsqlPostgresUrl"
+ :cloudsql-sqlserver-url="cloudsqlSqlserverUrl"
+ firestore-url="#"
+ memorystore-redis-url="#"
+ />
+
+ <instance-table
+ :cloudsql-instances="cloudsqlInstances"
+ :empty-illustration-url="emptyIllustrationUrl"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/notebook/cells/code.vue b/app/assets/javascripts/notebook/cells/code.vue
index f5a6f3a9817..bc1bab62553 100644
--- a/app/assets/javascripts/notebook/cells/code.vue
+++ b/app/assets/javascripts/notebook/cells/code.vue
@@ -13,11 +13,6 @@ export default {
type: Object,
required: true,
},
- codeCssClass: {
- type: String,
- required: false,
- default: '',
- },
},
computed: {
rawInputCode() {
@@ -39,18 +34,12 @@ export default {
<template>
<div class="cell">
- <code-output
- :raw-code="rawInputCode"
- :count="cell.execution_count"
- :code-css-class="codeCssClass"
- type="input"
- />
+ <code-output :raw-code="rawInputCode" :count="cell.execution_count" type="input" />
<output-cell
v-if="hasOutput"
:count="cell.execution_count"
:outputs="outputs"
:metadata="cell.metadata"
- :code-css-class="codeCssClass"
/>
</div>
</template>
diff --git a/app/assets/javascripts/notebook/cells/code/index.vue b/app/assets/javascripts/notebook/cells/code/index.vue
index e1ef9aa6d79..64e801a7516 100644
--- a/app/assets/javascripts/notebook/cells/code/index.vue
+++ b/app/assets/javascripts/notebook/cells/code/index.vue
@@ -1,10 +1,11 @@
<script>
-import Prism from '../../lib/highlight';
+import CodeBlockHighlighted from '~/vue_shared/components/code_block_highlighted.vue';
import Prompt from '../prompt.vue';
export default {
name: 'CodeOutput',
components: {
+ CodeBlockHighlighted,
Prompt,
},
props: {
@@ -13,11 +14,6 @@ export default {
required: false,
default: 0,
},
- codeCssClass: {
- type: String,
- required: false,
- default: '',
- },
type: {
type: String,
required: true,
@@ -41,22 +37,21 @@ export default {
return type.charAt(0).toUpperCase() + type.slice(1);
},
- cellCssClass() {
- return {
- [this.codeCssClass]: true,
- 'jupyter-notebook-scrolled': this.metadata.scrolled,
- };
+ maxHeight() {
+ return this.metadata.scrolled ? '20rem' : 'initial';
},
},
- mounted() {
- Prism.highlightElement(this.$refs.code);
- },
};
</script>
<template>
<div :class="type">
<prompt :type="promptType" :count="count" />
- <pre ref="code" :class="cellCssClass" class="language-python" v-text="code"></pre>
+ <code-block-highlighted
+ language="python"
+ :code="code"
+ :max-height="maxHeight"
+ class="gl-border"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/notebook/cells/output/index.vue b/app/assets/javascripts/notebook/cells/output/index.vue
index 5f7ef4a4377..88d01ffa659 100644
--- a/app/assets/javascripts/notebook/cells/output/index.vue
+++ b/app/assets/javascripts/notebook/cells/output/index.vue
@@ -6,11 +6,6 @@ import LatexOutput from './latex.vue';
export default {
props: {
- codeCssClass: {
- type: String,
- required: false,
- default: '',
- },
count: {
type: Number,
required: false,
@@ -96,7 +91,6 @@ export default {
:index="index"
:raw-code="rawCode(output)"
:metadata="metadata"
- :code-css-class="codeCssClass"
/>
</div>
</template>
diff --git a/app/assets/javascripts/notebook/index.vue b/app/assets/javascripts/notebook/index.vue
index 44dc1856e49..df9694b7cd8 100644
--- a/app/assets/javascripts/notebook/index.vue
+++ b/app/assets/javascripts/notebook/index.vue
@@ -11,11 +11,6 @@ export default {
type: Object,
required: true,
},
- codeCssClass: {
- type: String,
- required: false,
- default: '',
- },
},
computed: {
cells() {
@@ -52,7 +47,6 @@ export default {
v-for="(cell, index) in cells"
:key="index"
:cell="cell"
- :code-css-class="codeCssClass"
/>
</div>
</template>
diff --git a/app/assets/javascripts/notebook/lib/highlight.js b/app/assets/javascripts/notebook/lib/highlight.js
deleted file mode 100644
index 313aeecbd51..00000000000
--- a/app/assets/javascripts/notebook/lib/highlight.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import Prism from 'prismjs';
-import 'prismjs/components/prism-python';
-import 'prismjs/themes/prism.css';
-
-export default Prism;
diff --git a/app/assets/javascripts/pages/projects/google_cloud/databases/index.js b/app/assets/javascripts/pages/projects/google_cloud/databases/index.js
deleted file mode 100644
index 5482324f1cd..00000000000
--- a/app/assets/javascripts/pages/projects/google_cloud/databases/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import init from '~/google_cloud/databases/index';
-
-init();
diff --git a/app/assets/javascripts/pages/projects/google_cloud/databases/index/index.js b/app/assets/javascripts/pages/projects/google_cloud/databases/index/index.js
new file mode 100644
index 00000000000..e1dc0116707
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/google_cloud/databases/index/index.js
@@ -0,0 +1,3 @@
+import init from '~/google_cloud/databases/init_index';
+
+init();
diff --git a/app/assets/javascripts/pages/projects/google_cloud/databases/new/index.js b/app/assets/javascripts/pages/projects/google_cloud/databases/new/index.js
new file mode 100644
index 00000000000..698e788789b
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/google_cloud/databases/new/index.js
@@ -0,0 +1,3 @@
+import init from '~/google_cloud/databases/init_new';
+
+init();
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
index 14c8c53dd19..71ff3e892b1 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
@@ -1,12 +1,18 @@
<script>
-import { GlAlert, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlAlert, GlSprintf, GlLink, GlSafeHtmlDirective } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
+import { helpPagePath } from '~/helpers/help_page_helper';
import { __, sprintf } from '~/locale';
import ServiceDeskSetting from './service_desk_setting.vue';
export default {
+ customEmailHelpPath: helpPagePath('/user/project/service_desk.html', {
+ anchor: 'using-a-custom-email-address',
+ }),
components: {
GlAlert,
+ GlSprintf,
+ GlLink,
ServiceDeskSetting,
},
directives: {
@@ -43,6 +49,9 @@ export default {
templates: {
default: [],
},
+ publicProject: {
+ default: false,
+ },
},
data() {
return {
@@ -127,6 +136,27 @@ export default {
<template>
<div>
+ <gl-alert
+ v-if="publicProject && isEnabled"
+ class="mb-3"
+ variant="warning"
+ data-testid="public-project-alert"
+ :dismissible="false"
+ >
+ <gl-sprintf
+ :message="
+ __(
+ 'This project is public. Non-members can guess the Service Desk email address, because it contains the group and project name. %{linkStart}How do I create a custom email address?%{linkEnd}',
+ )
+ "
+ >
+ <template #link="{ content }">
+ <gl-link :href="$options.customEmailHelpPath" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
<gl-alert v-if="isAlertShowing" class="mb-3" :variant="alertVariant" @dismiss="onDismiss">
<span v-safe-html="alertMessage"></span>
</gl-alert>
diff --git a/app/assets/javascripts/projects/settings_service_desk/index.js b/app/assets/javascripts/projects/settings_service_desk/index.js
index e14cdee17ce..26435a5fac9 100644
--- a/app/assets/javascripts/projects/settings_service_desk/index.js
+++ b/app/assets/javascripts/projects/settings_service_desk/index.js
@@ -20,6 +20,7 @@ export default () => {
selectedTemplate,
selectedFileTemplateProjectId,
templates,
+ publicProject,
} = el.dataset;
return new Vue({
@@ -35,6 +36,7 @@ export default () => {
selectedTemplate,
selectedFileTemplateProjectId: parseInt(selectedFileTemplateProjectId, 10) || null,
templates: JSON.parse(templates),
+ publicProject: parseBoolean(publicProject),
},
render: (createElement) => createElement(ServiceDeskRoot),
});
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue
index 9b46dbc7b31..8482234c7b1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { GlButton, GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { normalizeHeaders } from '~/lib/utils/common_utils';
import { sprintf, __ } from '~/locale';
@@ -8,11 +8,13 @@ import StatusIcon from '../extensions/status_icon.vue';
import { EXTENSION_ICONS } from '../../constants';
const FETCH_TYPE_COLLAPSED = 'collapsed';
+const FETCH_TYPE_EXPANDED = 'expanded';
export default {
components: {
StatusIcon,
GlButton,
+ GlLoadingIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -20,7 +22,7 @@ export default {
props: {
/**
* @param {value.collapsed} Object
- * @param {value.extended} Object
+ * @param {value.expanded} Object
*/
value: {
type: Object,
@@ -40,7 +42,7 @@ export default {
type: Function,
required: true,
},
- fetchExtendedData: {
+ fetchExpandedData: {
type: Function,
required: false,
default: undefined,
@@ -79,8 +81,10 @@ export default {
},
data() {
return {
+ isExpandedForTheFirstTime: true,
isCollapsed: true,
isLoading: false,
+ isLoadingExpandedContent: false,
error: null,
};
},
@@ -111,6 +115,22 @@ export default {
methods: {
toggleCollapsed() {
this.isCollapsed = !this.isCollapsed;
+
+ if (this.isExpandedForTheFirstTime && typeof this.fetchExpandedData === 'function') {
+ this.isExpandedForTheFirstTime = false;
+ this.fetchExpandedContent();
+ }
+ },
+ async fetchExpandedContent() {
+ this.isLoadingExpandedContent = true;
+
+ try {
+ await this.fetch(this.fetchExpandedData, FETCH_TYPE_EXPANDED);
+ } catch {
+ this.error = this.errorText;
+ }
+
+ this.isLoadingExpandedContent = false;
},
fetch(handler, dataType) {
const requests = this.multiPolling ? handler() : [handler];
@@ -161,7 +181,6 @@ export default {
<slot v-if="!error" name="summary">{{ isLoading ? loadingText : summary }}</slot>
<span v-else>{{ error }}</span>
</div>
- <!-- actions will go here -->
<div
v-if="isCollapsible"
class="gl-border-l-1 gl-border-l-solid gl-border-gray-100 gl-ml-3 gl-pl-3 gl-h-6"
@@ -185,7 +204,10 @@ export default {
class="mr-widget-grouped-section gl-relative"
data-testid="widget-extension-collapsed-section"
>
- <slot name="content">{{ content }}</slot>
+ <div v-if="isLoadingExpandedContent" class="report-block-container gl-text-center">
+ <gl-loading-icon size="sm" inline /> {{ __('Loading...') }}
+ </div>
+ <slot v-else name="content">{{ content }}</slot>
</div>
</section>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/code_block.stories.js b/app/assets/javascripts/vue_shared/components/code_block.stories.js
new file mode 100644
index 00000000000..ad53afe3676
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/code_block.stories.js
@@ -0,0 +1,18 @@
+import CodeBlock from './code_block.vue';
+
+export default {
+ component: CodeBlock,
+ title: 'vue_shared/components/code_block',
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { CodeBlock },
+ props: Object.keys(argTypes),
+ template: '<code-block v-bind="$props" />',
+});
+
+export const Default = Template.bind({});
+Default.args = {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ code: `git commit -a "Message"\ngit push`,
+};
diff --git a/app/assets/javascripts/vue_shared/components/code_block.vue b/app/assets/javascripts/vue_shared/components/code_block.vue
index 9856f35c7f6..4a69845d3a4 100644
--- a/app/assets/javascripts/vue_shared/components/code_block.vue
+++ b/app/assets/javascripts/vue_shared/components/code_block.vue
@@ -4,7 +4,8 @@ export default {
props: {
code: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
maxHeight: {
type: String,
@@ -32,5 +33,5 @@ export default {
class="code-block rounded code"
:class="$options.userColorScheme"
:style="styleObject"
- ><code class="d-block">{{ code }}</code></pre>
+ ><slot><code class="d-block">{{ code }}</code></slot></pre>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/code_block_highlighted.stories.js b/app/assets/javascripts/vue_shared/components/code_block_highlighted.stories.js
new file mode 100644
index 00000000000..1939575ae40
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/code_block_highlighted.stories.js
@@ -0,0 +1,18 @@
+import CodeBlockHighlighted from './code_block_highlighted.vue';
+
+export default {
+ component: CodeBlockHighlighted,
+ title: 'vue_shared/components/code_block_highlighted',
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { CodeBlockHighlighted },
+ props: Object.keys(argTypes),
+ template: '<code-block-highlighted v-bind="$props" />',
+});
+
+export const Default = Template.bind({});
+Default.args = {
+ code: `const foo = 1;\nconsole.log(foo + ' yay')`,
+ language: 'javascript',
+};
diff --git a/app/assets/javascripts/vue_shared/components/code_block_highlighted.vue b/app/assets/javascripts/vue_shared/components/code_block_highlighted.vue
new file mode 100644
index 00000000000..65b08b608e8
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/code_block_highlighted.vue
@@ -0,0 +1,72 @@
+<script>
+import { GlSafeHtmlDirective } from '@gitlab/ui';
+
+import languageLoader from '~/content_editor/services/highlight_js_language_loader';
+import CodeBlock from './code_block.vue';
+
+export default {
+ name: 'CodeBlockHighlighted',
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
+ components: {
+ CodeBlock,
+ },
+ props: {
+ code: {
+ type: String,
+ required: true,
+ },
+ language: {
+ type: String,
+ required: true,
+ },
+ maxHeight: {
+ type: String,
+ required: false,
+ default: 'initial',
+ },
+ },
+ data() {
+ return {
+ hljs: null,
+ languageLoaded: false,
+ };
+ },
+ computed: {
+ highlighted() {
+ if (this.hljs && this.languageLoaded) {
+ return this.hljs.highlight(this.code, { language: this.language }).value;
+ }
+
+ return this.code;
+ },
+ },
+ async mounted() {
+ this.hljs = await this.loadHighlightJS();
+ if (this.language) {
+ await this.loadLanguage();
+ }
+ },
+ methods: {
+ async loadLanguage() {
+ try {
+ const { default: languageDefinition } = await languageLoader[this.language]();
+
+ this.hljs.registerLanguage(this.language, languageDefinition);
+ this.languageLoaded = true;
+ } catch (e) {
+ this.$emit('error', e);
+ }
+ },
+ loadHighlightJS() {
+ return import('highlight.js/lib/core');
+ },
+ },
+};
+</script>
+<template>
+ <code-block :max-height="maxHeight" class="highlight">
+ <span v-safe-html="highlighted"></span>
+ </code-block>
+</template>
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index b980d7fdaa7..cba8f48071b 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -471,11 +471,6 @@ span.idiff {
}
}
-.jupyter-notebook-scrolled {
- overflow-y: auto;
- max-height: 20rem;
-}
-
#js-openapi-viewer {
pre.version,
code {
diff --git a/app/controllers/projects/google_cloud/databases_controller.rb b/app/controllers/projects/google_cloud/databases_controller.rb
index 7b1cf6e5ce1..fb442f87666 100644
--- a/app/controllers/projects/google_cloud/databases_controller.rb
+++ b/app/controllers/projects/google_cloud/databases_controller.rb
@@ -3,14 +3,138 @@
module Projects
module GoogleCloud
class DatabasesController < Projects::GoogleCloud::BaseController
+ before_action :validate_gcp_token!
+ before_action :validate_product, only: :new
+
def index
js_data = {
configurationUrl: project_google_cloud_configuration_path(project),
deploymentsUrl: project_google_cloud_deployments_path(project),
- databasesUrl: project_google_cloud_databases_path(project)
+ databasesUrl: project_google_cloud_databases_path(project),
+ cloudsqlPostgresUrl: new_project_google_cloud_database_path(project, :postgres),
+ cloudsqlMysqlUrl: new_project_google_cloud_database_path(project, :mysql),
+ cloudsqlSqlserverUrl: new_project_google_cloud_database_path(project, :sqlserver),
+ cloudsqlInstances: ::GoogleCloud::GetCloudsqlInstancesService.new(project).execute,
+ emptyIllustrationUrl: ActionController::Base.helpers.image_path('illustrations/pipelines_empty.svg')
}
@js_data = js_data.to_json
- track_event('databases#index', 'success', js_data)
+
+ track_event('databases#index', 'success', nil)
+ end
+
+ def new
+ product = permitted_params[:product].to_sym
+
+ @title = title(product)
+
+ @js_data = {
+ gcpProjects: gcp_projects,
+ refs: refs,
+ cancelPath: project_google_cloud_databases_path(project),
+ formTitle: form_title(product),
+ formDescription: description(product),
+ databaseVersions: Projects::GoogleCloud::CloudsqlHelper::VERSIONS[product],
+ tiers: Projects::GoogleCloud::CloudsqlHelper::TIERS
+ }.to_json
+
+ render template: 'projects/google_cloud/databases/cloudsql_form', formats: :html
+ end
+
+ def create
+ enable_response = ::GoogleCloud::EnableCloudsqlService
+ .new(project, current_user, enable_service_params)
+ .execute
+
+ if enable_response[:status] == :error
+ track_event('databases#cloudsql_create', 'error_enable_cloudsql_service', enable_response)
+ flash[:error] = error_message(enable_response[:message])
+ else
+ permitted_params = params.permit(:gcp_project, :ref, :database_version, :tier)
+ create_response = ::GoogleCloud::CreateCloudsqlInstanceService
+ .new(project, current_user, create_service_params(permitted_params))
+ .execute
+
+ if create_response[:status] == :error
+ track_event('databases#cloudsql_create', 'error_create_cloudsql_instance', create_response)
+ flash[:warning] = error_message(create_response[:message])
+ else
+ track_event('databases#cloudsql_create', 'success', nil)
+ flash[:notice] = success_message
+ end
+ end
+
+ redirect_to project_google_cloud_databases_path(project)
+ end
+
+ private
+
+ def enable_service_params
+ { google_oauth2_token: token_in_session }
+ end
+
+ def create_service_params(permitted_params)
+ {
+ google_oauth2_token: token_in_session,
+ gcp_project_id: permitted_params[:gcp_project],
+ environment_name: permitted_params[:ref],
+ database_version: permitted_params[:database_version],
+ tier: permitted_params[:tier]
+ }
+ end
+
+ def error_message(message)
+ format(s_("CloudSeed|Google Cloud Error - %{message}"), message: message)
+ end
+
+ def success_message
+ s_('CloudSeed|Cloud SQL instance creation request successful. Expected resolution time is ~5 minutes.')
+ end
+
+ def validate_product
+ not_found unless permitted_params[:product].in?(%w[postgres mysql sqlserver])
+ end
+
+ def permitted_params
+ params.permit(:product)
+ end
+
+ def title(product)
+ case product
+ when :postgres
+ s_('CloudSeed|Create Postgres Instance')
+ when :mysql
+ s_('CloudSeed|Create MySQL Instance')
+ else
+ s_('CloudSeed|Create MySQL Instance')
+ end
+ end
+
+ def form_title(product)
+ case product
+ when :postgres
+ s_('CloudSeed|Cloud SQL for Postgres')
+ when :mysql
+ s_('CloudSeed|Cloud SQL for MySQL')
+ else
+ s_('CloudSeed|Cloud SQL for SQL Server')
+ end
+ end
+
+ def description(product)
+ case product
+ when :postgres
+ s_('CloudSeed|Cloud SQL instances are fully managed, relational PostgreSQL databases. '\
+ 'Google handles replication, patch management, and database management '\
+ 'to ensure availability and performance.')
+ when :mysql
+ s_('Cloud SQL instances are fully managed, relational MySQL databases. '\
+ 'Google handles replication, patch management, and database management '\
+ 'to ensure availability and performance.')
+ else
+ s_('Cloud SQL instances are fully managed, relational SQL Server databases. ' \
+ 'Google handles replication, patch management, and database management ' \
+ 'to ensure availability and performance.')
+ end
end
end
end
diff --git a/app/finders/deployments_finder.rb b/app/finders/deployments_finder.rb
index 04b82ee04ec..e5e08d2971f 100644
--- a/app/finders/deployments_finder.rb
+++ b/app/finders/deployments_finder.rb
@@ -9,8 +9,8 @@
# updated_before: DateTime
# finished_after: DateTime
# finished_before: DateTime
-# environment: String
-# status: String (see Deployment.statuses)
+# environment: String (name) or Integer (ID)
+# status: String or Array<String> (see Deployment.statuses)
# order_by: String (see ALLOWED_SORT_VALUES constant)
# sort: String (asc | desc)
class DeploymentsFinder
@@ -33,6 +33,7 @@ class DeploymentsFinder
def initialize(params = {})
@params = params
+ @params[:status] = Array(@params[:status]).map(&:to_s) if @params[:status]
validate!
end
@@ -68,16 +69,25 @@ class DeploymentsFinder
raise error if raise_for_inefficient_updated_at_query?
end
- if (filter_by_finished_at? && !order_by_finished_at?) || (!filter_by_finished_at? && order_by_finished_at?)
- raise InefficientQueryError, '`finished_at` filter and `finished_at` sorting must be paired'
+ if filter_by_finished_at? && !order_by_finished_at?
+ raise InefficientQueryError, '`finished_at` filter requires `finished_at` sort.'
+ end
+
+ if order_by_finished_at? && !(filter_by_finished_at? || filter_by_finished_statuses?)
+ raise InefficientQueryError,
+ '`finished_at` sort requires `finished_at` filter or a filter with at least one of the finished statuses.'
end
if filter_by_finished_at? && !filter_by_successful_deployment?
raise InefficientQueryError, '`finished_at` filter must be combined with `success` status filter.'
end
- if params[:environment].present? && !params[:project].present?
- raise InefficientQueryError, '`environment` filter must be combined with `project` scope.'
+ if filter_by_environment_name? && !params[:project].present?
+ raise InefficientQueryError, '`environment` name filter must be combined with `project` scope.'
+ end
+
+ if filter_by_finished_statuses? && filter_by_upcoming_statuses?
+ raise InefficientQueryError, 'finished statuses and upcoming statuses must be separately queried.'
end
end
@@ -86,6 +96,8 @@ class DeploymentsFinder
params[:project].deployments
elsif params[:group].present?
::Deployment.for_projects(params[:group].all_projects)
+ elsif filter_by_environment_id?
+ ::Deployment.for_environment(params[:environment])
else
::Deployment.none
end
@@ -112,7 +124,7 @@ class DeploymentsFinder
end
def by_environment(items)
- if params[:project].present? && params[:environment].present?
+ if params[:project].present? && filter_by_environment_name?
items.for_environment_name(params[:project], params[:environment])
else
items
@@ -122,7 +134,7 @@ class DeploymentsFinder
def by_status(items)
return items unless params[:status].present?
- unless Deployment.statuses.key?(params[:status])
+ unless Deployment.statuses.keys.intersection(params[:status]) == params[:status]
raise ArgumentError, "The deployment status #{params[:status]} is invalid"
end
@@ -165,7 +177,23 @@ class DeploymentsFinder
end
def filter_by_successful_deployment?
- params[:status].to_s == 'success'
+ params[:status].present? && params[:status].count == 1 && params[:status].first.to_s == 'success'
+ end
+
+ def filter_by_finished_statuses?
+ params[:status].present? && Deployment::FINISHED_STATUSES.map(&:to_s).intersection(params[:status]).any?
+ end
+
+ def filter_by_upcoming_statuses?
+ params[:status].present? && Deployment::UPCOMING_STATUSES.map(&:to_s).intersection(params[:status]).any?
+ end
+
+ def filter_by_environment_name?
+ params[:environment].present? && params[:environment].is_a?(String)
+ end
+
+ def filter_by_environment_id?
+ params[:environment].present? && params[:environment].is_a?(Integer)
end
def order_by_updated_at?
diff --git a/app/graphql/resolvers/deployment_resolver.rb b/app/graphql/resolvers/deployment_resolver.rb
new file mode 100644
index 00000000000..7d9ce0f023c
--- /dev/null
+++ b/app/graphql/resolvers/deployment_resolver.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class DeploymentResolver < BaseResolver
+ argument :iid,
+ GraphQL::Types::ID,
+ required: true,
+ description: 'Project-level internal ID of the Deployment.'
+
+ type Types::DeploymentType, null: true
+
+ alias_method :project, :object
+
+ def resolve(iid:)
+ return unless project.present? && project.is_a?(::Project)
+
+ Deployment.for_iid(project, iid)
+ end
+ end
+end
diff --git a/app/graphql/resolvers/deployments_resolver.rb b/app/graphql/resolvers/deployments_resolver.rb
new file mode 100644
index 00000000000..341d23c2ccb
--- /dev/null
+++ b/app/graphql/resolvers/deployments_resolver.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class DeploymentsResolver < BaseResolver
+ argument :statuses, [Types::DeploymentStatusEnum],
+ description: 'Statuses of the deployments.',
+ required: false,
+ as: :status
+
+ argument :order_by, Types::DeploymentsOrderByInputType,
+ description: 'Order by a specified field.',
+ required: false
+
+ type Types::DeploymentType, null: true
+
+ alias_method :environment, :object
+
+ def resolve(**args)
+ return unless environment.present? && environment.is_a?(::Environment)
+
+ args = transform_args_for_finder(**args)
+
+ # GraphQL BatchLoader shouldn't be used here because pagination query will be inefficient
+ # that fetches thousands of rows before limiting and offsetting.
+ DeploymentsFinder.new(environment: environment.id, **args).execute
+ end
+
+ private
+
+ def transform_args_for_finder(**args)
+ if (order_by = args.delete(:order_by))
+ order_by = order_by.to_h.map { |k, v| { order_by: k.to_s, sort: v } }.first
+ args.merge!(order_by)
+ end
+
+ args
+ end
+ end
+end
diff --git a/app/graphql/types/deployment_details_type.rb b/app/graphql/types/deployment_details_type.rb
new file mode 100644
index 00000000000..c1ea436bf3d
--- /dev/null
+++ b/app/graphql/types/deployment_details_type.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Types
+ class DeploymentDetailsType < DeploymentType
+ graphql_name 'DeploymentDetails'
+ description 'The details of the deployment'
+ authorize :read_deployment
+ end
+end
diff --git a/app/graphql/types/deployment_status_enum.rb b/app/graphql/types/deployment_status_enum.rb
new file mode 100644
index 00000000000..7ef69d3f1c1
--- /dev/null
+++ b/app/graphql/types/deployment_status_enum.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Types
+ class DeploymentStatusEnum < BaseEnum
+ graphql_name 'DeploymentStatus'
+ description 'All deployment statuses.'
+
+ ::Deployment.statuses.each_key do |status|
+ value status.upcase,
+ description: "A deployment that is #{status.tr('_', ' ')}.",
+ value: status
+ end
+ end
+end
diff --git a/app/graphql/types/deployment_type.rb b/app/graphql/types/deployment_type.rb
new file mode 100644
index 00000000000..1b61001fc10
--- /dev/null
+++ b/app/graphql/types/deployment_type.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module Types
+ # If you're considering to add a new field in DeploymentType, please follow this guideline:
+ # - If the field is preloadable in batch, define it in DeploymentType.
+ # In this case, you should extend DeploymentsResolver logic to preload the field. Also, add a new test that
+ # fetching the specific field for multiple deployments doesn't cause N+1 query problem.
+ # - If the field is NOT preloadable in batch, define it in DeploymentDetailsType.
+ # This type can be only fetched for a single deployment, so you don't need to take care of the preloading.
+ class DeploymentType < BaseObject
+ graphql_name 'Deployment'
+ description 'The deployment of an environment'
+
+ present_using Deployments::DeploymentPresenter
+
+ authorize :read_deployment
+
+ field :id,
+ GraphQL::Types::ID,
+ description: 'Global ID of the deployment.'
+
+ field :iid,
+ GraphQL::Types::ID,
+ description: 'Project-level internal ID of the deployment.'
+
+ field :ref,
+ GraphQL::Types::String,
+ description: 'Git-Ref that the deployment ran on.'
+
+ field :tag,
+ GraphQL::Types::Boolean,
+ description: 'True or false if the deployment ran on a Git-tag.'
+
+ field :sha,
+ GraphQL::Types::String,
+ description: 'Git-SHA that the deployment ran on.'
+
+ field :created_at,
+ Types::TimeType,
+ description: 'When the deployment record was created.'
+
+ field :updated_at,
+ Types::TimeType,
+ description: 'When the deployment record was updated.'
+
+ field :finished_at,
+ Types::TimeType,
+ description: 'When the deployment finished.'
+
+ field :status,
+ Types::DeploymentStatusEnum,
+ description: 'Status of the deployment.'
+ end
+end
diff --git a/app/graphql/types/deployments_order_by_input_type.rb b/app/graphql/types/deployments_order_by_input_type.rb
new file mode 100644
index 00000000000..a87fef9fe8a
--- /dev/null
+++ b/app/graphql/types/deployments_order_by_input_type.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Types
+ class DeploymentsOrderByInputType < BaseInputObject
+ graphql_name 'DeploymentsOrderByInput'
+ description 'Values for ordering deployments by a specific field'
+
+ argument :created_at,
+ Types::SortDirectionEnum,
+ required: false,
+ description: 'Order by Created time.'
+
+ argument :finished_at,
+ Types::SortDirectionEnum,
+ required: false,
+ description: 'Order by Finished time.'
+
+ def prepare
+ raise GraphQL::ExecutionError, 'orderBy parameter must contain one key-value pair.' unless to_h.size == 1
+
+ super
+ end
+ end
+end
diff --git a/app/graphql/types/environment_type.rb b/app/graphql/types/environment_type.rb
index 2a7076cc3c9..994be0e5f5a 100644
--- a/app/graphql/types/environment_type.rb
+++ b/app/graphql/types/environment_type.rb
@@ -29,5 +29,14 @@ module Types
Types::AlertManagement::AlertType,
null: true,
description: 'Most severe open alert for the environment. If multiple alerts have equal severity, the most recent is returned.'
+
+ # Setting high complexity for preventing users from querying deployments for multiple environments,
+ # which could result in N+1 issue.
+ field :deployments,
+ Types::DeploymentType.connection_type,
+ null: true,
+ description: 'Deployments of the environment.',
+ resolver: Resolvers::DeploymentsResolver,
+ complexity: 150
end
end
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index ecc6c9d7811..436fec4b8ef 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -179,6 +179,12 @@ module Types
description: 'A single environment of the project.',
resolver: Resolvers::EnvironmentsResolver.single
+ field :deployment,
+ Types::DeploymentDetailsType,
+ null: true,
+ description: 'Details of the deployment of the project.',
+ resolver: Resolvers::DeploymentResolver.single
+
field :issue,
Types::IssueType,
null: true,
diff --git a/app/graphql/types/sort_direction_enum.rb b/app/graphql/types/sort_direction_enum.rb
new file mode 100644
index 00000000000..28dba1abfb6
--- /dev/null
+++ b/app/graphql/types/sort_direction_enum.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Types
+ class SortDirectionEnum < BaseEnum
+ graphql_name 'SortDirectionEnum'
+ description 'Values for sort direction'
+
+ value 'ASC', 'Ascending order.', value: 'asc'
+ value 'DESC', 'Descending order.', value: 'desc'
+ end
+end
diff --git a/app/helpers/projects/google_cloud/cloudsql_helper.rb b/app/helpers/projects/google_cloud/cloudsql_helper.rb
new file mode 100644
index 00000000000..0c24254d9b4
--- /dev/null
+++ b/app/helpers/projects/google_cloud/cloudsql_helper.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+module Projects
+ module GoogleCloud
+ module CloudsqlHelper
+ # Sources:
+ # - https://cloud.google.com/sql/docs/postgres/instance-settings
+ # - https://cloud.google.com/sql/docs/mysql/instance-settings
+ # - https://cloud.google.com/sql/docs/sqlserver/instance-settings
+
+ TIERS = [
+ { value: 'db-custom-1-3840', label: '1 vCPU, 3840 MB RAM - Standard' },
+ { value: 'db-custom-2-7680', label: '2 vCPU, 7680 MB RAM - Standard' },
+ { value: 'db-custom-2-13312', label: '2 vCPU, 13312 MB RAM - High memory' },
+ { value: 'db-custom-4-15360', label: '4 vCPU, 15360 MB RAM - Standard' },
+ { value: 'db-custom-4-26624', label: '4 vCPU, 26624 MB RAM - High memory' },
+ { value: 'db-custom-8-30720', label: '8 vCPU, 30720 MB RAM - Standard' },
+ { value: 'db-custom-8-53248', label: '8 vCPU, 53248 MB RAM - High memory' },
+ { value: 'db-custom-16-61440', label: '16 vCPU, 61440 MB RAM - Standard' },
+ { value: 'db-custom-16-106496', label: '16 vCPU, 106496 MB RAM - High memory' },
+ { value: 'db-custom-32-122880', label: '32 vCPU, 122880 MB RAM - Standard' },
+ { value: 'db-custom-32-212992', label: '32 vCPU, 212992 MB RAM - High memory' },
+ { value: 'db-custom-64-245760', label: '64 vCPU, 245760 MB RAM - Standard' },
+ { value: 'db-custom-64-425984', label: '64 vCPU, 425984 MB RAM - High memory' },
+ { value: 'db-custom-96-368640', label: '96 vCPU, 368640 MB RAM - Standard' },
+ { value: 'db-custom-96-638976', label: '96 vCPU, 638976 MB RAM - High memory' }
+ ].freeze
+
+ VERSIONS = {
+ postgres: [
+ { value: 'POSTGRES_14', label: 'PostgreSQL 14' },
+ { value: 'POSTGRES_13', label: 'PostgreSQL 13' },
+ { value: 'POSTGRES_12', label: 'PostgreSQL 12' },
+ { value: 'POSTGRES_11', label: 'PostgreSQL 11' },
+ { value: 'POSTGRES_10', label: 'PostgreSQL 10' },
+ { value: 'POSTGRES_9_6', label: 'PostgreSQL 9.6' }
+ ],
+ mysql: [
+ { value: 'MYSQL_8_0', label: 'MySQL 8' },
+ { value: 'MYSQL_5_7', label: 'MySQL 5.7' },
+ { value: 'MYSQL_5_6', label: 'MySQL 5.6' }
+ ],
+ sqlserver: [
+ { value: 'SQLSERVER_2017_STANDARD', label: 'SQL Server 2017 Standard' },
+ { value: 'SQLSERVER_2017_ENTERPRISE', label: 'SQL Server 2017 Enterprise' },
+ { value: 'SQLSERVER_2017_EXPRESS', label: 'SQL Server 2017 Express' },
+ { value: 'SQLSERVER_2017_WEB', label: 'SQL Server 2017 Web' },
+ { value: 'SQLSERVER_2019_STANDARD', label: 'SQL Server 2019 Standard' },
+ { value: 'SQLSERVER_2019_ENTERPRISE', label: 'SQL Server 2019 Enterprise' },
+ { value: 'SQLSERVER_2019_EXPRESS', label: 'SQL Server 2019 Express' },
+ { value: 'SQLSERVER_2019_WEB', label: 'SQL Server 2019 Web' }
+ ]
+ }.freeze
+ end
+ end
+end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index a3213a59bed..325754f001a 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -36,6 +36,7 @@ class Deployment < ApplicationRecord
delegate :name, to: :environment, prefix: true
delegate :kubernetes_namespace, to: :deployment_cluster, allow_nil: true
+ scope :for_iid, -> (project, iid) { where(project: project, iid: iid) }
scope :for_environment, -> (environment) { where(environment_id: environment) }
scope :for_environment_name, -> (project, name) do
where('deployments.environment_id = (?)',
@@ -61,6 +62,7 @@ class Deployment < ApplicationRecord
VISIBLE_STATUSES = %i[running success failed canceled blocked].freeze
FINISHED_STATUSES = %i[success failed canceled].freeze
+ UPCOMING_STATUSES = %i[created blocked running].freeze
state_machine :status, initial: :created do
event :run do
diff --git a/app/presenters/deployments/deployment_presenter.rb b/app/presenters/deployments/deployment_presenter.rb
new file mode 100644
index 00000000000..97d4accc59c
--- /dev/null
+++ b/app/presenters/deployments/deployment_presenter.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Deployments
+ class DeploymentPresenter < Gitlab::View::Presenter::Delegated
+ presents ::Deployment, as: :deployment
+ end
+end
diff --git a/app/services/google_cloud/create_cloudsql_instance_service.rb b/app/services/google_cloud/create_cloudsql_instance_service.rb
index f7fca277c52..8d040c6c908 100644
--- a/app/services/google_cloud/create_cloudsql_instance_service.rb
+++ b/app/services/google_cloud/create_cloudsql_instance_service.rb
@@ -11,7 +11,7 @@ module GoogleCloud
trigger_instance_setup_worker
success
rescue Google::Apis::Error => err
- error(err.to_json)
+ error(err.message)
end
private
diff --git a/app/services/google_cloud/enable_cloudsql_service.rb b/app/services/google_cloud/enable_cloudsql_service.rb
index a466b2f3696..e4a411d0fab 100644
--- a/app/services/google_cloud/enable_cloudsql_service.rb
+++ b/app/services/google_cloud/enable_cloudsql_service.rb
@@ -12,6 +12,8 @@ module GoogleCloud
end
success({ gcp_project_ids: unique_gcp_project_ids })
+ rescue Google::Apis::Error => err
+ error(err.message)
end
private
diff --git a/app/views/projects/_service_desk_settings.html.haml b/app/views/projects/_service_desk_settings.html.haml
index cee3d9071b6..349cd88437f 100644
--- a/app/views/projects/_service_desk_settings.html.haml
+++ b/app/views/projects/_service_desk_settings.html.haml
@@ -17,6 +17,7 @@
selected_file_template_project_id: "#{@project.service_desk_setting&.file_template_project_id}",
outgoing_name: "#{@project.service_desk_setting&.outgoing_name}",
project_key: "#{@project.service_desk_setting&.project_key}",
- templates: available_service_desk_templates_for(@project) } }
+ templates: available_service_desk_templates_for(@project),
+ public_project: "#{@project.public?}" } }
- elsif show_callout?('promote_service_desk_dismissed')
= render 'shared/promotions/promote_servicedesk'
diff --git a/app/views/projects/google_cloud/databases/cloudsql_form.html.haml b/app/views/projects/google_cloud/databases/cloudsql_form.html.haml
new file mode 100644
index 00000000000..05838717b49
--- /dev/null
+++ b/app/views/projects/google_cloud/databases/cloudsql_form.html.haml
@@ -0,0 +1,9 @@
+- add_to_breadcrumbs _('Google Cloud'), project_google_cloud_path(@project)
+- add_to_breadcrumbs s_('CloudSeed|Databases'), project_google_cloud_databases_path(@project)
+- breadcrumb_title @title
+- page_title @title
+
+- @content_class = "limit-container-width" unless fluid_layout
+
+= form_tag project_google_cloud_databases_path(@project), method: 'post' do
+ #js-google-cloud-databases-cloudsql-form{ data: @js_data }
diff --git a/app/views/projects/google_cloud/gcp_regions/index.html.haml b/app/views/projects/google_cloud/gcp_regions/index.html.haml
index 36b5630611e..4cc218ff548 100644
--- a/app/views/projects/google_cloud/gcp_regions/index.html.haml
+++ b/app/views/projects/google_cloud/gcp_regions/index.html.haml
@@ -1,5 +1,5 @@
- add_to_breadcrumbs _('Google Cloud'), project_google_cloud_path(@project)
-- breadcrumb_title _('CloudSeed|Regions')
+- breadcrumb_title s_('CloudSeed|Regions')
- page_title s_('CloudSeed|Regions')
- @content_class = "limit-container-width" unless fluid_layout
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index 164773f9b60..f8304d5e44e 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -1,7 +1,7 @@
- user = local_assigns.fetch(:user, current_user)
- access = user&.max_member_access_for_group(group.id)
-%li.group-row.py-3.gl-align-items-center{ class: "gl-display-flex!#{' no-description' if group.description.blank?}" }
+%li.group-row.py-3.gl-align-items-center{ class: "gl-display-flex!" }
.avatar-container.rect-avatar.s40.gl-flex-shrink-0
= link_to group do
= group_icon(group, class: "avatar s40")
diff --git a/app/workers/google_cloud/create_cloudsql_instance_worker.rb b/app/workers/google_cloud/create_cloudsql_instance_worker.rb
index 3c15c59b8d9..8c4f4c83339 100644
--- a/app/workers/google_cloud/create_cloudsql_instance_worker.rb
+++ b/app/workers/google_cloud/create_cloudsql_instance_worker.rb
@@ -8,30 +8,15 @@ module GoogleCloud
feature_category :not_owned # rubocop:disable Gitlab/AvoidFeatureCategoryNotOwned
idempotent!
- def perform(user_id, project_id, options = {})
+ def perform(user_id, project_id, params = {})
user = User.find(user_id)
project = Project.find(project_id)
+ params = params.with_indifferent_access
- google_oauth2_token = options[:google_oauth2_token]
- gcp_project_id = options[:gcp_project_id]
- instance_name = options[:instance_name]
- database_version = options[:database_version]
- environment_name = options[:environment_name]
- is_protected = options[:is_protected]
-
- params = {
- google_oauth2_token: google_oauth2_token,
- gcp_project_id: gcp_project_id,
- instance_name: instance_name,
- database_version: database_version,
- environment_name: environment_name,
- is_protected: is_protected
- }
-
- response = GoogleCloud::SetupCloudsqlInstanceService.new(project, user, params).execute
+ response = ::GoogleCloud::SetupCloudsqlInstanceService.new(project, user, params).execute
if response[:status] == :error
- raise response[:message]
+ raise "Error SetupCloudsqlInstanceService: #{response.to_json}"
end
end
end
diff --git a/config/feature_flags/development/ci_forked_source_public_cost_factor.yml b/config/feature_flags/development/ci_forked_source_public_cost_factor.yml
new file mode 100644
index 00000000000..6e5a6c1e1f1
--- /dev/null
+++ b/config/feature_flags/development/ci_forked_source_public_cost_factor.yml
@@ -0,0 +1,8 @@
+---
+name: ci_forked_source_public_cost_factor
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/94870
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/370147
+milestone: '15.4'
+type: development
+group: group::pipeline execution
+default_enabled: false
diff --git a/config/feature_flags/development/incubation_5mp_google_cloud.yml b/config/feature_flags/development/incubation_5mp_google_cloud.yml
index b687a656b40..1b3ba503f5d 100644
--- a/config/feature_flags/development/incubation_5mp_google_cloud.yml
+++ b/config/feature_flags/development/incubation_5mp_google_cloud.yml
@@ -1,7 +1,7 @@
---
name: incubation_5mp_google_cloud
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70715
-rollout_issue_url:
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/371332
milestone: '14.3'
type: development
group: group::incubation
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 53d9be13611..83a13c62458 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -312,7 +312,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get '/deployments/cloud_run', to: 'deployments#cloud_run'
get '/deployments/cloud_storage', to: 'deployments#cloud_storage'
- get '/databases', to: 'databases#index'
+ resources :databases, only: [:index, :create, :new], path_names: { new: 'new/:product' }
end
resources :environments, except: [:destroy] do
diff --git a/data/whats_new/2022082200001_15_03.yml b/data/whats_new/2022082200001_15_03.yml
new file mode 100644
index 00000000000..34f6496c825
--- /dev/null
+++ b/data/whats_new/2022082200001_15_03.yml
@@ -0,0 +1,70 @@
+- name: "Create tasks in issues"
+ description: |
+ Tasks provide a robust way to refine an issue into smaller, discrete work units. Previously in GitLab, you could break down an issue into smaller parts using markdown checklists within the description. However, these checklist items could not be easily assigned, labeled, or managed anywhere outside of the description field.
+
+ You can now create tasks within issues from the Child Items widget. Then, you can open the task directly within the issue to quickly update the title, set the weight, or add a description. Tasks break down work within projects for GitLab Free and increase the planning hierarchy for our GitLab Premium customers to three levels (epic, issue, and task). In our next iteration, you will be able to add labels, milestones, and iterations to each task.
+
+ Tasks represent our first step toward evolving issues, epics, incidents, requirements, and test cases to [work items](https://docs.gitlab.com/ee/development/work_items.html). If you have feedback or suggestions about tasks, please comment on [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/363613).
+ stage: plan
+ self-managed: true
+ gitlab-com: true
+ available_in: [Free, Premium, Ultimate]
+ documentation_link: https://docs.gitlab.com/ee/user/tasks.html
+ image_url: https://about.gitlab.com/images/unreleased/create-tasks.gif
+ published_at: 2022-08-22
+ release: 15.3
+- name: "GitOps features are now free"
+ description: |
+ When you use GitOps to update a Kubernetes cluster, also called a pull-based deployment, you get an improved security model, better scalability and stability.
+
+ The GitLab agent for Kubernetes has supported [GitOps workflows](https://docs.gitlab.com/ee/user/clusters/agent/gitops.html) from its initial release, but until now, the functionality was available only if you had a GitLab Premium or Ultimate subscription. Now if you have a Free subscription, you also get pull-based deployment support. The features available in GitLab Free should serve small, high-trust teams or be suitable to test the agent before upgrading to a higher tier.
+
+ In the future, we plan to add [built-in multi-tenant support](https://gitlab.com/gitlab-org/gitlab/-/issues/337904) for Premium subscriptions. This feature would be similar to the impersonation feature already available for the [CI/CD workflow](https://docs.gitlab.com/ee/user/clusters/agent/ci_cd_workflow.html#restrict-project-and-group-access-by-using-impersonation).
+ stage: configure
+ self-managed: true
+ gitlab-com: true
+ available_in: [Free, Premium, Ultimate]
+ documentation_link: https://docs.gitlab.com/ee/user/clusters/agent/gitops.html
+ image_url: https://img.youtube.com/vi/jgVxOnMfOZA/hqdefault.jpg
+ published_at: 2022-08-22
+ release: 15.3
+- name: "Submit merge request review with summary comment"
+ description: |
+ When you finish reviewing a merge request, there are probably some common things that you do, like summarizing your review for others or approving the changes if they look good to you. Those common tasks are now quicker and easier: when you submit your review, you can add a summary comment along with any [quick actions](https://docs.gitlab.com/ee/user/project/quick_actions.html) like `/approve`.
+ stage: create
+ self-managed: true
+ gitlab-com: true
+ available_in: [Free, Premium, Ultimate]
+ documentation_link: https://docs.gitlab.com/ee/user/project/merge_requests/reviews/#submit-a-review
+ image_url: https://about.gitlab.com/images/unreleased/create-mr-review-summary.png
+ published_at: 2022-08-22
+ release: 15.3
+- name: "Define password complexity requirements"
+ description: |
+ GitLab administrators can now define password complexity requirements in addition to minimum password length. For new passwords, you can now require:
+
+ - Numbers.
+ - Uppercase letters.
+ - Lowercase letters.
+ - Symbols.
+
+ Complex passwords are less likely to be compromised, and the ability to configure password complexity requirements helps administrators enforce their password policies.
+ stage: manage
+ self-managed: true
+ gitlab-com: false
+ available_in: [Premium, Ultimate]
+ documentation_link: https://docs.gitlab.com/ee/user/admin_area/settings/sign_up_restrictions.html#password-complexity-requirements
+ image_url: https://about.gitlab.com/images/unreleased/manage-password-complexity-policy.png
+ published_at: 2022-08-22
+ release: 15.3
+- name: "Maintain SAML Group Links with API"
+ description: |
+ Until now, SAML group links had to be configured in the UI. Now, you can manage SAML group links programmatically using the API so you can automate SAML groups management.
+ stage: manage
+ self-managed: true
+ gitlab-com: true
+ available_in: [Premium, Ultimate]
+ documentation_link: https://docs.gitlab.com/ee/api/groups.html#saml-group-links
+ image_url: https://img.youtube.com/vi/Pft61UFM5LM/hqdefault.jpg
+ published_at: 2022-08-22
+ release: 15.3
diff --git a/db/migrate/20220803235114_add_auto_ban_user_to_application_settings.rb b/db/migrate/20220803235114_add_auto_ban_user_to_application_settings.rb
new file mode 100644
index 00000000000..3b57c340921
--- /dev/null
+++ b/db/migrate/20220803235114_add_auto_ban_user_to_application_settings.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+class AddAutoBanUserToApplicationSettings < Gitlab::Database::Migration[2.0]
+ def change
+ add_column :application_settings, :auto_ban_user_on_excessive_projects_download, :boolean,
+ default: false, null: false
+ end
+end
diff --git a/db/schema_migrations/20220803235114 b/db/schema_migrations/20220803235114
new file mode 100644
index 00000000000..469005b8bef
--- /dev/null
+++ b/db/schema_migrations/20220803235114
@@ -0,0 +1 @@
+a669aca9370ecd086b582164e68366ca459754b26e096301c2dc7121a7e9ab58 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index b9bfe44117d..ca1f9c1c061 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -11458,6 +11458,7 @@ CREATE TABLE application_settings (
error_tracking_access_token_encrypted text,
package_registry_cleanup_policies_worker_capacity integer DEFAULT 2 NOT NULL,
deactivate_dormant_users_period integer DEFAULT 90 NOT NULL,
+ auto_ban_user_on_excessive_projects_download boolean DEFAULT false NOT NULL,
CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
CONSTRAINT app_settings_container_registry_pre_import_tags_rate_positive CHECK ((container_registry_pre_import_tags_rate >= (0)::numeric)),
CONSTRAINT app_settings_dep_proxy_ttl_policies_worker_capacity_positive CHECK ((dependency_proxy_ttl_group_policy_worker_capacity >= 0)),
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 30cdd9fc188..698412c470d 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -6984,6 +6984,29 @@ The edge type for [`DependencyProxyManifest`](#dependencyproxymanifest).
| <a id="dependencyproxymanifestedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="dependencyproxymanifestedgenode"></a>`node` | [`DependencyProxyManifest`](#dependencyproxymanifest) | The item at the end of the edge. |
+#### `DeploymentConnection`
+
+The connection type for [`Deployment`](#deployment).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="deploymentconnectionedges"></a>`edges` | [`[DeploymentEdge]`](#deploymentedge) | A list of edges. |
+| <a id="deploymentconnectionnodes"></a>`nodes` | [`[Deployment]`](#deployment) | A list of nodes. |
+| <a id="deploymentconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
+
+#### `DeploymentEdge`
+
+The edge type for [`Deployment`](#deployment).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="deploymentedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
+| <a id="deploymentedgenode"></a>`node` | [`Deployment`](#deployment) | The item at the end of the edge. |
+
#### `DesignAtVersionConnection`
The connection type for [`DesignAtVersion`](#designatversion).
@@ -10975,6 +10998,42 @@ Group-level Dependency Proxy settings.
| ---- | ---- | ----------- |
| <a id="dependencyproxysettingenabled"></a>`enabled` | [`Boolean!`](#boolean) | Indicates whether the dependency proxy is enabled for the group. |
+### `Deployment`
+
+The deployment of an environment.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="deploymentcreatedat"></a>`createdAt` | [`Time`](#time) | When the deployment record was created. |
+| <a id="deploymentfinishedat"></a>`finishedAt` | [`Time`](#time) | When the deployment finished. |
+| <a id="deploymentid"></a>`id` | [`ID`](#id) | Global ID of the deployment. |
+| <a id="deploymentiid"></a>`iid` | [`ID`](#id) | Project-level internal ID of the deployment. |
+| <a id="deploymentref"></a>`ref` | [`String`](#string) | Git-Ref that the deployment ran on. |
+| <a id="deploymentsha"></a>`sha` | [`String`](#string) | Git-SHA that the deployment ran on. |
+| <a id="deploymentstatus"></a>`status` | [`DeploymentStatus`](#deploymentstatus) | Status of the deployment. |
+| <a id="deploymenttag"></a>`tag` | [`Boolean`](#boolean) | True or false if the deployment ran on a Git-tag. |
+| <a id="deploymentupdatedat"></a>`updatedAt` | [`Time`](#time) | When the deployment record was updated. |
+
+### `DeploymentDetails`
+
+The details of the deployment.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="deploymentdetailscreatedat"></a>`createdAt` | [`Time`](#time) | When the deployment record was created. |
+| <a id="deploymentdetailsfinishedat"></a>`finishedAt` | [`Time`](#time) | When the deployment finished. |
+| <a id="deploymentdetailsid"></a>`id` | [`ID`](#id) | Global ID of the deployment. |
+| <a id="deploymentdetailsiid"></a>`iid` | [`ID`](#id) | Project-level internal ID of the deployment. |
+| <a id="deploymentdetailsref"></a>`ref` | [`String`](#string) | Git-Ref that the deployment ran on. |
+| <a id="deploymentdetailssha"></a>`sha` | [`String`](#string) | Git-SHA that the deployment ran on. |
+| <a id="deploymentdetailsstatus"></a>`status` | [`DeploymentStatus`](#deploymentstatus) | Status of the deployment. |
+| <a id="deploymentdetailstag"></a>`tag` | [`Boolean`](#boolean) | True or false if the deployment ran on a Git-tag. |
+| <a id="deploymentdetailsupdatedat"></a>`updatedAt` | [`Time`](#time) | When the deployment record was updated. |
+
### `Design`
A single design.
@@ -11406,6 +11465,23 @@ Describes where code is deployed for a project.
#### Fields with arguments
+##### `Environment.deployments`
+
+Deployments of the environment.
+
+Returns [`DeploymentConnection`](#deploymentconnection).
+
+This field returns a [connection](#connections). It accepts the
+four standard [pagination arguments](#connection-pagination-arguments):
+`before: String`, `after: String`, `first: Int`, `last: Int`.
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="environmentdeploymentsorderby"></a>`orderBy` | [`DeploymentsOrderByInput`](#deploymentsorderbyinput) | Order by a specified field. |
+| <a id="environmentdeploymentsstatuses"></a>`statuses` | [`[DeploymentStatus!]`](#deploymentstatus) | Statuses of the deployments. |
+
##### `Environment.metricsDashboard`
Metrics dashboard schema for the environment.
@@ -15838,6 +15914,18 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="projectdastsitevalidationsnormalizedtargeturls"></a>`normalizedTargetUrls` | [`[String!]`](#string) | Normalized URL of the target to be scanned. |
| <a id="projectdastsitevalidationsstatus"></a>`status` | [`DastSiteValidationStatusEnum`](#dastsitevalidationstatusenum) | Status of the site validation. |
+##### `Project.deployment`
+
+Details of the deployment of the project.
+
+Returns [`DeploymentDetails`](#deploymentdetails).
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="projectdeploymentiid"></a>`iid` | [`ID!`](#id) | Project-level internal ID of the Deployment. |
+
##### `Project.environment`
A single environment of the project.
@@ -19605,6 +19693,20 @@ Weight of the data visualization palette.
| <a id="dependencyproxymanifeststatuspending_destruction"></a>`PENDING_DESTRUCTION` | Dependency proxy manifest has a status of pending_destruction. |
| <a id="dependencyproxymanifeststatusprocessing"></a>`PROCESSING` | Dependency proxy manifest has a status of processing. |
+### `DeploymentStatus`
+
+All deployment statuses.
+
+| Value | Description |
+| ----- | ----------- |
+| <a id="deploymentstatusblocked"></a>`BLOCKED` | A deployment that is blocked. |
+| <a id="deploymentstatuscanceled"></a>`CANCELED` | A deployment that is canceled. |
+| <a id="deploymentstatuscreated"></a>`CREATED` | A deployment that is created. |
+| <a id="deploymentstatusfailed"></a>`FAILED` | A deployment that is failed. |
+| <a id="deploymentstatusrunning"></a>`RUNNING` | A deployment that is running. |
+| <a id="deploymentstatusskipped"></a>`SKIPPED` | A deployment that is skipped. |
+| <a id="deploymentstatussuccess"></a>`SUCCESS` | A deployment that is success. |
+
### `DeploymentTier`
All environment deployment tiers.
@@ -20621,6 +20723,15 @@ Common sort values.
| <a id="sortupdated_asc"></a>`updated_asc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `UPDATED_ASC`. |
| <a id="sortupdated_desc"></a>`updated_desc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `UPDATED_DESC`. |
+### `SortDirectionEnum`
+
+Values for sort direction.
+
+| Value | Description |
+| ----- | ----------- |
+| <a id="sortdirectionenumasc"></a>`ASC` | Ascending order. |
+| <a id="sortdirectionenumdesc"></a>`DESC` | Descending order. |
+
### `TestCaseStatus`
| Value | Description |
@@ -22375,6 +22486,17 @@ Input type for DastSiteProfile authentication.
| <a id="dastsiteprofileauthinputusername"></a>`username` | [`String`](#string) | Username to authenticate with on the target. |
| <a id="dastsiteprofileauthinputusernamefield"></a>`usernameField` | [`String`](#string) | Name of username field at the sign-in HTML form. |
+### `DeploymentsOrderByInput`
+
+Values for ordering deployments by a specific field.
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="deploymentsorderbyinputcreatedat"></a>`createdAt` | [`SortDirectionEnum`](#sortdirectionenum) | Order by Created time. |
+| <a id="deploymentsorderbyinputfinishedat"></a>`finishedAt` | [`SortDirectionEnum`](#sortdirectionenum) | Order by Finished time. |
+
### `DiffImagePositionInput`
#### Arguments
diff --git a/doc/api/jobs.md b/doc/api/jobs.md
index 647f8eafa62..70838d6642e 100644
--- a/doc/api/jobs.md
+++ b/doc/api/jobs.md
@@ -816,7 +816,7 @@ You can't delete archived jobs with the API, but you can
## Run a job
-Triggers a manual action to start a job.
+For a job in manual status, trigger an action to start the job.
```plaintext
POST /projects/:id/jobs/:job_id/play
diff --git a/doc/api/settings.md b/doc/api/settings.md
index bb799d3d2c3..5e1be5bce51 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -385,6 +385,7 @@ listed in the descriptions of the relevant settings.
| `max_number_of_repository_downloads` **(ULTIMATE SELF)** | integer | no | Maximum number of unique repositories a user can download in the specified time period before they are banned. Default: 0, Maximum: 10,000 repositories. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/87980) in GitLab 15.1. |
| `max_number_of_repository_downloads_within_time_period` **(ULTIMATE SELF)** | integer | no | Reporting time period (in seconds). Default: 0, Maximum: 864000 seconds (10 days). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/87980) in GitLab 15.1. |
| `git_rate_limit_users_allowlist` **(ULTIMATE SELF)** | array of strings | no | List of usernames excluded from Git anti-abuse rate limits. Default: `[]`, Maximum: 100 usernames. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/90815) in GitLab 15.2. |
+| `auto_ban_user_on_excessive_projects_download` **(ULTIMATE SELF)** | boolean | no | When enabled, users will get automatically banned from the application when they download more than the maximum number of unique projects in the time period specified by `max_number_of_repository_downloads` and `max_number_of_repository_downloads_within_time_period` respectively. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/94153) in GitLab 15.4 |
| `mirror_available` | boolean | no | Allow repository mirroring to configured by project Maintainers. If disabled, only Administrators can configure repository mirroring. |
| `mirror_capacity_threshold` **(PREMIUM)** | integer | no | Minimum capacity to be available before scheduling more mirrors preemptively. |
| `mirror_max_capacity` **(PREMIUM)** | integer | no | Maximum number of mirrors that can be synchronizing at the same time. |
diff --git a/doc/ci/pipelines/downstream_pipelines.md b/doc/ci/pipelines/downstream_pipelines.md
new file mode 100644
index 00000000000..224b171625f
--- /dev/null
+++ b/doc/ci/pipelines/downstream_pipelines.md
@@ -0,0 +1,65 @@
+---
+stage: Verify
+group: Pipeline Authoring
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
+---
+
+# Downstream pipelines **(FREE)**
+
+A downstream pipeline is any GitLab CI/CD pipeline triggered by another pipeline.
+A downstream pipeline can be either:
+
+- A [parent-child pipeline](parent_child_pipelines.md), which is a downstream pipeline triggered
+ in the same project as the first pipeline.
+- A [multi-project pipeline](multi_project_pipelines.md), which is a downstream pipeline triggered
+ in a different project than the first pipeline.
+
+Parent-child pipelines and multi-project pipelines can sometimes be used for similar purposes,
+but there are some key differences.
+
+Parent-child pipelines:
+
+- Run under the same project, ref, and commit SHA as the parent pipeline.
+- Affect the overall status of the ref the pipeline runs against. For example,
+ if a pipeline fails for the main branch, it's common to say that "main is broken".
+ The status of child pipelines don't directly affect the status of the ref, unless the child
+ pipeline is triggered with [`strategy:depend`](../yaml/index.md#triggerstrategy).
+- Are automatically canceled if the pipeline is configured with [`interruptible`](../yaml/index.md#interruptible)
+ when a new pipeline is created for the same ref.
+- Display only the parent pipelines in the pipeline index page. Child pipelines are
+ visible when visiting their parent pipeline's page.
+- Are limited to 2 levels of nesting. A parent pipeline can trigger multiple child pipelines,
+ and those child pipeline can trigger multiple child pipelines (`A -> B -> C`).
+
+Multi-project pipelines:
+
+- Are triggered from another pipeline, but the upstream (triggering) pipeline does
+ not have much control over the downstream (triggered) pipeline. However, it can
+ choose the ref of the downstream pipeline, and pass CI/CD variables to it.
+- Affect the overall status of the ref of the project it runs in, but does not
+ affect the status of the triggering pipeline's ref, unless it was triggered with
+ [`strategy:depend`](../yaml/index.md#triggerstrategy).
+- Are not automatically canceled in the downstream project when using [`interruptible`](../yaml/index.md#interruptible)
+ if a new pipeline runs for the same ref in the upstream pipeline. They can be
+ automatically canceled if a new pipeline is triggered for the same ref on the downstream project.
+- Multi-project pipelines are standalone pipelines because they are normal pipelines
+ that happened to be triggered by an external project. They are all visible on the pipeline index page.
+- Are independent, so there are no nesting limits.
+
+## View a downstream pipeline
+
+In the [pipeline graph view](index.md#view-full-pipeline-graph), downstream pipelines display
+as a list of cards on the right of the graph.
+
+### Cancel or retry downstream pipelines from the graph view
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/354974) in GitLab 15.0 [with a flag](../../administration/feature_flags.md) named `downstream_retry_action`. Disabled by default.
+> - [Generally available and feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/357406) in GitLab 15.1.
+
+To cancel a downstream pipeline that is still running, select **Cancel** (**{cancel}**)
+on the pipeline's card.
+
+To retry a failed downstream pipeline, select **Retry** (**{retry}**)
+on the pipeline's card.
+
+![downstream pipeline actions](img/downstream_pipeline_actions.png)
diff --git a/doc/ci/pipelines/index.md b/doc/ci/pipelines/index.md
index 7e0734d5319..c76ee74f5f8 100644
--- a/doc/ci/pipelines/index.md
+++ b/doc/ci/pipelines/index.md
@@ -62,40 +62,6 @@ Pipelines can be configured in many different ways:
run in the same project and with the same SHA. This pipeline architecture is commonly used for mono-repos.
- [Multi-project pipelines](multi_project_pipelines.md) combine pipelines for different projects together.
-### How parent-child pipelines compare to multi-project pipelines
-
-Parent-child pipelines and multi-project pipelines can sometimes be used for similar
-purposes, but there are some key differences:
-
-Parent-child pipelines:
-
-- Run under the same project, ref, and commit SHA as the parent pipeline.
-- Affect the overall status of the ref the pipeline runs against. For example,
- if a pipeline fails for the main branch, it's common to say that "main is broken".
- The status of child pipelines don't directly affect the status of the ref, unless the child
- pipeline is triggered with [`strategy:depend`](../yaml/index.md#triggerstrategy).
-- Are automatically canceled if the pipeline is configured with [`interruptible`](../yaml/index.md#interruptible)
- when a new pipeline is created for the same ref.
-- Display only the parent pipelines in the pipeline index page. Child pipelines are
- visible when visiting their parent pipeline's page.
-- Are limited to 2 levels of nesting. A parent pipeline can trigger multiple child pipelines,
- and those child pipeline can trigger multiple child pipelines (`A -> B -> C`).
-
-Multi-project pipelines:
-
-- Are triggered from another pipeline, but the upstream (triggering) pipeline does
- not have much control over the downstream (triggered) pipeline. However, it can
- choose the ref of the downstream pipeline, and pass CI/CD variables to it.
-- Affect the overall status of the ref of the project it runs in, but does not
- affect the status of the triggering pipeline's ref, unless it was triggered with
- [`strategy:depend`](../yaml/index.md#triggerstrategy).
-- Are not automatically canceled in the downstream project when using [`interruptible`](../yaml/index.md#interruptible)
- if a new pipeline runs for the same ref in the upstream pipeline. They can be
- automatically canceled if a new pipeline is triggered for the same ref on the downstream project.
-- Multi-project pipelines are standalone pipelines because they are normal pipelines
- that happened to be triggered by an external project. They are all visible on the pipeline index page.
-- Are independent, so there are no nesting limits.
-
## Configure a pipeline
Pipelines and their component jobs and stages are defined in the CI/CD pipeline configuration file for each project.
@@ -456,25 +422,6 @@ Pipeline analytics are available on the [**CI/CD Analytics** page](../../user/an
Pipeline status and test coverage report badges are available and configurable for each project.
For information on adding pipeline badges to projects, see [Pipeline badges](settings.md#pipeline-badges).
-### Downstream pipelines
-
-In the pipeline graph view, downstream pipelines ([Multi-project pipelines](multi_project_pipelines.md)
-and [Parent-child pipelines](parent_child_pipelines.md)) display as a list of cards
-on the right of the graph.
-
-#### Cancel or retry downstream pipelines from the graph view
-
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/354974) in GitLab 15.0 [with a flag](../../administration/feature_flags.md) named `downstream_retry_action`. Disabled by default.
-> - [Generally available and feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/357406) in GitLab 15.1.
-
-To cancel a downstream pipeline that is still running, select **Cancel** (**{cancel}**)
-on the pipeline's card.
-
-To retry a failed downstream pipeline, select **Retry** (**{retry}**)
-on the pipeline's card.
-
-![downstream pipeline actions](img/downstream_pipeline_actions.png)
-
## Pipelines API
GitLab provides API endpoints to:
diff --git a/doc/ci/pipelines/multi_project_pipelines.md b/doc/ci/pipelines/multi_project_pipelines.md
index a71af78f410..e7d5e3e102a 100644
--- a/doc/ci/pipelines/multi_project_pipelines.md
+++ b/doc/ci/pipelines/multi_project_pipelines.md
@@ -415,5 +415,5 @@ displays to the right of the mini graph.
If you have permission to trigger pipelines in the downstream project, you can
retry or cancel multi-project pipelines:
-- [In the main graph view](index.md#downstream-pipelines).
+- [In the main graph view](downstream_pipelines.md#view-a-downstream-pipeline).
- From the downstream pipeline's details page.
diff --git a/doc/ci/pipelines/parent_child_pipelines.md b/doc/ci/pipelines/parent_child_pipelines.md
index 3fd739087ec..2d36aaac1f7 100644
--- a/doc/ci/pipelines/parent_child_pipelines.md
+++ b/doc/ci/pipelines/parent_child_pipelines.md
@@ -224,5 +224,5 @@ multi-project pipelines:
You can retry or cancel child pipelines:
-- [In the main graph view](index.md#downstream-pipelines).
+- [In the main graph view](downstream_pipelines.md#view-a-downstream-pipeline).
- In the child pipeline's details page.
diff --git a/doc/user/clusters/management_project_template.md b/doc/user/clusters/management_project_template.md
index 4b00784a7ae..1d4cd401242 100644
--- a/doc/user/clusters/management_project_template.md
+++ b/doc/user/clusters/management_project_template.md
@@ -89,7 +89,7 @@ the paths for the apps that you would like to use in your cluster.
By default, each `helmfile.yaml` in these sub-paths has the attribute `installed: true`. This means that every time
the pipeline runs, Helmfile tries to either install or update your apps according to the current state of your
cluster and Helm releases. If you change this attribute to `installed: false`, Helmfile tries try to uninstall this app
-from your cluster. [Read more](https://github.com/roboll/helmfile) about how Helmfile works.
+from your cluster. [Read more](https://helmfile.readthedocs.io/en/latest/) about how Helmfile works.
### Built-in applications
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index c98e0d3e65d..86bc5403d10 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8392,6 +8392,12 @@ msgstr ""
msgid "Cloud Run"
msgstr ""
+msgid "Cloud SQL instances are fully managed, relational MySQL databases. Google handles replication, patch management, and database management to ensure availability and performance."
+msgstr ""
+
+msgid "Cloud SQL instances are fully managed, relational SQL Server databases. Google handles replication, patch management, and database management to ensure availability and performance."
+msgstr ""
+
msgid "Cloud Storage"
msgstr ""
@@ -8419,12 +8425,24 @@ msgstr ""
msgid "CloudSeed|Cloud SQL for SQL Server"
msgstr ""
+msgid "CloudSeed|Cloud SQL instance creation request successful. Expected resolution time is ~5 minutes."
+msgstr ""
+
+msgid "CloudSeed|Cloud SQL instances are fully managed, relational PostgreSQL databases. Google handles replication, patch management, and database management to ensure availability and performance."
+msgstr ""
+
msgid "CloudSeed|CloudSQL Instance"
msgstr ""
msgid "CloudSeed|Configuration"
msgstr ""
+msgid "CloudSeed|Create MySQL Instance"
+msgstr ""
+
+msgid "CloudSeed|Create Postgres Instance"
+msgstr ""
+
msgid "CloudSeed|Create cluster"
msgstr ""
@@ -8479,6 +8497,9 @@ msgstr ""
msgid "CloudSeed|Generated database instance is linked to the selected branch or tag"
msgstr ""
+msgid "CloudSeed|Google Cloud Error - %{message}"
+msgstr ""
+
msgid "CloudSeed|Google Cloud Project"
msgstr ""
@@ -40354,6 +40375,9 @@ msgstr ""
msgid "This project is not subscribed to any project pipelines."
msgstr ""
+msgid "This project is public. Non-members can guess the Service Desk email address, because it contains the group and project name. %{linkStart}How do I create a custom email address?%{linkEnd}"
+msgstr ""
+
msgid "This project manages its dependencies using %{strong_start}%{manager_name}%{strong_end}"
msgstr ""
diff --git a/package.json b/package.json
index fd9818f267f..ed78775ed3c 100644
--- a/package.json
+++ b/package.json
@@ -150,7 +150,6 @@
"popper.js": "^1.16.1",
"portal-vue": "^2.1.7",
"postcss": "8.4.14",
- "prismjs": "^1.21.0",
"prosemirror-markdown": "1.9.1",
"prosemirror-model": "^1.18.1",
"prosemirror-state": "^1.4.1",
@@ -178,13 +177,13 @@
"url-loader": "^4.1.1",
"uuid": "8.1.0",
"visibilityjs": "^1.2.4",
- "vue": "^2.7.8",
+ "vue": "^2.7.9",
"vue-apollo": "^3.0.7",
"vue-loader": "^15.10",
"vue-observe-visibility": "^1.0.0",
"vue-resize": "^1.0.1",
"vue-router": "3.4.9",
- "vue-template-compiler": "^2.7.8",
+ "vue-template-compiler": "^2.7.9",
"vue-virtual-scroll-list": "^1.4.7",
"vuedraggable": "^2.23.0",
"vuex": "^3.6.2",
diff --git a/spec/finders/deployments_finder_spec.rb b/spec/finders/deployments_finder_spec.rb
index 51c293bcfd1..efb739c3d2f 100644
--- a/spec/finders/deployments_finder_spec.rb
+++ b/spec/finders/deployments_finder_spec.rb
@@ -32,7 +32,17 @@ RSpec.describe DeploymentsFinder do
it 'raises an error' do
expect { subject }.to raise_error(
described_class::InefficientQueryError,
- '`finished_at` filter and `finished_at` sorting must be paired')
+ '`finished_at` filter requires `finished_at` sort.')
+ end
+ end
+
+ context 'when running status filter and finished_at sorting' do
+ let(:params) { { status: :running, order_by: :finished_at } }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(
+ described_class::InefficientQueryError,
+ '`finished_at` sort requires `finished_at` filter or a filter with at least one of the finished statuses.')
end
end
@@ -52,7 +62,17 @@ RSpec.describe DeploymentsFinder do
it 'raises an error' do
expect { subject }.to raise_error(
described_class::InefficientQueryError,
- '`environment` filter must be combined with `project` scope.')
+ '`environment` name filter must be combined with `project` scope.')
+ end
+ end
+
+ context 'when status filter with mixed finished and upcoming statuses' do
+ let(:params) { { status: [:success, :running] } }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(
+ described_class::InefficientQueryError,
+ 'finished statuses and upcoming statuses must be separately queried.')
end
end
end
@@ -103,6 +123,24 @@ RSpec.describe DeploymentsFinder do
end
end
+ context 'when the environment ID is specified' do
+ let!(:environment1) { create(:environment, project: project) }
+ let!(:environment2) { create(:environment, project: project) }
+ let!(:deployment1) do
+ create(:deployment, project: project, environment: environment1)
+ end
+
+ let!(:deployment2) do
+ create(:deployment, project: project, environment: environment2)
+ end
+
+ let(:params) { { environment: environment1.id } }
+
+ it 'returns deployments for the given environment' do
+ is_expected.to match_array([deployment1])
+ end
+ end
+
context 'when the deployment status is specified' do
let!(:deployment1) { create(:deployment, :success, project: project) }
let!(:deployment2) { create(:deployment, :failed, project: project) }
diff --git a/spec/frontend/google_cloud/databases/panel_spec.js b/spec/frontend/google_cloud/databases/panel_spec.js
index 490c0136651..e6a0d74f348 100644
--- a/spec/frontend/google_cloud/databases/panel_spec.js
+++ b/spec/frontend/google_cloud/databases/panel_spec.js
@@ -2,6 +2,8 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import Panel from '~/google_cloud/databases/panel.vue';
import IncubationBanner from '~/google_cloud/components/incubation_banner.vue';
import GoogleCloudMenu from '~/google_cloud/components/google_cloud_menu.vue';
+import ServiceTable from '~/google_cloud/databases/service_table.vue';
+import InstanceTable from '~/google_cloud/databases/cloudsql/instance_table.vue';
describe('google_cloud/databases/panel', () => {
let wrapper;
@@ -10,6 +12,11 @@ describe('google_cloud/databases/panel', () => {
configurationUrl: 'configuration-url',
deploymentsUrl: 'deployments-url',
databasesUrl: 'databases-url',
+ cloudsqlPostgresUrl: 'cloudsql-postgres-url',
+ cloudsqlMysqlUrl: 'cloudsql-mysql-url',
+ cloudsqlSqlserverUrl: 'cloudsql-sqlserver-url',
+ cloudsqlInstances: [],
+ emptyIllustrationUrl: 'empty-illustration-url',
};
beforeEach(() => {
@@ -33,4 +40,14 @@ describe('google_cloud/databases/panel', () => {
expect(target.props('deploymentsUrl')).toBe(props.deploymentsUrl);
expect(target.props('databasesUrl')).toBe(props.databasesUrl);
});
+
+ it('contains Databases service table', () => {
+ const target = wrapper.findComponent(ServiceTable);
+ expect(target.exists()).toBe(true);
+ });
+
+ it('contains CloudSQL instance table', () => {
+ const target = wrapper.findComponent(InstanceTable);
+ expect(target.exists()).toBe(true);
+ });
});
diff --git a/spec/frontend/notebook/index_spec.js b/spec/frontend/notebook/index_spec.js
index 475c41a72f6..b79000a3505 100644
--- a/spec/frontend/notebook/index_spec.js
+++ b/spec/frontend/notebook/index_spec.js
@@ -11,7 +11,7 @@ describe('Notebook component', () => {
function buildComponent(notebook) {
return mount(Component, {
- propsData: { notebook, codeCssClass: 'js-code-class' },
+ propsData: { notebook },
provide: { relativeRawPath: '' },
}).vm;
}
@@ -46,10 +46,6 @@ describe('Notebook component', () => {
it('renders code cell', () => {
expect(vm.$el.querySelector('pre')).not.toBeNull();
});
-
- it('add code class to code blocks', () => {
- expect(vm.$el.querySelector('.js-code-class')).not.toBeNull();
- });
});
describe('with worksheets', () => {
@@ -72,9 +68,5 @@ describe('Notebook component', () => {
it('renders code cell', () => {
expect(vm.$el.querySelector('pre')).not.toBeNull();
});
-
- it('add code class to code blocks', () => {
- expect(vm.$el.querySelector('.js-code-class')).not.toBeNull();
- });
});
});
diff --git a/spec/frontend/notebook/lib/highlight_spec.js b/spec/frontend/notebook/lib/highlight_spec.js
deleted file mode 100644
index 944ccd6aa9f..00000000000
--- a/spec/frontend/notebook/lib/highlight_spec.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import Prism from '~/notebook/lib/highlight';
-
-describe('Highlight library', () => {
- it('imports python language', () => {
- expect(Prism.languages.python).toBeDefined();
- });
-
- it('uses custom CSS classes', () => {
- const el = document.createElement('div');
- el.innerHTML = Prism.highlight('console.log("a");', Prism.languages.javascript);
-
- expect(el.querySelector('.string')).not.toBeNull();
- expect(el.querySelector('.function')).not.toBeNull();
- });
-});
diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
index 62224612387..763d54be753 100644
--- a/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
+++ b/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
@@ -1,4 +1,4 @@
-import { GlAlert } from '@gitlab/ui';
+import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
@@ -23,11 +23,16 @@ describe('ServiceDeskRoot', () => {
selectedTemplate: 'Bug',
selectedFileTemplateProjectId: 42,
templates: ['Bug', 'Documentation'],
+ publicProject: false,
};
const getAlertText = () => wrapper.find(GlAlert).text();
- const createComponent = () => shallowMount(ServiceDeskRoot, { provide: provideData });
+ const createComponent = (customInject = {}) =>
+ shallowMount(ServiceDeskRoot, {
+ provide: { ...provideData, ...customInject },
+ stubs: { GlSprintf },
+ });
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
@@ -60,6 +65,25 @@ describe('ServiceDeskRoot', () => {
});
});
+ it('shows alert about email inference when current project is public', () => {
+ wrapper = createComponent({
+ publicProject: true,
+ });
+
+ const alertEl = wrapper.find('[data-testid="public-project-alert"]');
+ expect(alertEl.exists()).toBe(true);
+ expect(alertEl.text()).toContain(
+ 'This project is public. Non-members can guess the Service Desk email address, because it contains the group and project name.',
+ );
+
+ const alertBodyLink = alertEl.findComponent(GlLink);
+ expect(alertBodyLink.exists()).toBe(true);
+ expect(alertBodyLink.attributes('href')).toBe(
+ '/help/user/project/service_desk.html#using-a-custom-email-address',
+ );
+ expect(alertBodyLink.text()).toBe('How do I create a custom email address?');
+ });
+
describe('toggle event', () => {
describe('when toggling service desk on', () => {
beforeEach(async () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
index f6af7b31051..b5721b6644d 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
@@ -197,5 +197,43 @@ describe('MR Widget', () => {
expect(findToggleButton().exists()).toBe(false);
});
+
+ it('fetches expanded data when clicked for the first time', async () => {
+ const mockDataCollapsed = {
+ headers: {},
+ status: 200,
+ data: { vulnerabilities: [{ vuln: 1 }] },
+ };
+
+ const mockDataExpanded = {
+ headers: {},
+ status: 200,
+ data: { vulnerabilities: [{ vuln: 2 }] },
+ };
+
+ createComponent({
+ propsData: {
+ isCollapsible: true,
+ fetchCollapsedData: () => Promise.resolve(mockDataCollapsed),
+ fetchExpandedData: () => Promise.resolve(mockDataExpanded),
+ },
+ });
+
+ findToggleButton().vm.$emit('click');
+
+ await waitForPromises();
+
+ // First fetches the collapsed data
+ expect(wrapper.emitted('input')[0][0]).toEqual({
+ collapsed: mockDataCollapsed.data,
+ expanded: null,
+ });
+
+ // Then fetches the expanded data
+ expect(wrapper.emitted('input')[1][0]).toEqual({
+ collapsed: null,
+ expanded: mockDataExpanded.data,
+ });
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/__snapshots__/code_block_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/code_block_spec.js.snap
deleted file mode 100644
index 7f655d67ae8..00000000000
--- a/spec/frontend/vue_shared/components/__snapshots__/code_block_spec.js.snap
+++ /dev/null
@@ -1,26 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Code Block with default props renders correctly 1`] = `
-<pre
- class="code-block rounded code"
->
- <code
- class="d-block"
- >
- test-code
- </code>
-</pre>
-`;
-
-exports[`Code Block with maxHeight set to "200px" renders correctly 1`] = `
-<pre
- class="code-block rounded code"
- style="max-height: 200px; overflow-y: auto;"
->
- <code
- class="d-block"
- >
- test-code
- </code>
-</pre>
-`;
diff --git a/spec/frontend/vue_shared/components/code_block_highlighted_spec.js b/spec/frontend/vue_shared/components/code_block_highlighted_spec.js
new file mode 100644
index 00000000000..181692e61b5
--- /dev/null
+++ b/spec/frontend/vue_shared/components/code_block_highlighted_spec.js
@@ -0,0 +1,65 @@
+import { shallowMount } from '@vue/test-utils';
+import CodeBlock from '~/vue_shared/components/code_block_highlighted.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+
+describe('Code Block Highlighted', () => {
+ let wrapper;
+
+ const code = 'const foo = 1;';
+
+ const createComponent = (propsData = {}) => {
+ wrapper = shallowMount(CodeBlock, { propsData });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders highlighted code if language is supported', async () => {
+ createComponent({ code, language: 'javascript' });
+
+ await waitForPromises();
+
+ expect(wrapper.element).toMatchInlineSnapshot(`
+ <code-block-stub
+ class="highlight"
+ code=""
+ maxheight="initial"
+ >
+ <span>
+ <span
+ class="hljs-keyword"
+ >
+ const
+ </span>
+ foo =
+ <span
+ class="hljs-number"
+ >
+ 1
+ </span>
+ ;
+ </span>
+ </code-block-stub>
+ `);
+ });
+
+ it("renders plain text if language isn't supported", async () => {
+ createComponent({ code, language: 'foobar' });
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[expect.any(TypeError)]]);
+
+ expect(wrapper.element).toMatchInlineSnapshot(`
+ <code-block-stub
+ class="highlight"
+ code=""
+ maxheight="initial"
+ >
+ <span>
+ const foo = 1;
+ </span>
+ </code-block-stub>
+ `);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/code_block_spec.js b/spec/frontend/vue_shared/components/code_block_spec.js
index 60b0b0b566b..9a4dbcc47ff 100644
--- a/spec/frontend/vue_shared/components/code_block_spec.js
+++ b/spec/frontend/vue_shared/components/code_block_spec.js
@@ -4,41 +4,77 @@ import CodeBlock from '~/vue_shared/components/code_block.vue';
describe('Code Block', () => {
let wrapper;
- const defaultProps = {
- code: 'test-code',
- };
+ const code = 'test-code';
- const createComponent = (props = {}) => {
+ const createComponent = (propsData, slots = {}) => {
wrapper = shallowMount(CodeBlock, {
- propsData: {
- ...defaultProps,
- ...props,
- },
+ slots,
+ propsData,
});
};
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
- describe('with default props', () => {
- beforeEach(() => {
- createComponent();
- });
+ it('overwrites the default slot', () => {
+ createComponent({}, { default: 'DEFAULT SLOT' });
- it('renders correctly', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
+ expect(wrapper.element).toMatchInlineSnapshot(`
+ <pre
+ class="code-block rounded code"
+ >
+ DEFAULT SLOT
+ </pre>
+ `);
});
- describe('with maxHeight set to "200px"', () => {
- beforeEach(() => {
- createComponent({ maxHeight: '200px' });
- });
+ it('renders with empty code prop', () => {
+ createComponent({});
- it('renders correctly', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
+ expect(wrapper.element).toMatchInlineSnapshot(`
+ <pre
+ class="code-block rounded code"
+ >
+ <code
+ class="d-block"
+ >
+
+ </code>
+ </pre>
+ `);
+ });
+
+ it('renders code prop when provided', () => {
+ createComponent({ code });
+
+ expect(wrapper.element).toMatchInlineSnapshot(`
+ <pre
+ class="code-block rounded code"
+ >
+ <code
+ class="d-block"
+ >
+ test-code
+ </code>
+ </pre>
+ `);
+ });
+
+ it('sets maxHeight properly when provided', () => {
+ createComponent({ code, maxHeight: '200px' });
+
+ expect(wrapper.element).toMatchInlineSnapshot(`
+ <pre
+ class="code-block rounded code"
+ style="max-height: 200px; overflow-y: auto;"
+ >
+ <code
+ class="d-block"
+ >
+ test-code
+ </code>
+ </pre>
+ `);
});
});
diff --git a/spec/graphql/resolvers/deployment_resolver_spec.rb b/spec/graphql/resolvers/deployment_resolver_spec.rb
new file mode 100644
index 00000000000..9231edefddc
--- /dev/null
+++ b/spec/graphql/resolvers/deployment_resolver_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::DeploymentResolver do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :repository, :private) }
+ let_it_be(:environment) { create(:environment, project: project) }
+ let_it_be(:deployment) { create(:deployment, :created, environment: environment, project: project) }
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+
+ let(:current_user) { developer }
+
+ describe '#resolve' do
+ it 'finds the deployment' do
+ expect(resolve_deployments(iid: deployment.iid)).to contain_exactly(deployment)
+ end
+
+ it 'does not find the deployment if the IID does not match' do
+ expect(resolve_deployments(iid: non_existing_record_id)).to be_empty
+ end
+ end
+
+ def resolve_deployments(args = {}, context = { current_user: current_user })
+ resolve(described_class, obj: project, args: args, ctx: context)
+ end
+end
diff --git a/spec/graphql/resolvers/deployments_resolver_spec.rb b/spec/graphql/resolvers/deployments_resolver_spec.rb
new file mode 100644
index 00000000000..4e5564aad0b
--- /dev/null
+++ b/spec/graphql/resolvers/deployments_resolver_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::DeploymentsResolver do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :repository, :private) }
+ let_it_be(:environment) { create(:environment, project: project) }
+ let_it_be(:deployment) { create(:deployment, :created, environment: environment, project: project) }
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+
+ let(:current_user) { developer }
+
+ describe '#resolve' do
+ it 'finds the deployment' do
+ expect(resolve_deployments).to contain_exactly(deployment)
+ end
+
+ it 'finds the deployment when status matches' do
+ expect(resolve_deployments(statuses: [:created])).to contain_exactly(deployment)
+ end
+
+ it 'does not find the deployment when status does not match' do
+ expect(resolve_deployments(statuses: [:success])).to be_empty
+ end
+
+ it 'transforms order_by for finder' do
+ expect(DeploymentsFinder)
+ .to receive(:new)
+ .with(environment: environment.id, status: ['success'], order_by: 'finished_at', sort: 'asc')
+ .and_call_original
+
+ resolve_deployments(statuses: [:success], order_by: { finished_at: :asc })
+ end
+ end
+
+ def resolve_deployments(args = {}, context = { current_user: current_user })
+ resolve(described_class, obj: environment, args: args, ctx: context)
+ end
+end
diff --git a/spec/graphql/types/deployment_details_type_spec.rb b/spec/graphql/types/deployment_details_type_spec.rb
new file mode 100644
index 00000000000..58756798ffb
--- /dev/null
+++ b/spec/graphql/types/deployment_details_type_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['DeploymentDetails'] do
+ specify { expect(described_class.graphql_name).to eq('DeploymentDetails') }
+
+ it 'has the expected fields' do
+ expected_fields = %w[
+ id iid ref tag sha created_at updated_at finished_at status
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+
+ specify { expect(described_class).to require_graphql_authorizations(:read_deployment) }
+end
diff --git a/spec/graphql/types/deployment_type_spec.rb b/spec/graphql/types/deployment_type_spec.rb
new file mode 100644
index 00000000000..21e445c24b2
--- /dev/null
+++ b/spec/graphql/types/deployment_type_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['Deployment'] do
+ specify { expect(described_class.graphql_name).to eq('Deployment') }
+
+ it 'has the expected fields' do
+ expected_fields = %w[
+ id iid ref tag sha created_at updated_at finished_at status
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+
+ specify { expect(described_class).to require_graphql_authorizations(:read_deployment) }
+end
diff --git a/spec/graphql/types/environment_type_spec.rb b/spec/graphql/types/environment_type_spec.rb
index 3671d35e8a5..16859394978 100644
--- a/spec/graphql/types/environment_type_spec.rb
+++ b/spec/graphql/types/environment_type_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe GitlabSchema.types['Environment'] do
it 'has the expected fields' do
expected_fields = %w[
- name id state metrics_dashboard latest_opened_most_severe_alert path
+ name id state metrics_dashboard latest_opened_most_severe_alert path deployments
]
expect(described_class).to have_graphql_fields(*expected_fields)
diff --git a/spec/helpers/projects/google_cloud/cloudsql_helper_spec.rb b/spec/helpers/projects/google_cloud/cloudsql_helper_spec.rb
new file mode 100644
index 00000000000..6b82518592f
--- /dev/null
+++ b/spec/helpers/projects/google_cloud/cloudsql_helper_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::GoogleCloud::CloudsqlHelper do
+ describe '#TIERS' do
+ it 'is an array' do
+ expect(described_class::TIERS).to be_an_instance_of(Array)
+ end
+ end
+
+ describe '#VERSIONS' do
+ it 'returns versions for :postgres' do
+ expect(described_class::VERSIONS[:postgres]).to be_an_instance_of(Array)
+ end
+
+ it 'returns versions for :mysql' do
+ expect(described_class::VERSIONS[:mysql]).to be_an_instance_of(Array)
+ end
+
+ it 'returns versions for :sqlserver' do
+ expect(described_class::VERSIONS[:sqlserver]).to be_an_instance_of(Array)
+ end
+ end
+end
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index 0a4ee73f3d3..c5ce18739aa 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -74,6 +74,27 @@ RSpec.describe Deployment do
end
end
+ describe '.for_iid' do
+ subject { described_class.for_iid(project, iid) }
+
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:deployment) { create(:deployment, project: project) }
+
+ let(:iid) { deployment.iid }
+
+ it 'finds the deployment' do
+ is_expected.to contain_exactly(deployment)
+ end
+
+ context 'when iid does not match' do
+ let(:iid) { non_existing_record_id }
+
+ it 'does not find the deployment' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
describe '.for_environment_name' do
subject { described_class.for_environment_name(project, environment_name) }
diff --git a/spec/requests/api/graphql/environments/deployments_query_spec.rb b/spec/requests/api/graphql/environments/deployments_query_spec.rb
new file mode 100644
index 00000000000..fbfd9c5d0ac
--- /dev/null
+++ b/spec/requests/api/graphql/environments/deployments_query_spec.rb
@@ -0,0 +1,345 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Environments Deployments query' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :private, :repository) }
+ let_it_be(:environment) { create(:environment, project: project) }
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+ let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } }
+
+ let(:user) { developer }
+
+ subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
+
+ context 'when there are deployments in the environment' do
+ let_it_be(:finished_deployment_old) do
+ create(:deployment, :success, environment: environment, project: project, finished_at: 2.days.ago)
+ end
+
+ let_it_be(:finished_deployment_new) do
+ create(:deployment, :success, environment: environment, project: project, finished_at: 1.day.ago)
+ end
+
+ let_it_be(:upcoming_deployment_old) do
+ create(:deployment, :created, environment: environment, project: project, created_at: 2.hours.ago)
+ end
+
+ let_it_be(:upcoming_deployment_new) do
+ create(:deployment, :created, environment: environment, project: project, created_at: 1.hour.ago)
+ end
+
+ let_it_be(:other_environment) { create(:environment, project: project) }
+ let_it_be(:other_deployment) { create(:deployment, :success, environment: other_environment, project: project) }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments {
+ nodes {
+ id
+ iid
+ ref
+ tag
+ sha
+ createdAt
+ updatedAt
+ finishedAt
+ status
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns all deployments of the environment' do
+ deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+ expect(deployments.count).to eq(4)
+ end
+
+ context 'when query last deployment' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments(statuses: [SUCCESS], orderBy: { finishedAt: DESC }, first: 1) {
+ nodes {
+ iid
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns deployment' do
+ deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+ expect(deployments.count).to eq(1)
+ expect(deployments[0]['iid']).to eq(finished_deployment_new.iid.to_s)
+ end
+ end
+
+ context 'when query latest upcoming deployment' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments(statuses: [CREATED RUNNING BLOCKED], orderBy: { createdAt: DESC }, first: 1) {
+ nodes {
+ iid
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns deployment' do
+ deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+ expect(deployments.count).to eq(1)
+ expect(deployments[0]['iid']).to eq(upcoming_deployment_new.iid.to_s)
+ end
+ end
+
+ context 'when query finished deployments in descending order' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments(statuses: [SUCCESS FAILED CANCELED], orderBy: { finishedAt: DESC }) {
+ nodes {
+ iid
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns deployments' do
+ deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+ expect(deployments.count).to eq(2)
+ expect(deployments[0]['iid']).to eq(finished_deployment_new.iid.to_s)
+ expect(deployments[1]['iid']).to eq(finished_deployment_old.iid.to_s)
+ end
+ end
+
+ context 'when query finished deployments in ascending order' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments(statuses: [SUCCESS FAILED CANCELED], orderBy: { finishedAt: ASC }) {
+ nodes {
+ iid
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns deployments' do
+ deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+ expect(deployments.count).to eq(2)
+ expect(deployments[0]['iid']).to eq(finished_deployment_old.iid.to_s)
+ expect(deployments[1]['iid']).to eq(finished_deployment_new.iid.to_s)
+ end
+ end
+
+ context 'when query upcoming deployments in descending order' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments(statuses: [CREATED RUNNING BLOCKED], orderBy: { createdAt: DESC }) {
+ nodes {
+ iid
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns deployments' do
+ deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+ expect(deployments.count).to eq(2)
+ expect(deployments[0]['iid']).to eq(upcoming_deployment_new.iid.to_s)
+ expect(deployments[1]['iid']).to eq(upcoming_deployment_old.iid.to_s)
+ end
+ end
+
+ context 'when query upcoming deployments in ascending order' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments(statuses: [CREATED RUNNING BLOCKED], orderBy: { createdAt: ASC }) {
+ nodes {
+ iid
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns deployments' do
+ deployments = subject.dig('data', 'project', 'environment', 'deployments', 'nodes')
+
+ expect(deployments.count).to eq(2)
+ expect(deployments[0]['iid']).to eq(upcoming_deployment_old.iid.to_s)
+ expect(deployments[1]['iid']).to eq(upcoming_deployment_new.iid.to_s)
+ end
+ end
+
+ context 'when query last deployments of multiple environments' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environments {
+ nodes {
+ name
+ deployments(statuses: [SUCCESS], orderBy: { finishedAt: DESC }, first: 1) {
+ nodes {
+ iid
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returnes an error for preventing N+1 queries' do
+ expect(subject['errors'][0]['message']).to include('exceeds max complexity')
+ end
+ end
+
+ context 'when query finished and upcoming deployments together' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments(statuses: [CREATED SUCCESS]) {
+ nodes {
+ iid
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(DeploymentsFinder::InefficientQueryError)
+ end
+ end
+
+ context 'when multiple orderBy input are specified' do
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments(orderBy: { finishedAt: DESC, createdAt: ASC }) {
+ nodes {
+ iid
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'raises an error' do
+ expect(subject['errors'][0]['message']).to include('orderBy parameter must contain one key-value pair.')
+ end
+ end
+
+ context 'when user is guest' do
+ let(:user) { guest }
+
+ it 'returns nothing' do
+ expect(subject['data']['project']['environment']).to be_nil
+ end
+ end
+
+ describe 'sorting and pagination' do
+ let(:data_path) { [:project, :environment, :deployments] }
+ let(:current_user) { user }
+
+ def pagination_query(params)
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ deployments(statuses: [SUCCESS], #{params}) {
+ nodes {
+ iid
+ }
+ pageInfo {
+ startCursor
+ endCursor
+ hasNextPage
+ hasPreviousPage
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ def pagination_results_data(nodes)
+ nodes.map { |deployment| deployment['iid'].to_i }
+ end
+
+ context 'when sorting by finished_at in ascending order' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_argument) { graphql_args(orderBy: { finishedAt: :ASC }) }
+ let(:first_param) { 2 }
+ let(:all_records) { [finished_deployment_old.iid, finished_deployment_new.iid] }
+ end
+ end
+
+ context 'when sorting by finished_at in descending order' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_argument) { graphql_args(orderBy: { finishedAt: :DESC }) }
+ let(:first_param) { 2 }
+ let(:all_records) { [finished_deployment_new.iid, finished_deployment_old.iid] }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/deployment_spec.rb b/spec/requests/api/graphql/project/deployment_spec.rb
new file mode 100644
index 00000000000..e5ef7bcafbf
--- /dev/null
+++ b/spec/requests/api/graphql/project/deployment_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Project Deployment query' do
+ let_it_be(:project) { create(:project, :private, :repository) }
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+ let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } }
+ let_it_be(:environment) { create(:environment, project: project) }
+ let_it_be(:deployment) { create(:deployment, environment: environment, project: project) }
+
+ subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
+
+ let(:user) { developer }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ deployment(iid: #{deployment.iid}) {
+ id
+ iid
+ ref
+ tag
+ sha
+ createdAt
+ updatedAt
+ finishedAt
+ status
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns the deployment of the project' do
+ deployment_data = subject.dig('data', 'project', 'deployment')
+
+ expect(deployment_data['iid']).to eq(deployment.iid.to_s)
+ end
+
+ context 'when user is guest' do
+ let(:user) { guest }
+
+ it 'returns nothing' do
+ deployment_data = subject.dig('data', 'project', 'deployment')
+
+ expect(deployment_data).to be_nil
+ end
+ end
+end
diff --git a/spec/requests/projects/google_cloud/configuration_controller_spec.rb b/spec/requests/projects/google_cloud/configuration_controller_spec.rb
index 08d4ad2f9ba..cb7f0d76930 100644
--- a/spec/requests/projects/google_cloud/configuration_controller_spec.rb
+++ b/spec/requests/projects/google_cloud/configuration_controller_spec.rb
@@ -2,9 +2,6 @@
require 'spec_helper'
-# Mock Types
-MockGoogleOAuth2Credentials = Struct.new(:app_id, :app_secret)
-
RSpec.describe Projects::GoogleCloud::ConfigurationController do
let_it_be(:project) { create(:project, :public) }
let_it_be(:url) { project_google_cloud_configuration_path(project) }
@@ -56,7 +53,7 @@ RSpec.describe Projects::GoogleCloud::ConfigurationController do
context 'but gitlab instance is not configured for google oauth2' do
it 'returns forbidden' do
- unconfigured_google_oauth2 = MockGoogleOAuth2Credentials.new('', '')
+ unconfigured_google_oauth2 = Struct.new(:app_id, :app_secret).new('', '')
allow(Gitlab::Auth::OAuth::Provider).to receive(:config_for)
.with('google_oauth2')
.and_return(unconfigured_google_oauth2)
diff --git a/spec/requests/projects/google_cloud/databases_controller_spec.rb b/spec/requests/projects/google_cloud/databases_controller_spec.rb
index c9335f8f317..cdd0555d526 100644
--- a/spec/requests/projects/google_cloud/databases_controller_spec.rb
+++ b/spec/requests/projects/google_cloud/databases_controller_spec.rb
@@ -2,133 +2,169 @@
require 'spec_helper'
-# Mock Types
-MockGoogleOAuth2Credentials = Struct.new(:app_id, :app_secret)
+RSpec.describe Projects::GoogleCloud::DatabasesController, :snowplow do
+ shared_examples 'shared examples for database controller endpoints' do
+ include_examples 'requires `admin_project_google_cloud` role'
-RSpec.describe Projects::GoogleCloud::DatabasesController do
- let_it_be(:project) { create(:project, :public) }
- let_it_be(:url) { project_google_cloud_databases_path(project) }
+ include_examples 'requires feature flag `incubation_5mp_google_cloud` enabled'
- let_it_be(:user_guest) { create(:user) }
- let_it_be(:user_developer) { create(:user) }
- let_it_be(:user_maintainer) { create(:user) }
+ include_examples 'requires valid Google OAuth2 configuration'
- let_it_be(:unauthorized_members) { [user_guest, user_developer] }
- let_it_be(:authorized_members) { [user_maintainer] }
+ include_examples 'requires valid Google Oauth2 token' do
+ let_it_be(:mock_gcp_projects) { [{}, {}, {}] }
+ let_it_be(:mock_branches) { [] }
+ let_it_be(:mock_tags) { [] }
+ end
+ end
+
+ context '-/google_cloud/databases' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:renders_template) { 'projects/google_cloud/databases/index' }
+ let_it_be(:redirects_to) { nil }
- before do
- project.add_guest(user_guest)
- project.add_developer(user_developer)
- project.add_maintainer(user_maintainer)
+ subject { get project_google_cloud_databases_path(project) }
+
+ include_examples 'shared examples for database controller endpoints'
end
- context 'when accessed by unauthorized members' do
- it 'returns not found on GET request' do
- unauthorized_members.each do |unauthorized_member|
- sign_in(unauthorized_member)
+ context '-/google_cloud/databases/new/postgres' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:renders_template) { 'projects/google_cloud/databases/cloudsql_form' }
+ let_it_be(:redirects_to) { nil }
- get url
- expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'admin_project_google_cloud!',
- label: 'error_access_denied',
- property: 'invalid_user',
- project: project,
- user: unauthorized_member
- )
+ subject { get new_project_google_cloud_database_path(project, :postgres) }
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
+ include_examples 'shared examples for database controller endpoints'
end
- context 'when accessed by authorized members' do
- it 'returns successful' do
- authorized_members.each do |authorized_member|
- sign_in(authorized_member)
+ context '-/google_cloud/databases/new/mysql' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:renders_template) { 'projects/google_cloud/databases/cloudsql_form' }
+ let_it_be(:redirects_to) { nil }
- get url
+ subject { get new_project_google_cloud_database_path(project, :mysql) }
- expect(response).to be_successful
- expect(response).to render_template('projects/google_cloud/databases/index')
- end
- end
+ include_examples 'shared examples for database controller endpoints'
+ end
- context 'but gitlab instance is not configured for google oauth2' do
- it 'returns forbidden' do
- unconfigured_google_oauth2 = MockGoogleOAuth2Credentials.new('', '')
- allow(Gitlab::Auth::OAuth::Provider).to receive(:config_for)
- .with('google_oauth2')
- .and_return(unconfigured_google_oauth2)
+ context '-/google_cloud/databases/new/sqlserver' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:renders_template) { 'projects/google_cloud/databases/cloudsql_form' }
+ let_it_be(:redirects_to) { nil }
- authorized_members.each do |authorized_member|
- sign_in(authorized_member)
+ subject { get new_project_google_cloud_database_path(project, :sqlserver) }
- get url
+ include_examples 'shared examples for database controller endpoints'
+ end
- expect(response).to have_gitlab_http_status(:forbidden)
- expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'google_oauth2_enabled!',
- label: 'error_access_denied',
- extra: { reason: 'google_oauth2_not_configured',
- config: unconfigured_google_oauth2 },
- project: project,
- user: authorized_member
- )
+ context '-/google_cloud/databases/create' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:renders_template) { nil }
+ let_it_be(:redirects_to) { project_google_cloud_databases_path(project) }
+
+ subject { post project_google_cloud_databases_path(project) }
+
+ include_examples 'shared examples for database controller endpoints'
+
+ context 'when the request is valid' do
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+
+ allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
+ allow(client).to receive(:validate_token).and_return(true)
+ allow(client).to receive(:list_projects).and_return(mock_gcp_projects)
+ end
+
+ allow_next_instance_of(BranchesFinder) do |finder|
+ allow(finder).to receive(:execute).and_return(mock_branches)
+ end
+
+ allow_next_instance_of(TagsFinder) do |finder|
+ allow(finder).to receive(:execute).and_return(mock_branches)
end
end
- end
- context 'but feature flag is disabled' do
- before do
- stub_feature_flags(incubation_5mp_google_cloud: false)
+ subject do
+ post project_google_cloud_databases_path(project)
end
- it 'returns not found' do
- authorized_members.each do |authorized_member|
- sign_in(authorized_member)
+ it 'calls EnableCloudsqlService and redirects on error' do
+ expect_next_instance_of(::GoogleCloud::EnableCloudsqlService) do |service|
+ expect(service).to receive(:execute)
+ .and_return({ status: :error, message: 'error' })
+ end
- get url
+ subject
- expect(response).to have_gitlab_http_status(:not_found)
- expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'feature_flag_enabled!',
- label: 'error_access_denied',
- property: 'feature_flag_not_enabled',
- project: project,
- user: authorized_member
- )
- end
+ expect(response).to redirect_to(project_google_cloud_databases_path(project))
+
+ expect_snowplow_event(
+ category: 'Projects::GoogleCloud',
+ action: 'databases#cloudsql_create',
+ label: 'error_enable_cloudsql_service',
+ extra: { status: :error, message: 'error' },
+ project: project,
+ user: user
+ )
end
- end
- context 'but google oauth2 token is not valid' do
- it 'does not return revoke oauth url' do
- allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
- allow(client).to receive(:validate_token).and_return(false)
+ context 'when EnableCloudsqlService is successful' do
+ before do
+ allow_next_instance_of(::GoogleCloud::EnableCloudsqlService) do |service|
+ allow(service).to receive(:execute)
+ .and_return({ status: :success, message: 'success' })
+ end
end
- authorized_members.each do |authorized_member|
- sign_in(authorized_member)
+ it 'calls CreateCloudsqlInstanceService and redirects on error' do
+ expect_next_instance_of(::GoogleCloud::CreateCloudsqlInstanceService) do |service|
+ expect(service).to receive(:execute)
+ .and_return({ status: :error, message: 'error' })
+ end
+
+ subject
- get url
+ expect(response).to redirect_to(project_google_cloud_databases_path(project))
- expect(response).to be_successful
expect_snowplow_event(
category: 'Projects::GoogleCloud',
- action: 'databases#index',
- label: 'success',
- extra: {
- configurationUrl: project_google_cloud_configuration_path(project),
- deploymentsUrl: project_google_cloud_deployments_path(project),
- databasesUrl: project_google_cloud_databases_path(project)
- },
+ action: 'databases#cloudsql_create',
+ label: 'error_create_cloudsql_instance',
+ extra: { status: :error, message: 'error' },
project: project,
- user: authorized_member
+ user: user
)
end
+
+ context 'when CreateCloudsqlInstanceService is successful' do
+ before do
+ allow_next_instance_of(::GoogleCloud::CreateCloudsqlInstanceService) do |service|
+ allow(service).to receive(:execute)
+ .and_return({ status: :success, message: 'success' })
+ end
+ end
+
+ it 'redirects as expected' do
+ subject
+
+ expect(response).to redirect_to(project_google_cloud_databases_path(project))
+
+ expect_snowplow_event(
+ category: 'Projects::GoogleCloud',
+ action: 'databases#cloudsql_create',
+ label: 'success',
+ extra: nil,
+ project: project,
+ user: user
+ )
+ end
+ end
end
end
end
diff --git a/spec/services/google_cloud/enable_cloudsql_service_spec.rb b/spec/services/google_cloud/enable_cloudsql_service_spec.rb
index e54e5a8d446..f267f6d3bc2 100644
--- a/spec/services/google_cloud/enable_cloudsql_service_spec.rb
+++ b/spec/services/google_cloud/enable_cloudsql_service_spec.rb
@@ -23,6 +23,11 @@ RSpec.describe GoogleCloud::EnableCloudsqlService do
project.save!
end
+ after do
+ project.variables.destroy_all # rubocop:disable Cop/DestroyAll
+ project.save!
+ end
+
it 'enables cloudsql, compute and service networking Google APIs', :aggregate_failures do
expect_next_instance_of(GoogleApi::CloudPlatform::Client) do |instance|
expect(instance).to receive(:enable_cloud_sql_admin).with('prj-prod')
@@ -35,5 +40,22 @@ RSpec.describe GoogleCloud::EnableCloudsqlService do
expect(result[:status]).to eq(:success)
end
+
+ context 'when Google APIs raise an error' do
+ it 'returns error result' do
+ allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |instance|
+ allow(instance).to receive(:enable_cloud_sql_admin).with('prj-prod')
+ allow(instance).to receive(:enable_compute).with('prj-prod')
+ allow(instance).to receive(:enable_service_networking).with('prj-prod')
+ allow(instance).to receive(:enable_cloud_sql_admin).with('prj-staging')
+ allow(instance).to receive(:enable_compute).with('prj-staging')
+ allow(instance).to receive(:enable_service_networking).with('prj-staging')
+ .and_raise(Google::Apis::Error.new('error'))
+ end
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq('error')
+ end
+ end
end
end
diff --git a/spec/support/shared_examples/requests/projects/google_cloud/google_cloud_ff_examples.rb b/spec/support/shared_examples/requests/projects/google_cloud/google_cloud_ff_examples.rb
new file mode 100644
index 00000000000..d49fe517c60
--- /dev/null
+++ b/spec/support/shared_examples/requests/projects/google_cloud/google_cloud_ff_examples.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'requires feature flag `incubation_5mp_google_cloud` enabled' do
+ context 'when feature flag is disabled' do
+ before do
+ project.add_maintainer(user)
+ stub_feature_flags(incubation_5mp_google_cloud: false)
+ end
+
+ it 'renders not found' do
+ sign_in(user)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/projects/google_cloud/google_cloud_role_examples.rb b/spec/support/shared_examples/requests/projects/google_cloud/google_cloud_role_examples.rb
new file mode 100644
index 00000000000..4c616b59be0
--- /dev/null
+++ b/spec/support/shared_examples/requests/projects/google_cloud/google_cloud_role_examples.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'requires `admin_project_google_cloud` role' do
+ shared_examples 'returns not_found' do
+ it 'returns not found' do
+ sign_in(user)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ shared_examples 'redirects to authorize url' do
+ it 'redirects to authorize url' do
+ sign_in(user)
+
+ subject
+
+ expect(response).to redirect_to(assigns(:authorize_url))
+ end
+ end
+
+ context 'when requested by users with different roles' do
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
+
+ before do
+ project.add_guest(guest)
+ project.add_developer(developer)
+ project.add_maintainer(maintainer)
+ end
+
+ context 'for unauthorized users' do
+ include_examples 'returns not_found' do
+ let(:user) { guest }
+ end
+
+ include_examples 'returns not_found' do
+ let(:user) { developer }
+ end
+ end
+
+ context 'for authorized users' do
+ include_examples 'redirects to authorize url' do
+ let(:user) { maintainer }
+ end
+
+ include_examples 'redirects to authorize url' do
+ let(:user) { project.owner }
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/projects/google_cloud/google_oauth2_config_examples.rb b/spec/support/shared_examples/requests/projects/google_cloud/google_oauth2_config_examples.rb
new file mode 100644
index 00000000000..63f6cffb3a0
--- /dev/null
+++ b/spec/support/shared_examples/requests/projects/google_cloud/google_oauth2_config_examples.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'requires valid Google OAuth2 configuration' do
+ context 'when GitLab instance does not have valid Google OAuth2 configuration ' do
+ before do
+ project.add_maintainer(user)
+ unconfigured_google_oauth2 = Struct.new(:app_id, :app_secret)
+ .new('', '')
+ allow(Gitlab::Auth::OAuth::Provider).to receive(:config_for)
+ .with('google_oauth2')
+ .and_return(unconfigured_google_oauth2)
+ end
+
+ it 'renders forbidden' do
+ sign_in(user)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/projects/google_cloud/google_oauth2_token_examples.rb b/spec/support/shared_examples/requests/projects/google_cloud/google_oauth2_token_examples.rb
new file mode 100644
index 00000000000..379327be0db
--- /dev/null
+++ b/spec/support/shared_examples/requests/projects/google_cloud/google_oauth2_token_examples.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'requires valid Google Oauth2 token' do
+ context 'when a valid Google OAuth2 token does not exist' do
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ it 'triggers Google OAuth2 flow on request' do
+ subject
+
+ expect(response).to redirect_to(assigns(:authorize_url))
+ end
+
+ context 'and a valid Google OAuth2 token gets created' do
+ before do
+ allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
+ allow(client).to receive(:validate_token).and_return(true)
+ allow(client).to receive(:list_projects).and_return(mock_gcp_projects) if mock_gcp_projects
+ end
+
+ allow_next_instance_of(BranchesFinder) do |finder|
+ allow(finder).to receive(:execute).and_return(mock_branches) if mock_branches
+ end
+
+ allow_next_instance_of(TagsFinder) do |finder|
+ allow(finder).to receive(:execute).and_return(mock_branches) if mock_branches
+ end
+ end
+
+ it 'renders template as expected' do
+ if renders_template
+ subject
+ expect(response).to render_template(renders_template)
+ end
+ end
+
+ it 'redirects as expected' do
+ if redirects_to
+ subject
+ expect(response).to redirect_to(redirects_to)
+ end
+ end
+ end
+ end
+end
diff --git a/storybook/config/preview.js b/storybook/config/preview.js
index a55d0d52a0c..6f3b8190742 100644
--- a/storybook/config/preview.js
+++ b/storybook/config/preview.js
@@ -6,13 +6,16 @@ import translateMixin from '~/vue_shared/translate';
const stylesheetsRequireCtx = require.context(
'../../app/assets/stylesheets',
true,
- /(application|application_utilities)\.scss$/,
+ /(application|application_utilities|highlight\/themes\/white)\.scss$/,
);
-window.gon = {};
+window.gon = {
+ user_color_scheme: 'white',
+};
translateMixin(Vue);
stylesheetsRequireCtx('./application.scss');
stylesheetsRequireCtx('./application_utilities.scss');
+stylesheetsRequireCtx('./highlight/themes/white.scss');
export const decorators = [withServer(createMockServer)];
diff --git a/yarn.lock b/yarn.lock
index 2c8ce508616..868ae38c64f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2214,10 +2214,10 @@
semver "^6.3.0"
tsutils "^3.17.1"
-"@vue/compiler-sfc@2.7.8":
- version "2.7.8"
- resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-2.7.8.tgz#731aadd6beafdb9c72fd8614ce189ac6cee87612"
- integrity sha512-2DK4YWKfgLnW9VDR9gnju1gcYRk3flKj8UNsms7fsRmFcg35slVTZEkqwBtX+wJBXaamFfn6NxSsZh3h12Ix/Q==
+"@vue/compiler-sfc@2.7.9":
+ version "2.7.9"
+ resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-2.7.9.tgz#aa31813c94de39f4977e4b924eb261c2199218ad"
+ integrity sha512-TD2FvT0fPUezw5RVP4tfwTZnKHP0QjeEUb39y7tORvOJQTjbOuHJEk4GPHUPsRaTeQ8rjuKjntyrYcEIx+ODxg==
dependencies:
"@babel/parser" "^7.18.4"
postcss "^8.4.14"
@@ -3429,7 +3429,7 @@ clean-stack@^2.0.0:
resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==
-clipboard@^2.0.0, clipboard@^2.0.8:
+clipboard@^2.0.8:
version "2.0.8"
resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.8.tgz#ffc6c103dd2967a83005f3f61976aa4655a4cdba"
integrity sha512-Y6WO0unAIQp5bLmk1zdThRhgJt/x3ks6f30s3oE3H1mgIEU33XyQjEf8gsf6DxC7NPX8Y1SsNWjUjL/ywLnnbQ==
@@ -9731,13 +9731,6 @@ pretty@^2.0.0:
extend-shallow "^2.0.1"
js-beautify "^1.6.12"
-prismjs@^1.21.0:
- version "1.21.0"
- resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.21.0.tgz#36c086ec36b45319ec4218ee164c110f9fc015a3"
- integrity sha512-uGdSIu1nk3kej2iZsLyDoJ7e9bnPzIgY0naW/HdknGj61zScaprVEVGHrPoXqI+M9sP0NDnTK2jpkvmldpuqDw==
- optionalDependencies:
- clipboard "^2.0.0"
-
process-nextick-args@~2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
@@ -12023,10 +12016,10 @@ vue-style-loader@^4.1.0:
hash-sum "^1.0.2"
loader-utils "^1.0.2"
-vue-template-compiler@^2.7.8:
- version "2.7.8"
- resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.7.8.tgz#eadd54ed8fbff55b7deb07093a976c07f451a1dc"
- integrity sha512-eQqdcUpJKJpBRPDdxCNsqUoT0edNvdt1jFjtVnVS/LPPmr0BU2jWzXlrf6BVMeODtdLewB3j8j3WjNiB+V+giw==
+vue-template-compiler@^2.7.9:
+ version "2.7.9"
+ resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.7.9.tgz#ffbeb1769ae6af65cd405a6513df6b1e20e33616"
+ integrity sha512-NPJxt6OjVlzmkixYg0SdIN2Lw/rMryQskObY89uAMcM9flS/HrmLK5LaN1ReBTuhBgnYuagZZEkSS6FESATQUQ==
dependencies:
de-indent "^1.0.2"
he "^1.2.0"
@@ -12041,12 +12034,12 @@ vue-virtual-scroll-list@^1.4.7:
resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-1.4.7.tgz#12ee26833885f5bb4d37dc058085ccf3ce5b5a74"
integrity sha512-R8bk+k7WMGGoFQ9xF0krGCAlZhQjbJOkDUX+YZD2J+sHQWTzDtmTLS6kiIJToOHK1d/8QPGiD8fd9w0lDP4arg==
-vue@^2.7.8:
- version "2.7.8"
- resolved "https://registry.yarnpkg.com/vue/-/vue-2.7.8.tgz#34e06553137611d8cecc4b0cd78b7a59f80b1299"
- integrity sha512-ncwlZx5qOcn754bCu5/tS/IWPhXHopfit79cx+uIlLMyt3vCMGcXai5yCG5y+I6cDmEj4ukRYyZail9FTQh7lQ==
+vue@^2.7.9:
+ version "2.7.9"
+ resolved "https://registry.yarnpkg.com/vue/-/vue-2.7.9.tgz#ac3ccb4a4ac5dd31a8eec4c68a094efe7f83dfbb"
+ integrity sha512-GeWCvAUkjzD5q4A3vgi8ka5r9bM6g8fmNmx/9VnHDKCaEzBcoVw+7UcQktZHrJ2jhlI+Zv8L57pMCIwM4h4MWg==
dependencies:
- "@vue/compiler-sfc" "2.7.8"
+ "@vue/compiler-sfc" "2.7.9"
csstype "^3.1.0"
vuedraggable@^2.23.0: