Files
modrinth/apps/frontend/src/components/ui/servers/PanelServerActionButton.vue
T
Calum H. fa4711ff7b feat: implement improved flow for server panel edit installation (#5711)
* feat: implement improved flow for server panel edit installation

* feat: installation form finalized

* feat: error state for InstallingBanner

* feat: action button refactor + save banner text fix

* fix: lint

* fix: content card alignment

* feat: better copy

* fix: lint

* fix: hide shift click + fix NeoForge chip

* fix: lint
2026-04-01 17:53:19 +00:00

277 lines
7.0 KiB
Vue

<template>
<div class="contents">
<NewModal ref="confirmActionModal" header="Confirming power action" @close="resetPowerAction">
<div class="flex flex-col gap-4 md:w-[400px]">
<p class="m-0">
Are you sure you want to
<span class="lowercase">{{ pendingAction }}</span> the server?
</p>
<Checkbox
v-model="dontAskAgain"
label="Don't ask me again"
class="text-sm"
:disabled="!pendingAction"
/>
<div class="flex flex-row gap-4">
<ButtonStyled type="standard" color="brand" @click="executePowerAction">
<button>
<CheckIcon class="h-5 w-5" />
{{ pendingAction }} server
</button>
</ButtonStyled>
<ButtonStyled @click="resetPowerAction">
<button>
<XIcon class="h-5 w-5" />
Cancel
</button>
</ButtonStyled>
</div>
</div>
</NewModal>
<NewModal
ref="detailsModal"
:header="`All of ${server.name || 'Server'} info`"
@close="detailsModal?.hide()"
>
<ServerInfoLabels
:server-data="server"
:show-game-label="true"
:show-loader-label="true"
:uptime-seconds="uptimeSeconds"
:column="true"
class="mb-6 flex flex-col gap-2"
/>
<div v-if="flags.advancedDebugInfo" class="markdown-body">
<pre>{{ server }}</pre>
</div>
<ButtonStyled type="standard" color="brand" @click="detailsModal?.hide()">
<button class="w-full">Close</button>
</ButtonStyled>
</NewModal>
<div class="flex flex-row items-center gap-2 rounded-lg">
<ButtonStyled v-if="isInstalling" type="standard" color="brand" size="large">
<button disabled class="flex-shrink-0">
<PanelSpinner class="size-5" /> Installing...
</button>
</ButtonStyled>
<template v-else>
<ButtonStyled v-if="showStopButton" type="transparent" size="large">
<button :disabled="!canTakeAction" @click="initiateAction('Stop')">
<div class="flex gap-1">
<StopCircleIcon class="h-5 w-5" />
<span>{{ isStopping ? 'Stopping...' : 'Stop' }}</span>
</div>
</button>
</ButtonStyled>
<ButtonStyled type="standard" color="brand" size="large">
<button v-tooltip="busyTooltip" :disabled="!canTakeAction" @click="handlePrimaryAction">
<div v-if="isTransitioning" class="grid place-content-center">
<LoadingIcon />
</div>
<component :is="isRunning ? UpdatedIcon : PlayIcon" v-else />
<span>{{ primaryActionText }}</span>
</button>
</ButtonStyled>
<ButtonStyled circular type="transparent" size="large">
<TeleportOverflowMenu :options="[...menuOptions]">
<MoreVerticalIcon aria-hidden="true" />
<template #kill>
<SlashIcon class="h-5 w-5" />
<span>Kill server</span>
</template>
<template #allServers>
<ServerIcon class="h-5 w-5" />
<span>All servers</span>
</template>
<template #details>
<InfoIcon class="h-5 w-5" />
<span>Details</span>
</template>
<template #copy-id>
<ClipboardCopyIcon class="h-5 w-5" aria-hidden="true" />
<span>Copy ID</span>
</template>
</TeleportOverflowMenu>
</ButtonStyled>
</template>
</div>
</div>
</template>
<script setup lang="ts">
import {
CheckIcon,
ClipboardCopyIcon,
InfoIcon,
MoreVerticalIcon,
PlayIcon,
ServerIcon,
SlashIcon,
StopCircleIcon,
UpdatedIcon,
XIcon,
} from '@modrinth/assets'
import {
ButtonStyled,
Checkbox,
injectModrinthClient,
injectModrinthServerContext,
injectNotificationManager,
NewModal,
ServerInfoLabels,
useVIntl,
} from '@modrinth/ui'
import { useStorage } from '@vueuse/core'
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import LoadingIcon from './icons/LoadingIcon.vue'
import PanelSpinner from './PanelSpinner.vue'
import TeleportOverflowMenu from './TeleportOverflowMenu.vue'
type PowerAction = 'Start' | 'Stop' | 'Restart' | 'Kill'
const props = defineProps<{
disabled?: boolean
uptimeSeconds: number
}>()
const { formatMessage } = useVIntl()
const flags = useFeatureFlags()
const router = useRouter()
const client = injectModrinthClient()
const { serverId, server, powerState, busyReasons } = injectModrinthServerContext()
const { addNotification } = injectNotificationManager()
const confirmActionModal = ref<InstanceType<typeof NewModal> | null>(null)
const detailsModal = ref<InstanceType<typeof NewModal> | null>(null)
const pendingAction = ref<PowerAction | null>(null)
const dontAskAgain = ref(false)
const userPreferences = useStorage(`pyro-server-${serverId}-preferences`, {
powerDontAskAgain: false,
})
const isInstalling = computed(() => server.value.status === 'installing')
const isRunning = computed(() => powerState.value === 'running')
const isStopping = computed(() => powerState.value === 'stopping')
const isTransitioning = computed(
() => powerState.value === 'starting' || powerState.value === 'stopping',
)
const showStopButton = computed(() => isRunning.value || isStopping.value)
const busyTooltip = computed(() =>
busyReasons.value.length > 0 ? formatMessage(busyReasons.value[0].reason) : undefined,
)
const canTakeAction = computed(
() => !isTransitioning.value && !props.disabled && busyReasons.value.length === 0,
)
const primaryActionText = computed(() => {
switch (powerState.value) {
case 'starting':
return 'Starting...'
case 'stopping':
return 'Stopping...'
case 'running':
return 'Restart'
default:
return 'Start'
}
})
const menuOptions = computed(() => [
...(isInstalling.value
? []
: [
{
id: 'kill',
label: 'Kill server',
icon: SlashIcon,
action: () => initiateAction('Kill'),
},
]),
{
id: 'allServers',
label: 'All servers',
icon: ServerIcon,
action: () => router.push('/hosting/manage'),
},
{
id: 'details',
label: 'Details',
icon: InfoIcon,
action: () => detailsModal.value?.show(),
},
{
id: 'copy-id',
label: 'Copy ID',
icon: ClipboardCopyIcon,
action: () => copyId(),
shown: flags.value.developerMode,
},
])
async function copyId() {
await navigator.clipboard.writeText(serverId)
}
async function sendPowerAction(action: PowerAction) {
try {
await client.archon.servers_v0.power(serverId, action)
} catch (error) {
console.error(`Error performing ${action} on server:`, error)
addNotification({
type: 'error',
title: `Failed to ${action.toLowerCase()} server`,
text: 'An error occurred while performing this action.',
})
}
}
function initiateAction(action: PowerAction) {
if (!canTakeAction.value) return
if (action === 'Start') {
sendPowerAction(action)
return
}
pendingAction.value = action
if (userPreferences.value.powerDontAskAgain) {
executePowerAction()
} else {
confirmActionModal.value?.show()
}
}
function handlePrimaryAction() {
initiateAction(isRunning.value ? 'Restart' : 'Start')
}
function executePowerAction() {
if (!pendingAction.value) return
sendPowerAction(pendingAction.value)
if (dontAskAgain.value) {
userPreferences.value.powerDontAskAgain = true
}
resetPowerAction()
}
function resetPowerAction() {
confirmActionModal.value?.hide()
pendingAction.value = null
dontAskAgain.value = false
}
</script>