diff options
Diffstat (limited to 'app/assets/javascripts/environments')
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; -}; |