client/containers/workflow/component.vue (411 lines of code) (raw):

<script> // Copyright (c) 2017-2024 Uber Technologies Inc. // Portions of the Software are attributed to Copyright (c) 2020-2024 Temporal 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 { RETRY_COUNT_MAX, RETRY_TIMEOUT } from './constants'; import { getHistoryEvents, getHistoryTimelineEvents, getSummary, } from './helpers'; import { NOTIFICATION_TYPE_ERROR } from '~constants'; import { getErrorMessage } from '~helpers'; import { NavigationBar, NavigationLink } from '~components'; import { httpService } from '~services'; export default { data() { return { baseApiUrlRetryCount: 0, events: [], isWorkflowRunning: undefined, nextPageToken: undefined, fetchHistoryPageRetryCount: 0, wfLoading: true, history: { loading: undefined, }, summary: { input: undefined, isWorkflowRunning: undefined, parentWorkflowRoute: undefined, cronSchedule: undefined, result: undefined, wfStatus: undefined, workflow: undefined, }, taskList: {}, unwatch: [], }; }, props: [ 'clusterName', 'dateFormat', 'displayWorkflowId', 'domain', 'pendingTaskCount', 'runId', 'taskListName', 'timeFormat', 'timezone', 'workflow', 'workflowHistoryEventHighlightList', 'workflowHistoryEventHighlightListEnabled', 'workflowId', ], created() { this.unwatch.push( this.$watch('baseAPIURL', this.onBaseApiUrlChange, { immediate: true }) ); this.unwatch.push( this.$watch('historyUrl', this.onHistoryUrlChange, { immediate: true }) ); }, beforeDestroy() { this.clearWatches(); }, components: { 'navigation-bar': NavigationBar, 'navigation-link': NavigationLink, }, computed: { baseAPIURL() { const { domain, workflowId, runId } = this; return `/api/domains/${domain}/workflows/${workflowId}/${runId}`; }, historyEvents() { const { clusterName, dateFormat, events, timeFormat, timezone, workflowHistoryEventHighlightList, workflowHistoryEventHighlightListEnabled, } = this; return getHistoryEvents({ clusterName, dateFormat, events, timeFormat, timezone, workflowHistoryEventHighlightList, workflowHistoryEventHighlightListEnabled, }); }, historyTimelineEvents() { const { clusterName, historyEvents } = this; return getHistoryTimelineEvents({ clusterName, historyEvents }); }, historyUrl() { const historyUrl = `${this.baseAPIURL}/history?waitForNewEvent=true`; if (!this.nextPageToken) { return historyUrl; } return `${historyUrl}&nextPageToken=${encodeURIComponent( this.nextPageToken )}`; }, isWorkerRunning() { return this.taskList.pollers && this.taskList.pollers.length > 0; }, }, methods: { clearState() { this.events = []; this.isWorkflowRunning = undefined; this.nextPageToken = undefined; this.fetchHistoryPageRetryCount = 0; this.wfLoading = true; this.history.loading = undefined; this.summary.input = undefined; this.summary.cronSchedule = undefined; this.summary.isWorkflowRunning = undefined; this.summary.parentWorkflowRoute = undefined; this.summary.result = undefined; this.summary.wfStatus = undefined; this.summary.workflow = undefined; this.$emit('clearWorkflow'); }, clearWatches() { while (this.unwatch.length) { this.unwatch.pop()(); } }, fetchHistoryPage(pagedHistoryUrl) { if ( this._isDestroyed || !pagedHistoryUrl || this.fetchHistoryPageRetryCount >= RETRY_COUNT_MAX ) { this.history.loading = false; return Promise.resolve(); } this.history.loading = true; return httpService .get(pagedHistoryUrl) .then(res => { // eslint-disable-next-line no-underscore-dangle if (this._isDestroyed) { return null; } if (res.nextPageToken && this.nextPageToken === res.nextPageToken) { // nothing happened, and same query is still valid, so let's long pool again return this.fetchHistoryPage(pagedHistoryUrl); } if (res.nextPageToken) { this.isWorkflowRunning = JSON.parse( atob(res.nextPageToken) ).IsWorkflowRunning; this.nextPageToken = res.nextPageToken; } else { this.isWorkflowRunning = false; } const shouldHighlightEventId = this.$route.query.eventId && this.events.length <= this.$route.query.eventId; const { events } = res.history; this.events = this.events.concat(events); this.summary = getSummary({ clusterName: this.clusterName, events: this.events, isWorkflowRunning: this.isWorkflowRunning, workflow: this.workflow, }); if (shouldHighlightEventId) { this.$emit('highlight-event-id', this.$route.query.eventId); } this.fetchHistoryPageRetryCount = 0; return this.events; }) .catch(error => { // eslint-disable-next-line no-console console.error(error); // eslint-disable-next-line no-underscore-dangle if (this._isDestroyed) { return; } this.$emit('onNotification', { message: getErrorMessage(error), type: NOTIFICATION_TYPE_ERROR, }); this.fetchHistoryPageRetryCount += 1; setTimeout( () => this.fetchHistoryPage(pagedHistoryUrl), RETRY_TIMEOUT ); }) .finally(() => { // eslint-disable-next-line no-underscore-dangle if (this._isDestroyed || !this.isWorkflowRunning) { this.history.loading = false; } }); }, fetchTaskList() { const { taskListName } = this; if (!taskListName) { return Promise.reject('task list name is required'); } httpService .get( `/api/domains/${this.$route.params.domain}/task-lists/${taskListName}` ) .then( taskList => { this.taskList = { name: taskListName, ...taskList }; }, error => { this.taskList = { name: taskListName }; this.error = (error.json && error.json.message) || error.status || error.message; } ) .finally(() => { this.loading = false; }); }, fetchWorkflowInfo() { const { baseAPIURL } = this; if (this.baseApiUrlRetryCount >= RETRY_COUNT_MAX) { return; } this.wfLoading = true; return httpService .get(baseAPIURL) .then( wf => { this.$emit('setWorkflow', wf); this.isWorkflowRunning = !wf.workflowExecutionInfo.closeTime; this.baseApiUrlRetryCount = 0; return wf; }, error => { this.$emit('onNotification', { message: getErrorMessage(error), type: NOTIFICATION_TYPE_ERROR, }); this.baseApiUrlRetryCount += 1; setTimeout(() => this.fetchWorkflowInfo(), RETRY_TIMEOUT); } ) .finally(() => { this.wfLoading = false; }); }, onBaseApiUrlChange() { this.clearState(); }, async onHistoryUrlChange(historyUrl) { const workflowInfo = await this.fetchWorkflowInfo(); if (workflowInfo) { await this.fetchHistoryPage(historyUrl); this.fetchTaskList(); } }, onNotification(event) { this.$emit('onNotification', event); }, onWorkflowHistoryEventParamToggle(event) { this.$emit('onWorkflowHistoryEventParamToggle', event); }, }, }; </script> <template> <section class="execution" :class="{ loading: wfLoading || (history.loading && (!historyEvents || !historyEvents.length)), ready: !wfLoading && !history.loading, }" > <navigation-bar> <navigation-link id="nav-link-summary" icon="icon_receipt" label="Summary" :to="{ name: 'workflow/summary', params: { clusterName }, }" data-cy="summary-link" /> <navigation-link id="nav-link-history" icon="icon_trip-history" label="History" :to="{ name: 'workflow/history', params: { clusterName }, }" data-cy="history-link" /> <navigation-link id="nav-link-pending" icon="icon_send" label="Pending" :notification-count="pendingTaskCount" :to="{ name: 'workflow/pending', params: { clusterName }, }" data-cy="pending-link" /> <navigation-link id="nav-link-stack-trace" icon="icon_trips" label="Stack Trace" :to="{ name: 'workflow/stack-trace', params: { clusterName }, }" data-cy="stack-trace-link" /> <navigation-link id="nav-link-query" icon="icon_lost" label="Query" :to="{ name: 'workflow/query', params: { clusterName }, }" data-cy="query-link" /> </navigation-bar> <router-view name="summary" :baseAPIURL="baseAPIURL" :date-format="dateFormat" :display-workflow-id="displayWorkflowId" :domain="domain" :input="summary.input" :cronSchedule="summary.cronSchedule" :isWorkflowRunning="summary.isWorkflowRunning" :parentWorkflowRoute="summary.parentWorkflowRoute" :result="summary.result" :time-format="timeFormat" :timezone="timezone" :wfStatus="summary.wfStatus" :workflow="summary.workflow" @onNotification="onNotification" /> <router-view name="history" :baseAPIURL="baseAPIURL" :events="historyEvents" :isWorkflowRunning="isWorkflowRunning" :loading="history.loading" :timelineEvents="historyTimelineEvents" :workflow-history-event-highlight-list="workflowHistoryEventHighlightList" :workflow-history-event-highlight-list-enabled=" workflowHistoryEventHighlightListEnabled " @onNotification="onNotification" @onWorkflowHistoryEventParamToggle="onWorkflowHistoryEventParamToggle" /> <router-view name="pending" :baseAPIURL="baseAPIURL" :time-format="timeFormat" :timezone="timezone" /> <router-view name="stacktrace" :baseAPIURL="baseAPIURL" :date-format="dateFormat" :isWorkerRunning="isWorkerRunning" :taskListName="taskList.name" :time-format="timeFormat" :timezone="timezone" @onNotification="onNotification" /> <router-view name="query" :baseAPIURL="baseAPIURL" :isWorkerRunning="isWorkerRunning" :taskListName="taskList.name" @onNotification="onNotification" /> </section> </template>