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>