Skip to content

Commit fa4711f

Browse files
authored
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
1 parent c52abec commit fa4711f

File tree

19 files changed

+785
-168
lines changed

19 files changed

+785
-168
lines changed

apps/frontend/src/components/ui/servers/PanelServerActionButton.vue

Lines changed: 73 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,19 @@
44
<div class="flex flex-col gap-4 md:w-[400px]">
55
<p class="m-0">
66
Are you sure you want to
7-
<span class="lowercase">{{ confirmActionText }}</span> the server?
7+
<span class="lowercase">{{ pendingAction }}</span> the server?
88
</p>
99
<Checkbox
1010
v-model="dontAskAgain"
1111
label="Don't ask me again"
1212
class="text-sm"
13-
:disabled="!powerAction"
13+
:disabled="!pendingAction"
1414
/>
1515
<div class="flex flex-row gap-4">
1616
<ButtonStyled type="standard" color="brand" @click="executePowerAction">
1717
<button>
1818
<CheckIcon class="h-5 w-5" />
19-
{{ confirmActionText }} server
19+
{{ pendingAction }} server
2020
</button>
2121
</ButtonStyled>
2222
<ButtonStyled @click="resetPowerAction">
@@ -31,21 +31,21 @@
3131

3232
<NewModal
3333
ref="detailsModal"
34-
:header="`All of ${serverName || 'Server'} info`"
35-
@close="closeDetailsModal"
34+
:header="`All of ${server.name || 'Server'} info`"
35+
@close="detailsModal?.hide()"
3636
>
3737
<ServerInfoLabels
38-
:server-data="serverData"
38+
:server-data="server"
3939
:show-game-label="true"
4040
:show-loader-label="true"
4141
:uptime-seconds="uptimeSeconds"
4242
:column="true"
4343
class="mb-6 flex flex-col gap-2"
4444
/>
4545
<div v-if="flags.advancedDebugInfo" class="markdown-body">
46-
<pre>{{ serverData }}</pre>
46+
<pre>{{ server }}</pre>
4747
</div>
48-
<ButtonStyled type="standard" color="brand" @click="closeDetailsModal">
48+
<ButtonStyled type="standard" color="brand" @click="detailsModal?.hide()">
4949
<button class="w-full">Close</button>
5050
</ButtonStyled>
5151
</NewModal>
@@ -62,14 +62,14 @@
6262
<button :disabled="!canTakeAction" @click="initiateAction('Stop')">
6363
<div class="flex gap-1">
6464
<StopCircleIcon class="h-5 w-5" />
65-
<span>{{ isStoppingState ? 'Stopping...' : 'Stop' }}</span>
65+
<span>{{ isStopping ? 'Stopping...' : 'Stop' }}</span>
6666
</div>
6767
</button>
6868
</ButtonStyled>
6969

