feat: impl world scoped power endpoint

This commit is contained in:
Calum H. (IMB11)
2026-06-26 17:09:22 +01:00
parent ad7a4d7d76
commit 2c1b8a2326
10 changed files with 61 additions and 37 deletions
@@ -14,7 +14,7 @@
</button>
</ButtonStyled>
<ButtonStyled v-if="props.restart" color="brand">
<button :disabled="props.isUpdating || isTransitioning" @click="saveAndPower">
<button :disabled="props.isUpdating || isTransitioning || !worldId" @click="saveAndPower">
<SpinnerIcon v-if="props.isUpdating || isTransitioning" class="animate-spin" />
{{ powerButtonLabel }}
</button>
@@ -43,7 +43,7 @@ const props = defineProps<{
const client = injectModrinthClient()
const { powerState } = injectModrinthServerContext()
const { powerState, worldId } = injectModrinthServerContext()
const isStopped = computed(() => powerState.value === 'stopped' || powerState.value === 'crashed')
@@ -63,6 +63,9 @@ const saveAndPower = async () => {
} catch {
return
}
await client.archon.servers_v0.power(props.serverId, isStopped.value ? 'Start' : 'Restart')
if (!worldId.value) return
await client.archon.servers_v1.powerWorld(props.serverId, worldId.value, {
action: isStopped.value ? 'start' : 'restart',
})
}
</script>
@@ -76,7 +76,7 @@ const props = withDefaults(
loader: null,
loaderVersion: null,
lastActive: null,
fallbackName: 'World',
fallbackName: 'Instance',
headerClass: '',
actions: () => [],
},
@@ -1,3 +1,3 @@
export { default as PanelServerActionButton } from './PanelServerActionButton.vue'
export { default as ServerManageHeader } from './ServerManageHeader.vue'
export { default as WorldManageHeader } from './WorldManageHeader.vue'
export { default as ServerInstanceManageHeader } from './ServerInstanceManageHeader.vue'
@@ -9,12 +9,19 @@ import {
injectNotificationManager,
} from '#ui/providers'
export type PowerAction = Archon.Servers.v0.PowerAction
export type PowerAction = 'Start' | 'Stop' | 'Restart' | 'Kill'
const powerActionMap = {
Start: 'start',
Stop: 'stop',
Restart: 'restart',
Kill: 'kill',
} as const satisfies Record<PowerAction, Archon.Servers.v1.WorldPowerAction>
export function useServerPowerAction(options?: { disabled?: Ref<boolean> }) {
const { formatMessage } = useVIntl()
const client = injectModrinthClient()
const { serverId, server, powerState, isSyncingContent, busyReasons } =
const { serverId, worldId, server, powerState, isSyncingContent, busyReasons } =
injectModrinthServerContext()
const { addNotification } = injectNotificationManager()
const { canUsePowerActions, permissionDeniedMessage } = useServerPermissions()
@@ -46,16 +53,18 @@ export function useServerPowerAction(options?: { disabled?: Ref<boolean> }) {
const busyTooltip = computed(() => {
if (!canUsePowerActions.value) return permissionDeniedMessage.value
if (!worldId.value) return 'Your server instance is loading'
if (isStarting.value) return 'Your server is starting'
return busyReasons.value.length > 0 ? formatMessage(busyReasons.value[0].reason) : undefined
})
const canTakeAction = computed(
() => !isTransitioning.value && !isBlockedByPropsBusyOrPermission.value,
() => !!worldId.value && !isTransitioning.value && !isBlockedByPropsBusyOrPermission.value,
)
const canKill = computed(
() =>
!!worldId.value &&
!isBlockedByPropsBusyOrPermission.value &&
(isStopping.value || isRunning.value || isStarting.value),
)
@@ -72,9 +81,13 @@ export function useServerPowerAction(options?: { disabled?: Ref<boolean> }) {
}
})
async function sendPowerAction(action: PowerAction) {
async function sendPowerAction(action: PowerAction, targetWorldId = worldId.value) {
if (!targetWorldId) return
try {
await client.archon.servers_v0.power(serverId, action)
await client.archon.servers_v1.powerWorld(serverId, targetWorldId, {
action: powerActionMap[action],
})
} catch (error) {
console.error(`Error performing ${action} on server:`, error)
addNotification({
@@ -85,13 +98,13 @@ export function useServerPowerAction(options?: { disabled?: Ref<boolean> }) {
}
}
function initiateAction(action: PowerAction) {
function initiateAction(action: PowerAction, targetWorldId = worldId.value) {
if (action === 'Kill') {
if (!canKill.value) return
} else {
if (!canTakeAction.value) return
}
void sendPowerAction(action)
void sendPowerAction(action, targetWorldId)
}
function handlePrimaryAction() {
@@ -389,8 +389,8 @@ onMounted(async () => {
// Restart
async function restartServer() {
if (!canUsePowerActions.value) return
await client.archon.servers_v0.power(serverId, 'Restart')
if (!canUsePowerActions.value || !worldId.value) return
await client.archon.servers_v1.powerWorld(serverId, worldId.value, { action: 'restart' })
}
function getSessionUploadFilename(fileName: string) {
@@ -1,6 +1,6 @@
<template>
<div class="flex min-h-[36rem] flex-col gap-4 text-primary">
<WorldManageHeader
<ServerInstanceManageHeader
:name="worldName"
:game-version="gameVersion"
:loader="loader"
@@ -38,7 +38,7 @@ import { useRouter } from 'vue-router'
import type { JoinedButtonAction } from '#ui/components/base/JoinedButtons.vue'
import NavTabs from '#ui/components/base/NavTabs.vue'
import { WorldManageHeader } from '#ui/components/servers/server-header'
import { ServerInstanceManageHeader } from '#ui/components/servers/server-header'
import { useServerPowerAction } from '#ui/components/servers/server-header/use-server-power-action'
import { useRelativeTime } from '#ui/composables'
import { defineMessages, useVIntl } from '#ui/composables/i18n'
@@ -179,12 +179,11 @@ const restartSplitActions = computed<JoinedButtonAction[]>(() => [
icon: UpdatedIcon,
action: () => initiateAction('Restart'),
},
// TODO: Implement world scoping when Archon/Kyros support target worlds in power requests.
...restartableWorlds.value.map((world) => ({
id: `restart-${world.id}`,
label: `Restart with ${world.name}`,
icon: GlobeIcon,
action: () => initiateAction('Restart'),
action: () => initiateAction('Restart', world.id),
})),
])
const powerActions = computed(() => {
@@ -1,11 +1,11 @@
import { PlayIcon, SettingsIcon } from '@modrinth/assets'
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import WorldManageHeader from '../../components/servers/server-header/WorldManageHeader.vue'
import ServerInstanceManageHeader from '../../components/servers/server-header/ServerInstanceManageHeader.vue'
const meta = {
title: 'Servers/WorldManageHeader',
component: WorldManageHeader,
title: 'Servers/ServerInstanceManageHeader',
component: ServerInstanceManageHeader,
parameters: {
layout: 'padded',
},
@@ -15,7 +15,7 @@ const meta = {
template: '<div style="max-width: 920px;"><story /></div>',
}),
],
} satisfies Meta<typeof WorldManageHeader>
} satisfies Meta<typeof ServerInstanceManageHeader>
export default meta
type Story = StoryObj<typeof meta>