summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/environments
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/environments')
-rw-r--r--app/assets/javascripts/environments/components/commit.vue1
-rw-r--r--app/assets/javascripts/environments/components/delete_environment_modal.vue3
-rw-r--r--app/assets/javascripts/environments/components/deployment.vue8
-rw-r--r--app/assets/javascripts/environments/components/enable_review_app_modal.vue10
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.vue10
-rw-r--r--app/assets/javascripts/environments/components/environment_folder.vue (renamed from app/assets/javascripts/environments/components/new_environment_folder.vue)19
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue390
-rw-r--r--app/assets/javascripts/environments/components/new_environment_item.vue21
-rw-r--r--app/assets/javascripts/environments/components/new_environments_app.vue252
-rw-r--r--app/assets/javascripts/environments/constants.js10
-rw-r--r--app/assets/javascripts/environments/graphql/client.js49
-rw-r--r--app/assets/javascripts/environments/graphql/queries/folder.query.graphql4
-rw-r--r--app/assets/javascripts/environments/graphql/resolvers.js17
-rw-r--r--app/assets/javascripts/environments/index.js55
-rw-r--r--app/assets/javascripts/environments/new_index.js38
15 files changed, 393 insertions, 494 deletions
diff --git a/app/assets/javascripts/environments/components/commit.vue b/app/assets/javascripts/environments/components/commit.vue
index 54b94480685..8577bf629a3 100644
--- a/app/assets/javascripts/environments/components/commit.vue
+++ b/app/assets/javascripts/environments/components/commit.vue
@@ -22,7 +22,6 @@ export default {
return this.commit?.message;
},
commitAuthorPath() {
- // eslint-disable-next-line @gitlab/require-i18n-strings
return this.commit?.author?.path || `mailto:${escape(this.commit?.authorEmail)}`;
},
commitAuthorAvatar() {
diff --git a/app/assets/javascripts/environments/components/delete_environment_modal.vue b/app/assets/javascripts/environments/components/delete_environment_modal.vue
index d3d4c7d23d8..3173c2bd644 100644
--- a/app/assets/javascripts/environments/components/delete_environment_modal.vue
+++ b/app/assets/javascripts/environments/components/delete_environment_modal.vue
@@ -62,7 +62,8 @@ export default {
mutation: deleteEnvironmentMutation,
variables: { environment: this.environment },
})
- .then(([message]) => {
+ .then(({ data }) => {
+ const [message] = data?.deleteEvironment?.errors ?? [];
if (message) {
createFlash({ message });
}
diff --git a/app/assets/javascripts/environments/components/deployment.vue b/app/assets/javascripts/environments/components/deployment.vue
index f98edb6bb7d..19284b26d51 100644
--- a/app/assets/javascripts/environments/components/deployment.vue
+++ b/app/assets/javascripts/environments/components/deployment.vue
@@ -102,6 +102,9 @@ export default {
refPath() {
return this.ref?.refPath;
},
+ needsApproval() {
+ return this.deployment.pendingApprovalCount > 0;
+ },
},
methods: {
toggleCollapse() {
@@ -116,6 +119,7 @@ export default {
showDetails: __('Show details'),
hideDetails: __('Hide details'),
triggerer: s__('Deployment|Triggerer'),
+ needsApproval: s__('Deployment|Needs Approval'),
job: __('Job'),
api: __('API'),
branch: __('Branch'),
@@ -153,6 +157,9 @@ export default {
<div :class="$options.headerDetailsClasses">
<div :class="$options.deploymentStatusClasses">
<deployment-status-badge v-if="status" :status="status" />
+ <gl-badge v-if="needsApproval" variant="warning">
+ {{ $options.i18n.needsApproval }}
+ </gl-badge>
<gl-badge v-if="latest" variant="info">{{ $options.i18n.latestBadge }}</gl-badge>
</div>
<div class="gl-display-flex gl-align-items-center gl-gap-x-5">
@@ -199,6 +206,7 @@ export default {
</gl-button>
</div>
<commit v-if="commit" :commit="commit" class="gl-mt-3" />
+ <div class="gl-mt-3"><slot name="approval"></slot></div>
<gl-collapse :visible="visible">
<div
class="gl-display-flex gl-md-align-items-center gl-mt-5 gl-flex-direction-column gl-md-flex-direction-row gl-pr-4 gl-md-pr-0"
diff --git a/app/assets/javascripts/environments/components/enable_review_app_modal.vue b/app/assets/javascripts/environments/components/enable_review_app_modal.vue
index b757c55bfdb..4d43ee156fb 100644
--- a/app/assets/javascripts/environments/components/enable_review_app_modal.vue
+++ b/app/assets/javascripts/environments/components/enable_review_app_modal.vue
@@ -1,5 +1,6 @@
<script>
import { GlLink, GlModal, GlSprintf } from '@gitlab/ui';
+import { uniqueId } from 'lodash';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
@@ -44,6 +45,11 @@ export default {
copyToClipboardText: s__('EnableReviewApp|Copy snippet text'),
title: s__('ReviewApp|Enable Review App'),
},
+ data() {
+ const modalInfoCopyId = uniqueId('enable-review-app-copy-string-');
+
+ return { modalInfoCopyId };
+ },
computed: {
modalInfoCopyStr() {
return `deploy_review:
@@ -99,14 +105,14 @@ export default {
</gl-sprintf>
</p>
<div class="gl-display-flex align-items-start">
- <pre class="gl-w-full" data-testid="enable-review-app-copy-string">
+ <pre :id="modalInfoCopyId" class="gl-w-full" data-testid="enable-review-app-copy-string">
{{ modalInfoCopyStr }} </pre
>
<modal-copy-button
:title="$options.modalInfo.copyToClipboardText"
- :text="$options.modalInfo.copyString"
:modal-id="modalId"
css-classes="border-0"
+ :target="`#${modalInfoCopyId}`"
/>
</div>
</div>
diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue
index 98c95507168..c7e024aadec 100644
--- a/app/assets/javascripts/environments/components/environment_actions.vue
+++ b/app/assets/javascripts/environments/components/environment_actions.vue
@@ -1,5 +1,6 @@
<script>
import { GlDropdown, GlDropdownItem, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { formatTime } from '~/lib/utils/datetime_utility';
import { __, s__, sprintf } from '~/locale';
import eventHub from '../event_hub';
@@ -37,7 +38,7 @@ export default {
},
},
methods: {
- onClickAction(action) {
+ async onClickAction(action) {
if (action.scheduledAt) {
const confirmationMessage = sprintf(
s__(
@@ -45,9 +46,10 @@ export default {
),
{ jobName: action.name },
);
- // https://gitlab.com/gitlab-org/gitlab-foss/issues/52156
- // eslint-disable-next-line no-alert
- if (!window.confirm(confirmationMessage)) {
+
+ const confirmed = await confirmAction(confirmationMessage);
+
+ if (!confirmed) {
return;
}
}
diff --git a/app/assets/javascripts/environments/components/new_environment_folder.vue b/app/assets/javascripts/environments/components/environment_folder.vue
index 0d3867a4d74..d5c6d26cfd0 100644
--- a/app/assets/javascripts/environments/components/new_environment_folder.vue
+++ b/app/assets/javascripts/environments/components/environment_folder.vue
@@ -1,7 +1,9 @@
<script>
import { GlButton, GlCollapse, GlIcon, GlBadge, GlLink } from '@gitlab/ui';
import { __, s__ } from '~/locale';
+import pollIntervalQuery from '../graphql/queries/poll_interval.query.graphql';
import folderQuery from '../graphql/queries/folder.query.graphql';
+import { ENVIRONMENT_COUNT_BY_SCOPE } from '../constants';
import EnvironmentItem from './new_environment_item.vue';
export default {
@@ -18,16 +20,26 @@ export default {
type: Object,
required: true,
},
+ scope: {
+ type: String,
+ required: true,
+ },
},
data() {
- return { visible: false };
+ return { visible: false, interval: undefined };
},
apollo: {
folder: {
query: folderQuery,
variables() {
- return { environment: this.nestedEnvironment.latest };
+ return { environment: this.nestedEnvironment.latest, scope: this.scope };
},
+ pollInterval() {
+ return this.interval;
+ },
+ },
+ interval: {
+ query: pollIntervalQuery,
},
},
i18n: {
@@ -45,7 +57,8 @@ export default {
return this.visible ? this.$options.i18n.collapse : this.$options.i18n.expand;
},
count() {
- return this.folder?.availableCount ?? 0;
+ const count = ENVIRONMENT_COUNT_BY_SCOPE[this.scope];
+ return this.folder?.[count] ?? 0;
},
folderClass() {
return { 'gl-font-weight-bold': this.visible };
diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue
index acc16ecd874..c7008c03099 100644
--- a/app/assets/javascripts/environments/components/environments_app.vue
+++ b/app/assets/javascripts/environments/components/environments_app.vue
@@ -1,188 +1,272 @@
<script>
-import { GlBadge, GlButton, GlModalDirective, GlTab, GlTabs } from '@gitlab/ui';
-import createFlash from '~/flash';
-import { s__ } from '~/locale';
-import eventHub from '../event_hub';
-import environmentsMixin from '../mixins/environments_mixin';
-import EnvironmentsPaginationApiMixin from '../mixins/environments_pagination_api_mixin';
-import ConfirmRollbackModal from './confirm_rollback_modal.vue';
-import DeleteEnvironmentModal from './delete_environment_modal.vue';
-import emptyState from './empty_state.vue';
+import { GlBadge, GlPagination, GlTab, GlTabs } from '@gitlab/ui';
+import { s__, __, sprintf } from '~/locale';
+import { updateHistory, setUrlParams, queryToObject } from '~/lib/utils/url_utility';
+import environmentAppQuery from '../graphql/queries/environment_app.query.graphql';
+import pollIntervalQuery from '../graphql/queries/poll_interval.query.graphql';
+import pageInfoQuery from '../graphql/queries/page_info.query.graphql';
+import environmentToDeleteQuery from '../graphql/queries/environment_to_delete.query.graphql';
+import environmentToRollbackQuery from '../graphql/queries/environment_to_rollback.query.graphql';
+import environmentToStopQuery from '../graphql/queries/environment_to_stop.query.graphql';
+import environmentToChangeCanaryQuery from '../graphql/queries/environment_to_change_canary.query.graphql';
+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 EnvironmentItem from './new_environment_item.vue';
+import ConfirmRollbackModal from './confirm_rollback_modal.vue';
+import DeleteEnvironmentModal from './delete_environment_modal.vue';
+import CanaryUpdateModal from './canary_update_modal.vue';
+import EmptyState from './empty_state.vue';
export default {
- i18n: {
- newEnvironmentButtonLabel: s__('Environments|New environment'),
- reviewAppButtonLabel: s__('Environments|Enable review app'),
- },
- modal: {
- id: 'enable-review-app-info',
- },
components: {
+ DeleteEnvironmentModal,
+ CanaryUpdateModal,
ConfirmRollbackModal,
- emptyState,
+ EmptyState,
+ EnvironmentFolder,
EnableReviewAppModal,
+ EnvironmentItem,
+ StopEnvironmentModal,
GlBadge,
- GlButton,
+ GlPagination,
GlTab,
GlTabs,
- StopEnvironmentModal,
- DeleteEnvironmentModal,
},
- directives: {
- 'gl-modal': GlModalDirective,
- },
- mixins: [EnvironmentsPaginationApiMixin, environmentsMixin],
- props: {
- endpoint: {
- type: String,
- required: true,
+ apollo: {
+ environmentApp: {
+ query: environmentAppQuery,
+ variables() {
+ return {
+ scope: this.scope,
+ page: this.page ?? 1,
+ };
+ },
+ pollInterval() {
+ return this.interval;
+ },
+ },
+ interval: {
+ query: pollIntervalQuery,
+ },
+ pageInfo: {
+ query: pageInfoQuery,
+ },
+ environmentToDelete: {
+ query: environmentToDeleteQuery,
},
- canCreateEnvironment: {
- type: Boolean,
- required: true,
+ environmentToRollback: {
+ query: environmentToRollbackQuery,
},
- newEnvironmentPath: {
- type: String,
- required: true,
+ environmentToStop: {
+ query: environmentToStopQuery,
},
- helpPagePath: {
- type: String,
- required: true,
+ environmentToChangeCanary: {
+ query: environmentToChangeCanaryQuery,
+ },
+ weight: {
+ query: environmentToChangeCanaryQuery,
},
},
-
- created() {
- eventHub.$on('toggleFolder', this.toggleFolder);
- eventHub.$on('toggleDeployBoard', this.toggleDeployBoard);
+ inject: ['newEnvironmentPath', 'canCreateEnvironment', 'helpPagePath'],
+ i18n: {
+ newEnvironmentButtonLabel: s__('Environments|New environment'),
+ reviewAppButtonLabel: s__('Environments|Enable review app'),
+ available: __('Available'),
+ stopped: __('Stopped'),
+ prevPage: __('Go to previous page'),
+ nextPage: __('Go to next page'),
+ next: __('Next'),
+ prev: __('Prev'),
+ goto: (page) => sprintf(__('Go to page %{page}'), { page }),
},
-
- beforeDestroy() {
- // eslint-disable-next-line @gitlab/no-global-event-off
- eventHub.$off('toggleFolder');
- // eslint-disable-next-line @gitlab/no-global-event-off
- eventHub.$off('toggleDeployBoard');
+ modalId: 'enable-review-app-info',
+ data() {
+ const { page = '1', scope } = queryToObject(window.location.search);
+ return {
+ interval: undefined,
+ isReviewAppModalVisible: false,
+ page: parseInt(page, 10),
+ pageInfo: {},
+ scope: Object.values(ENVIRONMENTS_SCOPE).includes(scope)
+ ? scope
+ : ENVIRONMENTS_SCOPE.AVAILABLE,
+ environmentToDelete: {},
+ environmentToRollback: {},
+ environmentToStop: {},
+ environmentToChangeCanary: {},
+ weight: 0,
+ };
},
-
- methods: {
- toggleDeployBoard(model) {
- this.store.toggleDeployBoard(model.id);
+ computed: {
+ canSetupReviewApp() {
+ return this.environmentApp?.reviewApp?.canSetupReviewApp;
},
- toggleFolder(folder) {
- this.store.toggleFolder(folder);
-
- if (!folder.isOpen) {
- this.fetchChildEnvironments(folder, true);
- }
+ folders() {
+ return this.environmentApp?.environments?.filter((e) => e.size > 1) ?? [];
},
-
- fetchChildEnvironments(folder, showLoader = false) {
- this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader);
-
- this.service
- .getFolderContent(folder.folder_path, folder.state)
- .then((response) => this.store.setfolderContent(folder, response.data.environments))
- .then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false))
- .catch(() => {
- createFlash({
- message: s__('Environments|An error occurred while fetching the environments.'),
- });
- this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false);
- });
+ environments() {
+ return this.environmentApp?.environments?.filter((e) => e.size === 1) ?? [];
},
+ hasEnvironments() {
+ return this.environments.length > 0 || this.folders.length > 0;
+ },
+ availableCount() {
+ return this.environmentApp?.availableCount;
+ },
+ addEnvironment() {
+ if (!this.canCreateEnvironment) {
+ return null;
+ }
- successCallback(resp) {
- this.saveData(resp);
-
- // We need to verify if any folder is open to also update it
- const openFolders = this.store.getOpenFolders();
- if (openFolders.length) {
- openFolders.forEach((folder) => this.fetchChildEnvironments(folder));
+ return {
+ text: this.$options.i18n.newEnvironmentButtonLabel,
+ attributes: {
+ href: this.newEnvironmentPath,
+ category: 'primary',
+ variant: 'confirm',
+ },
+ };
+ },
+ openReviewAppModal() {
+ if (!this.canSetupReviewApp) {
+ return null;
}
+
+ return {
+ text: this.$options.i18n.reviewAppButtonLabel,
+ attributes: {
+ category: 'secondary',
+ variant: 'confirm',
+ },
+ };
+ },
+ stoppedCount() {
+ return this.environmentApp?.stoppedCount;
},
+ totalItems() {
+ return this.pageInfo?.total;
+ },
+ itemsPerPage() {
+ return this.pageInfo?.perPage;
+ },
+ },
+ mounted() {
+ window.addEventListener('popstate', this.syncPageFromQueryParams);
},
+ destroyed() {
+ window.removeEventListener('popstate', this.syncPageFromQueryParams);
+ this.$apollo.queries.environmentApp.stopPolling();
+ },
+ methods: {
+ showReviewAppModal() {
+ this.isReviewAppModalVisible = true;
+ },
+ setScope(scope) {
+ this.scope = scope;
+ this.moveToPage(1);
+ },
+ movePage(direction) {
+ this.moveToPage(this.pageInfo[`${direction}Page`]);
+ },
+ moveToPage(page) {
+ this.page = page;
+ updateHistory({
+ url: setUrlParams({ page: this.page }),
+ title: document.title,
+ });
+ this.resetPolling();
+ },
+ syncPageFromQueryParams() {
+ const { page = '1' } = queryToObject(window.location.search);
+ this.page = parseInt(page, 10);
+ },
+ resetPolling() {
+ this.$apollo.queries.environmentApp.stopPolling();
+ this.$apollo.queries.environmentApp.refetch();
+ this.$nextTick(() => {
+ if (this.interval) {
+ this.$apollo.queries.environmentApp.startPolling(this.interval);
+ }
+ });
+ },
+ },
+ ENVIRONMENTS_SCOPE,
};
</script>
<template>
- <div class="environments-section">
- <stop-environment-modal :environment="environmentInStopModal" />
- <delete-environment-modal :environment="environmentInDeleteModal" />
- <confirm-rollback-modal :environment="environmentInRollbackModal" />
-
- <div class="gl-w-full">
- <div class="gl-display-flex gl-flex-direction-column gl-mt-3 gl-md-display-none!">
- <gl-button
- v-if="state.reviewAppDetails.can_setup_review_app"
- v-gl-modal="$options.modal.id"
- data-testid="enable-review-app"
- variant="info"
- category="secondary"
- type="button"
- class="gl-mb-3 gl-flex-grow-1"
- >{{ $options.i18n.reviewAppButtonLabel }}</gl-button
- >
- <gl-button
- v-if="canCreateEnvironment"
- :href="newEnvironmentPath"
- data-testid="new-environment"
- category="primary"
- variant="confirm"
- >{{ $options.i18n.newEnvironmentButtonLabel }}</gl-button
- >
- </div>
- <gl-tabs :value="activeTab" content-class="gl-display-none">
- <gl-tab
- v-for="(tab, idx) in tabs"
- :key="idx"
- :title-item-class="`js-environments-tab-${tab.scope}`"
- @click="onChangeTab(tab.scope)"
- >
- <template #title>
- <span>{{ tab.name }}</span>
- <gl-badge size="sm" class="gl-tab-counter-badge">{{ tab.count }}</gl-badge>
- </template>
- </gl-tab>
- <template #tabs-end>
- <div
- class="gl-display-none gl-md-display-flex gl-lg-align-items-center gl-lg-flex-direction-row gl-lg-flex-fill-1 gl-lg-justify-content-end gl-lg-mt-0"
- >
- <gl-button
- v-if="state.reviewAppDetails.can_setup_review_app"
- v-gl-modal="$options.modal.id"
- data-testid="enable-review-app"
- variant="info"
- category="secondary"
- type="button"
- class="gl-mb-3 gl-lg-mr-3 gl-lg-mb-0"
- >{{ $options.i18n.reviewAppButtonLabel }}</gl-button
- >
- <gl-button
- v-if="canCreateEnvironment"
- :href="newEnvironmentPath"
- data-testid="new-environment"
- category="primary"
- variant="confirm"
- >{{ $options.i18n.newEnvironmentButtonLabel }}</gl-button
- >
- </div>
+ <div>
+ <enable-review-app-modal
+ v-if="canSetupReviewApp"
+ v-model="isReviewAppModalVisible"
+ :modal-id="$options.modalId"
+ data-testid="enable-review-app-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"
+ sync-active-tab-with-query-params
+ query-param-name="scope"
+ @primary="showReviewAppModal"
+ >
+ <gl-tab
+ :query-param-value="$options.ENVIRONMENTS_SCOPE.AVAILABLE"
+ @click="setScope($options.ENVIRONMENTS_SCOPE.AVAILABLE)"
+ >
+ <template #title>
+ <span>{{ $options.i18n.available }}</span>
+ <gl-badge size="sm" class="gl-tab-counter-badge">
+ {{ availableCount }}
+ </gl-badge>
</template>
- </gl-tabs>
- <container
- :is-loading="isLoading"
- :environments="state.environments"
- :pagination="state.paginationInformation"
- @onChangePage="onChangePage"
+ </gl-tab>
+ <gl-tab
+ :query-param-value="$options.ENVIRONMENTS_SCOPE.STOPPED"
+ @click="setScope($options.ENVIRONMENTS_SCOPE.STOPPED)"
>
- <template v-if="!isLoading && state.environments.length === 0" #empty-state>
- <empty-state :help-path="helpPagePath" />
+ <template #title>
+ <span>{{ $options.i18n.stopped }}</span>
+ <gl-badge size="sm" class="gl-tab-counter-badge">
+ {{ stoppedCount }}
+ </gl-badge>
</template>
- </container>
- <enable-review-app-modal
- v-if="state.reviewAppDetails.can_setup_review_app"
- :modal-id="$options.modal.id"
- data-testid="enable-review-app-modal"
+ </gl-tab>
+ </gl-tabs>
+ <template v-if="hasEnvironments">
+ <environment-folder
+ v-for="folder in folders"
+ :key="folder.name"
+ class="gl-mb-3"
+ :scope="scope"
+ :nested-environment="folder"
+ />
+ <environment-item
+ v-for="environment in environments"
+ :key="environment.name"
+ class="gl-mb-3 gl-border-gray-100 gl-border-1 gl-border-b-solid"
+ :environment="environment.latest"
+ @change="resetPolling"
/>
- </div>
+ </template>
+ <empty-state v-else :help-path="helpPagePath" />
+ <gl-pagination
+ align="center"
+ :total-items="totalItems"
+ :per-page="itemsPerPage"
+ :value="page"
+ :next="$options.i18n.next"
+ :prev="$options.i18n.prev"
+ :label-previous-page="$options.prevPage"
+ :label-next-page="$options.nextPage"
+ :label-page="$options.goto"
+ @next="movePage('next')"
+ @previous="movePage('previous')"
+ @input="moveToPage"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/environments/components/new_environment_item.vue b/app/assets/javascripts/environments/components/new_environment_item.vue
index 27a763fb9c4..f35fabccae7 100644
--- a/app/assets/javascripts/environments/components/new_environment_item.vue
+++ b/app/assets/javascripts/environments/components/new_environment_item.vue
@@ -40,6 +40,9 @@ export default {
Terminal,
TimeAgoTooltip,
Delete,
+ EnvironmentAlert: () => import('ee_component/environments/components/environment_alert.vue'),
+ EnvironmentApproval: () =>
+ import('ee_component/environments/components/environment_approval.vue'),
},
directives: {
GlTooltip,
@@ -97,6 +100,9 @@ export default {
hasDeployment() {
return Boolean(this.environment?.upcomingDeployment || this.environment?.lastDeployment);
},
+ hasOpenedAlert() {
+ return this.environment?.hasOpenedAlert;
+ },
actions() {
if (!this.lastDeployment) {
return [];
@@ -296,12 +302,20 @@ export default {
class="gl-pl-4"
/>
</div>
- <div v-if="upcomingDeployment" :class="$options.deploymentClasses">
+ <div
+ v-if="upcomingDeployment"
+ :class="$options.deploymentClasses"
+ data-testid="upcoming-deployment-content"
+ >
<deployment
:deployment="upcomingDeployment"
:class="{ 'gl-ml-7': inFolder }"
class="gl-pl-4"
- />
+ >
+ <template #approval>
+ <environment-approval :environment="environment" @change="$emit('change')" />
+ </template>
+ </deployment>
</div>
</template>
<div v-else :class="$options.deploymentClasses">
@@ -319,6 +333,9 @@ export default {
class="gl-pl-4"
/>
</div>
+ <div v-if="hasOpenedAlert" class="gl-bg-gray-10 gl-md-px-7">
+ <environment-alert :environment="environment" class="gl-pl-4 gl-py-5" />
+ </div>
</gl-collapse>
</div>
</template>
diff --git a/app/assets/javascripts/environments/components/new_environments_app.vue b/app/assets/javascripts/environments/components/new_environments_app.vue
deleted file mode 100644
index 3699f39b611..00000000000
--- a/app/assets/javascripts/environments/components/new_environments_app.vue
+++ /dev/null
@@ -1,252 +0,0 @@
-<script>
-import { GlBadge, GlPagination, GlTab, GlTabs } from '@gitlab/ui';
-import { s__, __, sprintf } from '~/locale';
-import { updateHistory, setUrlParams, queryToObject } from '~/lib/utils/url_utility';
-import environmentAppQuery from '../graphql/queries/environment_app.query.graphql';
-import pollIntervalQuery from '../graphql/queries/poll_interval.query.graphql';
-import pageInfoQuery from '../graphql/queries/page_info.query.graphql';
-import environmentToDeleteQuery from '../graphql/queries/environment_to_delete.query.graphql';
-import environmentToRollbackQuery from '../graphql/queries/environment_to_rollback.query.graphql';
-import environmentToStopQuery from '../graphql/queries/environment_to_stop.query.graphql';
-import environmentToChangeCanaryQuery from '../graphql/queries/environment_to_change_canary.query.graphql';
-import EnvironmentFolder from './new_environment_folder.vue';
-import EnableReviewAppModal from './enable_review_app_modal.vue';
-import StopEnvironmentModal from './stop_environment_modal.vue';
-import EnvironmentItem from './new_environment_item.vue';
-import ConfirmRollbackModal from './confirm_rollback_modal.vue';
-import DeleteEnvironmentModal from './delete_environment_modal.vue';
-import CanaryUpdateModal from './canary_update_modal.vue';
-
-export default {
- components: {
- DeleteEnvironmentModal,
- CanaryUpdateModal,
- ConfirmRollbackModal,
- EnvironmentFolder,
- EnableReviewAppModal,
- EnvironmentItem,
- StopEnvironmentModal,
- GlBadge,
- GlPagination,
- GlTab,
- GlTabs,
- },
- apollo: {
- environmentApp: {
- query: environmentAppQuery,
- variables() {
- return {
- scope: this.scope,
- page: this.page ?? 1,
- };
- },
- pollInterval() {
- return this.interval;
- },
- },
- interval: {
- query: pollIntervalQuery,
- },
- pageInfo: {
- query: pageInfoQuery,
- },
- environmentToDelete: {
- query: environmentToDeleteQuery,
- },
- environmentToRollback: {
- query: environmentToRollbackQuery,
- },
- environmentToStop: {
- query: environmentToStopQuery,
- },
- environmentToChangeCanary: {
- query: environmentToChangeCanaryQuery,
- },
- weight: {
- query: environmentToChangeCanaryQuery,
- },
- },
- inject: ['newEnvironmentPath', 'canCreateEnvironment'],
- i18n: {
- newEnvironmentButtonLabel: s__('Environments|New environment'),
- reviewAppButtonLabel: s__('Environments|Enable review app'),
- available: __('Available'),
- stopped: __('Stopped'),
- prevPage: __('Go to previous page'),
- nextPage: __('Go to next page'),
- next: __('Next'),
- prev: __('Prev'),
- goto: (page) => sprintf(__('Go to page %{page}'), { page }),
- },
- modalId: 'enable-review-app-info',
- data() {
- const { page = '1', scope = 'available' } = queryToObject(window.location.search);
- return {
- interval: undefined,
- isReviewAppModalVisible: false,
- page: parseInt(page, 10),
- scope,
- environmentToDelete: {},
- environmentToRollback: {},
- environmentToStop: {},
- environmentToChangeCanary: {},
- weight: 0,
- };
- },
- computed: {
- canSetupReviewApp() {
- return this.environmentApp?.reviewApp?.canSetupReviewApp;
- },
- folders() {
- return this.environmentApp?.environments?.filter((e) => e.size > 1) ?? [];
- },
- environments() {
- return this.environmentApp?.environments?.filter((e) => e.size === 1) ?? [];
- },
- availableCount() {
- return this.environmentApp?.availableCount;
- },
- addEnvironment() {
- if (!this.canCreateEnvironment) {
- return null;
- }
-
- return {
- text: this.$options.i18n.newEnvironmentButtonLabel,
- attributes: {
- href: this.newEnvironmentPath,
- category: 'primary',
- variant: 'confirm',
- },
- };
- },
- openReviewAppModal() {
- if (!this.canSetupReviewApp) {
- return null;
- }
-
- return {
- text: this.$options.i18n.reviewAppButtonLabel,
- attributes: {
- category: 'secondary',
- variant: 'confirm',
- },
- };
- },
- stoppedCount() {
- return this.environmentApp?.stoppedCount;
- },
- totalItems() {
- return this.pageInfo?.total;
- },
- itemsPerPage() {
- return this.pageInfo?.perPage;
- },
- },
- mounted() {
- window.addEventListener('popstate', this.syncPageFromQueryParams);
- },
- destroyed() {
- window.removeEventListener('popstate', this.syncPageFromQueryParams);
- this.$apollo.queries.environmentApp.stopPolling();
- },
- methods: {
- showReviewAppModal() {
- this.isReviewAppModalVisible = true;
- },
- setScope(scope) {
- this.scope = scope;
- this.moveToPage(1);
- },
- movePage(direction) {
- this.moveToPage(this.pageInfo[`${direction}Page`]);
- },
- moveToPage(page) {
- this.page = page;
- updateHistory({
- url: setUrlParams({ page: this.page }),
- title: document.title,
- });
- this.resetPolling();
- },
- syncPageFromQueryParams() {
- const { page = '1' } = queryToObject(window.location.search);
- this.page = parseInt(page, 10);
- },
- resetPolling() {
- this.$apollo.queries.environmentApp.stopPolling();
- this.$nextTick(() => {
- if (this.interval) {
- this.$apollo.queries.environmentApp.startPolling(this.interval);
- } else {
- this.$apollo.queries.environmentApp.refetch({ scope: this.scope, page: this.page });
- }
- });
- },
- },
-};
-</script>
-<template>
- <div>
- <enable-review-app-modal
- v-if="canSetupReviewApp"
- v-model="isReviewAppModalVisible"
- :modal-id="$options.modalId"
- data-testid="enable-review-app-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"
- sync-active-tab-with-query-params
- query-param-name="scope"
- @primary="showReviewAppModal"
- >
- <gl-tab query-param-value="available" @click="setScope('available')">
- <template #title>
- <span>{{ $options.i18n.available }}</span>
- <gl-badge size="sm" class="gl-tab-counter-badge">
- {{ availableCount }}
- </gl-badge>
- </template>
- </gl-tab>
- <gl-tab query-param-value="stopped" @click="setScope('stopped')">
- <template #title>
- <span>{{ $options.i18n.stopped }}</span>
- <gl-badge size="sm" class="gl-tab-counter-badge">
- {{ stoppedCount }}
- </gl-badge>
- </template>
- </gl-tab>
- </gl-tabs>
- <environment-folder
- v-for="folder in folders"
- :key="folder.name"
- class="gl-mb-3"
- :nested-environment="folder"
- />
- <environment-item
- v-for="environment in environments"
- :key="environment.name"
- class="gl-mb-3 gl-border-gray-100 gl-border-1 gl-border-b-solid"
- :environment="environment.latest"
- />
- <gl-pagination
- align="center"
- :total-items="totalItems"
- :per-page="itemsPerPage"
- :value="page"
- :next="$options.i18n.next"
- :prev="$options.i18n.prev"
- :label-previous-page="$options.prevPage"
- :label-next-page="$options.nextPage"
- :label-page="$options.goto"
- @next="movePage('next')"
- @previous="movePage('previous')"
- @input="moveToPage"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/environments/constants.js b/app/assets/javascripts/environments/constants.js
index 6d427bef4e6..942491039d6 100644
--- a/app/assets/javascripts/environments/constants.js
+++ b/app/assets/javascripts/environments/constants.js
@@ -38,3 +38,13 @@ export const CANARY_STATUS = {
};
export const CANARY_UPDATE_MODAL = 'confirm-canary-change';
+
+export const ENVIRONMENTS_SCOPE = {
+ AVAILABLE: 'available',
+ STOPPED: 'stopped',
+};
+
+export const ENVIRONMENT_COUNT_BY_SCOPE = {
+ [ENVIRONMENTS_SCOPE.AVAILABLE]: 'availableCount',
+ [ENVIRONMENTS_SCOPE.STOPPED]: 'stoppedCount',
+};
diff --git a/app/assets/javascripts/environments/graphql/client.js b/app/assets/javascripts/environments/graphql/client.js
index 64b18c2003b..26514b59995 100644
--- a/app/assets/javascripts/environments/graphql/client.js
+++ b/app/assets/javascripts/environments/graphql/client.js
@@ -2,6 +2,9 @@ import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import environmentApp from './queries/environment_app.query.graphql';
import pageInfoQuery from './queries/page_info.query.graphql';
+import environmentToDeleteQuery from './queries/environment_to_delete.query.graphql';
+import environmentToRollbackQuery from './queries/environment_to_rollback.query.graphql';
+import environmentToStopQuery from './queries/environment_to_stop.query.graphql';
import { resolvers } from './resolvers';
import typeDefs from './typedefs.graphql';
@@ -33,6 +36,52 @@ export const apolloProvider = (endpoint) => {
},
},
});
+
+ cache.writeQuery({
+ query: environmentToDeleteQuery,
+ data: {
+ environmentToDelete: {
+ name: 'null',
+ __typename: 'LocalEnvironment',
+ id: '0',
+ deletePath: null,
+ folderPath: null,
+ retryUrl: null,
+ autoStopPath: null,
+ lastDeployment: null,
+ },
+ },
+ });
+ cache.writeQuery({
+ query: environmentToStopQuery,
+ data: {
+ environmentToStop: {
+ name: 'null',
+ __typename: 'LocalEnvironment',
+ id: '0',
+ deletePath: null,
+ folderPath: null,
+ retryUrl: null,
+ autoStopPath: null,
+ lastDeployment: null,
+ },
+ },
+ });
+ cache.writeQuery({
+ query: environmentToRollbackQuery,
+ data: {
+ environmentToRollback: {
+ name: 'null',
+ __typename: 'LocalEnvironment',
+ id: '0',
+ deletePath: null,
+ folderPath: null,
+ retryUrl: null,
+ autoStopPath: null,
+ lastDeployment: null,
+ },
+ },
+ });
return new VueApollo({
defaultClient,
});
diff --git a/app/assets/javascripts/environments/graphql/queries/folder.query.graphql b/app/assets/javascripts/environments/graphql/queries/folder.query.graphql
index 3292c916b2e..e8c145ee916 100644
--- a/app/assets/javascripts/environments/graphql/queries/folder.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/folder.query.graphql
@@ -1,5 +1,5 @@
-query getEnvironmentFolder($environment: NestedLocalEnvironment) {
- folder(environment: $environment) @client {
+query getEnvironmentFolder($environment: NestedLocalEnvironment, $scope: String) {
+ folder(environment: $environment, scope: $scope) @client {
availableCount
environments
stoppedCount
diff --git a/app/assets/javascripts/environments/graphql/resolvers.js b/app/assets/javascripts/environments/graphql/resolvers.js
index dc763b77157..a7866c1e778 100644
--- a/app/assets/javascripts/environments/graphql/resolvers.js
+++ b/app/assets/javascripts/environments/graphql/resolvers.js
@@ -11,6 +11,7 @@ import environmentToRollbackQuery from './queries/environment_to_rollback.query.
import environmentToStopQuery from './queries/environment_to_stop.query.graphql';
import environmentToDeleteQuery from './queries/environment_to_delete.query.graphql';
import environmentToChangeCanaryQuery from './queries/environment_to_change_canary.query.graphql';
+import isEnvironmentStoppingQuery from './queries/is_environment_stopping.query.graphql';
import pageInfoQuery from './queries/page_info.query.graphql';
const buildErrors = (errors = []) => ({
@@ -58,8 +59,8 @@ export const resolvers = (endpoint) => ({
};
});
},
- folder(_, { environment: { folderPath } }) {
- return axios.get(folderPath, { params: { per_page: 3 } }).then((res) => ({
+ folder(_, { environment: { folderPath }, scope }) {
+ return axios.get(folderPath, { params: { scope, per_page: 3 } }).then((res) => ({
availableCount: res.data.available_count,
environments: res.data.environments.map(mapEnvironment),
stoppedCount: res.data.stopped_count,
@@ -71,11 +72,21 @@ export const resolvers = (endpoint) => ({
},
},
Mutation: {
- stopEnvironment(_, { environment }) {
+ stopEnvironment(_, { environment }, { client }) {
+ client.writeQuery({
+ query: isEnvironmentStoppingQuery,
+ variables: { environment },
+ data: { isEnvironmentStopping: true },
+ });
return axios
.post(environment.stopPath)
.then(() => buildErrors())
.catch(() => {
+ client.writeQuery({
+ query: isEnvironmentStoppingQuery,
+ variables: { environment },
+ data: { isEnvironmentStopping: false },
+ });
return buildErrors([
s__('Environments|An error occurred while stopping the environment, please try again'),
]);
diff --git a/app/assets/javascripts/environments/index.js b/app/assets/javascripts/environments/index.js
index 3b1d35c1f22..d9a523fd806 100644
--- a/app/assets/javascripts/environments/index.js
+++ b/app/assets/javascripts/environments/index.js
@@ -1,48 +1,37 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '../lib/utils/common_utils';
-import Translate from '../vue_shared/translate';
-import environmentsComponent from './components/environments_app.vue';
+import { apolloProvider } from './graphql/client';
+import EnvironmentsApp from './components/environments_app.vue';
-Vue.use(Translate);
Vue.use(VueApollo);
-const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(),
-});
-
export default (el) => {
if (el) {
+ const {
+ canCreateEnvironment,
+ endpoint,
+ newEnvironmentPath,
+ helpPagePath,
+ projectPath,
+ defaultBranchName,
+ projectId,
+ } = el.dataset;
+
return new Vue({
el,
- components: {
- environmentsComponent,
- },
- apolloProvider,
+ apolloProvider: apolloProvider(endpoint),
provide: {
- projectPath: el.dataset.projectPath,
- defaultBranchName: el.dataset.defaultBranchName,
- },
- data() {
- const environmentsData = el.dataset;
-
- return {
- endpoint: environmentsData.environmentsDataEndpoint,
- newEnvironmentPath: environmentsData.newEnvironmentPath,
- helpPagePath: environmentsData.helpPagePath,
- canCreateEnvironment: parseBoolean(environmentsData.canCreateEnvironment),
- };
+ projectPath,
+ defaultBranchName,
+ endpoint,
+ newEnvironmentPath,
+ helpPagePath,
+ projectId,
+ canCreateEnvironment: parseBoolean(canCreateEnvironment),
},
- render(createElement) {
- return createElement('environments-component', {
- props: {
- endpoint: this.endpoint,
- newEnvironmentPath: this.newEnvironmentPath,
- helpPagePath: this.helpPagePath,
- canCreateEnvironment: this.canCreateEnvironment,
- },
- });
+ render(h) {
+ return h(EnvironmentsApp);
},
});
}
diff --git a/app/assets/javascripts/environments/new_index.js b/app/assets/javascripts/environments/new_index.js
deleted file mode 100644
index dd5c709c75a..00000000000
--- a/app/assets/javascripts/environments/new_index.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import { parseBoolean } from '../lib/utils/common_utils';
-import { apolloProvider } from './graphql/client';
-import EnvironmentsApp from './components/new_environments_app.vue';
-
-Vue.use(VueApollo);
-
-export default (el) => {
- if (el) {
- const {
- canCreateEnvironment,
- endpoint,
- newEnvironmentPath,
- helpPagePath,
- projectPath,
- defaultBranchName,
- } = el.dataset;
-
- return new Vue({
- el,
- apolloProvider: apolloProvider(endpoint),
- provide: {
- projectPath,
- defaultBranchName,
- endpoint,
- newEnvironmentPath,
- helpPagePath,
- canCreateEnvironment: parseBoolean(canCreateEnvironment),
- },
- render(h) {
- return h(EnvironmentsApp);
- },
- });
- }
-
- return null;
-};