diff --git a/frontend/e2e/qemu/clusters.spec.ts b/frontend/e2e/qemu/clusters.spec.ts index 9697329a9..3985be3eb 100644 --- a/frontend/e2e/qemu/clusters.spec.ts +++ b/frontend/e2e/qemu/clusters.spec.ts @@ -354,7 +354,7 @@ test('node overview tabs', async ({ page }) => { }) await test.step('Validate console logs tab', async () => { - await page.getByRole('tab', { name: 'Console Logs', exact: true }).click() + await page.getByRole('tab', { name: 'Logs', exact: true }).click() await page.getByPlaceholder('Search').fill('[talos] [initramfs] booting Talos') await expect diff --git a/frontend/eslint-suppressions.json b/frontend/eslint-suppressions.json index a57a2183b..bc378e4b8 100644 --- a/frontend/eslint-suppressions.json +++ b/frontend/eslint-suppressions.json @@ -108,11 +108,6 @@ "count": 1 } }, - "src/pages/(authenticated)/clusters/[cluster]/machine/[machine]/logs/[service].vue": { - "vue/no-ref-object-reactivity-loss": { - "count": 1 - } - }, "src/views/ClusterMachines/MachineRequest.vue": { "vue/define-props-destructuring": { "count": 1 diff --git a/frontend/src/api/options.ts b/frontend/src/api/options.ts index f4d9c9222..c43ec5b14 100644 --- a/frontend/src/api/options.ts +++ b/frontend/src/api/options.ts @@ -2,8 +2,6 @@ // // Use of this software is governed by the Business Source License // included in the LICENSE file. - -import { clusterName } from '@/context' import type { OmniRequestOptions } from '@/methods/interceptor' import { Runtime } from './common/omni.pb' @@ -56,11 +54,6 @@ export const withContext = (context: WatchContext) => { if (context.cluster) { md.cluster = context.cluster - } else { - const currentContext = clusterName() - if (currentContext) { - md.cluster = md.cluster || currentContext - } } if (context.node) { diff --git a/frontend/src/components/TInput/TInput.vue b/frontend/src/components/TInput/TInput.vue index e97b4b541..a428ff3d6 100644 --- a/frontend/src/components/TInput/TInput.vue +++ b/frontend/src/components/TInput/TInput.vue @@ -97,7 +97,7 @@ onMounted(() => focus && inputRef.value?.focus())
diff --git a/frontend/src/context.ts b/frontend/src/context.ts index d275252eb..b8cb3cc78 100644 --- a/frontend/src/context.ts +++ b/frontend/src/context.ts @@ -10,6 +10,9 @@ import type { WatchContext } from '@/api/watch' export const current = useLocalStorage('context', null) +/** + * @deprecated Pass context in explicitly instead of using this function. It relies on routing & local storage and can be flimsy. + */ export function getContext(route = useRoute()): WatchContext { const cluster = clusterName(route) @@ -25,7 +28,7 @@ export function getContext(route = useRoute()): WatchContext { return res } -export function clusterName(route = useRoute()) { +function clusterName(route: ReturnType) { if ('cluster' in route.params) { return route.params.cluster } diff --git a/frontend/src/methods/logs.ts b/frontend/src/methods/logs.ts index e4150e242..f62ed1a9c 100644 --- a/frontend/src/methods/logs.ts +++ b/frontend/src/methods/logs.ts @@ -79,8 +79,8 @@ export class LineDelimitedLogParser { export const setupLogStream = ( logs: Ref, method: StreamingRequest, - params: MaybeRefOrGetter, - logParser: LogParser = new DefaultLogParser((msg) => ({ msg })), + params: MaybeRefOrGetter, + logParser: MaybeRefOrGetter = new DefaultLogParser((msg) => ({ msg })), ...options: fetchOption[] ) => { let clearLogs = false @@ -89,19 +89,13 @@ export const setupLogStream = ( const reset = () => { logs.value = [] - logParser.reset() + toValue(logParser).reset() clearTimeout(flush) } - const p = toValue(params) - - if (!p) { - return - } - const stream = subscribe( method, - p, + toValue(params), (resp: Data & { error?: string }) => { clearTimeout(flush) @@ -118,7 +112,7 @@ export const setupLogStream = ( const line = window.atob(resp.bytes.toString()) try { - buffer.push(...logParser.parse(line)) + buffer.push(...toValue(logParser).parse(line)) } catch (e) { console.error(`failed to parse line ${line}`, e) } @@ -134,7 +128,7 @@ export const setupLogStream = ( ) return { - ...stream, + stream, shutdown() { reset() stream.shutdown() diff --git a/frontend/src/methods/useMachineServices.ts b/frontend/src/methods/useMachineServices.ts new file mode 100644 index 000000000..f6c1a929b --- /dev/null +++ b/frontend/src/methods/useMachineServices.ts @@ -0,0 +1,73 @@ +// Copyright (c) 2026 Sidero Labs, Inc. +// +// Use of this software is governed by the Business Source License +// included in the LICENSE file. +import { type MaybeRefOrGetter, ref, toValue, watchEffect } from 'vue' + +import { Runtime } from '@/api/common/omni.pb' +import { subscribe } from '@/api/grpc' +import { withAbortController, withContext, withRuntime } from '@/api/options' +import { MachineService, type ServiceEvent } from '@/api/talos/machine/machine.pb' +import type { WatchContext } from '@/api/watch' +import { TCommonStatuses } from '@/constants' + +interface Service { + name?: string + state?: string + status: TCommonStatuses + events?: ServiceEvent[] +} + +export function useMachineServices(context: MaybeRefOrGetter) { + const services = ref([]) + const serviceListVersion = ref(0) + + watchEffect(async (onCleanup) => { + // To track for forced updates + void serviceListVersion.value + + const abortController = new AbortController() + onCleanup(() => abortController.abort()) + + const { messages = [] } = await MachineService.ServiceList( + {}, + withRuntime(Runtime.Talos), + withContext(toValue(context)), + withAbortController(abortController), + ) + + services.value = messages.flatMap(({ services = [] }) => + services.map((service) => ({ + name: service.id, + state: service.state, + status: service.health?.unknown + ? TCommonStatuses.HEALTH_UNKNOWN + : service.health?.healthy + ? TCommonStatuses.HEALTHY + : TCommonStatuses.UNHEALTHY, + events: service.events?.events, + })), + ) + }) + + watchEffect((onCleanup) => { + const stream = subscribe( + MachineService.Events, + {}, + (event) => { + // For some reason @type is not typed on Any + const data = event.data as (typeof event.data & { ['@type']?: string }) | undefined + + if (data?.['@type']?.includes('machine.ServiceStateEvent')) { + // Trigger a services refetch + serviceListVersion.value++ + } + }, + [withRuntime(Runtime.Talos), withContext(toValue(context))], + ) + + onCleanup(() => stream.shutdown()) + }) + + return { services } +} diff --git a/frontend/src/pages/(authenticated)/clusters/[cluster]/machine/[machine].vue b/frontend/src/pages/(authenticated)/clusters/[cluster]/machine/[machine].vue index a62a3e3d4..d292fe8d8 100644 --- a/frontend/src/pages/(authenticated)/clusters/[cluster]/machine/[machine].vue +++ b/frontend/src/pages/(authenticated)/clusters/[cluster]/machine/[machine].vue @@ -32,8 +32,8 @@ const routes = computed(() => { to: { name: 'NodeMonitor', params: { machine: machine.value } }, }, { - name: 'Console Logs', - to: { name: 'NodeLogs', params: { machine: machine.value, service: 'machine' } }, + name: 'Logs', + to: { name: 'NodeLogs', params: { machine: machine.value } }, }, { name: 'Config', diff --git a/frontend/src/pages/(authenticated)/clusters/[cluster]/machine/[machine]/index.vue b/frontend/src/pages/(authenticated)/clusters/[cluster]/machine/[machine]/index.vue index 6314781ab..8022e7f15 100644 --- a/frontend/src/pages/(authenticated)/clusters/[cluster]/machine/[machine]/index.vue +++ b/frontend/src/pages/(authenticated)/clusters/[cluster]/machine/[machine]/index.vue @@ -7,15 +7,13 @@ included in the LICENSE file. + + diff --git a/frontend/src/pages/(authenticated)/clusters/[cluster]/machine/[machine]/logs/[service].vue b/frontend/src/pages/(authenticated)/clusters/[cluster]/machine/[machine]/logs/[service].vue deleted file mode 100644 index d349a17ce..000000000 --- a/frontend/src/pages/(authenticated)/clusters/[cluster]/machine/[machine]/logs/[service].vue +++ /dev/null @@ -1,190 +0,0 @@ - - - - - - diff --git a/frontend/src/pages/(authenticated)/machines/[machine]/logs.vue b/frontend/src/pages/(authenticated)/machines/[machine]/logs/[[service]].vue similarity index 75% rename from frontend/src/pages/(authenticated)/machines/[machine]/logs.vue rename to frontend/src/pages/(authenticated)/machines/[machine]/logs/[[service]].vue index 7cb8ad962..74bc44c47 100644 --- a/frontend/src/pages/(authenticated)/machines/[machine]/logs.vue +++ b/frontend/src/pages/(authenticated)/machines/[machine]/logs/[[service]].vue @@ -13,6 +13,10 @@ definePage({ name: 'MachineLogs' }) diff --git a/frontend/src/views/Machines/MachineLogsContainer.vue b/frontend/src/views/Machines/MachineLogsContainer.vue index 6097af52c..683d763e7 100644 --- a/frontend/src/views/Machines/MachineLogsContainer.vue +++ b/frontend/src/views/Machines/MachineLogsContainer.vue @@ -5,56 +5,201 @@ Use of this software is governed by the Business Source License included in the LICENSE file. --> diff --git a/frontend/typed-router.d.ts b/frontend/typed-router.d.ts index 1886e02a3..8025f4e39 100644 --- a/frontend/typed-router.d.ts +++ b/frontend/typed-router.d.ts @@ -218,9 +218,9 @@ declare module 'vue-router/auto-routes' { >, 'NodeLogs': RouteRecordInfo< 'NodeLogs', - '/clusters/:cluster/machine/:machine/logs/:service', - { cluster: ParamValue, machine: ParamValue, service: ParamValue }, - { cluster: ParamValue, machine: ParamValue, service: ParamValue }, + '/clusters/:cluster/machine/:machine/logs/:service?', + { cluster: ParamValue, machine: ParamValue, service?: ParamValueZeroOrOne }, + { cluster: ParamValue, machine: ParamValue, service?: ParamValueZeroOrOne }, | never >, 'NodeMonitor': RouteRecordInfo< @@ -371,9 +371,9 @@ declare module 'vue-router/auto-routes' { >, 'MachineLogs': RouteRecordInfo< 'MachineLogs', - '/machines/:machine/logs', - { machine: ParamValue }, - { machine: ParamValue }, + '/machines/:machine/logs/:service?', + { machine: ParamValue, service?: ParamValueZeroOrOne }, + { machine: ParamValue, service?: ParamValueZeroOrOne }, | never >, 'MachineConfigPatches': RouteRecordInfo< @@ -850,7 +850,7 @@ declare module 'vue-router/auto-routes' { views: | never } - 'src/pages/(authenticated)/clusters/[cluster]/machine/[machine]/logs/[service].vue': { + 'src/pages/(authenticated)/clusters/[cluster]/machine/[machine]/logs/[[service]].vue': { routes: | 'NodeLogs' views: @@ -983,7 +983,7 @@ declare module 'vue-router/auto-routes' { views: | never } - 'src/pages/(authenticated)/machines/[machine]/logs.vue': { + 'src/pages/(authenticated)/machines/[machine]/logs/[[service]].vue': { routes: | 'MachineLogs' views: