From e93ce4436521d35c85829f935202588abe503728 Mon Sep 17 00:00:00 2001 From: Cees Voesenek Date: Wed, 18 Dec 2024 13:12:17 +0100 Subject: [PATCH 1/9] Add task runs panel This adds a button to the application bar that opens a task runs menu, showing currently running tasks and their properties. --- .../tasks/BaseTaskFilterControl.vue | 114 ++++++++++++ src/components/tasks/PeriodFilterControl.vue | 68 ++++++++ src/components/tasks/TaskRunProgress.vue | 120 +++++++++++++ src/components/tasks/TaskRunSummary.vue | 164 ++++++++++++++++++ src/components/tasks/TaskRunsPanel.vue | 157 +++++++++++++++++ .../tasks/TaskStatusFilterControl.vue | 87 ++++++++++ .../tasks/WorkflowFilterControl.vue | 31 ++++ src/layouts/DefaultLayout.vue | 14 +- src/lib/date/index.ts | 3 + src/lib/period/convert.ts | 12 ++ src/lib/period/index.ts | 1 + src/lib/period/types.ts | 9 + src/lib/taskruns/convert.ts | 84 +++++++++ src/lib/taskruns/index.ts | 3 + src/lib/taskruns/status.ts | 87 ++++++++++ src/lib/taskruns/types.ts | 32 ++++ src/lib/timer/create.ts | 44 +++++ src/lib/timer/index.ts | 2 + src/lib/timer/types.ts | 3 + src/services/useTasksRuns/index.ts | 126 ++++++++++++++ src/views/TopologyDisplayView.vue | 24 ++- 21 files changed, 1181 insertions(+), 4 deletions(-) create mode 100644 src/components/tasks/BaseTaskFilterControl.vue create mode 100644 src/components/tasks/PeriodFilterControl.vue create mode 100644 src/components/tasks/TaskRunProgress.vue create mode 100644 src/components/tasks/TaskRunSummary.vue create mode 100644 src/components/tasks/TaskRunsPanel.vue create mode 100644 src/components/tasks/TaskStatusFilterControl.vue create mode 100644 src/components/tasks/WorkflowFilterControl.vue create mode 100644 src/lib/period/convert.ts create mode 100644 src/lib/period/index.ts create mode 100644 src/lib/period/types.ts create mode 100644 src/lib/taskruns/convert.ts create mode 100644 src/lib/taskruns/index.ts create mode 100644 src/lib/taskruns/status.ts create mode 100644 src/lib/taskruns/types.ts create mode 100644 src/lib/timer/create.ts create mode 100644 src/lib/timer/index.ts create mode 100644 src/lib/timer/types.ts create mode 100644 src/services/useTasksRuns/index.ts diff --git a/src/components/tasks/BaseTaskFilterControl.vue b/src/components/tasks/BaseTaskFilterControl.vue new file mode 100644 index 000000000..8c88d6ffa --- /dev/null +++ b/src/components/tasks/BaseTaskFilterControl.vue @@ -0,0 +1,114 @@ + + diff --git a/src/components/tasks/PeriodFilterControl.vue b/src/components/tasks/PeriodFilterControl.vue new file mode 100644 index 000000000..2e89fe253 --- /dev/null +++ b/src/components/tasks/PeriodFilterControl.vue @@ -0,0 +1,68 @@ + + diff --git a/src/components/tasks/TaskRunProgress.vue b/src/components/tasks/TaskRunProgress.vue new file mode 100644 index 000000000..9ea311984 --- /dev/null +++ b/src/components/tasks/TaskRunProgress.vue @@ -0,0 +1,120 @@ + + diff --git a/src/components/tasks/TaskRunSummary.vue b/src/components/tasks/TaskRunSummary.vue new file mode 100644 index 000000000..74232605a --- /dev/null +++ b/src/components/tasks/TaskRunSummary.vue @@ -0,0 +1,164 @@ + + + + diff --git a/src/components/tasks/TaskRunsPanel.vue b/src/components/tasks/TaskRunsPanel.vue new file mode 100644 index 000000000..1e584b928 --- /dev/null +++ b/src/components/tasks/TaskRunsPanel.vue @@ -0,0 +1,157 @@ + + + + diff --git a/src/components/tasks/TaskStatusFilterControl.vue b/src/components/tasks/TaskStatusFilterControl.vue new file mode 100644 index 000000000..3e3bf4829 --- /dev/null +++ b/src/components/tasks/TaskStatusFilterControl.vue @@ -0,0 +1,87 @@ + + diff --git a/src/components/tasks/WorkflowFilterControl.vue b/src/components/tasks/WorkflowFilterControl.vue new file mode 100644 index 000000000..38743a8dc --- /dev/null +++ b/src/components/tasks/WorkflowFilterControl.vue @@ -0,0 +1,31 @@ + + diff --git a/src/layouts/DefaultLayout.vue b/src/layouts/DefaultLayout.vue index e2e74376b..73434d2cf 100644 --- a/src/layouts/DefaultLayout.vue +++ b/src/layouts/DefaultLayout.vue @@ -11,6 +11,7 @@
- - - +
+
+ + + +
+
+
current.category === category, + ) + if (!definition) { + throw new Error(`No definition for task status category "${category}".`) + } + return definition.name +} + +export function getTaskStatusCategory(status: TaskStatus): TaskStatusCategory { + const definition = STATUS_CATEGORIES.find((current) => + current.statuses.includes(status), + ) + if (!definition) { + throw new Error( + `Task status "${status}" is not in any task status category.`, + ) + } + return definition.category +} + +export function getCompleteTaskStatusCategories( + statuses: TaskStatus[], +): TaskStatusCategory[] { + const completeCategories: TaskStatusCategory[] = [] + for (const category of STATUS_CATEGORIES) { + // If all statuses in this category are in the specified array, the category + // is defined "complete" and included. + if (category.statuses.every((status) => statuses.includes(status))) { + completeCategories.push(category.category) + } + } + return completeCategories +} + +export function getTaskStatusesForCategory( + selectedCategory: TaskStatusCategory, +): TaskStatus[] { + const category = STATUS_CATEGORIES.find( + (current) => current.category === selectedCategory, + ) + return category?.statuses ?? [] +} diff --git a/src/lib/taskruns/types.ts b/src/lib/taskruns/types.ts new file mode 100644 index 000000000..239f1a1eb --- /dev/null +++ b/src/lib/taskruns/types.ts @@ -0,0 +1,32 @@ +export enum TaskStatus { + Invalid = 'invalid', + Pending = 'pending', + Terminated = 'terminated', + Running = 'running', + Failed = 'failed', + CompletedFullySuccessful = 'completed fully successful', + CompletePartlySuccessful = 'completed partly successful', + Approved = 'approved', + ApprovedPartlySuccessful = 'approved partly successful', + Amalgamated = 'amalgamated', + PartlyCompleted = 'partly completed', +} + +export enum TaskStatusCategory { + Pending = 'pending', + Running = 'running', + Completed = 'completed', + Failed = 'failed', +} + +export interface TaskRun { + taskId: string + workflowId: string + status: TaskStatus + description: string | null + timeZeroTimestamp: number + dispatchTimestamp: number | null + completionTimestamp: number | null + userId: string | null + isScheduled: boolean +} diff --git a/src/lib/timer/create.ts b/src/lib/timer/create.ts new file mode 100644 index 000000000..0fdfa918c --- /dev/null +++ b/src/lib/timer/create.ts @@ -0,0 +1,44 @@ +import { Timer } from './types' + +export function createTimer( + callback: () => void, + intervalSeconds: number, + immediate: boolean, +): Timer { + // Evaluate callback immediately, if so requested. + if (immediate) { + callback() + } + + const createInterval = () => setInterval(callback, intervalSeconds * 1000) + let id = createInterval() + + // Intervals are paused when a browser tab/window is inactive, so we add a + // listener to the window focus event to check whether a refresh is overdue + // if the window becomes active again. + let lastUpdatedTimestamp: number = Date.now() + const onFocus = () => { + const now = Date.now() + if (now - lastUpdatedTimestamp > intervalSeconds * 1000) { + // Call callback and update last updated time. + callback() + lastUpdatedTimestamp = now + // Reset interval so it starts counting from now, otherwise we may + // update too often. + clearInterval(id) + id = createInterval() + } + } + window.addEventListener('focus', onFocus) + + // Create function to deactivate the current timer. It cannot be reactivated, + // only by creating a new timer. + const deactivate = () => { + clearInterval(id) + window.removeEventListener('focus', onFocus) + } + + return { + deactivate, + } +} diff --git a/src/lib/timer/index.ts b/src/lib/timer/index.ts new file mode 100644 index 000000000..e94961485 --- /dev/null +++ b/src/lib/timer/index.ts @@ -0,0 +1,2 @@ +export * from './create' +export * from './types' diff --git a/src/lib/timer/types.ts b/src/lib/timer/types.ts new file mode 100644 index 000000000..c891cc2cd --- /dev/null +++ b/src/lib/timer/types.ts @@ -0,0 +1,3 @@ +export interface Timer { + deactivate: () => void +} diff --git a/src/services/useTasksRuns/index.ts b/src/services/useTasksRuns/index.ts new file mode 100644 index 000000000..26d20dd5b --- /dev/null +++ b/src/services/useTasksRuns/index.ts @@ -0,0 +1,126 @@ +import { + DocumentFormat, + PiWebserviceProvider, + TaskStatus as TaskStatusId, + TaskRun as FewsPiTaskRun, +} from '@deltares/fews-pi-requests' +import { + computed, + MaybeRefOrGetter, + onUnmounted, + ref, + toValue, + watch, +} from 'vue' + +import { convertTimestampToFewsPiParameter } from '@/lib/date' +import { RelativePeriod } from '@/lib/period' +import { createTransformRequestFn } from '@/lib/requests/transformRequest' +import { + convertFewsPiTaskRunToTaskRun, + TaskRun, + TaskStatus, +} from '@/lib/taskruns' +import { createTimer } from '@/lib/timer' + +import { configManager } from '../application-config' +import { convertRelativeToAbsolutePeriod } from '@/lib/period/convert' + +export function useTaskRuns( + refreshIntervalSeconds: number, + dispatchPeriod: MaybeRefOrGetter, + workflowIds: MaybeRefOrGetter, + statuses: MaybeRefOrGetter, +) { + const baseUrl = configManager.get('VITE_FEWS_WEBSERVICES_URL') + const piProvider = new PiWebserviceProvider(baseUrl, { + transformRequestFn: createTransformRequestFn(), + }) + + const isLoading = ref(false) + const lastUpdatedTimestamp = ref(null) + const allTaskRuns = ref([]) + const filteredTaskRuns = computed(filterTasks) + + const timer = createTimer( + () => { + fetch().catch((error) => console.error(`Failed to fetch tasks: ${error}`)) + }, + refreshIntervalSeconds, + true, + ) + onUnmounted(() => timer.deactivate()) + + // Fetch taskruns if a new dispatch period is selected. + watch(() => toValue(dispatchPeriod), fetch) + + async function fetch(): Promise { + const _dispatchPeriod = toValue(dispatchPeriod) + const hasPeriod = _dispatchPeriod !== null + + isLoading.value = true + const fetchedTaskRuns = hasPeriod + ? await fetchTaskRunsForPeriod(_dispatchPeriod) + : await fetchAllTaskRuns() + isLoading.value = false + + allTaskRuns.value = fetchedTaskRuns.map(convertFewsPiTaskRunToTaskRun) + lastUpdatedTimestamp.value = Date.now() + } + + async function fetchAllTaskRuns(): Promise { + const response = await piProvider.getTaskRuns({ + documentFormat: DocumentFormat.PI_JSON, + }) + return response.taskRuns + } + + async function fetchTaskRunsForPeriod( + dispatchPeriod: RelativePeriod, + ): Promise { + const period = convertRelativeToAbsolutePeriod(dispatchPeriod) + const startDispatchTime = convertTimestampToFewsPiParameter( + period.startTimestamp, + ) + const endDispatchTime = convertTimestampToFewsPiParameter( + period.endTimestamp, + ) + // FIXME: it is documented that tasks can only be fetched for a specific + // workflowId (i.e. that that is a required parameter), but that + // seems to be untrue. + // Fetch pending tasks separately; they are not returned when we specify + // a dispatch period, as they have not yet been dispatched. We always return + // all pending tasks. + const [pendingTasksResponse, tasksResponse] = await Promise.all([ + piProvider.getTaskRuns({ + documentFormat: DocumentFormat.PI_JSON, + taskRunStatusIds: TaskStatusId.P, + }), + piProvider.getTaskRuns({ + documentFormat: DocumentFormat.PI_JSON, + startDispatchTime, + endDispatchTime, + }), + ]) + + return [...pendingTasksResponse.taskRuns, ...tasksResponse.taskRuns] + } + + function filterTasks(): TaskRun[] { + const _workflowIds = toValue(workflowIds) + const _statuses = toValue(statuses) + return allTaskRuns.value.filter( + (task) => + _workflowIds.includes(task.workflowId) && + _statuses.includes(task.status), + ) + } + + return { + allTaskRuns, + filteredTaskRuns, + lastUpdatedTimestamp, + isLoading, + fetch, + } +} diff --git a/src/views/TopologyDisplayView.vue b/src/views/TopologyDisplayView.vue index 48efea029..f40755932 100644 --- a/src/views/TopologyDisplayView.vue +++ b/src/views/TopologyDisplayView.vue @@ -97,7 +97,7 @@ import { useWorkflowsStore } from '@/stores/workflows' import type { TopologyNode } from '@deltares/fews-pi-requests' import type { WebOcTopologyDisplayConfig } from '@deltares/fews-pi-requests' -import { computed, ref, StyleValue, watch, watchEffect } from 'vue' +import { computed, onUnmounted, ref, StyleValue, watch, watchEffect } from 'vue' import { onBeforeRouteUpdate, RouteLocationNormalized, @@ -115,6 +115,7 @@ import { type DisplayTab, } from '@/lib/topology/displayTabs.js' import { useTopologyNodesStore } from '@/stores/topologyNodes' +import { useAvailableWorkflowsStore } from '@/stores/availableWorkflows' interface Props { topologyId?: string @@ -131,6 +132,7 @@ const props = defineProps() const configStore = useConfigStore() const settings = useUserSettingsStore() const workflowsStore = useWorkflowsStore() +const availableWorkflowsStore = useAvailableWorkflowsStore() const menuType = computed(() => { const configured = settings.get('ui.hierarchical-menu-style')?.value as string @@ -159,6 +161,26 @@ const activeNode = computed(() => { return node }) +// Set preferred workflow IDs for the running tasks menu, if this node has +// associated workflows. +watch( + activeNode, + (node) => { + const primaryWorkflowId = node?.workflowId ? [node.workflowId] : [] + const secondaryWorkflowIds = + node?.secondaryWorkflows?.map( + (workflow) => workflow.secondaryWorkflowId, + ) ?? [] + // Note: this list of workflow IDs may be empty, in which case we have no + // preferred workflow. + const workflowIds = [...primaryWorkflowId, ...secondaryWorkflowIds] + availableWorkflowsStore.setPreferredWorkflowIds(workflowIds) + }, + { immediate: true }, +) +// Clear the preferred workflow IDs when we unmount. +onUnmounted(() => availableWorkflowsStore.clearPreferredWorkflowIds()) + const secondaryWorkflows = computed(() => { if (!activeNode.value?.secondaryWorkflows) return null return activeNode.value.secondaryWorkflows From 543d585f5333f63a9d0cd6476a7c795f1e1293d8 Mon Sep 17 00:00:00 2001 From: Werner Kramer Date: Fri, 20 Dec 2024 15:03:34 +0100 Subject: [PATCH 2/9] Clean up UI --- src/components/tasks/BaseTaskFilterControl.vue | 4 ++++ src/components/tasks/PeriodFilterControl.vue | 2 ++ src/components/tasks/TaskRunsPanel.vue | 4 ++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/tasks/BaseTaskFilterControl.vue b/src/components/tasks/BaseTaskFilterControl.vue index 8c88d6ffa..57e43490a 100644 --- a/src/components/tasks/BaseTaskFilterControl.vue +++ b/src/components/tasks/BaseTaskFilterControl.vue @@ -3,8 +3,11 @@ diff --git a/src/components/tasks/PeriodFilterControl.vue b/src/components/tasks/PeriodFilterControl.vue index 2e89fe253..1a43dd440 100644 --- a/src/components/tasks/PeriodFilterControl.vue +++ b/src/components/tasks/PeriodFilterControl.vue @@ -4,6 +4,8 @@ :items="options" item-value="numSecondsBack" density="compact" + variant="plain" + flat hide-details /> diff --git a/src/components/tasks/TaskRunsPanel.vue b/src/components/tasks/TaskRunsPanel.vue index 1e584b928..13eebfb8f 100644 --- a/src/components/tasks/TaskRunsPanel.vue +++ b/src/components/tasks/TaskRunsPanel.vue @@ -2,7 +2,7 @@
-
+ -
+ Date: Fri, 20 Dec 2024 15:39:48 +0100 Subject: [PATCH 3/9] Clean up UI --- .../tasks/BaseTaskFilterControl.vue | 79 +++++++++++-------- .../tasks/TaskStatusFilterControl.vue | 2 +- 2 files changed, 47 insertions(+), 34 deletions(-) diff --git a/src/components/tasks/BaseTaskFilterControl.vue b/src/components/tasks/BaseTaskFilterControl.vue index 57e43490a..4cf6d8eb0 100644 --- a/src/components/tasks/BaseTaskFilterControl.vue +++ b/src/components/tasks/BaseTaskFilterControl.vue @@ -22,38 +22,37 @@ mdi-chevron-down - - -
- Select all - Select none -
-
- -
- + + + + + + - - - - - -
-
+ + +
+
+ diff --git a/src/components/tasks/TaskStatusFilterControl.vue b/src/components/tasks/TaskStatusFilterControl.vue index 3e3bf4829..70bfff7a6 100644 --- a/src/components/tasks/TaskStatusFilterControl.vue +++ b/src/components/tasks/TaskStatusFilterControl.vue @@ -6,7 +6,7 @@ @update:model-value="updateSelectedCategories" >