7070
<ButtonStyled type="standard" color="brand" size="large">
71-
<button v-tooltip="busyReason" :disabled="!canTakeAction" @click="handlePrimaryAction">
72-
<div v-if="isTransitionState" class="grid place-content-center">
71+
<button v-tooltip="busyTooltip" :disabled="!canTakeAction" @click="handlePrimaryAction">
72+
<div v-if="isTransitioning" class="grid place-content-center">
7373
<LoadingIcon />
7474
</div>
7575
<component :is="isRunning ? UpdatedIcon : PlayIcon" v-else />
@@ -116,8 +116,16 @@ import {
116116
UpdatedIcon,
117117
XIcon,
118118
} from '@modrinth/assets'
119-
import { ButtonStyled, Checkbox, NewModal, ServerInfoLabels } from '@modrinth/ui'
120-
import type { PowerAction as ServerPowerAction, ServerState } from '@modrinth/utils'
119+
import {
120+
ButtonStyled,
121+
Checkbox,
122+
injectModrinthClient,
123+
injectModrinthServerContext,
124+
injectNotificationManager,
125+
NewModal,
126+
ServerInfoLabels,
127+
useVIntl,
128+
} from '@modrinth/ui'
121129
import { useStorage } from '@vueuse/core'
122130
import { computed, ref } from 'vue'
123131
import { useRouter } from 'vue-router'
@@ -126,70 +134,60 @@ import LoadingIcon from './icons/LoadingIcon.vue'
126134
import PanelSpinner from './PanelSpinner.vue'
127135
import TeleportOverflowMenu from './TeleportOverflowMenu.vue'
128136
129-
const flags = useFeatureFlags()
130-
131-
interface PowerAction {
132-
action: ServerPowerAction
133-
nextState: ServerState
134-
}
137+
type PowerAction = 'Start' | 'Stop' | 'Restart' | 'Kill'
135138
136139
const props = defineProps<{
137-
isOnline: boolean
138-
isActioning: boolean
139-
isInstalling: boolean
140-
disabled: boolean
141-
serverName?: string
142-
serverData: object
140+
disabled?: boolean
143141
uptimeSeconds: number
144-
busyReason?: string
145-
}>()
146-
147-
const emit = defineEmits<{
148-
(e: 'action', action: ServerPowerAction): void
149142
}>()
150143
144+
const { formatMessage } = useVIntl()
145+
const flags = useFeatureFlags()
151146
const router = useRouter()
152-
const serverId = router.currentRoute.value.params.id
147+
const client = injectModrinthClient()
148+
const { serverId, server, powerState, busyReasons } = injectModrinthServerContext()
149+
const { addNotification } = injectNotificationManager()
150+
153151
const confirmActionModal = ref<InstanceType<typeof NewModal> | null>(null)
154152
const detailsModal = ref<InstanceType<typeof NewModal> | null>(null)
153+
const pendingAction = ref<PowerAction | null>(null)
154+
const dontAskAgain = ref(false)
155155
156156
const userPreferences = useStorage(`pyro-server-${serverId}-preferences`, {
157157
powerDontAskAgain: false,
158158
})
159159
160-
const serverState = ref<ServerState>(props.isOnline ? 'running' : 'stopped')
161-
const powerAction = ref<PowerAction | null>(null)
162-
const dontAskAgain = ref(false)
163-
const startingDelay = ref(false)
160+
const isInstalling = computed(() => server.value.status === 'installing')
161+
const isRunning = computed(() => powerState.value === 'running')
162+
const isStopping = computed(() => powerState.value === 'stopping')
163+
const isTransitioning = computed(
164+
() => powerState.value === 'starting' || powerState.value === 'stopping',
165+
)
166+
const showStopButton = computed(() => isRunning.value || isStopping.value)
164167
165-
const canTakeAction = computed(
166-
() => !props.isActioning && !startingDelay.value && !isTransitionState.value && !props.busyReason,
168+
const busyTooltip = computed(() =>
169+
busyReasons.value.length > 0 ? formatMessage(busyReasons.value[0].reason) : undefined,
167170
)
168-
const isRunning = computed(() => serverState.value === 'running')
169-
const isTransitionState = computed(() =>
170-
['starting', 'stopping', 'restarting'].includes(serverState.value),
171+
172+
const canTakeAction = computed(
173+
() => !isTransitioning.value && !props.disabled && busyReasons.value.length === 0,
171174
)
172-
const isStoppingState = computed(() => serverState.value === 'stopping')
173-
const showStopButton = computed(() => isRunning.value || isStoppingState.value)
174175
175176
const primaryActionText = computed(() => {
176-
const states: Partial<Record<ServerState, string>> = {
177-
starting: 'Starting...',
178-
restarting: 'Restarting...',
179-
running: 'Restart',
180-
stopping: 'Stopping...',
181-
stopped: 'Start',
177+
switch (powerState.value) {
178+
case 'starting':
179+
return 'Starting...'
180+
case 'stopping':
181+
return 'Stopping...'
182+
case 'running':
183+
return 'Restart'
184+
default:
185+
return 'Start'
182186
}
183-
return states[serverState.value]
184-
})
185-
186-
const confirmActionText = computed(() => {
187-
if (!powerAction.value) return ''
188-
return powerAction.value.action.charAt(0).toUpperCase() + powerAction.value.action.slice(1)
189187
})
190188
191189
const menuOptions = computed(() => [
192-
...(props.isInstalling
190+
...(isInstalling.value
193191
? []
194192
: [
195193
{
@@ -221,28 +219,31 @@ const menuOptions = computed(() => [
221219
])
222220
223221
async function copyId() {
224-
await navigator.clipboard.writeText(serverId as string)
222+
await navigator.clipboard.writeText(serverId)
225223
}
226224
227-
function initiateAction(action: ServerPowerAction) {
228-
if (!canTakeAction.value) return
229-
230-
const stateMap: Record<ServerPowerAction, ServerState> = {
231-
Start: 'starting',
232-
Stop: 'stopping',
233-
Restart: 'restarting',
234-
Kill: 'stopping',
225+
async function sendPowerAction(action: PowerAction) {
226+
try {
227+
await client.archon.servers_v0.power(serverId, action)
228+
} catch (error) {
229+
console.error(`Error performing ${action} on server:`, error)
230+
addNotification({
231+
type: 'error',
232+
title: `Failed to ${action.toLowerCase()} server`,
233+
text: 'An error occurred while performing this action.',
234+
})
235235
}
236+
}
237+
238+
function initiateAction(action: PowerAction) {
239+
if (!canTakeAction.value) return
236240
237241
if (action === 'Start') {
238-
emit('action', action)
239-
serverState.value = stateMap[action]
240-
startingDelay.value = true
241-
setTimeout(() => (startingDelay.value = false), 5000)
242+
sendPowerAction(action)
242243
return
243244
}
244245
245-
powerAction.value = { action, nextState: stateMap[action] }
246+
pendingAction.value = action
246247
247248
if (userPreferences.value.powerDontAskAgain) {
248249
executePowerAction()
@@ -256,41 +257,20 @@ function handlePrimaryAction() {
256257
}
257258
258259
function executePowerAction() {
259-
if (!powerAction.value) return
260+
if (!pendingAction.value) return
260261
261-
const { action, nextState } = powerAction.value
262-
emit('action', action)
263-
serverState.value = nextState
262+
sendPowerAction(pendingAction.value)
264263
265264
if (dontAskAgain.value) {
266265
userPreferences.value.powerDontAskAgain = true
267266
}
268267
269-
if (action === 'Start') {
270-
startingDelay.value = true
271-
setTimeout(() => (startingDelay.value = false), 5000)
272-
}
273-
274268
resetPowerAction()
275269
}
276270
277271
function resetPowerAction() {
278272
confirmActionModal.value?.hide()
279-
powerAction.value = null
273+
pendingAction.value = null
280274
dontAskAgain.value = false
281275
}
282-
283-
function closeDetailsModal() {
284-
detailsModal.value?.hide()
285-
}
286-
287-
watch(
288-
() => props.isOnline,
289-
(online) => (serverState.value = online ? 'running' : 'stopped'),
290-
)
291-
292-
watch(
293-
() => router.currentRoute.value.fullPath,
294-
() => closeDetailsModal(),
295-
)
296276
</script>

apps/frontend/src/components/ui/servers/SaveBanner.vue

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
</button>
1919
</ButtonStyled>
2020
<ButtonStyled v-if="props.restart" type="standard" color="brand">
21-
<button :disabled="props.isUpdating" @click="saveAndRestart">
22-
{{ props.isUpdating ? 'Saving...' : 'Save & restart' }}
21+
<button :disabled="props.isUpdating || isTransitioning" @click="saveAndPower">
22+
{{ powerButtonLabel }}
2323
</button>
2424
</ButtonStyled>
2525
</div>
@@ -30,7 +30,8 @@
3030
</template>
3131

3232
<script setup lang="ts">
33-
import { ButtonStyled, injectModrinthClient } from '@modrinth/ui'
33+
import { ButtonStyled, injectModrinthClient, injectModrinthServerContext } from '@modrinth/ui'
34+
import { computed } from 'vue'
3435
3536
const props = defineProps<{
3637
isUpdating: boolean
@@ -42,10 +43,23 @@ const props = defineProps<{
4243
}>()
4344
4445
const client = injectModrinthClient()
46+
const { powerState } = injectModrinthServerContext()
4547
46-
const saveAndRestart = async () => {
48+
const isStopped = computed(() => powerState.value === 'stopped' || powerState.value === 'crashed')
49+
50+
const isTransitioning = computed(
51+
() => powerState.value === 'starting' || powerState.value === 'stopping',
52+
)
53+
54+
const powerButtonLabel = computed(() => {
55+
if (props.isUpdating) return 'Saving...'
56+
if (isTransitioning.value) return isStopped.value ? 'Save & start' : 'Save & restart'
57+
return isStopped.value ? 'Save & start' : 'Save & restart'
58+
})
59+
60+
const saveAndPower = async () => {
4761
props.save()
48-
await client.archon.servers_v0.power(props.serverId, 'Restart')
62+
await client.archon.servers_v0.power(props.serverId, isStopped.value ? 'Start' : 'Restart')
4963
}
5064
</script>
5165

0 commit comments

Comments
 (0)