client/routes/workflow/summary.vue (333 lines of code) (raw):

<script> // Copyright (c) 2017-2024 Uber Technologies Inc. // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import { TERMINATE_DEFAULT_ERROR_MESSAGE } from './constants'; import { NOTIFICATION_TYPE_ERROR, NOTIFICATION_TYPE_SUCCESS } from '~constants'; import { getErrorMessage, getDatetimeFormattedString } from '~helpers'; import { BarLoader, ButtonFill, DataViewer, DetailList } from '~components'; import { featureFlagService, httpService } from '~services'; export default { data() { return { isAuthorized: false, isWorkflowTerminateFeatureFlagEnabled: false, terminationReason: undefined, }; }, props: [ 'baseAPIURL', 'clusterName', 'dateFormat', 'displayWorkflowId', 'domain', 'input', 'isWorkflowRunning', 'parentWorkflowRoute', 'cronSchedule', 'result', 'runId', 'timeFormat', 'timezone', 'wfStatus', 'workflow', ], components: { 'bar-loader': BarLoader, 'button-fill': ButtonFill, 'data-viewer': DataViewer, 'detail-list': DetailList, }, async mounted() { this.isWorkflowTerminateFeatureFlagEnabled = await featureFlagService.isFeatureFlagEnabled( { name: 'workflowTerminate' } ); this.initAuthorization(); }, computed: { isTerminateShown() { return this.isWorkflowTerminateFeatureFlagEnabled && this.isAuthorized; }, isTerminateDisabled() { return !this.isWorkflowRunning; }, terminateDisabledLabel() { return !this.isWorkflowRunning ? 'Workflow needs to be running to be able to terminate.' : ''; }, workflowCloseTime() { const { dateFormat, timeFormat, timezone } = this; const { closeTime } = this.workflow.workflowExecutionInfo; return closeTime ? getDatetimeFormattedString({ date: closeTime, dateFormat, timeFormat, timezone, }) : ''; }, workflowStartTime() { const { dateFormat, timeFormat, timezone } = this; const { startTime } = this.workflow.workflowExecutionInfo; return getDatetimeFormattedString({ date: startTime, dateFormat, timeFormat, timezone, }); }, }, methods: { async fetchDomainAuthorization() { const { domain } = this; try { const response = await httpService.get( `/api/domains/${domain}/authorization` ); return response.authorization; } catch (error) { this.$emit('onNotification', { message: getErrorMessage(error), type: NOTIFICATION_TYPE_ERROR, }); } }, async initAuthorization() { const isDomainAuthorizationFeatureFlagEnabled = await featureFlagService.isFeatureFlagEnabled( { name: 'domainAuthorization' } ); if (isDomainAuthorizationFeatureFlagEnabled) { const authorization = await this.fetchDomainAuthorization(); this.isAuthorized = authorization; } else { this.isAuthorized = true; } }, terminate() { this.$modal.hide('confirm-termination'); httpService .post(`${this.baseAPIURL}/terminate`, { reason: this.terminationReason, }) .then( r => { this.$emit('onNotification', { message: 'Workflow terminated.', type: NOTIFICATION_TYPE_SUCCESS, }); // eslint-disable-next-line no-console console.dir(r); }, error => { this.$emit('onNotification', { message: getErrorMessage(error, TERMINATE_DEFAULT_ERROR_MESSAGE), type: NOTIFICATION_TYPE_ERROR, }); } ); }, }, }; </script> <template> <section class="workflow-summary"> <aside class="actions"> <button-fill color="secondary" :disabled="isTerminateDisabled" :disabled-label="terminateDisabledLabel" label="TERMINATE" @click.prevent="$modal.show('confirm-termination')" v-if="isTerminateShown" /> </aside> <modal name="confirm-termination"> <h3>Are you sure you want to terminate this workflow?</h3> <input v-model="terminationReason" placeholder="Reason" /> <footer> <button-fill color="secondary" label="TERMINATE" name="button-terminate" @click.prevent="terminate" /> <button-fill color="primary" label="CANCEL" name="button-cancel" @click.prevent="$modal.hide('confirm-termination')" /> </footer> </modal> <dl v-if="workflow"> <div v-if="workflow.workflowExecutionInfo.isArchived"> <h5>Workflow archived</h5> <p> This workflow has been retrieved from archival. Some summary information may be missing. </p> </div> <div class="workflow-name"> <dt>Workflow Name</dt> <dd>{{ workflow.workflowExecutionInfo.type.name }}</dd> </div> <div class="started-at"> <dt>Started At</dt> <dd>{{ workflowStartTime }}</dd> </div> <div class="close-time" v-if="workflowCloseTime"> <dt>Closed Time</dt> <dd>{{ workflowCloseTime }}</dd> </div> <div class="workflow-status" :data-status=" wfStatus !== undefined && (typeof wfStatus === 'string' ? wfStatus : wfStatus.status) " > <dt>Status</dt> <dd> <bar-loader v-if="wfStatus === 'running'" /> <span v-if="typeof wfStatus === 'string'">{{ wfStatus }}</span> <router-link v-if="wfStatus !== undefined && wfStatus.to" :to="wfStatus.to" > {{ wfStatus.text }} </router-link> </dd> </div> <div class="workflow-result" v-if="result"> <dt>Result</dt> <dd> <data-viewer :item="result" :title="displayWorkflowId + ' Result'" /> </dd> </div> <div class="workflow-id"> <dt>Workflow Id</dt> <dd>{{ displayWorkflowId }}</dd> </div> <div class="run-id"> <dt>Run Id</dt> <dd>{{ runId }}</dd> </div> <div class="parent-workflow" v-if="parentWorkflowRoute"> <dt>Parent Workflow</dt> <dd> <router-link :to="parentWorkflowRoute.to"> {{ parentWorkflowRoute.text }} </router-link> </dd> </div> <div class="cron-schedule" v-if="cronSchedule"> <dt>Cron Schedule</dt> <dd>{{ cronSchedule }}</dd> </div> <div class="task-list"> <dt>Task List</dt> <dd> <router-link :to="{ name: 'task-list', params: { clusterName, taskList: workflow.executionConfiguration.taskList.name, }, }" > {{ workflow.executionConfiguration.taskList.name }} </router-link> </dd> </div> <div class="history-length"> <dt>History Events</dt> <dd>{{ workflow.workflowExecutionInfo.historyLength }}</dd> </div> <div class="workflow-input"> <dt>Input</dt> <dd> <data-viewer v-if="input !== undefined" :item="input" :title="displayWorkflowId + ' Input'" /> </dd> </div> <div class="pending-activities" v-if="workflow.pendingActivities"> <dt>Pending Activities</dt> <dd v-for="pa in workflow.pendingActivities" :key="pa.activityID"> <detail-list :item="pa" /> </dd> </div> </dl> </section> </template> <style lang="stylus"> @require "../../styles/definitions.styl" section.workflow-summary overflow auto padding layout-spacing-small .pending-activities { dl.details { dd { white-space: normal; } } } dl:not(.details) & > div margin-bottom 1em & > dt text-transform uppercase font-size 11px dd, dt line-height 1.5em dl.details border 1px solid uber-black-60 margin-bottom 1em dt padding 0 4px .run-id, .task-list, .workflow-id, .workflow-name, .cron-schedule dd font-weight 300 font-family monospace-font-family .workflow-status dd text-transform capitalize &[data-status="completed"] dd color uber-green &[data-status="failed"] dd color uber-orange pre max-height 18vh aside.actions float right a action-button(uber-orange) [data-modal="confirm-termination"] input margin layout-spacing-medium 0 footer display flex justify-content space-between </style>