diff --git a/core/bindings/MacroEventInner.ts b/core/bindings/MacroEventInner.ts index 8878bbab..369cbbb8 100644 --- a/core/bindings/MacroEventInner.ts +++ b/core/bindings/MacroEventInner.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { ExitStatus } from "./ExitStatus"; -export type MacroEventInner = { type: "Started" } | { type: "Detach" } | { type: "Stopped", exit_status: ExitStatus, }; \ No newline at end of file +export type MacroEventInner = { type: "Started", macro_name: string, time: bigint, } | { type: "Detach" } | { type: "Stopped", exit_status: ExitStatus, }; \ No newline at end of file diff --git a/core/src/events.rs b/core/src/events.rs index 5564fb48..aa639525 100644 --- a/core/src/events.rs +++ b/core/src/events.rs @@ -170,7 +170,10 @@ pub struct UserEvent { #[ts(export)] #[serde(tag = "type")] pub enum MacroEventInner { - Started, + Started { + macro_name: String, + time: i64 + }, /// Macro requests to be detached, useful for macros that run in the background such as prelaunch script Detach, Stopped { diff --git a/core/src/macro_executor.rs b/core/src/macro_executor.rs index f37f7504..e7721a3f 100644 --- a/core/src/macro_executor.rs +++ b/core/src/macro_executor.rs @@ -364,10 +364,15 @@ impl MacroExecutor { } }; + let macro_name: String = path_to_main_module.file_name().unwrap().to_str().unwrap().to_string(); + event_broadcaster.send( MacroEvent { macro_pid: pid, - macro_event_inner: MacroEventInner::Started, + macro_event_inner: MacroEventInner::Started { + macro_name: String::from(¯o_name[..macro_name.len() - 3]), + time: chrono::Utc::now().timestamp(), + }, instance_uuid: instance_uuid.clone(), } .into(), @@ -492,7 +497,7 @@ impl MacroExecutor { if let Ok(event) = rx.recv().await { if let EventInner::MacroEvent(MacroEvent { macro_pid, - macro_event_inner: MacroEventInner::Started, + macro_event_inner: MacroEventInner::Started { .. }, .. }) = event.event_inner { diff --git a/core/src/output_types.rs b/core/src/output_types.rs index 2bab7061..fa388330 100644 --- a/core/src/output_types.rs +++ b/core/src/output_types.rs @@ -29,7 +29,7 @@ impl From<&Event> for ClientEvent { }, EventInner::UserEvent(_) => EventLevel::Info, EventInner::MacroEvent(m) => match m.macro_event_inner { - MacroEventInner::Started => EventLevel::Info, + MacroEventInner::Started { .. } => EventLevel::Info, MacroEventInner::Stopped { ref exit_status } => { if exit_status.is_success() { EventLevel::Info diff --git a/dashboard/src/bindings/MacroEventInner.ts b/dashboard/src/bindings/MacroEventInner.ts index 8878bbab..80234049 100644 --- a/dashboard/src/bindings/MacroEventInner.ts +++ b/dashboard/src/bindings/MacroEventInner.ts @@ -1,4 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ExitStatus } from "./ExitStatus"; +import type { ExitStatus } from './ExitStatus'; -export type MacroEventInner = { type: "Started" } | { type: "Detach" } | { type: "Stopped", exit_status: ExitStatus, }; \ No newline at end of file +export type MacroEventInner = + | { type: 'Started'; macro_name: string; time: bigint } + | { type: 'Detach' } + | { type: 'Stopped'; exit_status: ExitStatus }; diff --git a/dashboard/src/data/EventStream.ts b/dashboard/src/data/EventStream.ts index d01c4268..305654d0 100644 --- a/dashboard/src/data/EventStream.ts +++ b/dashboard/src/data/EventStream.ts @@ -1,4 +1,4 @@ -import { useUid, useUserInfo } from 'data/UserInfo'; +import { useUid } from 'data/UserInfo'; import { addInstance, deleteInstance, updateInstance } from 'data/InstanceList'; import { LodestoneContext } from 'data/LodestoneContext'; import { useQueryClient } from '@tanstack/react-query'; @@ -14,6 +14,9 @@ import { UserPermission } from 'bindings/UserPermission'; import { PublicUser } from 'bindings/PublicUser'; import { toast } from 'react-toastify'; import { Player } from 'bindings/Player'; +import { TaskEntry } from 'bindings/TaskEntry'; +import { HistoryEntry } from 'bindings/HistoryEntry'; +import { MacroPID } from '../bindings/MacroPID'; /** * does not return anything, call this for the side effect of subscribing to the event stream @@ -248,7 +251,10 @@ export const useEventStream = () => { ['user', 'list'], (oldList: { [uid: string]: PublicUser } | undefined) => { if (!oldList) return oldList; - const newUser = {...oldList[uid], permissions: new_permissions}; + const newUser = { + ...oldList[uid], + permissions: new_permissions, + }; const newList = { ...oldList }; newList[uid] = newUser; return newList; @@ -268,8 +274,22 @@ export const useEventStream = () => { macro_event_inner: event_inner, }) => match(event_inner, { - Started: () => { + Started: ({ macro_name, time }) => { console.log(`Macro ${macro_pid} started on ${uuid}`); + + queryClient.setQueryData( + ['instance', uuid, 'taskList'], + (oldData?: TaskEntry[]): TaskEntry[] => { + const newTask: TaskEntry = { + name: macro_name, + creation_time: time, + pid: macro_pid, + }; + + return oldData ? [...oldData, newTask] : [newTask]; + } + ); + dispatch({ title: `Macro ${macro_pid} started on ${uuid}`, event, @@ -278,16 +298,40 @@ export const useEventStream = () => { }); }, Detach: () => { - console.log(`Macro ${macro_pid} detached on ${uuid}`); - dispatch({ - title: `Macro ${macro_pid} detached on ${uuid}`, - event, - type: 'add', - fresh, - }); + console.log(`Macro ${macro_pid} detached on ${uuid}`); + dispatch({ + title: `Macro ${macro_pid} detached on ${uuid}`, + event, + type: 'add', + fresh, + }); }, Stopped: ({ exit_status }) => { - console.log(`Macro ${macro_pid} stopped on ${uuid} with status ${exit_status.type}`); + console.log( + `Macro ${macro_pid} stopped on ${uuid} with status ${exit_status.type}` + ); + + let oldTask: TaskEntry | undefined; + queryClient.setQueryData( + ['instance', uuid, 'taskList'], + (oldData?: TaskEntry[]): TaskEntry[] | undefined => { + oldTask = oldData?.find((task) => task.pid === macro_pid); + return oldData?.filter((task) => task.pid !== macro_pid); + } + ); + + queryClient.setQueryData( + ['instance', uuid, 'historyList'], + (oldData?: HistoryEntry[]): HistoryEntry[] | undefined => { + if (!oldTask) return oldData; + const newHistory: HistoryEntry = { + task: oldTask, + exit_status, + }; + + return [newHistory, ...(oldData || [])]; + } + ); dispatch({ title: `Macro ${macro_pid} stopped on ${uuid} with status ${exit_status.type}`, event, @@ -320,14 +364,22 @@ export const useEventStream = () => { addInstance(instance_info, queryClient), InstanceDelete: ({ instance_uuid: uuid }) => deleteInstance(uuid, queryClient), - FSOperationCompleted: ({ instance_uuid, success, message }) => { + FSOperationCompleted: ({ + instance_uuid, + success, + message, + }) => { if (success) { - toast.success(message) + toast.success(message); } else { - toast.error(message) + toast.error(message); } - queryClient.invalidateQueries(['instance', instance_uuid, 'fileList']); - } + queryClient.invalidateQueries([ + 'instance', + instance_uuid, + 'fileList', + ]); + }, }, // eslint-disable-next-line @typescript-eslint/no-empty-function (_) => {} @@ -407,11 +459,11 @@ export const useEventStream = () => { if (!token) return; const connectWebsocket = () => { - const wsAddress = `${core.protocol === 'https' ? 'wss' : 'ws'}://${core.address}:${ - core.port ?? LODESTONE_PORT - }/api/${core.apiVersion}/events/all/stream?filter=${JSON.stringify( - eventQuery - )}`; + const wsAddress = `${core.protocol === 'https' ? 'wss' : 'ws'}://${ + core.address + }:${core.port ?? LODESTONE_PORT}/api/${ + core.apiVersion + }/events/all/stream?filter=${JSON.stringify(eventQuery)}`; if (wsRef.current) wsRef.current.close(); diff --git a/dashboard/src/pages/macros.tsx b/dashboard/src/pages/macros.tsx index 4251fd94..f9b93e7d 100644 --- a/dashboard/src/pages/macros.tsx +++ b/dashboard/src/pages/macros.tsx @@ -3,27 +3,26 @@ import { Table, TableColumn, TableRow } from 'components/Table'; import { faPlayCircle, faSkull } from '@fortawesome/free-solid-svg-icons'; import { ButtonMenuConfig } from 'components/ButtonMenu'; import { + createTask, + getInstanceHistory, getMacros, getTasks, - getInstanceHistory, - createTask, killTask, } from 'utils/apis'; import { InstanceContext } from 'data/InstanceContext'; -import { useContext, useEffect, useState, useMemo } from 'react'; -import { MacroEntry } from 'bindings/MacroEntry'; +import { useContext, useMemo, useState } from 'react'; import clsx from 'clsx'; -import { useQueryClient } from '@tanstack/react-query'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; import { toast } from 'react-toastify'; +import { MacroEntry } from 'bindings/MacroEntry'; +import { TaskEntry } from 'bindings/TaskEntry'; +import { HistoryEntry } from 'bindings/HistoryEntry'; + export type MacrosPage = 'All Macros' | 'Running Tasks' | 'History'; const Macros = () => { useDocumentTitle('Instance Macros - Lodestone'); const { selectedInstance } = useContext(InstanceContext); - const [macros, setMacros] = useState([]); - const [tasks, setTasks] = useState([]); - const [history, setHistory] = useState([]); - const unixToFormattedTime = (unix: string | undefined) => { if (!unix) return 'N/A'; const date = new Date(parseInt(unix) * 1000); @@ -42,66 +41,63 @@ const Macros = () => { const queryClient = useQueryClient(); - const fetchMacros = async (instanceUuid: string) => { - const response: MacroEntry[] = await getMacros(instanceUuid); - setMacros( - response.map( - (macro, i) => - ({ - id: i + 1, - name: macro.name, - last_run: unixToFormattedTime(macro.last_run?.toString()), - path: macro.path, - } as TableRow) - ) + const { data: macroEntry } = useQuery( + ['instance', selectedInstance?.uuid, 'macroList'], + () => getMacros(selectedInstance?.uuid as string), + { enabled: !!selectedInstance, initialData: [], refetchOnMount: 'always' } + ); + const macros = useMemo(() => { + return macroEntry.map( + (macro, i) => + ({ + id: i + 1, + name: macro.name, + last_run: unixToFormattedTime(macro.last_run?.toString()), + path: macro.path, + } as TableRow) ); - }; + }, [macroEntry]); - const fetchTasks = async (instanceUuid: string) => { - const response = await getTasks(instanceUuid); - setTasks( - response.map( - (task, i) => - ({ - id: i + 1, - name: task.name, - creation_time: unixToFormattedTime(task.creation_time.toString()), - pid: task.pid, - } as TableRow) - ) + const { data: taskEntry } = useQuery( + ['instance', selectedInstance?.uuid, 'taskList'], + () => getTasks(selectedInstance?.uuid as string), + { enabled: !!selectedInstance, initialData: [], refetchOnMount: 'always' } + ); + const tasks = useMemo(() => { + return taskEntry.map( + (task, i) => + ({ + id: i + 1, + name: task.name, + creation_time: unixToFormattedTime(task.creation_time?.toString()), + pid: task.pid, + } as TableRow) ); - }; + }, [taskEntry]); - const fetchHistory = async (instanceUuid: string) => { - const response = await getInstanceHistory(instanceUuid); - setHistory( - response.map( - (entry, i) => - ({ - id: i + 1, - name: entry.task.name, - creation_time: unixToFormattedTime( - entry.task.creation_time.toString() - ), - finished: unixToFormattedTime(entry.exit_status.time.toString()), - process_id: entry.task.pid, - } as TableRow) - ) + const { data: historyEntry } = useQuery( + ['instance', selectedInstance?.uuid, 'historyList'], + () => getInstanceHistory(selectedInstance?.uuid as string), + { + enabled: !!selectedInstance, + initialData: [], + refetchOnMount: 'always', + } + ); + const history = useMemo(() => { + return historyEntry.map( + (entry, i) => + ({ + id: i + 1, + name: entry.task.name, + creation_time: unixToFormattedTime( + entry.task.creation_time.toString() + ), + finished: unixToFormattedTime(entry.exit_status.time.toString()), + process_id: entry.task.pid, + } as TableRow) ); - }; - - useEffect(() => { - if (!selectedInstance) return; - - const fetchAll = async () => { - if (!selectedInstance) return; - fetchMacros(selectedInstance.uuid); - fetchTasks(selectedInstance.uuid); - fetchHistory(selectedInstance.uuid); - }; - - fetchAll(); - }, [selectedInstance]); + }, [historyEntry]); const [selectedPage, setSelectedPage] = useState('All Macros'); @@ -136,18 +132,24 @@ const Macros = () => { row.name as string, [] ); - const newMacros = macros.map((macro) => { - if (macro.name !== row.name) { - return macro; + + queryClient.setQueryData( + ['instance', selectedInstance?.uuid, 'macroList'], + (oldData: MacroEntry[] | undefined) => { + return oldData === undefined + ? undefined + : oldData.map((macro) => { + if (macro.name !== row.name) { + return macro; + } + const newMacro = { ...macro }; + newMacro.last_run = BigInt( + Math.floor(Date.now() / 1000).toString() + ); + return newMacro; + }); } - const newMacro = { ...macro }; - newMacro.last_run = unixToFormattedTime( - Math.floor(Date.now() / 1000).toString() - ); - return newMacro; - }); - setMacros(newMacros); - fetchTasks(selectedInstance.uuid); + ); }, }, ], @@ -188,17 +190,45 @@ const Macros = () => { selectedInstance.uuid, row.pid as string ); - setTasks(tasks.filter((task) => task.id !== row.id)); //rather than refetching, we just update the display - const newHistory = { - id: row.id, - name: row.name, - creation_time: row.creation_time, - finished: unixToFormattedTime( - Math.floor(Date.now() / 1000).toString() - ), //unix time in seconds - process_id: row.pid, - }; - setHistory([newHistory, ...history]); + + let oldTask: TaskEntry | undefined; + + queryClient.setQueryData( + ['instance', selectedInstance?.uuid, 'taskList'], + ( + oldData: TaskEntry[] | undefined + ): TaskEntry[] | undefined => { + return oldData === undefined + ? undefined + : oldData.filter((task) => { + const shouldKeep = task.pid !== row.pid; + if (!shouldKeep) { + oldTask = task; + } + return shouldKeep; + }); + } + ); + + queryClient.setQueryData( + ['instance', selectedInstance?.uuid, 'historyList'], + ( + oldData: HistoryEntry[] | undefined + ): HistoryEntry[] | undefined => { + if (oldTask === undefined) return oldData; + + const newHistory: HistoryEntry = { + task: oldTask, + exit_status: { + type: 'Killed', + time: BigInt(Math.floor(Date.now() / 1000).toString()), + }, + }; + return oldData === undefined + ? [newHistory] + : [newHistory, ...oldData]; + } + ); }, }, ],