summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/environments
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-02-20 13:49:51 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-02-20 13:49:51 +0000
commit71786ddc8e28fbd3cb3fcc4b3ff15e5962a1c82e (patch)
tree6a2d93ef3fb2d353bb7739e4b57e6541f51cdd71 /app/assets/javascripts/environments
parenta7253423e3403b8c08f8a161e5937e1488f5f407 (diff)
downloadgitlab-ce-71786ddc8e28fbd3cb3fcc4b3ff15e5962a1c82e.tar.gz
Add latest changes from gitlab-org/gitlab@15-9-stable-eev15.9.0-rc42
Diffstat (limited to 'app/assets/javascripts/environments')
-rw-r--r--app/assets/javascripts/environments/components/environment_form.vue22
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue38
-rw-r--r--app/assets/javascripts/environments/components/new_environment_item.vue1
-rw-r--r--app/assets/javascripts/environments/components/stop_stale_environments_modal.vue104
-rw-r--r--app/assets/javascripts/environments/constants.js10
-rw-r--r--app/assets/javascripts/environments/edit.js1
-rw-r--r--app/assets/javascripts/environments/environment_details/components/deployment_actions.vue31
-rw-r--r--app/assets/javascripts/environments/environment_details/constants.js6
-rw-r--r--app/assets/javascripts/environments/environment_details/deployments_table.vue5
-rw-r--r--app/assets/javascripts/environments/environment_details/index.vue6
-rw-r--r--app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql1
-rw-r--r--app/assets/javascripts/environments/graphql/queries/environment_details.query.graphql20
-rw-r--r--app/assets/javascripts/environments/graphql/resolvers.js1
-rw-r--r--app/assets/javascripts/environments/helpers/deployment_data_transformation_helper.js28
14 files changed, 263 insertions, 11 deletions
diff --git a/app/assets/javascripts/environments/components/environment_form.vue b/app/assets/javascripts/environments/components/environment_form.vue
index 1bac0ef1359..ee5d95ae6f0 100644
--- a/app/assets/javascripts/environments/components/environment_form.vue
+++ b/app/assets/javascripts/environments/components/environment_form.vue
@@ -3,6 +3,10 @@ import { GlButton, GlForm, GlFormGroup, GlFormInput, GlLink, GlSprintf } from '@
import { helpPagePath } from '~/helpers/help_page_helper';
import { isAbsolute } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
+import {
+ ENVIRONMENT_NEW_HELP_TEXT,
+ ENVIRONMENT_EDIT_HELP_TEXT,
+} from 'ee_else_ce/environments/constants';
export default {
components: {
@@ -13,6 +17,7 @@ export default {
GlLink,
GlSprintf,
},
+ inject: ['protectedEnvironmentSettingsPath'],
props: {
environment: {
required: true,
@@ -34,9 +39,8 @@ export default {
},
i18n: {
header: __('Environments'),
- helpMessage: __(
- 'Environments allow you to track deployments of your application. %{linkStart}More information%{linkEnd}.',
- ),
+ helpNewMessage: ENVIRONMENT_NEW_HELP_TEXT,
+ helpEditMessage: ENVIRONMENT_EDIT_HELP_TEXT,
nameLabel: __('Name'),
nameFeedback: __('This field is required'),
nameDisabledHelp: __("You cannot rename an environment after it's created."),
@@ -62,6 +66,9 @@ export default {
isNameDisabled() {
return Boolean(this.environment.id);
},
+ showEditHelp() {
+ return this.isNameDisabled && Boolean(this.protectedEnvironmentSettingsPath);
+ },
valid() {
return {
name: this.visited.name && this.environment.name !== '',
@@ -89,9 +96,14 @@ export default {
{{ $options.i18n.header }}
</h4>
<p class="gl-w-full">
- <gl-sprintf :message="$options.i18n.helpMessage">
+ <gl-sprintf
+ :message="showEditHelp ? $options.i18n.helpEditMessage : $options.i18n.helpNewMessage"
+ >
<template #link="{ content }">
- <gl-link :href="$options.helpPagePath">{{ content }}</gl-link>
+ <gl-link
+ :href="showEditHelp ? protectedEnvironmentSettingsPath : $options.helpPagePath"
+ >{{ content }}</gl-link
+ >
</template>
</gl-sprintf>
</p>
diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue
index 55e6a891e27..b2a69cdb6c6 100644
--- a/app/assets/javascripts/environments/components/environments_app.vue
+++ b/app/assets/javascripts/environments/components/environments_app.vue
@@ -15,6 +15,7 @@ import { ENVIRONMENTS_SCOPE } from '../constants';
import EnvironmentFolder from './environment_folder.vue';
import EnableReviewAppModal from './enable_review_app_modal.vue';
import StopEnvironmentModal from './stop_environment_modal.vue';
+import StopStaleEnvironmentsModal from './stop_stale_environments_modal.vue';
import EnvironmentItem from './new_environment_item.vue';
import ConfirmRollbackModal from './confirm_rollback_modal.vue';
import DeleteEnvironmentModal from './delete_environment_modal.vue';
@@ -31,6 +32,7 @@ export default {
EnableReviewAppModal,
EnvironmentItem,
StopEnvironmentModal,
+ StopStaleEnvironmentsModal,
GlBadge,
GlPagination,
GlSearchBoxByType,
@@ -75,6 +77,7 @@ export default {
i18n: {
newEnvironmentButtonLabel: s__('Environments|New environment'),
reviewAppButtonLabel: s__('Environments|Enable review app'),
+ cleanUpEnvsButtonLabel: s__('Environments|Clean up environments'),
available: __('Available'),
stopped: __('Stopped'),
prevPage: __('Go to previous page'),
@@ -85,11 +88,13 @@ export default {
searchPlaceholder: s__('Environments|Search by environment name'),
},
modalId: 'enable-review-app-info',
+ stopStaleEnvsModalId: 'stop-stale-environments-modal',
data() {
const { page = '1', search = '', scope } = queryToObject(window.location.search);
return {
interval: undefined,
isReviewAppModalVisible: false,
+ isStopStaleEnvModalVisible: false,
page: parseInt(page, 10),
pageInfo: {},
scope: Object.values(ENVIRONMENTS_SCOPE).includes(scope)
@@ -107,6 +112,9 @@ export default {
canSetupReviewApp() {
return this.environmentApp?.reviewApp?.canSetupReviewApp;
},
+ canCleanUpEnvs() {
+ return this.environmentApp?.canStopStaleEnvironments;
+ },
folders() {
return this.environmentApp?.environments?.filter((e) => e.size > 1) ?? [];
},
@@ -149,6 +157,19 @@ export default {
},
};
},
+ openCleanUpEnvsModal() {
+ if (!this.canCleanUpEnvs) {
+ return null;
+ }
+
+ return {
+ text: this.$options.i18n.cleanUpEnvsButtonLabel,
+ attributes: {
+ category: 'secondary',
+ variant: 'confirm',
+ },
+ };
+ },
stoppedCount() {
return this.environmentApp?.stoppedCount;
},
@@ -178,6 +199,9 @@ export default {
showReviewAppModal() {
this.isReviewAppModalVisible = true;
},
+ showCleanUpEnvsModal() {
+ this.isStopStaleEnvModalVisible = true;
+ },
setScope(scope) {
this.scope = scope;
this.moveToPage(1);
@@ -219,16 +243,24 @@ export default {
:modal-id="$options.modalId"
data-testid="enable-review-app-modal"
/>
+ <stop-stale-environments-modal
+ v-if="canCleanUpEnvs"
+ v-model="isStopStaleEnvModalVisible"
+ :modal-id="$options.stopStaleEnvsModalId"
+ data-testid="stop-stale-environments-modal"
+ />
<delete-environment-modal :environment="environmentToDelete" graphql />
<stop-environment-modal :environment="environmentToStop" graphql />
<confirm-rollback-modal :environment="environmentToRollback" graphql />
<canary-update-modal :environment="environmentToChangeCanary" :weight="weight" />
<gl-tabs
- :action-secondary="addEnvironment"
- :action-primary="openReviewAppModal"
+ :action-secondary="openReviewAppModal"
+ :action-primary="openCleanUpEnvsModal"
+ :action-tertiary="addEnvironment"
sync-active-tab-with-query-params
query-param-name="scope"
- @primary="showReviewAppModal"
+ @secondary="showReviewAppModal"
+ @primary="showCleanUpEnvsModal"
>
<gl-tab
:query-param-value="$options.ENVIRONMENTS_SCOPE.AVAILABLE"
diff --git a/app/assets/javascripts/environments/components/new_environment_item.vue b/app/assets/javascripts/environments/components/new_environment_item.vue
index 9a100e0199e..73dfd993c5b 100644
--- a/app/assets/javascripts/environments/components/new_environment_item.vue
+++ b/app/assets/javascripts/environments/components/new_environment_item.vue
@@ -323,6 +323,7 @@ export default {
>
<deployment
:deployment="upcomingDeployment"
+ :visible="visible"
:class="{ 'gl-ml-7': inFolder }"
class="gl-pl-4"
>
diff --git a/app/assets/javascripts/environments/components/stop_stale_environments_modal.vue b/app/assets/javascripts/environments/components/stop_stale_environments_modal.vue
new file mode 100644
index 00000000000..57873b28d37
--- /dev/null
+++ b/app/assets/javascripts/environments/components/stop_stale_environments_modal.vue
@@ -0,0 +1,104 @@
+<script>
+import { GlTooltipDirective, GlModal, GlDatepicker, GlFormGroup } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+import { stopStaleEnvironments } from '~/rest_api';
+import { MIN_STALE_ENVIRONMENT_DATE, MAX_STALE_ENVIRONMENT_DATE } from '../constants';
+
+export default {
+ id: 'stop-stale-environments-modal',
+ name: 'StopStaleEnvironmentsModal',
+
+ components: {
+ GlModal,
+ GlDatepicker,
+ GlFormGroup,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ inject: {
+ projectId: {
+ default: '',
+ },
+ },
+ model: {
+ prop: 'visible',
+ event: 'change',
+ },
+ props: {
+ modalId: {
+ type: String,
+ required: true,
+ },
+ visible: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ modalProps: {
+ primary: {
+ text: s__('Environments|Clean up'),
+ attributes: [{ variant: 'info' }],
+ },
+ cancel: {
+ text: __('Cancel'),
+ },
+ dateRange: {
+ minDate: MIN_STALE_ENVIRONMENT_DATE, // 10 years ago
+ maxDate: MAX_STALE_ENVIRONMENT_DATE,
+ },
+ },
+
+ data() {
+ return {
+ stopEnvironmentsBefore: MAX_STALE_ENVIRONMENT_DATE,
+ };
+ },
+
+ methods: {
+ onSubmit() {
+ stopStaleEnvironments(this.projectId, this.stopEnvironmentsBefore || this.maxDate);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-modal
+ :action-primary="$options.modalProps.primary"
+ :action-cancel="$options.modalProps.cancel"
+ :visible="visible"
+ :modal-id="modalId"
+ :title="s__('Environments|Clean up environments')"
+ static
+ @primary="onSubmit"
+ @change="$emit('change', $event)"
+ >
+ <p>
+ {{
+ s__(
+ 'Environments|Select which environments to clean up. \
+ Protected environments are excluded. Learn more about cleaning up environments.',
+ )
+ }}
+ </p>
+
+ <gl-form-group
+ :label="s__('Environments|Stop unused environments')"
+ :label-description="
+ s__('Environments|Stop environments that have not been updated since the specified date:')
+ "
+ label-for="stop_environments-before"
+ >
+ <gl-datepicker
+ v-model="stopEnvironmentsBefore"
+ input-id="stop-environments-before"
+ data-testid="stop-environments-before"
+ :min-date="$options.modalProps.dateRange.minDate"
+ :max-date="$options.modalProps.dateRange.maxDate"
+ :default-date="$options.modalProps.dateRange.maxDate"
+ />
+ </gl-form-group>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/environments/constants.js b/app/assets/javascripts/environments/constants.js
index c4d02da9d21..28424322dd2 100644
--- a/app/assets/javascripts/environments/constants.js
+++ b/app/assets/javascripts/environments/constants.js
@@ -1,4 +1,5 @@
import { __, s__ } from '~/locale';
+import { getDateInPast } from '~/lib/utils/datetime_utility';
// These statuses are based on how the backend defines pod phases here
// lib/gitlab/kubernetes/pod.rb
@@ -77,3 +78,12 @@ export const REVIEW_APP_MODAL_I18N = {
viewMoreExampleProjects: s__('EnableReviewApp|View more example projects'),
copyToClipboardText: s__('EnableReviewApp|Copy snippet'),
};
+
+export const MIN_STALE_ENVIRONMENT_DATE = getDateInPast(new Date(), 3650); // 10 years ago
+export const MAX_STALE_ENVIRONMENT_DATE = getDateInPast(new Date(), 7); // one week ago
+
+export const ENVIRONMENT_NEW_HELP_TEXT = __(
+ 'Environments allow you to track deployments of your application.%{linkStart} More information.%{linkEnd}',
+);
+
+export const ENVIRONMENT_EDIT_HELP_TEXT = ENVIRONMENT_NEW_HELP_TEXT;
diff --git a/app/assets/javascripts/environments/edit.js b/app/assets/javascripts/environments/edit.js
index dd6680f64bd..a128d2fb3c7 100644
--- a/app/assets/javascripts/environments/edit.js
+++ b/app/assets/javascripts/environments/edit.js
@@ -7,6 +7,7 @@ export default (el) =>
provide: {
projectEnvironmentsPath: el.dataset.projectEnvironmentsPath,
updateEnvironmentPath: el.dataset.updateEnvironmentPath,
+ protectedEnvironmentSettingsPath: el.dataset.protectedEnvironmentSettingsPath,
},
render(h) {
return h(EditEnvironment, {
diff --git a/app/assets/javascripts/environments/environment_details/components/deployment_actions.vue b/app/assets/javascripts/environments/environment_details/components/deployment_actions.vue
new file mode 100644
index 00000000000..77d9311743c
--- /dev/null
+++ b/app/assets/javascripts/environments/environment_details/components/deployment_actions.vue
@@ -0,0 +1,31 @@
+<script>
+import ActionsComponent from '~/environments/components/environment_actions.vue';
+
+export default {
+ components: {
+ ActionsComponent,
+ },
+ props: {
+ actions: {
+ // actions shape:
+ /* Array<{
+ playable: boolean,
+ playPath: url,
+ name: string
+ scheduledAt: ISO_timestamp | null
+ }>
+ */
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ isActionsShown() {
+ return this.actions.length > 0;
+ },
+ },
+};
+</script>
+<template>
+ <actions-component v-if="isActionsShown" :actions="actions" graphql />
+</template>
diff --git a/app/assets/javascripts/environments/environment_details/constants.js b/app/assets/javascripts/environments/environment_details/constants.js
index bf690ffedeb..3b33d6a676e 100644
--- a/app/assets/javascripts/environments/environment_details/constants.js
+++ b/app/assets/javascripts/environments/environment_details/constants.js
@@ -45,6 +45,12 @@ export const ENVIRONMENT_DETAILS_TABLE_FIELDS = [
columnClass: 'gl-w-10p',
tdClass: 'gl-vertical-align-middle! gl-white-space-nowrap',
},
+ {
+ key: 'actions',
+ label: __('Actions'),
+ columnClass: 'gl-w-10p',
+ tdClass: 'gl-vertical-align-middle! gl-white-space-nowrap',
+ },
];
export const translations = {
diff --git a/app/assets/javascripts/environments/environment_details/deployments_table.vue b/app/assets/javascripts/environments/environment_details/deployments_table.vue
index 41570ee44c0..10f8c06e581 100644
--- a/app/assets/javascripts/environments/environment_details/deployments_table.vue
+++ b/app/assets/javascripts/environments/environment_details/deployments_table.vue
@@ -5,11 +5,13 @@ import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import DeploymentStatusLink from './components/deployment_status_link.vue';
import DeploymentJob from './components/deployment_job.vue';
import DeploymentTriggerer from './components/deployment_triggerer.vue';
+import DeploymentActions from './components/deployment_actions.vue';
import { ENVIRONMENT_DETAILS_TABLE_FIELDS } from './constants';
export default {
components: {
DeploymentTriggerer,
+ DeploymentActions,
DeploymentJob,
Commit,
TimeAgoTooltip,
@@ -51,5 +53,8 @@ export default {
<template #cell(deployed)="{ item }">
<time-ago-tooltip :time="item.deployed" />
</template>
+ <template #cell(actions)="{ item }">
+ <deployment-actions :actions="item.actions" />
+ </template>
</gl-table-lite>
</template>
diff --git a/app/assets/javascripts/environments/environment_details/index.vue b/app/assets/javascripts/environments/environment_details/index.vue
index b43f4233b9c..f4657c5100a 100644
--- a/app/assets/javascripts/environments/environment_details/index.vue
+++ b/app/assets/javascripts/environments/environment_details/index.vue
@@ -59,7 +59,11 @@ export default {
},
computed: {
deployments() {
- return this.project.environment?.deployments.nodes.map(convertToDeploymentTableRow) || [];
+ return (
+ this.project.environment?.deployments.nodes.map((deployment) =>
+ convertToDeploymentTableRow(deployment, this.project.environment),
+ ) || []
+ );
},
isLoading() {
return this.$apollo.queries.project.loading;
diff --git a/app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql
index 1a572208a1c..7a50ded7d6c 100644
--- a/app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql
@@ -4,5 +4,6 @@ query getEnvironmentApp($page: Int, $scope: String, $search: String) {
stoppedCount
environments
reviewApp
+ canStopStaleEnvironments
}
}
diff --git a/app/assets/javascripts/environments/graphql/queries/environment_details.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment_details.query.graphql
index c6c2024c840..0182b3a7234 100644
--- a/app/assets/javascripts/environments/graphql/queries/environment_details.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/environment_details.query.graphql
@@ -13,6 +13,13 @@ query getEnvironmentDetails(
environment(name: $environmentName) {
id
name
+ lastDeployment(status: SUCCESS) {
+ id
+ job {
+ id
+ name
+ }
+ }
deployments(
orderBy: { createdAt: DESC }
first: $first
@@ -36,6 +43,19 @@ query getEnvironmentDetails(
name
id
webPath
+ playable
+ deploymentPipeline: pipeline {
+ id
+ jobs(whenExecuted: ["manual"], retried: false) {
+ nodes {
+ id
+ name
+ playable
+ scheduledAt
+ webPath
+ }
+ }
+ }
}
commit {
id
diff --git a/app/assets/javascripts/environments/graphql/resolvers.js b/app/assets/javascripts/environments/graphql/resolvers.js
index afd56d0cf0d..e21670870b8 100644
--- a/app/assets/javascripts/environments/graphql/resolvers.js
+++ b/app/assets/javascripts/environments/graphql/resolvers.js
@@ -54,6 +54,7 @@ export const resolvers = (endpoint) => ({
...convertObjectPropsToCamelCase(res.data.review_app),
__typename: 'ReviewApp',
},
+ canStopStaleEnvironments: res.data.can_stop_stale_environments,
stoppedCount: res.data.stopped_count,
__typename: 'LocalEnvironmentApp',
};
diff --git a/app/assets/javascripts/environments/helpers/deployment_data_transformation_helper.js b/app/assets/javascripts/environments/helpers/deployment_data_transformation_helper.js
index bfe92fe3125..9802dcbcf78 100644
--- a/app/assets/javascripts/environments/helpers/deployment_data_transformation_helper.js
+++ b/app/assets/javascripts/environments/helpers/deployment_data_transformation_helper.js
@@ -41,22 +41,46 @@ export const getCommitFromDeploymentNode = (deploymentNode) => {
};
};
+export const convertJobToDeploymentAction = (job) => {
+ return {
+ name: job.name,
+ playable: job.playable,
+ scheduledAt: job.scheduledAt,
+ playPath: `${job.webPath}/play`,
+ };
+};
+
+export const getActionsFromDeploymentNode = (deploymentNode, lastDeploymentName) => {
+ if (!deploymentNode || !lastDeploymentName) {
+ return [];
+ }
+
+ return (
+ deploymentNode.job?.deploymentPipeline?.jobs?.nodes
+ ?.filter((deployment) => deployment.name !== lastDeploymentName)
+ .map(convertJobToDeploymentAction) || []
+ );
+};
+
/**
* This function transforms deploymentNode object coming from GraphQL to object compatible with app/assets/javascripts/environments/environment_details/page.vue table
* @param {Object} deploymentNode
* @returns {Object}
*/
-export const convertToDeploymentTableRow = (deploymentNode) => {
+export const convertToDeploymentTableRow = (deploymentNode, environment) => {
+ const { lastDeployment } = environment;
+ const commit = getCommitFromDeploymentNode(deploymentNode);
return {
status: deploymentNode.status.toLowerCase(),
id: deploymentNode.iid,
triggerer: deploymentNode.triggerer,
- commit: getCommitFromDeploymentNode(deploymentNode),
+ commit,
job: deploymentNode.job && {
webPath: deploymentNode.job.webPath,
label: `${deploymentNode.job.name} (#${getIdFromGraphQLId(deploymentNode.job.id)})`,
},
created: deploymentNode.createdAt || '',
deployed: deploymentNode.finishedAt || '',
+ actions: getActionsFromDeploymentNode(deploymentNode, lastDeployment?.job?.name),
};
};