mirror of
https://github.com/modrinth/code.git
synced 2026-08-28 18:45:15 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0ffcb0df7 | ||
|
|
db3bc4eda1 | ||
|
|
c1687f5f78 | ||
|
|
36a09c495f |
@@ -6,7 +6,7 @@ import { computed, nextTick, ref } from 'vue'
|
||||
|
||||
import type { TabbedModalTab } from '#ui/components'
|
||||
import { TabbedModal } from '#ui/components'
|
||||
import { defineMessage, defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import {
|
||||
ServerSettingsAdvancedPage,
|
||||
ServerSettingsGeneralPage,
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
injectModrinthServerContext,
|
||||
injectNotificationManager,
|
||||
} from '#ui/providers'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
import { commonMessages, serverSettingsTabMessages } from '#ui/utils/common-messages'
|
||||
|
||||
type ShowOptions = {
|
||||
serverId: string
|
||||
@@ -89,10 +89,7 @@ const tabs = computed<TabbedModalTab[]>(() =>
|
||||
isOwner: isOwner.value,
|
||||
isAdmin: isAdmin.value,
|
||||
}
|
||||
const name = defineMessage({
|
||||
id: `server.settings.tabs.${tab.id}`,
|
||||
defaultMessage: tab.label,
|
||||
})
|
||||
const name = serverSettingsTabMessages[tab.id]
|
||||
const shown = tab.shown ? tab.shown(ctx) : true
|
||||
|
||||
if (tab.external) {
|
||||
@@ -222,7 +219,7 @@ defineExpose({ show, hide })
|
||||
>
|
||||
<template #title>
|
||||
<span class="flex items-center gap-2 text-lg font-semibold text-primary">
|
||||
{{ server.name || 'Server' }} <ChevronRightIcon />
|
||||
{{ server.name || formatMessage(commonMessages.serverLabel) }} <ChevronRightIcon />
|
||||
<span class="font-extrabold text-contrast">{{
|
||||
formatMessage(commonMessages.settingsLabel)
|
||||
}}</span>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<template>
|
||||
<NewModal ref="modal" header="Create backup" @show="focusInput">
|
||||
<NewModal ref="modal" :header="formatMessage(messages.modalTitle)" @show="focusInput">
|
||||
<div class="flex flex-col gap-2 md:w-[600px] -mb-2">
|
||||
<label for="backup-name-input">
|
||||
<span class="text-lg font-semibold text-contrast">Name</span>
|
||||
<span class="text-lg font-semibold text-contrast">{{
|
||||
formatMessage(messages.nameLabel)
|
||||
}}</span>
|
||||
</label>
|
||||
<StyledInput
|
||||
id="backup-name-input"
|
||||
ref="input"
|
||||
v-model="backupName"
|
||||
:placeholder="`Backup #${newBackupAmount}`"
|
||||
:placeholder="formatMessage(messages.namePlaceholder, { number: newBackupAmount })"
|
||||
:maxlength="48"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
@@ -26,8 +28,11 @@
|
||||
>
|
||||
<IssuesIcon class="hidden text-orange sm:block" />
|
||||
<span class="text-sm text-orange">
|
||||
You already have a backup named '<span class="font-semibold">{{ trimmedName }}</span
|
||||
>'
|
||||
<IntlFormatted :message-id="messages.duplicateName" :values="{ name: trimmedName }">
|
||||
<template #name-highlight="{ children }">
|
||||
<span class="font-semibold"><component :is="() => children" /></span>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</span>
|
||||
</div>
|
||||
</Transition>
|
||||
@@ -40,22 +45,22 @@
|
||||
leave-to-class="opacity-0 max-h-0"
|
||||
>
|
||||
<div v-if="isRateLimited" class="overflow-hidden text-sm text-red">
|
||||
You're creating backups too fast. Please wait a moment before trying again.
|
||||
{{ formatMessage(messages.rateLimitedInline) }}
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
<template #actions>
|
||||
<div class="w-full flex flex-row gap-2 justify-end">
|
||||
<div class="flex gap-2 justify-end">
|
||||
<ButtonStyled type="outlined">
|
||||
<button class="!border-[1px] !border-surface-4" @click="hideModal">
|
||||
<button class="!border !border-surface-4" @click="hideModal">
|
||||
<XIcon />
|
||||
Cancel
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="createMutation.isPending.value || nameExists" @click="createBackup">
|
||||
<PlusIcon />
|
||||
Create backup
|
||||
{{ formatMessage(messages.createBackup) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
@@ -69,20 +74,60 @@ import { IssuesIcon, PlusIcon, XIcon } from '@modrinth/assets'
|
||||
import { useMutation, useQueryClient } from '@tanstack/vue-query'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
|
||||
import IntlFormatted from '#ui/components/base/IntlFormatted.vue'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import {
|
||||
injectModrinthClient,
|
||||
injectModrinthServerContext,
|
||||
injectNotificationManager,
|
||||
} from '../../../providers'
|
||||
} from '#ui/providers'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
import ButtonStyled from '../../base/ButtonStyled.vue'
|
||||
import StyledInput from '../../base/StyledInput.vue'
|
||||
import NewModal from '../../modal/NewModal.vue'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
const client = injectModrinthClient()
|
||||
const queryClient = useQueryClient()
|
||||
const ctx = injectModrinthServerContext()
|
||||
|
||||
const messages = defineMessages({
|
||||
modalTitle: {
|
||||
id: 'servers.backups.create-modal.title',
|
||||
defaultMessage: 'Create backup',
|
||||
},
|
||||
nameLabel: {
|
||||
id: 'servers.backups.create-modal.name-label',
|
||||
defaultMessage: 'Name',
|
||||
},
|
||||
namePlaceholder: {
|
||||
id: 'servers.backups.create-modal.name-placeholder',
|
||||
defaultMessage: 'Backup #{number}',
|
||||
},
|
||||
duplicateName: {
|
||||
id: 'servers.backups.create-modal.duplicate-name',
|
||||
defaultMessage: "You already have a backup named '<name-highlight>{name}</name-highlight>'.",
|
||||
},
|
||||
rateLimitedInline: {
|
||||
id: 'servers.backups.create-modal.rate-limited-inline',
|
||||
defaultMessage: "You're creating backups too fast. Please wait a moment before trying again.",
|
||||
},
|
||||
createBackup: {
|
||||
id: 'servers.backups.create-modal.create-button',
|
||||
defaultMessage: 'Create backup',
|
||||
},
|
||||
errorTitle: {
|
||||
id: 'servers.backups.create-modal.notification.error.title',
|
||||
defaultMessage: 'Error creating backup',
|
||||
},
|
||||
rateLimitedNotification: {
|
||||
id: 'servers.backups.create-modal.notification.rate-limited',
|
||||
defaultMessage: "You're creating backups too fast.",
|
||||
},
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
backups?: Archon.Backups.v1.Backup[]
|
||||
}>()
|
||||
@@ -129,7 +174,8 @@ const hideModal = () => {
|
||||
}
|
||||
|
||||
const createBackup = () => {
|
||||
const name = trimmedName.value || `Backup #${newBackupAmount.value}`
|
||||
const name =
|
||||
trimmedName.value || formatMessage(messages.namePlaceholder, { number: newBackupAmount.value })
|
||||
isRateLimited.value = false
|
||||
|
||||
createMutation.mutate(name, {
|
||||
@@ -141,12 +187,16 @@ const createBackup = () => {
|
||||
isRateLimited.value = true
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Error creating backup',
|
||||
text: "You're creating backups too fast.",
|
||||
title: formatMessage(messages.errorTitle),
|
||||
text: formatMessage(messages.rateLimitedNotification),
|
||||
})
|
||||
} else {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
addNotification({ type: 'error', title: 'Error creating backup', text: message })
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: formatMessage(messages.errorTitle),
|
||||
text: message,
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
<template>
|
||||
<NewModal ref="modal" header="Delete backup" fade="danger">
|
||||
<NewModal ref="modal" :header="formatMessage(messages.modalTitle)" fade="danger">
|
||||
<div class="flex flex-col gap-6 max-w-[400px]">
|
||||
<Admonition type="critical" header="Delete warning">
|
||||
This backup will be permanently deleted. This action cannot be undone.
|
||||
<Admonition type="critical" :header="formatMessage(messages.warningHeader)">
|
||||
{{ formatMessage(messages.warningBody) }}
|
||||
</Admonition>
|
||||
|
||||
<div v-if="currentBackup" class="flex flex-col gap-2">
|
||||
<span class="font-semibold text-contrast">Backup</span>
|
||||
<span class="font-semibold text-contrast">{{ formatMessage(messages.backupLabel) }}</span>
|
||||
<BackupItem :backup="currentBackup" preview class="!bg-surface-2 !shadow-none" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #actions>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<ButtonStyled>
|
||||
<button @click="modal?.hide()">
|
||||
<ButtonStyled type="outlined">
|
||||
<button class="!border !border-surface-4" @click="modal?.hide()">
|
||||
<XIcon />
|
||||
Cancel
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="red">
|
||||
<button @click="deleteBackup">
|
||||
<TrashIcon />
|
||||
Delete backup
|
||||
{{ formatMessage(messages.deleteButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
@@ -35,11 +35,39 @@ import type { Archon } from '@modrinth/api-client'
|
||||
import { TrashIcon, XIcon } from '@modrinth/assets'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
import Admonition from '../../base/Admonition.vue'
|
||||
import ButtonStyled from '../../base/ButtonStyled.vue'
|
||||
import NewModal from '../../modal/NewModal.vue'
|
||||
import BackupItem from './BackupItem.vue'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
modalTitle: {
|
||||
id: 'servers.backups.delete-modal.title',
|
||||
defaultMessage: 'Delete backup',
|
||||
},
|
||||
warningHeader: {
|
||||
id: 'servers.backups.delete-modal.warning.header',
|
||||
defaultMessage: 'Delete warning',
|
||||
},
|
||||
warningBody: {
|
||||
id: 'servers.backups.delete-modal.warning.body',
|
||||
defaultMessage: 'This backup will be permanently deleted. This action cannot be undone.',
|
||||
},
|
||||
backupLabel: {
|
||||
id: 'servers.backups.delete-modal.backup-label',
|
||||
defaultMessage: 'Backup',
|
||||
},
|
||||
deleteButton: {
|
||||
id: 'servers.backups.delete-modal.delete-button',
|
||||
defaultMessage: 'Delete backup',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'delete', backup: Archon.Backups.v1.Backup | undefined): void
|
||||
}>()
|
||||
|
||||
@@ -2,20 +2,22 @@
|
||||
import type { Archon } from '@modrinth/api-client'
|
||||
import {
|
||||
ClipboardCopyIcon,
|
||||
ClockIcon,
|
||||
DownloadIcon,
|
||||
EditIcon,
|
||||
LoaderCircleIcon,
|
||||
MoreVerticalIcon,
|
||||
RotateCounterClockwiseIcon,
|
||||
ShieldIcon,
|
||||
TrashIcon,
|
||||
UserRoundIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useFormatDateTime } from '../../../composables'
|
||||
import { defineMessages, useVIntl } from '../../../composables/i18n'
|
||||
import { commonMessages } from '../../../utils'
|
||||
import { useFormatDateTime } from '#ui/composables'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
import ButtonStyled from '../../base/ButtonStyled.vue'
|
||||
import OverflowMenu, { type Option as OverflowOption } from '../../base/OverflowMenu.vue'
|
||||
|
||||
@@ -85,7 +87,7 @@ const activeOperation = computed(() => creating.value || restoring.value)
|
||||
|
||||
const backupIcon = computed(() => {
|
||||
if (props.backup.automated) {
|
||||
return ClockIcon
|
||||
return ShieldIcon
|
||||
}
|
||||
return UserRoundIcon
|
||||
})
|
||||
@@ -143,10 +145,6 @@ const messages = defineMessages({
|
||||
id: 'servers.backups.item.restore',
|
||||
defaultMessage: 'Restore',
|
||||
},
|
||||
rename: {
|
||||
id: 'servers.backups.item.rename',
|
||||
defaultMessage: 'Rename',
|
||||
},
|
||||
failedToCreateBackup: {
|
||||
id: 'servers.backups.item.failed-to-create-backup',
|
||||
defaultMessage: 'Failed to create backup',
|
||||
@@ -167,35 +165,54 @@ const messages = defineMessages({
|
||||
id: 'servers.backups.item.manual-backup',
|
||||
defaultMessage: 'Manual backup',
|
||||
},
|
||||
creatingBackup: {
|
||||
id: 'servers.backups.item.creating-backup',
|
||||
defaultMessage: 'Creating backup\u2026',
|
||||
},
|
||||
restoring: {
|
||||
id: 'servers.backups.item.restoring',
|
||||
defaultMessage: 'Restoring',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="grid items-center gap-4 rounded-2xl bg-bg-raised p-4 shadow-md"
|
||||
:class="
|
||||
preview
|
||||
? 'grid-cols-1'
|
||||
: 'grid-cols-[auto_1fr_auto] md:grid-cols-[minmax(0,1fr)_400px_minmax(0,1fr)]'
|
||||
"
|
||||
class="flex items-center gap-4 rounded-[20px] bg-surface-3 p-4 shadow-[0px_1px_2px_0px_rgba(0,0,0,0.3),0px_1px_3px_0px_rgba(0,0,0,0.15)]"
|
||||
>
|
||||
<div class="flex flex-row gap-4 items-center">
|
||||
<div class="flex min-w-0 flex-1 items-center gap-4">
|
||||
<!-- Icon tile -->
|
||||
<div
|
||||
class="flex size-12 shrink-0 items-center justify-center rounded-2xl border-solid border-[1px] border-surface-5 bg-surface-4 md:size-16"
|
||||
class="flex shrink-0 items-center justify-center rounded-2xl border border-solid border-surface-5 bg-surface-4"
|
||||
:class="preview ? 'size-10' : 'size-14'"
|
||||
>
|
||||
<component :is="backupIcon" class="size-7 text-secondary md:size-10" />
|
||||
<LoaderCircleIcon
|
||||
v-if="activeOperation"
|
||||
v-tooltip="restoring ? formatMessage(messages.restoring) : undefined"
|
||||
class="animate-spin text-secondary"
|
||||
:class="preview ? 'size-6' : 'size-10'"
|
||||
/>
|
||||
<component
|
||||
:is="backupIcon"
|
||||
v-else
|
||||
class="text-secondary"
|
||||
:class="preview ? 'size-6' : 'size-10'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Name + badge + subtitle -->
|
||||
<div class="flex min-w-0 flex-col gap-1.5">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="truncate font-semibold text-contrast max-w-[400px]">{{ backup.name }}</span>
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<span class="min-w-0 truncate font-semibold text-contrast">
|
||||
{{ creating ? formatMessage(messages.creatingBackup) : backup.name }}
|
||||
</span>
|
||||
<span
|
||||
v-if="backup.automated"
|
||||
class="rounded-full border-solid border-[1px] border-surface-5 bg-surface-4 px-2.5 py-1 text-sm text-secondary"
|
||||
class="shrink-0 rounded-full border border-solid border-surface-5 bg-surface-4 px-2.5 py-1 text-sm font-medium text-secondary"
|
||||
>
|
||||
{{ formatMessage(messages.auto) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 text-sm text-secondary">
|
||||
<div class="flex items-center gap-1.5 text-sm font-medium text-secondary">
|
||||
<template v-if="preview">
|
||||
<span>{{ formatDateTime(backup.created_at) }}</span>
|
||||
</template>
|
||||
@@ -227,47 +244,53 @@ const messages = defineMessages({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!preview"
|
||||
class="col-span-full row-start-2 flex flex-col gap-2 md:col-span-1 md:row-start-auto md:items-center"
|
||||
>
|
||||
<span class="w-full font-medium text-contrast md:text-center">
|
||||
{{ formatDateTime(backup.created_at) }}
|
||||
</span>
|
||||
<!-- Date + size (middle column) -->
|
||||
<div v-if="!preview" class="flex w-[240px] shrink-0 flex-col gap-1.5">
|
||||
<span class="whitespace-nowrap font-medium text-contrast">{{
|
||||
formatDateTime(backup.created_at)
|
||||
}}</span>
|
||||
<!-- TODO: Uncomment when API supports size field -->
|
||||
<!-- <span class="text-secondary">{{ formatBytes(backup.size) }}</span> -->
|
||||
<!-- <span class="font-normal text-secondary">{{ formatBytes(backup.size) }}</span> -->
|
||||
</div>
|
||||
|
||||
<div v-if="!preview" class="flex shrink-0 items-center gap-2 md:justify-self-end">
|
||||
<ButtonStyled v-if="!activeOperation" color="brand" type="outlined">
|
||||
<button
|
||||
v-tooltip="props.restoreDisabled"
|
||||
class="!border-[1px]"
|
||||
:disabled="!!props.restoreDisabled"
|
||||
@click="() => emit('restore')"
|
||||
>
|
||||
<RotateCounterClockwiseIcon class="size-5" />
|
||||
{{ formatMessage(messages.restore) }}
|
||||
<!-- Right side actions -->
|
||||
<div v-if="!preview" class="flex w-[180px] shrink-0 items-center justify-end gap-2">
|
||||
<ButtonStyled v-if="creating" type="outlined">
|
||||
<button class="!border !border-surface-4" @click="() => emit('delete', true)">
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<OverflowMenu :options="overflowMenuOptions">
|
||||
<MoreVerticalIcon class="size-5" />
|
||||
<template #copy-id>
|
||||
<ClipboardCopyIcon class="size-5" />
|
||||
{{ formatMessage(commonMessages.copyIdButton) }}
|
||||
</template>
|
||||
<template #download>
|
||||
<DownloadIcon class="size-5" /> {{ formatMessage(commonMessages.downloadButton) }}
|
||||
</template>
|
||||
<template #rename>
|
||||
<EditIcon class="size-5" /> {{ formatMessage(messages.rename) }}
|
||||
</template>
|
||||
<template #delete>
|
||||
<TrashIcon class="size-5" /> {{ formatMessage(commonMessages.deleteLabel) }}
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
<template v-else>
|
||||
<ButtonStyled v-if="!activeOperation" color="brand" type="outlined">
|
||||
<button
|
||||
v-tooltip="props.restoreDisabled"
|
||||
class="!border"
|
||||
:disabled="!!props.restoreDisabled"
|
||||
@click="() => emit('restore')"
|
||||
>
|
||||
<RotateCounterClockwiseIcon class="size-5" />
|
||||
{{ formatMessage(messages.restore) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<OverflowMenu :options="overflowMenuOptions">
|
||||
<MoreVerticalIcon class="size-5" />
|
||||
<template #copy-id>
|
||||
<ClipboardCopyIcon class="size-5" />
|
||||
{{ formatMessage(commonMessages.copyIdButton) }}
|
||||
</template>
|
||||
<template #download>
|
||||
<DownloadIcon class="size-5" /> {{ formatMessage(commonMessages.downloadButton) }}
|
||||
</template>
|
||||
<template #rename>
|
||||
<EditIcon class="size-5" /> {{ formatMessage(commonMessages.renameButton) }}
|
||||
</template>
|
||||
<template #delete>
|
||||
<TrashIcon class="size-5" /> {{ formatMessage(commonMessages.deleteLabel) }}
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<pre v-if="!preview && showDebugInfo" class="w-full rounded-xl bg-surface-4 p-2 text-xs">{{
|
||||
|
||||
@@ -11,11 +11,12 @@ import {
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { computed, reactive, watch } from 'vue'
|
||||
|
||||
import { useRelativeTime } from '../../../composables'
|
||||
import { defineMessages, useVIntl } from '../../../composables/i18n'
|
||||
import { injectModrinthClient, injectModrinthServerContext } from '../../../providers'
|
||||
import type { BackupProgressEntry } from '../../../providers/server-context'
|
||||
import { commonMessages } from '../../../utils'
|
||||
import { useRelativeTime } from '#ui/composables'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { injectModrinthClient, injectModrinthServerContext } from '#ui/providers'
|
||||
import type { BackupProgressEntry } from '#ui/providers/server-context'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
import Admonition from '../../base/Admonition.vue'
|
||||
import ButtonStyled from '../../base/ButtonStyled.vue'
|
||||
import ProgressBar from '../../base/ProgressBar.vue'
|
||||
|
||||
@@ -1,45 +1,52 @@
|
||||
<template>
|
||||
<NewModal ref="modal" header="Renaming backup" @show="focusInput">
|
||||
<NewModal ref="modal" :header="formatMessage(messages.modalTitle)" @show="focusInput">
|
||||
<div class="flex flex-col gap-2 md:w-[600px]">
|
||||
<label for="backup-name-input">
|
||||
<span class="text-lg font-semibold text-contrast"> Name </span>
|
||||
<span class="text-lg font-semibold text-contrast">{{
|
||||
formatMessage(messages.nameLabel)
|
||||
}}</span>
|
||||
</label>
|
||||
<StyledInput
|
||||
id="backup-name-input"
|
||||
ref="input"
|
||||
v-model="backupName"
|
||||
:placeholder="`Backup #${backupNumber}`"
|
||||
:placeholder="formatMessage(messages.namePlaceholder, { number: backupNumber })"
|
||||
:maxlength="48"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
<div v-if="nameExists" class="flex items-center gap-1">
|
||||
<IssuesIcon class="hidden text-orange sm:block" />
|
||||
<span class="text-sm text-orange">
|
||||
You already have a backup named '<span class="font-semibold">{{ trimmedName }}</span
|
||||
>'
|
||||
<IntlFormatted :message-id="messages.duplicateName" :values="{ name: trimmedName }">
|
||||
<template #name-highlight="{ children }">
|
||||
<span class="font-semibold"><component :is="() => children" /></span>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 flex justify-start gap-2">
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="renameMutation.isPending.value || nameExists" @click="renameBackup">
|
||||
<template v-if="renameMutation.isPending.value">
|
||||
<SpinnerIcon class="animate-spin" />
|
||||
Renaming...
|
||||
</template>
|
||||
<template v-else>
|
||||
<SaveIcon />
|
||||
Save changes
|
||||
</template>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="hide">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<template #actions>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<ButtonStyled type="outlined">
|
||||
<button class="!border !border-surface-4" @click="hide">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="renameMutation.isPending.value || nameExists" @click="renameBackup">
|
||||
<template v-if="renameMutation.isPending.value">
|
||||
<SpinnerIcon class="animate-spin" />
|
||||
{{ formatMessage(messages.renaming) }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<SaveIcon />
|
||||
{{ formatMessage(commonMessages.saveChangesButton) }}
|
||||
</template>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
</template>
|
||||
|
||||
@@ -49,20 +56,52 @@ import { IssuesIcon, SaveIcon, SpinnerIcon, XIcon } from '@modrinth/assets'
|
||||
import { useMutation, useQueryClient } from '@tanstack/vue-query'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
|
||||
import IntlFormatted from '#ui/components/base/IntlFormatted.vue'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import {
|
||||
injectModrinthClient,
|
||||
injectModrinthServerContext,
|
||||
injectNotificationManager,
|
||||
} from '../../../providers'
|
||||
} from '#ui/providers'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
import ButtonStyled from '../../base/ButtonStyled.vue'
|
||||
import StyledInput from '../../base/StyledInput.vue'
|
||||
import NewModal from '../../modal/NewModal.vue'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
const client = injectModrinthClient()
|
||||
const queryClient = useQueryClient()
|
||||
const ctx = injectModrinthServerContext()
|
||||
|
||||
const messages = defineMessages({
|
||||
modalTitle: {
|
||||
id: 'servers.backups.rename-modal.title',
|
||||
defaultMessage: 'Renaming backup',
|
||||
},
|
||||
nameLabel: {
|
||||
id: 'servers.backups.rename-modal.name-label',
|
||||
defaultMessage: 'Name',
|
||||
},
|
||||
namePlaceholder: {
|
||||
id: 'servers.backups.rename-modal.name-placeholder',
|
||||
defaultMessage: 'Backup #{number}',
|
||||
},
|
||||
duplicateName: {
|
||||
id: 'servers.backups.rename-modal.duplicate-name',
|
||||
defaultMessage: "You already have a backup named '<name-highlight>{name}</name-highlight>'.",
|
||||
},
|
||||
renaming: {
|
||||
id: 'servers.backups.rename-modal.renaming',
|
||||
defaultMessage: 'Renaming...',
|
||||
},
|
||||
errorTitle: {
|
||||
id: 'servers.backups.rename-modal.notification.error.title',
|
||||
defaultMessage: 'Error renaming backup',
|
||||
},
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
backups?: Archon.Backups.v1.Backup[]
|
||||
}>()
|
||||
@@ -125,7 +164,7 @@ const renameBackup = () => {
|
||||
if (!currentBackup.value) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Error renaming backup',
|
||||
title: formatMessage(messages.errorTitle),
|
||||
text: 'Current backup is null',
|
||||
})
|
||||
return
|
||||
@@ -138,7 +177,7 @@ const renameBackup = () => {
|
||||
|
||||
let newName = trimmedName.value
|
||||
if (newName.length === 0) {
|
||||
newName = `Backup #${backupNumber.value}`
|
||||
newName = formatMessage(messages.namePlaceholder, { number: backupNumber.value })
|
||||
}
|
||||
|
||||
renameMutation.mutate(
|
||||
@@ -149,7 +188,11 @@ const renameBackup = () => {
|
||||
},
|
||||
onError: (error) => {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
addNotification({ type: 'error', title: 'Error renaming backup', text: message })
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: formatMessage(messages.errorTitle),
|
||||
text: message,
|
||||
})
|
||||
hide()
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,33 +1,40 @@
|
||||
<template>
|
||||
<NewModal ref="modal" header="Restore backup" fade="danger">
|
||||
<NewModal ref="modal" :header="formatMessage(messages.modalTitle)" fade="danger">
|
||||
<div class="flex flex-col gap-6 max-w-[400px]">
|
||||
<Admonition v-if="ctx.isServerRunning.value" type="critical" header="Server is running">
|
||||
Stop the server before restoring a backup.
|
||||
<Admonition
|
||||
v-if="ctx.isServerRunning.value"
|
||||
type="critical"
|
||||
:header="formatMessage(messages.runningHeader)"
|
||||
>
|
||||
{{ formatMessage(messages.runningBody) }}
|
||||
</Admonition>
|
||||
<Admonition v-else type="critical" header="Restore warning">
|
||||
Restoring your server will replace the current world and server files. Any changes made
|
||||
since that backup will be permanently lost.
|
||||
<Admonition v-else type="critical" :header="formatMessage(messages.replaceFilesHeader)">
|
||||
{{ formatMessage(messages.replaceFilesBody) }}
|
||||
</Admonition>
|
||||
|
||||
<div v-if="currentBackup" class="flex flex-col gap-2">
|
||||
<span class="font-semibold text-contrast">Backup</span>
|
||||
<span class="font-semibold text-contrast">{{ formatMessage(messages.backupLabel) }}</span>
|
||||
<BackupItem :backup="currentBackup" preview class="!bg-surface-2 !shadow-none" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #actions>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<ButtonStyled>
|
||||
<button @click="modal?.hide()">
|
||||
<ButtonStyled type="outlined">
|
||||
<button class="!border !border-surface-4" @click="modal?.hide()">
|
||||
<XIcon />
|
||||
Cancel
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="red">
|
||||
<button :disabled="isRestoring || ctx.isServerRunning.value" @click="restoreBackup">
|
||||
<SpinnerIcon v-if="isRestoring" class="animate-spin" />
|
||||
<RotateCounterClockwiseIcon v-else />
|
||||
{{ isRestoring ? 'Restoring...' : 'Restore backup' }}
|
||||
{{
|
||||
isRestoring
|
||||
? formatMessage(messages.restoringButton)
|
||||
: formatMessage(messages.restoreButton)
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
@@ -41,21 +48,65 @@ import { RotateCounterClockwiseIcon, SpinnerIcon, XIcon } from '@modrinth/assets
|
||||
import { useMutation, useQueryClient } from '@tanstack/vue-query'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import {
|
||||
injectModrinthClient,
|
||||
injectModrinthServerContext,
|
||||
injectNotificationManager,
|
||||
} from '../../../providers'
|
||||
} from '#ui/providers'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
import Admonition from '../../base/Admonition.vue'
|
||||
import ButtonStyled from '../../base/ButtonStyled.vue'
|
||||
import NewModal from '../../modal/NewModal.vue'
|
||||
import BackupItem from './BackupItem.vue'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
const client = injectModrinthClient()
|
||||
const queryClient = useQueryClient()
|
||||
const ctx = injectModrinthServerContext()
|
||||
|
||||
const messages = defineMessages({
|
||||
modalTitle: {
|
||||
id: 'servers.backups.restore-modal.title',
|
||||
defaultMessage: 'Restore backup',
|
||||
},
|
||||
runningHeader: {
|
||||
id: 'servers.backups.restore-modal.running.header',
|
||||
defaultMessage: 'Server is running',
|
||||
},
|
||||
runningBody: {
|
||||
id: 'servers.backups.restore-modal.running.body',
|
||||
defaultMessage: 'Stop the server before restoring a backup.',
|
||||
},
|
||||
replaceFilesHeader: {
|
||||
id: 'servers.backups.restore-modal.replace-files.header',
|
||||
defaultMessage: 'Your server files will be replaced',
|
||||
},
|
||||
replaceFilesBody: {
|
||||
id: 'servers.backups.restore-modal.replace-files.body',
|
||||
defaultMessage:
|
||||
'Restoring your server will replace the current world and server files. Any changes made since that backup will be permanently lost.',
|
||||
},
|
||||
backupLabel: {
|
||||
id: 'servers.backups.restore-modal.backup-label',
|
||||
defaultMessage: 'Backup',
|
||||
},
|
||||
restoringButton: {
|
||||
id: 'servers.backups.restore-modal.restoring-button',
|
||||
defaultMessage: 'Restoring...',
|
||||
},
|
||||
restoreButton: {
|
||||
id: 'servers.backups.restore-modal.restore-button',
|
||||
defaultMessage: 'Restore backup',
|
||||
},
|
||||
errorTitle: {
|
||||
id: 'servers.backups.restore-modal.notification.error.title',
|
||||
defaultMessage: 'Failed to restore backup',
|
||||
},
|
||||
})
|
||||
|
||||
const backupsQueryKey = ['backups', 'list', ctx.serverId]
|
||||
const restoreMutation = useMutation({
|
||||
mutationFn: (backupId: string) =>
|
||||
@@ -77,7 +128,7 @@ const restoreBackup = () => {
|
||||
if (!currentBackup.value) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Failed to restore backup',
|
||||
title: formatMessage(messages.errorTitle),
|
||||
text: 'Current backup is null',
|
||||
})
|
||||
}
|
||||
@@ -95,7 +146,11 @@ const restoreBackup = () => {
|
||||
},
|
||||
onError: (error) => {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
addNotification({ type: 'error', title: 'Failed to restore backup', text: message })
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: formatMessage(messages.errorTitle),
|
||||
text: message,
|
||||
})
|
||||
},
|
||||
onSettled: () => {
|
||||
isRestoring.value = false
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { IssuesIcon } from '@modrinth/assets'
|
||||
|
||||
import IntlFormatted from '#ui/components/base/IntlFormatted.vue'
|
||||
import { defineMessages } from '#ui/composables/i18n'
|
||||
|
||||
import AutoLink from '../../base/AutoLink.vue'
|
||||
|
||||
defineProps<{
|
||||
backupLink: string
|
||||
}>()
|
||||
|
||||
const messages = defineMessages({
|
||||
body: {
|
||||
id: 'servers.backups.warning.body',
|
||||
defaultMessage:
|
||||
'You may want to <create-link>create a backup</create-link> before proceeding, as this process is irreversible and may permanently alter your world or the files on your server.',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
@@ -13,14 +24,16 @@ defineProps<{
|
||||
>
|
||||
<IssuesIcon class="mt-1 h-5 w-5 shrink-0 text-orange" />
|
||||
<span class="leading-normal">
|
||||
You may want to
|
||||
<AutoLink
|
||||
:to="backupLink"
|
||||
class="font-semibold text-orange hover:underline active:brightness-125"
|
||||
>create a backup</AutoLink
|
||||
>
|
||||
before proceeding, as this process is irreversible and may permanently alter your world or the
|
||||
files on your server.
|
||||
<IntlFormatted :message-id="messages.body">
|
||||
<template #create-link="{ children }">
|
||||
<AutoLink
|
||||
:to="backupLink"
|
||||
class="font-semibold text-orange hover:underline active:brightness-125"
|
||||
>
|
||||
<component :is="() => children" />
|
||||
</AutoLink>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { SearchIcon } from '@modrinth/assets'
|
||||
import { defineMessages, useVIntl } from '@modrinth/ui'
|
||||
import { computed, toValue } from 'vue'
|
||||
|
||||
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
||||
@@ -17,9 +18,51 @@ import type { SortType } from '#ui/utils/search'
|
||||
import BrowseInstallHeader from './header.vue'
|
||||
import { injectBrowseManager } from './providers/browse-manager'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
searchPlaceholder: {
|
||||
id: 'browse-tab.search-placeholder',
|
||||
defaultMessage:
|
||||
'{projectType, select, mod {Search mods…} modpack {Search modpacks…} resourcepack {Search resource packs…} shader {Search shaders…} datapack {Search datapacks…} plugin {Search plugins…} server {Search servers…} project {Search projects…} other {Search…}}',
|
||||
},
|
||||
sortByPlaceholder: {
|
||||
id: 'browse-tab.sort-by-placeholder',
|
||||
defaultMessage: 'Sort by',
|
||||
},
|
||||
sortByPrefix: {
|
||||
id: 'browse-tab.sort-by-prefix',
|
||||
defaultMessage: 'Sort by:',
|
||||
},
|
||||
viewPlaceholder: {
|
||||
id: 'browse-tab.view-placeholder',
|
||||
defaultMessage: 'View',
|
||||
},
|
||||
viewPrefix: {
|
||||
id: 'browse-tab.view-prefix',
|
||||
defaultMessage: 'View:',
|
||||
},
|
||||
filterResultsButton: {
|
||||
id: 'browse-tab.filter-results',
|
||||
defaultMessage: 'Filter results…',
|
||||
},
|
||||
offlineMessage: {
|
||||
id: 'browse-tab.offline',
|
||||
defaultMessage: 'You are currently offline. Connect to the internet to browse Modrinth!',
|
||||
},
|
||||
noResultsMessage: {
|
||||
id: 'browse-tab.no-results',
|
||||
defaultMessage: 'No results found for your query!',
|
||||
},
|
||||
})
|
||||
|
||||
const ctx = injectBrowseManager()
|
||||
const lockedMessages = computed(() => toValue(ctx.lockedFilterMessages))
|
||||
|
||||
const searchPlaceholderText = computed(() =>
|
||||
formatMessage(messages.searchPlaceholder, { projectType: ctx.projectType.value }),
|
||||
)
|
||||
|
||||
const sortOptions = computed<ComboboxOption<SortType>[]>(() =>
|
||||
ctx.effectiveSortTypes.value.map((st) => ({
|
||||
value: st,
|
||||
@@ -47,7 +90,7 @@ const maxResultsOptions = computed<ComboboxOption<number>[]>(() =>
|
||||
:icon="SearchIcon"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
:placeholder="`Search ${ctx.projectType.value}s...`"
|
||||
:placeholder="searchPlaceholderText"
|
||||
clearable
|
||||
wrapper-class="w-full"
|
||||
:input-class="ctx.variant === 'web' ? '!h-12' : 'h-12'"
|
||||
@@ -59,11 +102,11 @@ const maxResultsOptions = computed<ComboboxOption<number>[]>(() =>
|
||||
:model-value="ctx.effectiveCurrentSortType.value"
|
||||
:options="sortOptions"
|
||||
:class="ctx.variant === 'web' ? '!w-auto flex-grow md:flex-grow-0' : 'max-w-[16rem]'"
|
||||
placeholder="Sort by"
|
||||
:placeholder="formatMessage(messages.sortByPlaceholder)"
|
||||
@update:model-value="(val: SortType) => (ctx.effectiveCurrentSortType.value = val)"
|
||||
>
|
||||
<template #prefix>
|
||||
<span class="font-semibold text-primary">Sort by:</span>
|
||||
<span class="font-semibold text-primary">{{ formatMessage(messages.sortByPrefix) }}</span>
|
||||
</template>
|
||||
</Combobox>
|
||||
|
||||
@@ -71,17 +114,19 @@ const maxResultsOptions = computed<ComboboxOption<number>[]>(() =>
|
||||
:model-value="ctx.maxResults.value"
|
||||
:options="maxResultsOptions"
|
||||
:class="ctx.variant === 'web' ? '!w-auto flex-grow md:flex-grow-0' : 'max-w-[9rem]'"
|
||||
placeholder="View"
|
||||
:placeholder="formatMessage(messages.viewPlaceholder)"
|
||||
@update:model-value="(val: number) => (ctx.maxResults.value = val)"
|
||||
>
|
||||
<template #prefix>
|
||||
<span class="font-semibold text-primary">View:</span>
|
||||
<span class="font-semibold text-primary">{{ formatMessage(messages.viewPrefix) }}</span>
|
||||
</template>
|
||||
</Combobox>
|
||||
|
||||
<div v-if="ctx.filtersMenuOpen && !ctx.filtersMenuOpen.value" class="lg:hidden">
|
||||
<ButtonStyled>
|
||||
<button @click="ctx.filtersMenuOpen.value = true">Filter results...</button>
|
||||
<button @click="ctx.filtersMenuOpen.value = true">
|
||||
{{ formatMessage(messages.filterResultsButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
@@ -120,7 +165,7 @@ const maxResultsOptions = computed<ComboboxOption<number>[]>(() =>
|
||||
<component :is="ctx.loadingComponent ?? LoadingIndicator" />
|
||||
</section>
|
||||
<section v-else-if="ctx.offline?.value && ctx.totalHits.value === 0" class="offline">
|
||||
You are currently offline. Connect to the internet to browse Modrinth!
|
||||
{{ formatMessage(messages.offlineMessage) }}
|
||||
</section>
|
||||
<section
|
||||
v-else-if="
|
||||
@@ -130,7 +175,7 @@ const maxResultsOptions = computed<ComboboxOption<number>[]>(() =>
|
||||
"
|
||||
class="offline"
|
||||
>
|
||||
<p>No results found for your query!</p>
|
||||
<p>{{ formatMessage(messages.noResultsMessage) }}</p>
|
||||
</section>
|
||||
|
||||
<ProjectCardList v-else :layout="ctx.effectiveLayout.value">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { InfoIcon, XIcon } from '@modrinth/assets'
|
||||
import { defineMessages, useVIntl } from '@modrinth/ui'
|
||||
import { computed, toValue } from 'vue'
|
||||
|
||||
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
||||
@@ -8,6 +9,19 @@ import SearchSidebarFilter from '#ui/components/search/SearchSidebarFilter.vue'
|
||||
|
||||
import { injectBrowseManager } from './providers/browse-manager'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
filtersHeading: {
|
||||
id: 'browse-tab.filters-heading',
|
||||
defaultMessage: 'Filters',
|
||||
},
|
||||
hideInstalledDefault: {
|
||||
id: 'browse-tab.hide-installed-default',
|
||||
defaultMessage: 'Hide installed content',
|
||||
},
|
||||
})
|
||||
|
||||
const ctx = injectBrowseManager()
|
||||
|
||||
const isApp = computed(() => ctx.variant === 'app')
|
||||
@@ -80,7 +94,7 @@ function getFilterOpenByDefault(filterId: string): boolean {
|
||||
v-if="ctx.filtersMenuOpen?.value"
|
||||
class="sticky top-0 z-10 mx-1 flex items-center justify-between gap-3 border-0 border-b-[1px] border-solid border-divider bg-bg-raised px-6 py-4"
|
||||
>
|
||||
<h3 class="m-0 text-lg text-contrast">Filters</h3>
|
||||
<h3 class="m-0 text-lg text-contrast">{{ formatMessage(messages.filtersHeading) }}</h3>
|
||||
<ButtonStyled circular>
|
||||
<button @click="closeFiltersMenu">
|
||||
<XIcon />
|
||||
@@ -98,7 +112,7 @@ function getFilterOpenByDefault(filterId: string): boolean {
|
||||
>
|
||||
<Checkbox
|
||||
v-model="ctx.hideInstalled!.value"
|
||||
:label="ctx.hideInstalledLabel?.value ?? 'Hide installed content'"
|
||||
:label="ctx.hideInstalledLabel?.value ?? formatMessage(messages.hideInstalledDefault)"
|
||||
class="filter-checkbox"
|
||||
@update:model-value="ctx.onFilterChange()"
|
||||
@click.prevent.stop
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<ButtonStyled v-if="showClear && hasLogs" type="transparent">
|
||||
<button @click="emit('clear')">
|
||||
<XIcon />
|
||||
Clear
|
||||
{{ formatMessage(commonMessages.clearButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="showDelete" type="transparent" hover-color-fill="background" color="red">
|
||||
@@ -13,7 +13,7 @@
|
||||
@click="emit('delete')"
|
||||
>
|
||||
<TrashIcon />
|
||||
Delete
|
||||
{{ formatMessage(commonMessages.deleteLabel) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="hasLogs" type="transparent">
|
||||
@@ -24,14 +24,14 @@
|
||||
>
|
||||
<SpinnerIcon v-if="sharing" class="animate-spin" />
|
||||
<ShareIcon v-else />
|
||||
Share
|
||||
{{ formatMessage(messages.share) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled type="transparent">
|
||||
<button @click="emit('toggle-fullscreen')">
|
||||
<ContractIcon v-if="fullscreen" />
|
||||
<ExpandIcon v-else />
|
||||
{{ fullscreen ? 'Collapse' : 'Expand' }}
|
||||
{{ fullscreen ? formatMessage(messages.collapse) : formatMessage(messages.expand) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
@@ -48,6 +48,25 @@ import {
|
||||
} from '@modrinth/assets'
|
||||
|
||||
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
share: {
|
||||
id: 'servers.console.action.share',
|
||||
defaultMessage: 'Share',
|
||||
},
|
||||
expand: {
|
||||
id: 'servers.console.action.expand',
|
||||
defaultMessage: 'Expand',
|
||||
},
|
||||
collapse: {
|
||||
id: 'servers.console.action.collapse',
|
||||
defaultMessage: 'Collapse',
|
||||
},
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
showClear?: boolean
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<FilterPills v-model="selectedFilters" :options="visibleOptions">
|
||||
<template #all> All </template>
|
||||
<template #all>{{ formatMessage(commonMessages.consoleFilterAllLevels) }}</template>
|
||||
</FilterPills>
|
||||
</template>
|
||||
|
||||
@@ -8,22 +8,42 @@
|
||||
import { computed } from 'vue'
|
||||
|
||||
import FilterPills from '#ui/components/base/FilterPills.vue'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
import type { ConditionalLevel } from '../composables/console-filtering'
|
||||
import type { LogLevel } from '../types'
|
||||
|
||||
type FilterValue = LogLevel | 'all'
|
||||
|
||||
const ALWAYS_VISIBLE: Array<{ id: LogLevel; label: string }> = [
|
||||
{ id: 'error', label: 'Error' },
|
||||
{ id: 'warn', label: 'Warn' },
|
||||
{ id: 'info', label: 'Info' },
|
||||
]
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const CONDITIONAL_OPTIONS: Array<{ id: ConditionalLevel; label: string }> = [
|
||||
{ id: 'debug', label: 'Debug' },
|
||||
{ id: 'trace', label: 'Trace' },
|
||||
]
|
||||
const logLevelLabels = defineMessages({
|
||||
error: {
|
||||
id: 'servers.console.filter.log-level.error',
|
||||
defaultMessage: 'Error',
|
||||
},
|
||||
warn: {
|
||||
id: 'servers.console.filter.log-level.warn',
|
||||
defaultMessage: 'Warn',
|
||||
},
|
||||
info: {
|
||||
id: 'servers.console.filter.log-level.info',
|
||||
defaultMessage: 'Info',
|
||||
},
|
||||
debug: {
|
||||
id: 'servers.console.filter.log-level.debug',
|
||||
defaultMessage: 'Debug',
|
||||
},
|
||||
trace: {
|
||||
id: 'servers.console.filter.log-level.trace',
|
||||
defaultMessage: 'Trace',
|
||||
},
|
||||
})
|
||||
|
||||
const ALWAYS_VISIBLE: LogLevel[] = ['error', 'warn', 'info']
|
||||
|
||||
const CONDITIONAL_LEVELS: ConditionalLevel[] = ['debug', 'trace']
|
||||
|
||||
const props = defineProps<{
|
||||
presentLevels: Set<ConditionalLevel>
|
||||
@@ -36,8 +56,11 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const visibleOptions = computed(() => [
|
||||
...ALWAYS_VISIBLE,
|
||||
...CONDITIONAL_OPTIONS.filter((opt) => props.presentLevels.has(opt.id)),
|
||||
...ALWAYS_VISIBLE.map((id) => ({ id, label: formatMessage(logLevelLabels[id]) })),
|
||||
...CONDITIONAL_LEVELS.filter((id) => props.presentLevels.has(id)).map((id) => ({
|
||||
id,
|
||||
label: formatMessage(logLevelLabels[id]),
|
||||
})),
|
||||
])
|
||||
|
||||
const selectedFilters = computed({
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<StyledInput
|
||||
v-model="searchQuery"
|
||||
:icon="SearchIcon"
|
||||
placeholder="Search logs"
|
||||
:placeholder="formatMessage(messages.searchLogsPlaceholder)"
|
||||
wrapper-class="flex-1"
|
||||
input-class="!h-10"
|
||||
clearable
|
||||
@@ -65,11 +65,21 @@
|
||||
@ready="handleTerminalReady"
|
||||
/>
|
||||
</div>
|
||||
<ShareModal ref="shareModal" header="Share Logs" link :social-buttons="false" />
|
||||
<NewModal ref="deleteModal" header="Delete log file" :fade="'danger'" max-width="500px">
|
||||
<ShareModal
|
||||
ref="shareModal"
|
||||
:header="formatMessage(messages.shareLogsHeader)"
|
||||
link
|
||||
:social-buttons="false"
|
||||
/>
|
||||
<NewModal
|
||||
ref="deleteModal"
|
||||
:header="formatMessage(messages.deleteLogFileHeader)"
|
||||
:fade="'danger'"
|
||||
max-width="500px"
|
||||
>
|
||||
<div class="flex flex-col gap-6">
|
||||
<Admonition type="critical" header="This is irreversible">
|
||||
Deleting this log file cannot be undone. Are you sure you want to continue?
|
||||
<Admonition type="critical" :header="formatMessage(messages.deleteLogIrreversibleHeader)">
|
||||
{{ formatMessage(messages.deleteLogIrreversibleBody) }}
|
||||
</Admonition>
|
||||
</div>
|
||||
<template #actions>
|
||||
@@ -77,13 +87,13 @@
|
||||
<ButtonStyled type="outlined">
|
||||
<button class="!border !border-surface-4" @click="deleteModal?.hide()">
|
||||
<XIcon />
|
||||
Cancel
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="red">
|
||||
<button :disabled="isDeleting" @click="confirmDelete">
|
||||
<TrashIcon />
|
||||
Delete
|
||||
{{ formatMessage(commonMessages.deleteLabel) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
@@ -105,9 +115,11 @@ import Combobox from '#ui/components/base/Combobox.vue'
|
||||
import StyledInput from '#ui/components/base/StyledInput.vue'
|
||||
import NewModal from '#ui/components/modal/NewModal.vue'
|
||||
import ShareModal from '#ui/components/modal/ShareModal.vue'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { injectModrinthClient } from '#ui/providers'
|
||||
import { injectModalBehavior } from '#ui/providers/modal-behavior'
|
||||
import { injectNotificationManager } from '#ui/providers/web-notifications.ts'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
import ConsoleActionButtons from './components/ConsoleActionButtons.vue'
|
||||
import ConsoleFilterPills from './components/ConsoleFilterPills.vue'
|
||||
@@ -128,10 +140,51 @@ const client = injectModrinthClient()
|
||||
const modalBehavior = injectModalBehavior()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
searchLogsPlaceholder: {
|
||||
id: 'servers.console.search.logs.placeholder',
|
||||
defaultMessage: 'Search logs',
|
||||
},
|
||||
crashProblemsDetected: {
|
||||
id: 'servers.console.crash.problems-detected',
|
||||
defaultMessage: '{count, plural, one {# problem detected} other {# problems detected}}',
|
||||
},
|
||||
shareLogsHeader: {
|
||||
id: 'servers.console.share.logs.header',
|
||||
defaultMessage: 'Share Logs',
|
||||
},
|
||||
deleteLogFileHeader: {
|
||||
id: 'servers.console.delete-log-file.header',
|
||||
defaultMessage: 'Delete log file',
|
||||
},
|
||||
deleteLogIrreversibleHeader: {
|
||||
id: 'servers.console.delete-log-file.irreversible.header',
|
||||
defaultMessage: 'This is irreversible',
|
||||
},
|
||||
deleteLogIrreversibleBody: {
|
||||
id: 'servers.console.delete-log-file.irreversible.body',
|
||||
defaultMessage: 'Deleting this log file cannot be undone. Are you sure you want to continue?',
|
||||
},
|
||||
failedDeleteLogTitle: {
|
||||
id: 'servers.console.delete-log-file.error.title',
|
||||
defaultMessage: 'Failed to delete log file',
|
||||
},
|
||||
failedShareLogsTitle: {
|
||||
id: 'servers.console.share.logs.error.title',
|
||||
defaultMessage: 'Failed to share logs',
|
||||
},
|
||||
unknownErrorDetail: {
|
||||
id: 'servers.console.error.unknown-detail',
|
||||
defaultMessage: 'Unknown error.',
|
||||
},
|
||||
})
|
||||
|
||||
const crashHeader = computed(() => {
|
||||
const problems = ctx.crashAnalysis?.value?.analysis.problems ?? []
|
||||
const count = problems.length
|
||||
return `${count} problem${count !== 1 ? 's' : ''} detected`
|
||||
return formatMessage(messages.crashProblemsDetected, { count })
|
||||
})
|
||||
|
||||
const crashItems = computed<CollapsibleAdmonitionItem[]>(() => {
|
||||
@@ -338,8 +391,8 @@ async function confirmDelete() {
|
||||
console.error('Failed to delete log file:', err)
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Failed to delete log file',
|
||||
text: typeof err === 'string' ? err : 'Unknown error.',
|
||||
title: formatMessage(messages.failedDeleteLogTitle),
|
||||
text: typeof err === 'string' ? err : formatMessage(messages.unknownErrorDetail),
|
||||
})
|
||||
} finally {
|
||||
isDeleting.value = false
|
||||
@@ -361,8 +414,8 @@ async function handleShare() {
|
||||
console.error('Failed to share logs:', err)
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Failed to share logs',
|
||||
text: typeof err === 'string' ? err : 'Unknown error.',
|
||||
title: formatMessage(messages.failedShareLogsTitle),
|
||||
text: typeof err === 'string' ? err : formatMessage(messages.unknownErrorDetail),
|
||||
})
|
||||
} finally {
|
||||
isSharing.value = false
|
||||
|
||||
@@ -33,7 +33,10 @@
|
||||
<span class="text-secondary">
|
||||
{{
|
||||
formatMessage(messages.extracted, {
|
||||
size: 'bytes_processed' in op ? formatBytes(op.bytes_processed ?? 0) : '0 B',
|
||||
size:
|
||||
'bytes_processed' in op
|
||||
? formatBinaryIecSize(op.bytes_processed ?? 0)
|
||||
: formatBinaryIecSize(0),
|
||||
})
|
||||
}}
|
||||
<template v-if="'current_file' in op && op.current_file">
|
||||
@@ -77,7 +80,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PackageOpenIcon, XIcon } from '@modrinth/assets'
|
||||
import { formatBytes } from '@modrinth/utils'
|
||||
|
||||
import Admonition from '#ui/components/base/Admonition.vue'
|
||||
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
||||
@@ -86,7 +88,10 @@ import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { injectModrinthServerContext } from '#ui/providers'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
import { useFormatFileSizeI18n } from '../composables/format-file-size-i18n'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { formatBinaryIecSize } = useFormatFileSizeI18n()
|
||||
|
||||
const messages = defineMessages({
|
||||
extracting: {
|
||||
|
||||
@@ -122,10 +122,12 @@ import {
|
||||
startFileDrag,
|
||||
wasRecentDrag,
|
||||
} from '../composables/file-drag-state'
|
||||
import { useFormatFileSizeI18n } from '../composables/format-file-size-i18n'
|
||||
import { injectFileManager } from '../providers/file-manager'
|
||||
import type { FileItem } from '../types'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { formatTableRowSize } = useFormatFileSizeI18n()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const ctx = injectFileManager()
|
||||
|
||||
@@ -164,8 +166,6 @@ const isDropTarget = computed(
|
||||
)
|
||||
const isDragSource = computed(() => fileDragActive.value && fileDragData.value?.path === props.path)
|
||||
|
||||
const units = Object.freeze(['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'])
|
||||
|
||||
const formatDateTime = useFormatDateTime({
|
||||
year: '2-digit',
|
||||
month: '2-digit',
|
||||
@@ -307,12 +307,7 @@ const formattedSize = computed(() => {
|
||||
}
|
||||
|
||||
if (props.size === undefined) return ''
|
||||
const bytes = props.size
|
||||
if (bytes === 0) return '0 B'
|
||||
|
||||
const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1)
|
||||
const size = (bytes / Math.pow(1024, exponent)).toFixed(2)
|
||||
return `${size} ${units[exponent]}`
|
||||
return formatTableRowSize(props.size)
|
||||
})
|
||||
|
||||
function openContextMenu(event: MouseEvent) {
|
||||
|
||||
+5
-5
@@ -39,11 +39,7 @@
|
||||
v-model="url"
|
||||
:icon="LinkIcon"
|
||||
type="url"
|
||||
:placeholder="
|
||||
cf
|
||||
? 'https://www.curseforge.com/minecraft/modpacks/.../files/6412259'
|
||||
: 'https://www.example.com/.../modpack-name-1.0.2.zip'
|
||||
"
|
||||
:placeholder="cf ? CF_URL_PLACEHOLDER : ZIP_URL_PLACEHOLDER"
|
||||
:disabled="submitted"
|
||||
:error="touched && !!error"
|
||||
autocomplete="off"
|
||||
@@ -114,6 +110,10 @@ import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
import InlineBackupCreator from '../../../content-tab/components/modals/InlineBackupCreator.vue'
|
||||
|
||||
// Language-invariant example URLs for the input placeholder.
|
||||
const CF_URL_PLACEHOLDER = 'https://www.curseforge.com/minecraft/modpacks/.../files/6412259'
|
||||
const ZIP_URL_PLACEHOLDER = 'https://www.example.com/.../modpack-name-1.0.2.zip'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const client = injectModrinthClient()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
@@ -120,7 +120,10 @@ import { injectModrinthClient } from '#ui/providers/api-client'
|
||||
import { injectNotificationManager } from '#ui/providers/web-notifications'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
import { useFormatFileSizeI18n } from '../../composables/format-file-size-i18n'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { formatUploadQueueSize } = useFormatFileSizeI18n()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const client = injectModrinthClient()
|
||||
|
||||
@@ -161,10 +164,6 @@ const messages = defineMessages({
|
||||
id: 'files.upload-dropdown.incorrect-file-type',
|
||||
defaultMessage: 'Upload had incorrect file type',
|
||||
},
|
||||
failedToUpload: {
|
||||
id: 'files.upload-dropdown.failed-to-upload',
|
||||
defaultMessage: 'Failed to upload {fileName}',
|
||||
},
|
||||
})
|
||||
|
||||
interface UploadItem {
|
||||
@@ -240,13 +239,6 @@ watch(
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes < 1024) return bytes + ' B'
|
||||
if (bytes < 1024 ** 2) return (bytes / 1024).toFixed(1) + ' KB'
|
||||
if (bytes < 1024 ** 3) return (bytes / 1024 ** 2).toFixed(1) + ' MB'
|
||||
return (bytes / 1024 ** 3).toFixed(1) + ' GB'
|
||||
}
|
||||
|
||||
const cancelUpload = (item: UploadItem) => {
|
||||
if (item.uploader && item.status === 'uploading') {
|
||||
item.uploader.cancel()
|
||||
@@ -269,7 +261,7 @@ const uploadFile = async (file: File) => {
|
||||
file,
|
||||
progress: 0,
|
||||
status: 'pending',
|
||||
size: formatFileSize(file.size),
|
||||
size: formatUploadQueueSize(file.size),
|
||||
}
|
||||
|
||||
uploadQueue.value.push(uploadItem)
|
||||
@@ -343,7 +335,7 @@ const uploadFile = async (file: File) => {
|
||||
if (error instanceof Error && error.message !== 'Upload cancelled') {
|
||||
addNotification({
|
||||
title: formatMessage(commonMessages.uploadFailedLabel),
|
||||
text: formatMessage(messages.failedToUpload, { fileName: file.name }),
|
||||
text: formatMessage(commonMessages.uploadFailedFileDetail, { fileName: file.name }),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useVIntl } from '#ui/composables/i18n'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
const TABLE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'] as const
|
||||
|
||||
/** Localized file sizes for the shared files tab (table, upload queue, extraction progress). */
|
||||
export function useFormatFileSizeI18n() {
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
/** Match FileTableRow byte formatting. */
|
||||
function formatTableRowSize(bytes: number): string {
|
||||
if (bytes === 0) {
|
||||
return formatMessage(commonMessages.fileSizeFormatted, { value: '0', unit: 'B' })
|
||||
}
|
||||
const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), TABLE_UNITS.length - 1)
|
||||
const size = (bytes / Math.pow(1024, exponent)).toFixed(2)
|
||||
return formatMessage(commonMessages.fileSizeFormatted, {
|
||||
value: size,
|
||||
unit: TABLE_UNITS[exponent],
|
||||
})
|
||||
}
|
||||
|
||||
/** Match FileUploadDropdown queue item sizes. */
|
||||
function formatUploadQueueSize(bytes: number): string {
|
||||
if (bytes < 1024) {
|
||||
return formatMessage(commonMessages.fileSizeFormatted, { value: String(bytes), unit: 'B' })
|
||||
}
|
||||
if (bytes < 1024 ** 2) {
|
||||
return formatMessage(commonMessages.fileSizeFormatted, {
|
||||
value: (bytes / 1024).toFixed(1),
|
||||
unit: 'KB',
|
||||
})
|
||||
}
|
||||
if (bytes < 1024 ** 3) {
|
||||
return formatMessage(commonMessages.fileSizeFormatted, {
|
||||
value: (bytes / 1024 ** 2).toFixed(1),
|
||||
unit: 'MB',
|
||||
})
|
||||
}
|
||||
return formatMessage(commonMessages.fileSizeFormatted, {
|
||||
value: (bytes / 1024 ** 3).toFixed(1),
|
||||
unit: 'GB',
|
||||
})
|
||||
}
|
||||
|
||||
/** Match @modrinth/utils formatBytes (KiB / MiB / GiB). */
|
||||
function formatBinaryIecSize(bytes: number, decimals = 2): string {
|
||||
if (bytes === 0) {
|
||||
return formatMessage(commonMessages.fileSizeFormatted, { value: '0', unit: 'Bytes' })
|
||||
}
|
||||
const k = 1024
|
||||
const dm = decimals < 0 ? 0 : decimals
|
||||
const units = ['Bytes', 'KiB', 'MiB', 'GiB'] as const
|
||||
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), units.length - 1)
|
||||
const value = parseFloat((bytes / Math.pow(k, i)).toFixed(dm))
|
||||
return formatMessage(commonMessages.fileSizeFormatted, {
|
||||
value: String(value),
|
||||
unit: units[i],
|
||||
})
|
||||
}
|
||||
|
||||
return { formatTableRowSize, formatUploadQueueSize, formatBinaryIecSize }
|
||||
}
|
||||
@@ -5,26 +5,32 @@
|
||||
<!-- SFTP section -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col items-center justify-between gap-0.5 sm:flex-row">
|
||||
<span class="text-lg font-semibold text-contrast">SFTP</span>
|
||||
<span class="text-lg font-semibold text-contrast">{{
|
||||
formatMessage(messages.sftpSectionTitle)
|
||||
}}</span>
|
||||
<ButtonStyled>
|
||||
<a
|
||||
v-tooltip="'This button only works with compatible SFTP clients (e.g. WinSCP)'"
|
||||
v-tooltip="formatMessage(messages.sftpLaunchTooltip)"
|
||||
class="!w-full sm:!w-auto"
|
||||
:href="sftpUrl"
|
||||
target="_blank"
|
||||
>
|
||||
<ExternalIcon class="h-5 w-5" />
|
||||
Launch SFTP
|
||||
{{ formatMessage(messages.launchSftpButton) }}
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2.5 rounded-2xl bg-surface-2 p-4">
|
||||
<span class="text-lg font-semibold text-contrast">Server Address</span>
|
||||
<span class="text-lg font-semibold text-contrast">{{
|
||||
formatMessage(messages.serverAddressLabel)
|
||||
}}</span>
|
||||
<div
|
||||
v-tooltip="'Copy SFTP server address'"
|
||||
v-tooltip="formatMessage(messages.copySftpAddressTooltip)"
|
||||
class="copy-field hover:bg-button-bg-hover"
|
||||
@click="copyToClipboard('Server address', server?.sftp_host)"
|
||||
@click="
|
||||
copyToClipboard(formatMessage(messages.serverAddressLabel), server?.sftp_host)
|
||||
"
|
||||
>
|
||||
<span class="cursor-pointer font-semibold text-primary">
|
||||
{{ server?.sftp_host }}
|
||||
@@ -35,11 +41,18 @@
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 sm:mt-0 sm:flex-row">
|
||||
<div class="flex w-full flex-col justify-center gap-2">
|
||||
<span class="text-lg font-semibold text-contrast">Username</span>
|
||||
<span class="text-lg font-semibold text-contrast">{{
|
||||
formatMessage(commonMessages.usernameLabel)
|
||||
}}</span>
|
||||
<div
|
||||
v-tooltip="'Copy SFTP username'"
|
||||
v-tooltip="formatMessage(messages.copySftpUsernameTooltip)"
|
||||
class="copy-field hover:bg-button-bg-hover"
|
||||
@click="copyToClipboard('Username', server?.sftp_username)"
|
||||
@click="
|
||||
copyToClipboard(
|
||||
formatMessage(commonMessages.usernameLabel),
|
||||
server?.sftp_username,
|
||||
)
|
||||
"
|
||||
>
|
||||
<div class="truncate font-semibold">
|
||||
{{ server?.sftp_username }}
|
||||
@@ -50,14 +63,21 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full flex-col justify-center gap-2">
|
||||
<span class="text-lg font-semibold text-contrast">Password</span>
|
||||
<span class="text-lg font-semibold text-contrast">{{
|
||||
formatMessage(commonMessages.passwordLabel)
|
||||
}}</span>
|
||||
<div
|
||||
class="copy-field-has-button [&:hover:not(:has(button:hover))]:bg-button-bg-hover"
|
||||
@click="copyToClipboard('Password', server?.sftp_password)"
|
||||
@click="
|
||||
copyToClipboard(
|
||||
formatMessage(commonMessages.passwordLabel),
|
||||
server?.sftp_password,
|
||||
)
|
||||
"
|
||||
>
|
||||
<div class="flex items-center gap-1.5 h-full w-full">
|
||||
<div
|
||||
v-tooltip="'Copy SFTP Password'"
|
||||
v-tooltip="formatMessage(messages.copySftpPasswordTooltip)"
|
||||
class="h-full flex justify-between grow items-center"
|
||||
>
|
||||
<div class="truncate font-semibold">
|
||||
@@ -72,7 +92,11 @@
|
||||
|
||||
<ButtonStyled type="transparent" circular>
|
||||
<button
|
||||
v-tooltip="showPassword ? 'Hide password' : 'Show password'"
|
||||
v-tooltip="
|
||||
showPassword
|
||||
? formatMessage(messages.hidePasswordTooltip)
|
||||
: formatMessage(messages.showPasswordTooltip)
|
||||
"
|
||||
class="hover:bg-button-bg-hover grid h-10 w-10 place-content-center rounded-lg"
|
||||
@click.stop="showPassword = !showPassword"
|
||||
>
|
||||
@@ -92,7 +116,9 @@
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<div class="flex h-10 flex-col items-end justify-between gap-4 sm:flex-row">
|
||||
<label for="startup-command-field" class="mb-0.5 flex flex-col gap-2">
|
||||
<span class="text-lg font-semibold text-contrast">Startup command</span>
|
||||
<span class="text-lg font-semibold text-contrast">{{
|
||||
formatMessage(messages.startupCommandLabel)
|
||||
}}</span>
|
||||
</label>
|
||||
<ButtonStyled v-if="startupCommand !== defaultStartupCommand" type="transparent">
|
||||
<button
|
||||
@@ -101,7 +127,7 @@
|
||||
@click="resetToDefault"
|
||||
>
|
||||
<UpdatedIcon class="h-5 w-5" />
|
||||
Default
|
||||
{{ formatMessage(messages.defaultStartupButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
@@ -121,13 +147,15 @@
|
||||
<SpinnerIcon class="h-6 w-6 animate-spin text-secondary" />
|
||||
</div>
|
||||
</div>
|
||||
<span> The command that runs when your server is started. </span>
|
||||
<span>{{ formatMessage(messages.startupCommandDescription) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Java version section -->
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-lg font-semibold text-contrast">Java version</span>
|
||||
<span class="text-lg font-semibold text-contrast">{{
|
||||
formatMessage(messages.javaVersionLabel)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="relative max-w-xs">
|
||||
<Combobox
|
||||
@@ -135,7 +163,9 @@
|
||||
v-model="javaVersion"
|
||||
name="java-version"
|
||||
:options="displayedJavaVersions"
|
||||
:display-value="javaVersionLabel ?? 'Java Version'"
|
||||
:display-value="
|
||||
javaVersionLabel ?? formatMessage(messages.javaVersionComboboxFallback)
|
||||
"
|
||||
:disabled="isStartupLoading"
|
||||
>
|
||||
<template #dropdown-footer>
|
||||
@@ -146,7 +176,11 @@
|
||||
>
|
||||
<EyeOffIcon v-if="showAllVersions" class="size-4" />
|
||||
<EyeIcon v-else class="size-4" />
|
||||
{{ showAllVersions ? 'Hide extra versions' : 'Show all versions' }}
|
||||
{{
|
||||
showAllVersions
|
||||
? formatMessage(messages.hideExtraJavaVersions)
|
||||
: formatMessage(messages.showAllJavaVersions)
|
||||
}}
|
||||
</button>
|
||||
</template>
|
||||
</Combobox>
|
||||
@@ -157,21 +191,23 @@
|
||||
<SpinnerIcon class="h-5 w-5 animate-spin text-secondary" />
|
||||
</div>
|
||||
</div>
|
||||
<span> The Java version your server runs on. </span>
|
||||
<span>{{ formatMessage(messages.javaVersionDescription) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Java runtime section -->
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-lg font-semibold text-contrast">Java runtime</span>
|
||||
<span class="text-lg font-semibold text-contrast">{{
|
||||
formatMessage(messages.javaRuntimeLabel)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="relative max-w-xs">
|
||||
<Combobox
|
||||
:id="'runtime-field'"
|
||||
v-model="jreVendor"
|
||||
name="runtime"
|
||||
:options="JRE_VENDORS"
|
||||
:display-value="jreVendorLabel ?? 'Runtime'"
|
||||
:options="JRE_VENDOR_OPTIONS"
|
||||
:display-value="jreVendorLabel ?? formatMessage(messages.javaRuntimeComboboxFallback)"
|
||||
:disabled="isStartupLoading"
|
||||
/>
|
||||
<div
|
||||
@@ -181,7 +217,7 @@
|
||||
<SpinnerIcon class="h-5 w-5 animate-spin text-secondary" />
|
||||
</div>
|
||||
</div>
|
||||
<span> The Java runtime your server will use. </span>
|
||||
<span>{{ formatMessage(messages.javaRuntimeDescription) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -210,13 +246,111 @@ import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { ButtonStyled, Combobox, StyledInput } from '#ui/components'
|
||||
import SaveBanner from '#ui/components/servers/SaveBanner.vue'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import {
|
||||
injectModrinthClient,
|
||||
injectModrinthServerContext,
|
||||
injectNotificationManager,
|
||||
} from '#ui/providers'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
|
||||
const messages = defineMessages({
|
||||
sftpSectionTitle: {
|
||||
id: 'server.settings.advanced.sftp.title',
|
||||
defaultMessage: 'SFTP',
|
||||
},
|
||||
sftpLaunchTooltip: {
|
||||
id: 'server.settings.advanced.sftp.launch-tooltip',
|
||||
defaultMessage: 'This button only works with compatible SFTP clients (e.g. WinSCP)',
|
||||
},
|
||||
launchSftpButton: {
|
||||
id: 'server.settings.advanced.sftp.launch',
|
||||
defaultMessage: 'Launch SFTP',
|
||||
},
|
||||
serverAddressLabel: {
|
||||
id: 'server.settings.advanced.sftp.server-address',
|
||||
defaultMessage: 'Server Address',
|
||||
},
|
||||
copySftpAddressTooltip: {
|
||||
id: 'server.settings.advanced.sftp.copy-address-tooltip',
|
||||
defaultMessage: 'Copy SFTP server address',
|
||||
},
|
||||
copySftpUsernameTooltip: {
|
||||
id: 'server.settings.advanced.sftp.copy-username-tooltip',
|
||||
defaultMessage: 'Copy SFTP username',
|
||||
},
|
||||
copySftpPasswordTooltip: {
|
||||
id: 'server.settings.advanced.sftp.copy-password-tooltip',
|
||||
defaultMessage: 'Copy SFTP password',
|
||||
},
|
||||
showPasswordTooltip: {
|
||||
id: 'server.settings.advanced.sftp.show-password-tooltip',
|
||||
defaultMessage: 'Show password',
|
||||
},
|
||||
hidePasswordTooltip: {
|
||||
id: 'server.settings.advanced.sftp.hide-password-tooltip',
|
||||
defaultMessage: 'Hide password',
|
||||
},
|
||||
startupCommandLabel: {
|
||||
id: 'server.settings.advanced.startup-command.title',
|
||||
defaultMessage: 'Startup command',
|
||||
},
|
||||
defaultStartupButton: {
|
||||
id: 'server.settings.advanced.startup-command.default',
|
||||
defaultMessage: 'Default',
|
||||
},
|
||||
startupCommandDescription: {
|
||||
id: 'server.settings.advanced.startup-command.description',
|
||||
defaultMessage: 'The command that runs when your server is started.',
|
||||
},
|
||||
javaVersionLabel: {
|
||||
id: 'server.settings.advanced.java-version.title',
|
||||
defaultMessage: 'Java version',
|
||||
},
|
||||
javaVersionComboboxFallback: {
|
||||
id: 'server.settings.advanced.java-version.fallback',
|
||||
defaultMessage: 'Java version',
|
||||
},
|
||||
javaVersionDescription: {
|
||||
id: 'server.settings.advanced.java-version.description',
|
||||
defaultMessage: 'The Java version your server runs on.',
|
||||
},
|
||||
showAllJavaVersions: {
|
||||
id: 'server.settings.advanced.java-version.show-all',
|
||||
defaultMessage: 'Show all versions',
|
||||
},
|
||||
hideExtraJavaVersions: {
|
||||
id: 'server.settings.advanced.java-version.hide-extra',
|
||||
defaultMessage: 'Hide extra versions',
|
||||
},
|
||||
javaRuntimeLabel: {
|
||||
id: 'server.settings.advanced.java-runtime.title',
|
||||
defaultMessage: 'Java runtime',
|
||||
},
|
||||
javaRuntimeComboboxFallback: {
|
||||
id: 'server.settings.advanced.java-runtime.fallback',
|
||||
defaultMessage: 'Runtime',
|
||||
},
|
||||
javaRuntimeDescription: {
|
||||
id: 'server.settings.advanced.java-runtime.description',
|
||||
defaultMessage: 'The Java runtime your server will use.',
|
||||
},
|
||||
clipboardCopiedTitle: {
|
||||
id: 'server.settings.advanced.clipboard.copied.title',
|
||||
defaultMessage: '{label} copied to clipboard!',
|
||||
},
|
||||
startupUpdateFailedTitle: {
|
||||
id: 'server.settings.advanced.error.startup.title',
|
||||
defaultMessage: 'Failed to update server arguments',
|
||||
},
|
||||
startupUpdateFailedText: {
|
||||
id: 'server.settings.advanced.error.startup.text',
|
||||
defaultMessage: 'Please try again later.',
|
||||
},
|
||||
})
|
||||
const { server, serverId, worldId } = injectModrinthServerContext()
|
||||
const client = injectModrinthClient()
|
||||
const queryClient = useQueryClient()
|
||||
@@ -225,11 +359,11 @@ const queryClient = useQueryClient()
|
||||
const showPassword = ref(false)
|
||||
const sftpUrl = computed(() => `sftp://${server.value?.sftp_username}@${server.value?.sftp_host}`)
|
||||
|
||||
const copyToClipboard = (name: string, textToCopy?: string) => {
|
||||
const copyToClipboard = (label: string, textToCopy?: string) => {
|
||||
navigator.clipboard.writeText(textToCopy || '')
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: `${name} copied to clipboard!`,
|
||||
title: formatMessage(messages.clipboardCopiedTitle, { label }),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -242,7 +376,7 @@ const { data: startupData, isLoading: isStartupLoading } = useQuery({
|
||||
enabled: computed(() => worldId.value !== null),
|
||||
})
|
||||
|
||||
const JAVA_VERSIONS = [
|
||||
const JAVA_VERSION_OPTIONS: { value: number; label: string }[] = [
|
||||
{ value: 8, label: 'Java 8' },
|
||||
{ value: 11, label: 'Java 11' },
|
||||
{ value: 17, label: 'Java 17' },
|
||||
@@ -271,24 +405,24 @@ function parseMinecraftReleaseVersion(version: string): MinecraftReleaseVersion
|
||||
}
|
||||
|
||||
function filterJavaVersions(compatibleVersions: number[]) {
|
||||
return JAVA_VERSIONS.filter((version) => compatibleVersions.includes(version.value))
|
||||
return JAVA_VERSION_OPTIONS.filter((version) => compatibleVersions.includes(version.value))
|
||||
}
|
||||
|
||||
const displayedJavaVersions = computed(() => {
|
||||
if (showAllVersions.value) return JAVA_VERSIONS
|
||||
if (showAllVersions.value) return JAVA_VERSION_OPTIONS
|
||||
|
||||
const mcVersion = server.value?.mc_version ?? ''
|
||||
if (!mcVersion) return JAVA_VERSIONS
|
||||
if (!mcVersion) return JAVA_VERSION_OPTIONS
|
||||
|
||||
const releaseVersion = parseMinecraftReleaseVersion(mcVersion)
|
||||
if (!releaseVersion) return JAVA_VERSIONS
|
||||
if (!releaseVersion) return JAVA_VERSION_OPTIONS
|
||||
|
||||
if (releaseVersion.major > 1) {
|
||||
if (releaseVersion.major >= 26) {
|
||||
return filterJavaVersions([25])
|
||||
}
|
||||
|
||||
return JAVA_VERSIONS
|
||||
return JAVA_VERSION_OPTIONS
|
||||
}
|
||||
|
||||
if (releaseVersion.minor >= 20) return filterJavaVersions([21])
|
||||
@@ -298,7 +432,7 @@ const displayedJavaVersions = computed(() => {
|
||||
return filterJavaVersions([8])
|
||||
})
|
||||
|
||||
const JRE_VENDORS: { value: Archon.Content.v1.JreVendor; label: string }[] = [
|
||||
const JRE_VENDOR_OPTIONS: { value: Archon.Content.v1.JreVendor; label: string }[] = [
|
||||
{ value: 'corretto', label: 'Corretto' },
|
||||
{ value: 'temurin', label: 'Temurin' },
|
||||
{ value: 'graal', label: 'GraalVM' },
|
||||
@@ -316,9 +450,11 @@ const javaVersion = ref<number>()
|
||||
const jreVendor = ref<Archon.Content.v1.JreVendor>()
|
||||
|
||||
const javaVersionLabel = computed(
|
||||
() => JAVA_VERSIONS.find((v) => v.value === javaVersion.value)?.label,
|
||||
() => JAVA_VERSION_OPTIONS.find((v) => v.value === javaVersion.value)?.label,
|
||||
)
|
||||
const jreVendorLabel = computed(
|
||||
() => JRE_VENDOR_OPTIONS.find((v) => v.value === jreVendor.value)?.label,
|
||||
)
|
||||
const jreVendorLabel = computed(() => JRE_VENDORS.find((v) => v.value === jreVendor.value)?.label)
|
||||
|
||||
function syncFormFromData() {
|
||||
startupCommand.value = savedStartupCommand.value
|
||||
@@ -355,16 +491,16 @@ const { mutate: saveStartup, isPending } = useMutation({
|
||||
syncFormFromData()
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Server settings updated',
|
||||
text: 'Your server settings were successfully changed.',
|
||||
title: formatMessage(commonMessages.serverSettingsUpdatedTitle),
|
||||
text: formatMessage(commonMessages.serverSettingsUpdatedText),
|
||||
})
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error)
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Failed to update server arguments',
|
||||
text: 'Please try again later.',
|
||||
title: formatMessage(messages.startupUpdateFailedTitle),
|
||||
text: formatMessage(messages.startupUpdateFailedText),
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
<!-- Server name -->
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<label for="server-name-field" class="flex flex-col gap-2">
|
||||
<span class="text-lg font-semibold text-contrast">Server name</span>
|
||||
<span class="text-lg font-semibold text-contrast">{{
|
||||
formatMessage(messages.serverNameLabel)
|
||||
}}</span>
|
||||
</label>
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<StyledInput
|
||||
@@ -17,9 +19,11 @@
|
||||
:maxlength="48"
|
||||
@keyup.enter="!serverName && saveGeneral"
|
||||
/>
|
||||
<span>This name is only visible on Modrinth.</span>
|
||||
<span>{{ formatMessage(messages.serverNameDescription) }}</span>
|
||||
<div class="text-red font-medium">
|
||||
<span v-if="!isValidServerName"> Server name cannot be empty. </span>
|
||||
<span v-if="!isValidServerName">
|
||||
{{ formatMessage(messages.serverNameEmptyError) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -27,7 +31,9 @@
|
||||
<!-- Hostname -->
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<label for="server-subdomain" class="flex flex-col gap-2.5">
|
||||
<span class="text-lg font-semibold text-contrast">Hostname</span>
|
||||
<span class="text-lg font-semibold text-contrast">{{
|
||||
formatMessage(messages.hostnameLabel)
|
||||
}}</span>
|
||||
<div
|
||||
class="flex w-full overflow-hidden rounded-xl bg-button-bg px-3 [box-shadow:var(--shadow-inset-sm)] transition-[box-shadow] duration-100 ease-in-out focus-within:[box-shadow:0_0_0_0.25rem_var(--color-brand-shadow)]"
|
||||
>
|
||||
@@ -35,12 +41,12 @@
|
||||
<span
|
||||
class="pointer-events-none invisible whitespace-pre px-px text-base font-medium"
|
||||
aria-hidden="true"
|
||||
>{{ serverSubdomain || 'Enter subdomain...' }}</span
|
||||
>{{ serverSubdomain || formatMessage(messages.subdomainPlaceholder) }}</span
|
||||
>
|
||||
<input
|
||||
id="server-subdomain"
|
||||
:value="serverSubdomain"
|
||||
placeholder="Enter subdomain..."
|
||||
:placeholder="formatMessage(messages.subdomainPlaceholder)"
|
||||
:maxlength="32"
|
||||
class="absolute left-px inset-0 bg-transparent !p-0 text-base font-medium text-primary !shadow-none transition-colors placeholder:text-secondary focus:text-contrast"
|
||||
autocomplete="off"
|
||||
@@ -56,13 +62,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<span>Your friends can connect to your server using this address.</span>
|
||||
<span>{{ formatMessage(messages.hostnameDescription) }}</span>
|
||||
<div v-if="!isValidSubdomain" class="text-red font-medium">
|
||||
<span v-if="!isValidLengthSubdomain">
|
||||
Subdomain must be at least 5 characters long.
|
||||
{{ formatMessage(messages.subdomainLengthError) }}
|
||||
</span>
|
||||
<span v-if="!isValidCharsSubdomain">
|
||||
Subdomain can only contain alphanumeric characters and dashes.
|
||||
{{ formatMessage(messages.subdomainCharsError) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -79,15 +85,17 @@
|
||||
>
|
||||
<label :for="`pref-${key}`" class="flex flex-col gap-1">
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<span class="text-lg font-semibold text-contrast">{{ prefConfig.displayName }}</span>
|
||||
<span class="text-lg font-semibold text-contrast">{{
|
||||
formatMessage(prefConfig.title)
|
||||
}}</span>
|
||||
<div
|
||||
v-if="!prefConfig.implemented"
|
||||
class="hidden items-center gap-1 rounded-full bg-surface-2 p-1 px-1.5 text-xs font-semibold sm:flex"
|
||||
>
|
||||
Coming Soon
|
||||
{{ formatMessage(messages.comingSoonBadge) }}
|
||||
</div>
|
||||
</div>
|
||||
<span>{{ prefConfig.description }}</span>
|
||||
<span>{{ formatMessage(prefConfig.description) }}</span>
|
||||
</label>
|
||||
<div v-tooltip="getPreferenceTooltip(key)">
|
||||
<Toggle
|
||||
@@ -102,14 +110,16 @@
|
||||
|
||||
<!-- Info -->
|
||||
<div class="flex flex-col gap-2.5 pb-10">
|
||||
<div class="text-lg m-0 font-semibold text-contrast">Info</div>
|
||||
<div class="text-lg m-0 font-semibold text-contrast">
|
||||
{{ formatMessage(messages.infoSectionTitle) }}
|
||||
</div>
|
||||
<div class="flex flex-col gap-2.5 rounded-xl bg-surface-2 p-4">
|
||||
<div
|
||||
v-for="property in infoProperties"
|
||||
:key="property.name"
|
||||
class="flex items-start justify-between gap-4"
|
||||
>
|
||||
<template v-if="property.value !== 'Unknown'">
|
||||
<template v-if="property.value !== unknownLabelResolved">
|
||||
<span class="mt-1">{{ property.name }}</span>
|
||||
<CopyCode v-if="property.type === 'copy'" :text="property.value" />
|
||||
<div
|
||||
@@ -145,14 +155,142 @@ import { computed, ref, watch } from 'vue'
|
||||
import { CopyCode, StyledInput, Toggle } from '#ui/components'
|
||||
import EditServerIcon from '#ui/components/servers/edit-server-icon/EditServerIcon.vue'
|
||||
import SaveBanner from '#ui/components/servers/SaveBanner.vue'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import {
|
||||
injectModrinthClient,
|
||||
injectModrinthServerContext,
|
||||
injectNotificationManager,
|
||||
injectPageContext,
|
||||
} from '#ui/providers'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
|
||||
const messages = defineMessages({
|
||||
serverNameLabel: {
|
||||
id: 'server.settings.general.server-name',
|
||||
defaultMessage: 'Server name',
|
||||
},
|
||||
serverNameDescription: {
|
||||
id: 'server.settings.general.server-name-description',
|
||||
defaultMessage: 'This name is only visible on Modrinth.',
|
||||
},
|
||||
serverNameEmptyError: {
|
||||
id: 'server.settings.general.server-name-empty',
|
||||
defaultMessage: 'Server name cannot be empty.',
|
||||
},
|
||||
hostnameLabel: {
|
||||
id: 'server.settings.general.hostname',
|
||||
defaultMessage: 'Hostname',
|
||||
},
|
||||
subdomainPlaceholder: {
|
||||
id: 'server.settings.general.subdomain-placeholder',
|
||||
defaultMessage: 'Enter subdomain…',
|
||||
},
|
||||
hostnameDescription: {
|
||||
id: 'server.settings.general.hostname-description',
|
||||
defaultMessage: 'Your friends can connect to your server using this address.',
|
||||
},
|
||||
subdomainLengthError: {
|
||||
id: 'server.settings.general.subdomain-length',
|
||||
defaultMessage: 'Subdomain must be at least 5 characters long.',
|
||||
},
|
||||
subdomainCharsError: {
|
||||
id: 'server.settings.general.subdomain-chars',
|
||||
defaultMessage: 'Subdomain can only contain alphanumeric characters and dashes.',
|
||||
},
|
||||
prefHideSubdomainTitle: {
|
||||
id: 'server.settings.general.pref.hide-subdomain.title',
|
||||
defaultMessage: 'Hide subdomain label',
|
||||
},
|
||||
prefHideSubdomainDescription: {
|
||||
id: 'server.settings.general.pref.hide-subdomain.description',
|
||||
defaultMessage: 'When enabled, the subdomain label will be hidden from the server header.',
|
||||
},
|
||||
prefRamAsBytesTitle: {
|
||||
id: 'server.settings.general.pref.ram-bytes.title',
|
||||
defaultMessage: 'RAM as bytes',
|
||||
},
|
||||
prefRamAsBytesDescription: {
|
||||
id: 'server.settings.general.pref.ram-bytes.description',
|
||||
defaultMessage: 'Show RAM usage in bytes instead of a percentage.',
|
||||
},
|
||||
prefRamAsBytesForcedTooltip: {
|
||||
id: 'server.settings.general.pref.ram-bytes.forced-tooltip',
|
||||
defaultMessage: 'Feature flag enabled to always show RAM as bytes.',
|
||||
},
|
||||
comingSoonBadge: {
|
||||
id: 'server.settings.general.coming-soon',
|
||||
defaultMessage: 'Coming soon',
|
||||
},
|
||||
infoSectionTitle: {
|
||||
id: 'server.settings.general.info.title',
|
||||
defaultMessage: 'Info',
|
||||
},
|
||||
infoServerId: {
|
||||
id: 'server.settings.general.info.server-id',
|
||||
defaultMessage: 'Server ID',
|
||||
},
|
||||
infoNode: {
|
||||
id: 'server.settings.general.info.node',
|
||||
defaultMessage: 'Node',
|
||||
},
|
||||
infoHostname: {
|
||||
id: 'server.settings.general.info.hostname',
|
||||
defaultMessage: 'Hostname',
|
||||
},
|
||||
infoServerSpecs: {
|
||||
id: 'server.settings.general.info.server-specs',
|
||||
defaultMessage: 'Server specs',
|
||||
},
|
||||
specsAvailable: {
|
||||
id: 'server.settings.general.info.specs-available',
|
||||
defaultMessage: 'Available',
|
||||
},
|
||||
specsCpuRamLine: {
|
||||
id: 'server.settings.general.info.specs-cpu-ram-line',
|
||||
defaultMessage:
|
||||
'{shared} {sharedNum, plural, one {Shared CPU} other {Shared CPUs}} (Bursts up to {burst} CPUs)',
|
||||
},
|
||||
specsRamGb: {
|
||||
id: 'server.settings.general.info.specs-ram-gb',
|
||||
defaultMessage: '{gb} GB RAM',
|
||||
},
|
||||
specsSwapGb: {
|
||||
id: 'server.settings.general.info.specs-swap-gb',
|
||||
defaultMessage: '{gb} GB Swap',
|
||||
},
|
||||
specsStorageGb: {
|
||||
id: 'server.settings.general.info.specs-storage-gb',
|
||||
defaultMessage: '{gb} GB SSD',
|
||||
},
|
||||
subdomainUnavailableTitle: {
|
||||
id: 'server.settings.general.error.subdomain-taken.title',
|
||||
defaultMessage: 'Subdomain not available',
|
||||
},
|
||||
subdomainUnavailableText: {
|
||||
id: 'server.settings.general.error.subdomain-taken.text',
|
||||
defaultMessage: 'The subdomain you entered is already in use.',
|
||||
},
|
||||
subdomainCheckFailedTitle: {
|
||||
id: 'server.settings.general.error.subdomain-check.title',
|
||||
defaultMessage: 'Error checking availability',
|
||||
},
|
||||
subdomainCheckFailedText: {
|
||||
id: 'server.settings.general.error.subdomain-check.text',
|
||||
defaultMessage: 'Failed to verify if the subdomain is available.',
|
||||
},
|
||||
settingsUpdateFailedTitle: {
|
||||
id: 'server.settings.general.error.update-failed.title',
|
||||
defaultMessage: 'Failed to update server settings',
|
||||
},
|
||||
settingsUpdateFailedText: {
|
||||
id: 'server.settings.general.error.update-failed.text',
|
||||
defaultMessage: 'An error occurred while attempting to update your server settings.',
|
||||
},
|
||||
})
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const { server: data, serverId, busyReasons } = injectModrinthServerContext()
|
||||
const { featureFlags } = injectPageContext()
|
||||
@@ -185,18 +323,13 @@ watch(serverName, (newValue, oldValue) => {
|
||||
// Preferences
|
||||
const preferences = {
|
||||
hideSubdomainLabel: {
|
||||
displayName: 'Hide subdomain label',
|
||||
description: 'When enabled, the subdomain label will be hidden from the server header.',
|
||||
title: messages.prefHideSubdomainTitle,
|
||||
description: messages.prefHideSubdomainDescription,
|
||||
implemented: true,
|
||||
},
|
||||
// autoRestart: {
|
||||
// displayName: 'Auto restarts',
|
||||
// description: 'Automatically restart the server if it crashes.',
|
||||
// implemented: false,
|
||||
// },
|
||||
ramAsNumber: {
|
||||
displayName: 'RAM as bytes',
|
||||
description: 'Show RAM usage in bytes instead of a percentage.',
|
||||
title: messages.prefRamAsBytesTitle,
|
||||
description: messages.prefRamAsBytesDescription,
|
||||
implemented: true,
|
||||
},
|
||||
} as const
|
||||
@@ -229,7 +362,7 @@ const isPreferenceForcedByFeatureFlag = (key: string) =>
|
||||
|
||||
const getPreferenceTooltip = (key: string) =>
|
||||
isPreferenceForcedByFeatureFlag(key)
|
||||
? 'Feature flag enabled to always show RAM as bytes.'
|
||||
? formatMessage(messages.prefRamAsBytesForcedTooltip)
|
||||
: undefined
|
||||
|
||||
const getPreferenceValue = (key: string) =>
|
||||
@@ -289,8 +422,10 @@ const getServerSpecs = (product?: Labrinth.Billing.Internal.Product | null) => {
|
||||
}
|
||||
}
|
||||
|
||||
const unknownLabelResolved = computed(() => formatMessage(commonMessages.unknownLabel))
|
||||
|
||||
const serverHostname = computed(() =>
|
||||
serverSubdomain.value ? `${serverSubdomain.value}.modrinth.gg` : 'Unknown',
|
||||
serverSubdomain.value ? `${serverSubdomain.value}.modrinth.gg` : unknownLabelResolved.value,
|
||||
)
|
||||
|
||||
const serverSpecs = computed(() => getServerSpecs(serverProduct.value))
|
||||
@@ -314,24 +449,36 @@ type InfoProperty =
|
||||
}
|
||||
|
||||
// Info properties
|
||||
const infoProperties = computed<InfoProperty[]>(() => [
|
||||
{ name: 'Server ID', value: serverId ?? 'Unknown', type: 'copy' },
|
||||
{ name: 'Node', value: data.value?.node?.instance ?? 'Unknown', type: 'copy' },
|
||||
{ name: 'Hostname', value: serverHostname.value, type: 'copy' },
|
||||
{
|
||||
name: 'Server specs',
|
||||
value: serverSpecs.value ? 'Available' : 'Unknown',
|
||||
type: 'specs',
|
||||
lines: serverSpecs.value
|
||||
? [
|
||||
`${serverSpecs.value.sharedCpus} Shared CPU${Number(serverSpecs.value.sharedCpus) > 1 ? 's' : ''} (Bursts up to ${serverSpecs.value.burstCpus} CPUs)`,
|
||||
`${serverSpecs.value.ramGb} GB RAM`,
|
||||
`${serverSpecs.value.swapGb} GB Swap`,
|
||||
`${serverSpecs.value.storageGb} GB SSD`,
|
||||
]
|
||||
: [],
|
||||
},
|
||||
])
|
||||
const infoProperties = computed<InfoProperty[]>(() => {
|
||||
const u = unknownLabelResolved.value
|
||||
const specs = serverSpecs.value
|
||||
return [
|
||||
{ name: formatMessage(messages.infoServerId), value: serverId ?? u, type: 'copy' },
|
||||
{
|
||||
name: formatMessage(messages.infoNode),
|
||||
value: data.value?.node?.instance ?? u,
|
||||
type: 'copy',
|
||||
},
|
||||
{ name: formatMessage(messages.infoHostname), value: serverHostname.value, type: 'copy' },
|
||||
{
|
||||
name: formatMessage(messages.infoServerSpecs),
|
||||
value: specs ? formatMessage(messages.specsAvailable) : u,
|
||||
type: 'specs',
|
||||
lines: specs
|
||||
? [
|
||||
formatMessage(messages.specsCpuRamLine, {
|
||||
shared: specs.sharedCpus,
|
||||
sharedNum: Number(specs.sharedCpus),
|
||||
burst: specs.burstCpus,
|
||||
}),
|
||||
formatMessage(messages.specsRamGb, { gb: specs.ramGb }),
|
||||
formatMessage(messages.specsSwapGb, { gb: specs.swapGb }),
|
||||
formatMessage(messages.specsStorageGb, { gb: specs.storageGb }),
|
||||
]
|
||||
: [],
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
// Unsaved changes tracking (API fields + preferences)
|
||||
const hasUnsavedChanges = computed(
|
||||
@@ -359,8 +506,8 @@ const saveGeneral = async () => {
|
||||
if (!available) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Subdomain not available',
|
||||
text: 'The subdomain you entered is already in use.',
|
||||
title: formatMessage(messages.subdomainUnavailableTitle),
|
||||
text: formatMessage(messages.subdomainUnavailableText),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -370,8 +517,8 @@ const saveGeneral = async () => {
|
||||
console.error('Error checking subdomain availability:', error)
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Error checking availability',
|
||||
text: 'Failed to verify if the subdomain is available.',
|
||||
title: formatMessage(messages.subdomainCheckFailedTitle),
|
||||
text: formatMessage(messages.subdomainCheckFailedText),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -385,15 +532,15 @@ const saveGeneral = async () => {
|
||||
})
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Server settings updated',
|
||||
text: 'Your server settings were successfully changed.',
|
||||
title: formatMessage(commonMessages.serverSettingsUpdatedTitle),
|
||||
text: formatMessage(commonMessages.serverSettingsUpdatedText),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Failed to update server settings',
|
||||
text: 'An error occurred while attempting to update your server settings.',
|
||||
title: formatMessage(messages.settingsUpdateFailedTitle),
|
||||
text: formatMessage(messages.settingsUpdateFailedText),
|
||||
})
|
||||
} finally {
|
||||
isUpdating.value = false
|
||||
|
||||
@@ -2,24 +2,32 @@
|
||||
<div>
|
||||
<Teleport to="body">
|
||||
<div class="relative z-[100]">
|
||||
<NewModal ref="editAllocationModal" header="Edit allocation" width="550px">
|
||||
<NewModal
|
||||
ref="editAllocationModal"
|
||||
:header="formatMessage(messages.editAllocationHeader)"
|
||||
width="550px"
|
||||
>
|
||||
<form class="flex w-full flex-col gap-2" @submit.prevent="editAllocation">
|
||||
<label for="edit-allocation-name" class="font-semibold text-contrast"> Name </label>
|
||||
<label for="edit-allocation-name" class="font-semibold text-contrast">
|
||||
{{ formatMessage(messages.allocationNameLabel) }}
|
||||
</label>
|
||||
<StyledInput
|
||||
id="edit-allocation-name"
|
||||
ref="editAllocationInput"
|
||||
v-model="editAllocationName"
|
||||
wrapper-class="w-full"
|
||||
:maxlength="32"
|
||||
placeholder="e.g. Secondary allocation"
|
||||
:placeholder="formatMessage(messages.allocationNamePlaceholder)"
|
||||
/>
|
||||
<div class="mb-1 mt-4 flex justify-end gap-2.5">
|
||||
<ButtonStyled>
|
||||
<button @click="editAllocationModal?.hide()">Cancel</button>
|
||||
<button @click="editAllocationModal?.hide()">
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="!editAllocationName || creatingAllocation" type="submit">
|
||||
<SaveIcon /> Update allocation
|
||||
<SaveIcon /> {{ formatMessage(messages.updateAllocationButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
@@ -28,9 +36,9 @@
|
||||
|
||||
<ConfirmModal
|
||||
ref="confirmDeleteModal"
|
||||
title="Deleting allocation"
|
||||
:description="`You are deleting the allocation ${allocationToDelete}. This cannot be reserved again. Are you sure you want to proceed?`"
|
||||
proceed-label="Delete"
|
||||
:title="formatMessage(messages.deleteAllocationTitle)"
|
||||
:description="deleteAllocationDescriptionText"
|
||||
:proceed-label="formatMessage(commonMessages.deleteLabel)"
|
||||
@proceed="confirmDeleteAllocation"
|
||||
/>
|
||||
</div>
|
||||
@@ -47,16 +55,20 @@
|
||||
<div class="grid place-content-center rounded-full bg-bg-orange p-4">
|
||||
<IssuesIcon class="size-12 text-orange" />
|
||||
</div>
|
||||
<h1 class="m-0 mb-2 w-fit text-4xl font-semibold">Failed to load network settings</h1>
|
||||
<h1 class="m-0 mb-2 w-fit text-4xl font-semibold">
|
||||
{{ formatMessage(messages.loadNetworkErrorTitle) }}
|
||||
</h1>
|
||||
</div>
|
||||
<p class="text-md text-secondary">
|
||||
We couldn't load your server's network settings. Here's what we know:
|
||||
{{ formatMessage(messages.loadNetworkErrorDescription) }}
|
||||
<span class="break-all font-mono">{{
|
||||
allocationsError?.message ?? 'Unknown error'
|
||||
allocationsError?.message ?? formatMessage(commonMessages.unknownLabel)
|
||||
}}</span>
|
||||
</p>
|
||||
<ButtonStyled size="large" color="brand" @click="() => refetchAllocations()">
|
||||
<button class="mt-6 !w-full">Retry</button>
|
||||
<button class="mt-6 !w-full">
|
||||
{{ formatMessage(commonMessages.retryButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,31 +77,37 @@
|
||||
<div class="flex h-full flex-col gap-6">
|
||||
<!-- Allocations section -->
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<span class="text-lg font-semibold text-contrast">Allocations</span>
|
||||
<span class="text-lg font-semibold text-contrast">{{
|
||||
formatMessage(messages.allocationsSectionTitle)
|
||||
}}</span>
|
||||
|
||||
<div class="flex w-full flex-col items-center justify-start gap-2 sm:flex-row">
|
||||
<StyledInput
|
||||
v-model="createAllocationName"
|
||||
wrapper-class="grow max-w-[400px]"
|
||||
:maxlength="32"
|
||||
placeholder="e.g. Secondary allocation"
|
||||
:placeholder="formatMessage(messages.allocationNamePlaceholder)"
|
||||
/>
|
||||
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
v-tooltip="!createAllocationName ? 'Enter a name to create an allocation' : ''"
|
||||
v-tooltip="
|
||||
!createAllocationName ? formatMessage(messages.createAllocationTooltip) : ''
|
||||
"
|
||||
:disabled="!createAllocationName || creatingAllocation"
|
||||
@click="addNewAllocation"
|
||||
>
|
||||
<PlusIcon />
|
||||
<span>Create allocation</span>
|
||||
<span>{{ formatMessage(messages.createAllocationButton) }}</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<Table :columns="allocationColumns" :data="allocationRows" row-key="port">
|
||||
<template #cell-name="{ row }">
|
||||
<TagItem v-if="row.primary" class="!font-medium">Primary</TagItem>
|
||||
<TagItem v-if="row.primary" class="!font-medium">{{
|
||||
formatMessage(messages.primaryAllocationLabel)
|
||||
}}</TagItem>
|
||||
<span v-else class="font-semibold">{{ row.name }}</span>
|
||||
</template>
|
||||
<template #cell-port="{ row }">
|
||||
@@ -117,16 +135,15 @@
|
||||
</div>
|
||||
</template>
|
||||
</Table>
|
||||
<span>
|
||||
Create additional ports for internet-facing features like map viewers or voice chat
|
||||
mods.
|
||||
</span>
|
||||
<span>{{ formatMessage(messages.allocationsHelpText) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- DNS records section -->
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<label for="user-domain" class="flex flex-col gap-2">
|
||||
<span class="text-lg font-semibold text-contrast">DNS records</span>
|
||||
<span class="text-lg font-semibold text-contrast">{{
|
||||
formatMessage(messages.dnsRecordsTitle)
|
||||
}}</span>
|
||||
</label>
|
||||
<div class="flex w-full flex-col items-center justify-start gap-2 sm:flex-row">
|
||||
<StyledInput
|
||||
@@ -144,7 +161,7 @@
|
||||
@click="exportDnsRecords"
|
||||
>
|
||||
<UploadIcon />
|
||||
<span>Export</span>
|
||||
<span>{{ formatMessage(messages.exportDnsButton) }}</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
@@ -184,9 +201,7 @@
|
||||
</template>
|
||||
</Table>
|
||||
|
||||
<span>
|
||||
Set up your personal domain to connect to your server via custom DNS records.
|
||||
</span>
|
||||
<span>{{ formatMessage(messages.dnsRecordsHelpText) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -209,13 +224,145 @@ import { computed, nextTick, ref } from 'vue'
|
||||
|
||||
import { ButtonStyled, ConfirmModal, NewModal, StyledInput, Table, TagItem } from '#ui/components'
|
||||
import type { TableColumn } from '#ui/components/base'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import {
|
||||
injectModrinthClient,
|
||||
injectModrinthServerContext,
|
||||
injectNotificationManager,
|
||||
} from '#ui/providers'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
|
||||
const messages = defineMessages({
|
||||
editAllocationHeader: {
|
||||
id: 'server.settings.network.edit-allocation.title',
|
||||
defaultMessage: 'Edit allocation',
|
||||
},
|
||||
allocationNameLabel: {
|
||||
id: 'server.settings.network.allocation-name.label',
|
||||
defaultMessage: 'Name',
|
||||
},
|
||||
allocationNamePlaceholder: {
|
||||
id: 'server.settings.network.allocation-name.placeholder',
|
||||
defaultMessage: 'e.g. Secondary allocation',
|
||||
},
|
||||
updateAllocationButton: {
|
||||
id: 'server.settings.network.update-allocation',
|
||||
defaultMessage: 'Update allocation',
|
||||
},
|
||||
deleteAllocationTitle: {
|
||||
id: 'server.settings.network.delete-allocation.title',
|
||||
defaultMessage: 'Deleting allocation',
|
||||
},
|
||||
deleteAllocationDescription: {
|
||||
id: 'server.settings.network.delete-allocation.description',
|
||||
defaultMessage:
|
||||
'You are deleting the allocation on port {port}. This cannot be reserved again. Are you sure you want to proceed?',
|
||||
},
|
||||
loadNetworkErrorTitle: {
|
||||
id: 'server.settings.network.error.load.title',
|
||||
defaultMessage: 'Failed to load network settings',
|
||||
},
|
||||
loadNetworkErrorDescription: {
|
||||
id: 'server.settings.network.error.load.description',
|
||||
defaultMessage: "We couldn't load your server's network settings. Here's what we know:",
|
||||
},
|
||||
allocationsSectionTitle: {
|
||||
id: 'server.settings.network.allocations.title',
|
||||
defaultMessage: 'Allocations',
|
||||
},
|
||||
createAllocationTooltip: {
|
||||
id: 'server.settings.network.create-allocation.tooltip',
|
||||
defaultMessage: 'Enter a name to create an allocation',
|
||||
},
|
||||
createAllocationButton: {
|
||||
id: 'server.settings.network.create-allocation',
|
||||
defaultMessage: 'Create allocation',
|
||||
},
|
||||
primaryAllocationLabel: {
|
||||
id: 'server.settings.network.primary-allocation',
|
||||
defaultMessage: 'Primary',
|
||||
},
|
||||
primaryAllocationRowName: {
|
||||
id: 'server.settings.network.primary-allocation-row-name',
|
||||
defaultMessage: 'Primary allocation',
|
||||
},
|
||||
allocationsHelpText: {
|
||||
id: 'server.settings.network.allocations.help',
|
||||
defaultMessage:
|
||||
'Create additional ports for internet-facing features like map viewers or voice chat mods.',
|
||||
},
|
||||
dnsRecordsTitle: {
|
||||
id: 'server.settings.network.dns.title',
|
||||
defaultMessage: 'DNS records',
|
||||
},
|
||||
exportDnsButton: {
|
||||
id: 'server.settings.network.dns.export',
|
||||
defaultMessage: 'Export',
|
||||
},
|
||||
dnsRecordsHelpText: {
|
||||
id: 'server.settings.network.dns.help',
|
||||
defaultMessage: 'Set up your personal domain to connect to your server via custom DNS records.',
|
||||
},
|
||||
columnName: {
|
||||
id: 'server.settings.network.column.name',
|
||||
defaultMessage: 'Name',
|
||||
},
|
||||
columnPort: {
|
||||
id: 'server.settings.network.column.port',
|
||||
defaultMessage: 'Port',
|
||||
},
|
||||
columnActions: {
|
||||
id: 'server.settings.network.column.actions',
|
||||
defaultMessage: 'Actions',
|
||||
},
|
||||
columnRecordType: {
|
||||
id: 'server.settings.network.column.record-type',
|
||||
defaultMessage: 'Type',
|
||||
},
|
||||
columnRecordName: {
|
||||
id: 'server.settings.network.column.record-name',
|
||||
defaultMessage: 'Name',
|
||||
},
|
||||
columnRecordContent: {
|
||||
id: 'server.settings.network.column.record-content',
|
||||
defaultMessage: 'Content',
|
||||
},
|
||||
allocationReservedTitle: {
|
||||
id: 'server.settings.network.success.allocation-reserved.title',
|
||||
defaultMessage: 'Allocation reserved',
|
||||
},
|
||||
allocationReservedText: {
|
||||
id: 'server.settings.network.success.allocation-reserved.text',
|
||||
defaultMessage: 'Your allocation has been reserved.',
|
||||
},
|
||||
allocationRemovedTitle: {
|
||||
id: 'server.settings.network.success.allocation-removed.title',
|
||||
defaultMessage: 'Allocation removed',
|
||||
},
|
||||
allocationRemovedText: {
|
||||
id: 'server.settings.network.success.allocation-removed.text',
|
||||
defaultMessage: 'Your allocation has been removed.',
|
||||
},
|
||||
allocationUpdatedTitle: {
|
||||
id: 'server.settings.network.success.allocation-updated.title',
|
||||
defaultMessage: 'Allocation updated',
|
||||
},
|
||||
allocationUpdatedText: {
|
||||
id: 'server.settings.network.success.allocation-updated.text',
|
||||
defaultMessage: 'Your allocation has been updated.',
|
||||
},
|
||||
textCopiedTitle: {
|
||||
id: 'server.settings.network.success.text-copied.title',
|
||||
defaultMessage: 'Text copied',
|
||||
},
|
||||
textCopiedText: {
|
||||
id: 'server.settings.network.success.text-copied.text',
|
||||
defaultMessage: '{text} has been copied to your clipboard',
|
||||
},
|
||||
})
|
||||
const { server, serverId } = injectModrinthServerContext()
|
||||
const client = injectModrinthClient()
|
||||
const queryClient = useQueryClient()
|
||||
@@ -237,15 +384,15 @@ const {
|
||||
})
|
||||
const allocations = allocationsData
|
||||
|
||||
const allocationColumns: TableColumn[] = [
|
||||
{ key: 'name', label: 'Name', width: '40%' },
|
||||
{ key: 'port', label: 'Port' },
|
||||
{ key: 'actions', label: 'Actions', width: '33%', align: 'right' },
|
||||
]
|
||||
const allocationColumns = computed<TableColumn[]>(() => [
|
||||
{ key: 'name', label: formatMessage(messages.columnName), width: '40%' },
|
||||
{ key: 'port', label: formatMessage(messages.columnPort) },
|
||||
{ key: 'actions', label: formatMessage(messages.columnActions), width: '33%', align: 'right' },
|
||||
])
|
||||
|
||||
const allocationRows = computed(() => {
|
||||
const primary = {
|
||||
name: 'Primary allocation',
|
||||
name: formatMessage(messages.primaryAllocationRowName),
|
||||
port: serverPrimaryPort.value,
|
||||
primary: true,
|
||||
}
|
||||
@@ -257,11 +404,17 @@ const allocationRows = computed(() => {
|
||||
return [primary, ...extra]
|
||||
})
|
||||
|
||||
const dnsColumns: TableColumn[] = [
|
||||
{ key: 'type', label: 'Type', width: '20%' },
|
||||
{ key: 'name', label: 'Name', width: '35%' },
|
||||
{ key: 'content', label: 'Content' },
|
||||
]
|
||||
const dnsColumns = computed<TableColumn[]>(() => [
|
||||
{ key: 'type', label: formatMessage(messages.columnRecordType), width: '20%' },
|
||||
{ key: 'name', label: formatMessage(messages.columnRecordName), width: '35%' },
|
||||
{ key: 'content', label: formatMessage(messages.columnRecordContent) },
|
||||
])
|
||||
|
||||
const deleteAllocationDescriptionText = computed(() =>
|
||||
allocationToDelete.value != null
|
||||
? formatMessage(messages.deleteAllocationDescription, { port: allocationToDelete.value })
|
||||
: '',
|
||||
)
|
||||
|
||||
const editAllocationModal = ref<typeof NewModal>()
|
||||
const confirmDeleteModal = ref<typeof ConfirmModal>()
|
||||
@@ -284,8 +437,8 @@ const addNewAllocation = async () => {
|
||||
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Allocation reserved',
|
||||
text: 'Your allocation has been reserved.',
|
||||
title: formatMessage(messages.allocationReservedTitle),
|
||||
text: formatMessage(messages.allocationReservedText),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to reserve new allocation:', error)
|
||||
@@ -318,8 +471,8 @@ const confirmDeleteAllocation = async () => {
|
||||
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Allocation removed',
|
||||
text: 'Your allocation has been removed.',
|
||||
title: formatMessage(messages.allocationRemovedTitle),
|
||||
text: formatMessage(messages.allocationRemovedText),
|
||||
})
|
||||
|
||||
allocationToDelete.value = null
|
||||
@@ -342,8 +495,8 @@ const editAllocation = async () => {
|
||||
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Allocation updated',
|
||||
text: 'Your allocation has been updated.',
|
||||
title: formatMessage(messages.allocationUpdatedTitle),
|
||||
text: formatMessage(messages.allocationUpdatedText),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to reserve new allocation:', error)
|
||||
@@ -404,8 +557,8 @@ const copyText = (text: string) => {
|
||||
navigator.clipboard.writeText(text)
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Text copied',
|
||||
text: `${text} has been copied to your clipboard`,
|
||||
title: formatMessage(messages.textCopiedTitle),
|
||||
text: formatMessage(messages.textCopiedText, { text }),
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -5,32 +5,37 @@
|
||||
<Admonition
|
||||
v-if="hasNoProperties"
|
||||
type="warning"
|
||||
body="Some expected properties are missing from your server.properties - this usually means the server hasn't completed its first startup yet."
|
||||
:body="formatMessage(messages.missingPropertiesWarning)"
|
||||
/>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="m-0">
|
||||
Edit the Minecraft server properties file here, or use the
|
||||
<AutoLink
|
||||
class="goto-link !inline-block"
|
||||
:to="filesTabLink"
|
||||
@click="onFilesTabLinkClick"
|
||||
>
|
||||
Files tab
|
||||
</AutoLink>
|
||||
to edit the full file. If you're unsure about a setting, the
|
||||
<AutoLink
|
||||
class="goto-link !inline-block"
|
||||
to="https://minecraft.wiki/w/Server.properties"
|
||||
target="_blank"
|
||||
>
|
||||
Minecraft Wiki
|
||||
</AutoLink>
|
||||
has more details.
|
||||
<IntlFormatted :message-id="messages.introParagraph">
|
||||
<template #files-link="{ children }">
|
||||
<AutoLink
|
||||
class="goto-link !inline-block"
|
||||
:to="filesTabLink"
|
||||
@click="onFilesTabLinkClick"
|
||||
>
|
||||
<component :is="() => children" />
|
||||
</AutoLink>
|
||||
</template>
|
||||
<template #wiki-link="{ children }">
|
||||
<AutoLink
|
||||
class="goto-link !inline-block"
|
||||
to="https://minecraft.wiki/w/Server.properties"
|
||||
target="_blank"
|
||||
>
|
||||
<component :is="() => children" />
|
||||
</AutoLink>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full text-sm">
|
||||
<label for="search-server-properties" class="sr-only"> Search server properties </label>
|
||||
<label for="search-server-properties" class="sr-only">
|
||||
{{ formatMessage(messages.searchPropertiesAriaLabel) }}
|
||||
</label>
|
||||
<StyledInput
|
||||
id="search-server-properties"
|
||||
v-model="searchInput"
|
||||
@@ -39,7 +44,7 @@
|
||||
:icon="SearchIcon"
|
||||
name="search"
|
||||
autocomplete="off"
|
||||
placeholder="Search server properties..."
|
||||
:placeholder="formatMessage(messages.searchPropertiesPlaceholder)"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 pb-2">
|
||||
@@ -51,7 +56,9 @@
|
||||
>
|
||||
<div class="flex w-full flex-col gap-1.5">
|
||||
<div v-if="isPropertyVisible('gamemode')" class="flex flex-col gap-2.5 my-1">
|
||||
<span class="font-semibold text-contrast">Gamemode</span>
|
||||
<span class="font-semibold text-contrast">{{
|
||||
formatMessage(messages.labelGamemode)
|
||||
}}</span>
|
||||
<Chips
|
||||
v-model="combinedGamemode"
|
||||
:items="gamemodeItems"
|
||||
@@ -63,7 +70,9 @@
|
||||
v-if="combinedGamemode !== 'hardcore' && isPropertyVisible('difficulty')"
|
||||
class="flex flex-col gap-2.5 my-1"
|
||||
>
|
||||
<span class="font-semibold text-contrast">Difficulty</span>
|
||||
<span class="font-semibold text-contrast">{{
|
||||
formatMessage(messages.labelDifficulty)
|
||||
}}</span>
|
||||
<Chips
|
||||
v-model="selectedDifficulty"
|
||||
:items="difficultyItems"
|
||||
@@ -72,23 +81,27 @@
|
||||
</div>
|
||||
|
||||
<div v-if="isPropertyVisible('max_players')" class="flex flex-col gap-2.5 my-1">
|
||||
<span class="font-semibold text-contrast">Max players</span>
|
||||
<span class="font-semibold text-contrast">{{
|
||||
formatMessage(messages.labelMaxPlayers)
|
||||
}}</span>
|
||||
<StyledInput
|
||||
id="server-property-max-players"
|
||||
:model-value="liveProperties.max_players"
|
||||
type="number"
|
||||
placeholder="20"
|
||||
:placeholder="formatMessage(messages.placeholderDefaultMaxPlayers)"
|
||||
wrapper-class="w-full max-w-[450px]"
|
||||
@update:model-value="liveProperties.max_players = String($event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="isPropertyVisible('motd')" class="flex flex-col gap-2.5 my-1">
|
||||
<span class="font-semibold text-contrast">MOTD</span>
|
||||
<span class="font-semibold text-contrast">{{
|
||||
formatMessage(messages.labelMotd)
|
||||
}}</span>
|
||||
<StyledInput
|
||||
id="server-property-motd"
|
||||
v-model="liveProperties.motd"
|
||||
placeholder="A Minecraft Server"
|
||||
:placeholder="formatMessage(messages.placeholderDefaultMotd)"
|
||||
wrapper-class="w-full max-w-[450px]"
|
||||
/>
|
||||
</div>
|
||||
@@ -97,7 +110,9 @@
|
||||
v-if="isPropertyVisible('allow_flight')"
|
||||
class="flex flex-row items-center justify-between gap-4 h-10"
|
||||
>
|
||||
<span class="font-semibold text-contrast">Allow flight</span>
|
||||
<span class="font-semibold text-contrast">{{
|
||||
formatMessage(messages.labelAllowFlight)
|
||||
}}</span>
|
||||
<Toggle
|
||||
id="server-property-allow-flight"
|
||||
:model-value="liveProperties.allow_flight === 'true'"
|
||||
@@ -109,7 +124,9 @@
|
||||
v-if="isPropertyVisible('allow_cheats')"
|
||||
class="flex flex-row items-center justify-between gap-4 h-10"
|
||||
>
|
||||
<span class="font-semibold text-contrast">Allow cheats</span>
|
||||
<span class="font-semibold text-contrast">{{
|
||||
formatMessage(messages.labelAllowCheats)
|
||||
}}</span>
|
||||
<Toggle
|
||||
id="server-property-allow-cheats"
|
||||
:model-value="liveProperties.allow_cheats === 'true'"
|
||||
@@ -121,7 +138,9 @@
|
||||
v-if="isPropertyVisible('white_list')"
|
||||
class="flex flex-row items-center justify-between gap-4 h-10"
|
||||
>
|
||||
<span class="font-semibold text-contrast">Enable whitelist</span>
|
||||
<span class="font-semibold text-contrast">{{
|
||||
formatMessage(messages.labelEnableWhitelist)
|
||||
}}</span>
|
||||
<Toggle id="server-property-whitelist" v-model="whitelistEnabled" />
|
||||
</div>
|
||||
|
||||
@@ -129,7 +148,9 @@
|
||||
v-if="isPropertyVisible('spawn_protection')"
|
||||
class="flex flex-row items-center justify-between gap-4 h-10"
|
||||
>
|
||||
<span class="font-semibold text-contrast">Enable spawn protection</span>
|
||||
<span class="font-semibold text-contrast">{{
|
||||
formatMessage(messages.labelEnableSpawnProtection)
|
||||
}}</span>
|
||||
<Toggle
|
||||
id="server-property-spawn-protection-toggle"
|
||||
v-model="spawnProtectionEnabled"
|
||||
@@ -140,7 +161,9 @@
|
||||
v-if="spawnProtectionEnabled && isPropertyVisible('spawn_protection')"
|
||||
class="flex items-center justify-between h-10"
|
||||
>
|
||||
<span class="font-semibold text-contrast">Protection radius</span>
|
||||
<span class="font-semibold text-contrast">{{
|
||||
formatMessage(messages.labelProtectionRadius)
|
||||
}}</span>
|
||||
<StyledInput
|
||||
id="server-property-spawn-protection-radius"
|
||||
:model-value="liveProperties.spawn_protection"
|
||||
@@ -161,7 +184,9 @@
|
||||
button-class="flex w-full flex-col gap-2 bg-transparent m-0 p-0 border-none"
|
||||
>
|
||||
<template #title>
|
||||
<span class="text-lg font-semibold text-contrast">Advanced properties</span>
|
||||
<span class="text-lg font-semibold text-contrast">{{
|
||||
formatMessage(messages.advancedPropertiesTitle)
|
||||
}}</span>
|
||||
</template>
|
||||
|
||||
<div class="flex flex-col gap-6 pt-4">
|
||||
@@ -201,7 +226,7 @@
|
||||
:id="`server-property-${key}`"
|
||||
:model-value="liveProperties[key]"
|
||||
type="number"
|
||||
placeholder="Type here..."
|
||||
:placeholder="formatMessage(messages.propertyValuePlaceholder)"
|
||||
wrapper-class="w-full"
|
||||
:aria-labelledby="`property-label-${key}`"
|
||||
@update:model-value="liveProperties[key] = String($event)"
|
||||
@@ -211,7 +236,7 @@
|
||||
<StyledInput
|
||||
:id="`server-property-${key}`"
|
||||
v-model="liveProperties[key]"
|
||||
placeholder="Type here..."
|
||||
:placeholder="formatMessage(messages.propertyValuePlaceholder)"
|
||||
wrapper-class="w-full"
|
||||
:aria-labelledby="`property-label-${key}`"
|
||||
/>
|
||||
@@ -222,14 +247,17 @@
|
||||
</div>
|
||||
</template>
|
||||
<div>
|
||||
All other properties can be edited in server.properties via the
|
||||
<AutoLink
|
||||
class="goto-link !inline-block"
|
||||
:to="filesTabLink"
|
||||
@click="onFilesTabLinkClick"
|
||||
>
|
||||
Files tab </AutoLink
|
||||
>.
|
||||
<IntlFormatted :message-id="messages.footerParagraph">
|
||||
<template #files-link="{ children }">
|
||||
<AutoLink
|
||||
class="goto-link !inline-block"
|
||||
:to="filesTabLink"
|
||||
@click="onFilesTabLinkClick"
|
||||
>
|
||||
<component :is="() => children" />
|
||||
</AutoLink>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</div>
|
||||
</div>
|
||||
</Accordion>
|
||||
@@ -239,8 +267,12 @@
|
||||
class="flex flex-col items-center gap-2 py-8 text-center text-secondary"
|
||||
>
|
||||
<SearchIcon class="size-10" />
|
||||
<span class="text-lg font-semibold text-contrast">No properties found</span>
|
||||
<span>No properties match "{{ searchInput }}".</span>
|
||||
<span class="text-lg font-semibold text-contrast">{{
|
||||
formatMessage(messages.noSearchResultsTitle)
|
||||
}}</span>
|
||||
<span>{{
|
||||
formatMessage(messages.noSearchResultsDescription, { query: searchInput })
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -272,15 +304,230 @@ import Fuse from 'fuse.js'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { Accordion, Admonition, AutoLink, Chips, StyledInput, Toggle } from '#ui/components'
|
||||
import IntlFormatted from '#ui/components/base/IntlFormatted.vue'
|
||||
import SaveBanner from '#ui/components/servers/SaveBanner.vue'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { injectServerSettings } from '#ui/layouts/shared/server-settings'
|
||||
import {
|
||||
injectModrinthClient,
|
||||
injectModrinthServerContext,
|
||||
injectNotificationManager,
|
||||
} from '#ui/providers'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
|
||||
const messages = defineMessages({
|
||||
missingPropertiesWarning: {
|
||||
id: 'server.settings.properties.warning.missing',
|
||||
defaultMessage:
|
||||
"Some expected properties are missing from your server.properties — this usually means the server hasn't completed its first startup yet.",
|
||||
},
|
||||
introParagraph: {
|
||||
id: 'server.settings.properties.intro',
|
||||
defaultMessage:
|
||||
"Edit the Minecraft server properties file here, or use the <files-link>Files tab</files-link> to edit the full file. If you're unsure about a setting, the <wiki-link>Minecraft Wiki</wiki-link> has more details.",
|
||||
},
|
||||
searchPropertiesAriaLabel: {
|
||||
id: 'server.settings.properties.search.aria',
|
||||
defaultMessage: 'Search server properties',
|
||||
},
|
||||
searchPropertiesPlaceholder: {
|
||||
id: 'server.settings.properties.search.placeholder',
|
||||
defaultMessage: 'Search server properties…',
|
||||
},
|
||||
labelGamemode: {
|
||||
id: 'server.settings.properties.label.gamemode',
|
||||
defaultMessage: 'Gamemode',
|
||||
},
|
||||
labelDifficulty: {
|
||||
id: 'server.settings.properties.label.difficulty',
|
||||
defaultMessage: 'Difficulty',
|
||||
},
|
||||
labelMaxPlayers: {
|
||||
id: 'server.settings.properties.label.max-players',
|
||||
defaultMessage: 'Max players',
|
||||
},
|
||||
labelMotd: {
|
||||
id: 'server.settings.properties.label.motd',
|
||||
defaultMessage: 'MOTD',
|
||||
},
|
||||
labelAllowFlight: {
|
||||
id: 'server.settings.properties.label.allow-flight',
|
||||
defaultMessage: 'Allow flight',
|
||||
},
|
||||
labelAllowCheats: {
|
||||
id: 'server.settings.properties.label.allow-cheats',
|
||||
defaultMessage: 'Allow cheats',
|
||||
},
|
||||
labelEnableWhitelist: {
|
||||
id: 'server.settings.properties.label.enable-whitelist',
|
||||
defaultMessage: 'Enable whitelist',
|
||||
},
|
||||
labelEnableSpawnProtection: {
|
||||
id: 'server.settings.properties.label.enable-spawn-protection',
|
||||
defaultMessage: 'Enable spawn protection',
|
||||
},
|
||||
labelProtectionRadius: {
|
||||
id: 'server.settings.properties.label.protection-radius',
|
||||
defaultMessage: 'Protection radius',
|
||||
},
|
||||
advancedPropertiesTitle: {
|
||||
id: 'server.settings.properties.advanced.title',
|
||||
defaultMessage: 'Advanced properties',
|
||||
},
|
||||
groupPerformance: {
|
||||
id: 'server.settings.properties.group.performance',
|
||||
defaultMessage: 'Performance',
|
||||
},
|
||||
groupResourcePack: {
|
||||
id: 'server.settings.properties.group.resource-pack',
|
||||
defaultMessage: 'Resource pack',
|
||||
},
|
||||
propertyValuePlaceholder: {
|
||||
id: 'server.settings.properties.placeholder.value',
|
||||
defaultMessage: 'Type here…',
|
||||
},
|
||||
placeholderDefaultMaxPlayers: {
|
||||
id: 'server.settings.properties.placeholder.max-players',
|
||||
defaultMessage: '20',
|
||||
},
|
||||
placeholderDefaultMotd: {
|
||||
id: 'server.settings.properties.placeholder.motd',
|
||||
defaultMessage: 'A Minecraft Server',
|
||||
},
|
||||
footerParagraph: {
|
||||
id: 'server.settings.properties.footer',
|
||||
defaultMessage:
|
||||
'All other properties can be edited in server.properties via the <files-link>Files tab</files-link>.',
|
||||
},
|
||||
noSearchResultsTitle: {
|
||||
id: 'server.settings.properties.search.no-results.title',
|
||||
defaultMessage: 'No properties found',
|
||||
},
|
||||
noSearchResultsDescription: {
|
||||
id: 'server.settings.properties.search.no-results.description',
|
||||
defaultMessage: 'No properties match "{query}".',
|
||||
},
|
||||
propertiesUpdatedTitle: {
|
||||
id: 'server.settings.properties.success.updated.title',
|
||||
defaultMessage: 'Server properties updated',
|
||||
},
|
||||
propertiesUpdatedText: {
|
||||
id: 'server.settings.properties.success.updated.text',
|
||||
defaultMessage: 'Your server properties were successfully changed.',
|
||||
},
|
||||
propertiesUpdateFailedTitle: {
|
||||
id: 'server.settings.properties.error.update.title',
|
||||
defaultMessage: 'Failed to update server properties',
|
||||
},
|
||||
propertiesUpdateFailedFallback: {
|
||||
id: 'server.settings.properties.error.update.fallback',
|
||||
defaultMessage: 'An error occurred.',
|
||||
},
|
||||
})
|
||||
|
||||
const propertyFieldMessages = defineMessages({
|
||||
allow_cheats: {
|
||||
id: 'server.settings.properties.field.allow_cheats',
|
||||
defaultMessage: 'Allow cheats',
|
||||
},
|
||||
allow_flight: {
|
||||
id: 'server.settings.properties.field.allow_flight',
|
||||
defaultMessage: 'Allow flight',
|
||||
},
|
||||
difficulty: {
|
||||
id: 'server.settings.properties.field.difficulty',
|
||||
defaultMessage: 'Difficulty',
|
||||
},
|
||||
enforce_whitelist: {
|
||||
id: 'server.settings.properties.field.enforce_whitelist',
|
||||
defaultMessage: 'Enforce whitelist',
|
||||
},
|
||||
force_gamemode: {
|
||||
id: 'server.settings.properties.field.force_gamemode',
|
||||
defaultMessage: 'Force gamemode',
|
||||
},
|
||||
gamemode: {
|
||||
id: 'server.settings.properties.field.gamemode',
|
||||
defaultMessage: 'Gamemode',
|
||||
},
|
||||
generate_structures: {
|
||||
id: 'server.settings.properties.field.generate_structures',
|
||||
defaultMessage: 'Generate structures',
|
||||
},
|
||||
generator_settings: {
|
||||
id: 'server.settings.properties.field.generator_settings',
|
||||
defaultMessage: 'Generator settings',
|
||||
},
|
||||
hardcore: {
|
||||
id: 'server.settings.properties.field.hardcore',
|
||||
defaultMessage: 'Hardcore',
|
||||
},
|
||||
level_seed: {
|
||||
id: 'server.settings.properties.field.level_seed',
|
||||
defaultMessage: 'Level seed',
|
||||
},
|
||||
level_type: {
|
||||
id: 'server.settings.properties.field.level_type',
|
||||
defaultMessage: 'Level type',
|
||||
},
|
||||
max_players: {
|
||||
id: 'server.settings.properties.field.max_players',
|
||||
defaultMessage: 'Max players',
|
||||
},
|
||||
max_tick_time: {
|
||||
id: 'server.settings.properties.field.max_tick_time',
|
||||
defaultMessage: 'Max tick time',
|
||||
},
|
||||
motd: {
|
||||
id: 'server.settings.properties.field.motd',
|
||||
defaultMessage: 'MOTD',
|
||||
},
|
||||
pause_when_empty_seconds: {
|
||||
id: 'server.settings.properties.field.pause_when_empty_seconds',
|
||||
defaultMessage: 'Pause when empty (seconds)',
|
||||
},
|
||||
player_idle_timeout: {
|
||||
id: 'server.settings.properties.field.player_idle_timeout',
|
||||
defaultMessage: 'Player idle timeout',
|
||||
},
|
||||
require_resource_pack: {
|
||||
id: 'server.settings.properties.field.require_resource_pack',
|
||||
defaultMessage: 'Require resource pack',
|
||||
},
|
||||
resource_pack: {
|
||||
id: 'server.settings.properties.field.resource_pack',
|
||||
defaultMessage: 'Resource pack',
|
||||
},
|
||||
resource_pack_id: {
|
||||
id: 'server.settings.properties.field.resource_pack_id',
|
||||
defaultMessage: 'Resource pack ID',
|
||||
},
|
||||
resource_pack_sha1: {
|
||||
id: 'server.settings.properties.field.resource_pack_sha1',
|
||||
defaultMessage: 'Resource pack SHA-1',
|
||||
},
|
||||
simulation_distance: {
|
||||
id: 'server.settings.properties.field.simulation_distance',
|
||||
defaultMessage: 'Simulation distance',
|
||||
},
|
||||
spawn_protection: {
|
||||
id: 'server.settings.properties.field.spawn_protection',
|
||||
defaultMessage: 'Spawn protection',
|
||||
},
|
||||
sync_chunk_writes: {
|
||||
id: 'server.settings.properties.field.sync_chunk_writes',
|
||||
defaultMessage: 'Sync chunk writes',
|
||||
},
|
||||
view_distance: {
|
||||
id: 'server.settings.properties.field.view_distance',
|
||||
defaultMessage: 'View distance',
|
||||
},
|
||||
white_list: {
|
||||
id: 'server.settings.properties.field.white_list',
|
||||
defaultMessage: 'Whitelist',
|
||||
},
|
||||
})
|
||||
const client = injectModrinthClient()
|
||||
const { serverId, worldId, powerState, busyReasons } = injectModrinthServerContext()
|
||||
const queryClient = useQueryClient()
|
||||
@@ -329,9 +576,9 @@ function getPropertyDef(key: string): PropertyDef {
|
||||
return KNOWN_PROPERTIES[key] ?? { type: 'text' }
|
||||
}
|
||||
|
||||
const ADVANCED_GROUPS = [
|
||||
const ADVANCED_GROUP_DEFS = [
|
||||
{
|
||||
label: 'Performance',
|
||||
labelMessage: messages.groupPerformance,
|
||||
keys: [
|
||||
'view_distance',
|
||||
'simulation_distance',
|
||||
@@ -342,10 +589,10 @@ const ADVANCED_GROUPS = [
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Resource Pack',
|
||||
labelMessage: messages.groupResourcePack,
|
||||
keys: ['resource_pack', 'resource_pack_id', 'resource_pack_sha1', 'require_resource_pack'],
|
||||
},
|
||||
]
|
||||
] as const
|
||||
|
||||
type CombinedGamemode = 'survival' | 'creative' | 'hardcore'
|
||||
const gamemodeItems: CombinedGamemode[] = ['survival', 'creative', 'hardcore']
|
||||
@@ -494,15 +741,18 @@ const { mutateAsync: saveProperties, isPending: isUpdating } = useMutation({
|
||||
syncFormFromData()
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Server properties updated',
|
||||
text: 'Your server properties were successfully changed.',
|
||||
title: formatMessage(messages.propertiesUpdatedTitle),
|
||||
text: formatMessage(messages.propertiesUpdatedText),
|
||||
})
|
||||
},
|
||||
onError: (error) => {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Failed to update server properties',
|
||||
text: error instanceof Error ? error.message : 'An error occurred.',
|
||||
title: formatMessage(messages.propertiesUpdateFailedTitle),
|
||||
text:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: formatMessage(messages.propertiesUpdateFailedFallback),
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -512,8 +762,8 @@ function resetProperties() {
|
||||
}
|
||||
|
||||
const advancedGroupedProperties = computed(() =>
|
||||
ADVANCED_GROUPS.map((group) => ({
|
||||
label: group.label,
|
||||
ADVANCED_GROUP_DEFS.map((group) => ({
|
||||
label: formatMessage(group.labelMessage),
|
||||
properties: group.keys.filter((key) => key in liveProperties.value),
|
||||
})).filter((g) => g.properties.length > 0),
|
||||
)
|
||||
@@ -551,6 +801,10 @@ const hasVisibleAdvancedProperties = computed(() =>
|
||||
)
|
||||
|
||||
function formatPropertyName(name: string): string {
|
||||
const known = propertyFieldMessages[name as keyof typeof propertyFieldMessages]
|
||||
if (known) {
|
||||
return formatMessage(known)
|
||||
}
|
||||
return name
|
||||
.split('_')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
|
||||
@@ -29,7 +29,6 @@ export interface ServerSettingsTabContext {
|
||||
|
||||
export interface ServerSettingsTabDefinition {
|
||||
id: ServerSettingsTabId
|
||||
label: string
|
||||
icon: Component
|
||||
href?: (ctx: ServerSettingsTabContext) => string
|
||||
external?: boolean
|
||||
@@ -39,33 +38,27 @@ export interface ServerSettingsTabDefinition {
|
||||
export const serverSettingsTabDefinitions: ServerSettingsTabDefinition[] = [
|
||||
{
|
||||
id: 'general',
|
||||
label: 'General',
|
||||
icon: SettingsIcon,
|
||||
},
|
||||
{
|
||||
id: 'installation',
|
||||
label: 'Installation',
|
||||
icon: WrenchIcon,
|
||||
},
|
||||
{
|
||||
id: 'network',
|
||||
label: 'Network',
|
||||
icon: VersionIcon,
|
||||
},
|
||||
{
|
||||
id: 'properties',
|
||||
label: 'Properties',
|
||||
icon: ListIcon,
|
||||
shown: ({ serverStatus }) => serverStatus !== 'installing',
|
||||
},
|
||||
{
|
||||
id: 'advanced',
|
||||
label: 'Advanced',
|
||||
icon: TextQuoteIcon,
|
||||
},
|
||||
{
|
||||
id: 'billing',
|
||||
label: 'Billing',
|
||||
icon: CardIcon,
|
||||
href: ({ serverId }) => `/settings/billing#server-${serverId}`,
|
||||
external: true,
|
||||
@@ -73,7 +66,6 @@ export const serverSettingsTabDefinitions: ServerSettingsTabDefinition[] = [
|
||||
},
|
||||
{
|
||||
id: 'admin-billing',
|
||||
label: 'Admin Billing',
|
||||
icon: ModrinthIcon,
|
||||
href: ({ ownerId }) => `/admin/billing/${ownerId}`,
|
||||
external: true,
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
<template>
|
||||
<div class="mx-auto flex w-fit flex-col items-start gap-4 mt-16 max-w-[500px]">
|
||||
<div class="flex flex-col gap-2 w-full">
|
||||
<h2 class="m-0 text-2xl font-semibold text-contrast">Welcome to Modrinth Hosting</h2>
|
||||
<h2 class="m-0 text-2xl font-semibold text-contrast">
|
||||
{{ formatMessage(messages.welcomeTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 text-base text-secondary">
|
||||
Your server is ready. Here's what you need to do to start playing!
|
||||
{{ formatMessage(messages.welcomeDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<span class="text-base font-medium text-secondary"> Setup your server (~2mins) </span>
|
||||
<span class="text-base font-medium text-secondary">
|
||||
{{ formatMessage(messages.setupHeading) }}
|
||||
</span>
|
||||
|
||||
<div class="rounded-[20px] border border-solid border-surface-5 bg-surface-3 p-5">
|
||||
<div class="flex flex-col">
|
||||
@@ -41,11 +45,13 @@
|
||||
<ButtonStyled v-if="uploading" size="large">
|
||||
<button class="ml-auto" disabled>
|
||||
<SpinnerIcon class="animate-spin" />
|
||||
Uploading ({{ uploadPercent }}%)
|
||||
{{ formatMessage(messages.uploadingProgress, { percent: uploadPercent }) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else color="brand" size="large">
|
||||
<button class="ml-auto" @click="openModal">Setup server <RightArrowIcon /></button>
|
||||
<button class="ml-auto" @click="openModal">
|
||||
{{ formatMessage(messages.setupServerButton) }} <RightArrowIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
@@ -66,7 +72,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { Archon } from '@modrinth/api-client'
|
||||
import { GlobeIcon, PackageIcon, RightArrowIcon, SpinnerIcon, UsersIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, injectModrinthClient, injectNotificationManager } from '@modrinth/ui'
|
||||
import {
|
||||
ButtonStyled,
|
||||
defineMessages,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
@@ -77,6 +89,73 @@ import { injectModrinthServerContext } from '#ui/providers'
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
welcomeTitle: {
|
||||
id: 'servers.onboarding.welcome-title',
|
||||
defaultMessage: 'Welcome to Modrinth Hosting',
|
||||
},
|
||||
welcomeDescription: {
|
||||
id: 'servers.onboarding.welcome-description',
|
||||
defaultMessage: "Your server is ready. Here's what you need to do to start playing!",
|
||||
},
|
||||
setupHeading: {
|
||||
id: 'servers.onboarding.setup-heading',
|
||||
defaultMessage: 'Set up your server (~2 mins)',
|
||||
},
|
||||
uploadingProgress: {
|
||||
id: 'servers.onboarding.uploading-progress',
|
||||
defaultMessage: 'Uploading ({percent}%)',
|
||||
},
|
||||
setupServerButton: {
|
||||
id: 'servers.onboarding.setup-server-button',
|
||||
defaultMessage: 'Set up server',
|
||||
},
|
||||
step1Title: {
|
||||
id: 'servers.onboarding.step-1.title',
|
||||
defaultMessage: 'Choose what to play',
|
||||
},
|
||||
step1Description: {
|
||||
id: 'servers.onboarding.step-1.description',
|
||||
defaultMessage:
|
||||
'Pick your favorite modpack from Modrinth, or choose a loader and add the mods you want.',
|
||||
},
|
||||
step2Title: {
|
||||
id: 'servers.onboarding.step-2.title',
|
||||
defaultMessage: 'Configure your world',
|
||||
},
|
||||
step2Description: {
|
||||
id: 'servers.onboarding.step-2.description',
|
||||
defaultMessage:
|
||||
'Set up your world just like singleplayer. Choose your game mode and world seed.',
|
||||
},
|
||||
step3Title: {
|
||||
id: 'servers.onboarding.step-3.title',
|
||||
defaultMessage: 'Invite your friends',
|
||||
},
|
||||
step3Description: {
|
||||
id: 'servers.onboarding.step-3.description',
|
||||
defaultMessage:
|
||||
"Share your server with friends by copying the address and letting them know which mods they'll need to join.",
|
||||
},
|
||||
modpackUploadFailedTitle: {
|
||||
id: 'servers.onboarding.modpack-upload-failed.title',
|
||||
defaultMessage: 'Modpack upload failed',
|
||||
},
|
||||
modpackUploadFailedText: {
|
||||
id: 'servers.onboarding.modpack-upload-failed.text',
|
||||
defaultMessage: 'An unexpected error occurred while uploading. Please try again later.',
|
||||
},
|
||||
installationFailedTitle: {
|
||||
id: 'servers.onboarding.installation-failed.title',
|
||||
defaultMessage: 'Installation failed',
|
||||
},
|
||||
installationFailedText: {
|
||||
id: 'servers.onboarding.installation-failed.text',
|
||||
defaultMessage: 'An unexpected error occurred while installing. Please try again later.',
|
||||
},
|
||||
})
|
||||
|
||||
async function searchModpacks(query: string, limit: number = 10) {
|
||||
return client.labrinth.projects_v2.search({
|
||||
@@ -209,8 +288,8 @@ const onCreate = async (config: CreationFlowContextValue) => {
|
||||
await finalizeSetup()
|
||||
} catch {
|
||||
addNotification({
|
||||
title: 'Modpack upload failed',
|
||||
text: 'An unexpected error occurred while uploading. Please try again later.',
|
||||
title: formatMessage(messages.modpackUploadFailedTitle),
|
||||
text: formatMessage(messages.modpackUploadFailedText),
|
||||
type: 'error',
|
||||
})
|
||||
config.loading.value = false
|
||||
@@ -252,31 +331,29 @@ const onCreate = async (config: CreationFlowContextValue) => {
|
||||
await finalizeSetup()
|
||||
} catch {
|
||||
addNotification({
|
||||
title: 'Installation failed',
|
||||
text: 'An unexpected error occurred while installing. Please try again later.',
|
||||
title: formatMessage(messages.installationFailedTitle),
|
||||
text: formatMessage(messages.installationFailedText),
|
||||
type: 'error',
|
||||
})
|
||||
config.loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const steps = [
|
||||
const steps = computed(() => [
|
||||
{
|
||||
icon: PackageIcon,
|
||||
title: 'Choose what to play',
|
||||
description:
|
||||
'Pick your favorite modpack from Modrinth, or choose a loader and add the mods you want.',
|
||||
title: formatMessage(messages.step1Title),
|
||||
description: formatMessage(messages.step1Description),
|
||||
},
|
||||
{
|
||||
icon: GlobeIcon,
|
||||
title: 'Configure your world',
|
||||
description: 'Set up your world just like singleplayer. Choose your gamemode and world seed.',
|
||||
title: formatMessage(messages.step2Title),
|
||||
description: formatMessage(messages.step2Description),
|
||||
},
|
||||
{
|
||||
icon: UsersIcon,
|
||||
title: 'Invite your friends',
|
||||
description:
|
||||
"Share your server with friends by copying the address and letting them know which mods they'll need to join.",
|
||||
title: formatMessage(messages.step3Title),
|
||||
description: formatMessage(messages.step3Description),
|
||||
},
|
||||
]
|
||||
])
|
||||
</script>
|
||||
|
||||
@@ -11,16 +11,18 @@
|
||||
<div class="grid place-content-center rounded-full bg-bg-orange p-4">
|
||||
<IssuesIcon class="size-12 text-orange" />
|
||||
</div>
|
||||
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">Failed to load backups</h1>
|
||||
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">
|
||||
{{ formatMessage(messages.loadFailedTitle) }}
|
||||
</h1>
|
||||
</div>
|
||||
<p class="text-lg text-secondary">
|
||||
We couldn't load your server's backups. Here's what went wrong:
|
||||
{{ formatMessage(messages.loadFailedDescription) }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="break-all font-mono">{{ error.message }}</span>
|
||||
</p>
|
||||
<ButtonStyled size="large" color="brand" @click="refetch">
|
||||
<button class="mt-6 !w-full">Retry</button>
|
||||
<button class="mt-6 !w-full">{{ formatMessage(commonMessages.retryButton) }}</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
@@ -33,7 +35,9 @@
|
||||
<BackupDeleteModal ref="deleteBackupModal" @delete="deleteBackup" />
|
||||
|
||||
<div v-if="backupsData?.length" class="mb-2 flex items-center align-middle justify-between">
|
||||
<span class="text-2xl font-semibold text-contrast">Backups</span>
|
||||
<span class="text-2xl font-semibold text-contrast">{{
|
||||
formatMessage(messages.backupsHeading)
|
||||
}}</span>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
v-tooltip="backupCreationDisabled"
|
||||
@@ -41,7 +45,7 @@
|
||||
@click="showCreateModel"
|
||||
>
|
||||
<PlusIcon class="size-5" />
|
||||
Create backup
|
||||
{{ formatMessage(messages.createBackup) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
@@ -55,13 +59,13 @@
|
||||
>
|
||||
<template v-if="!backupsData">
|
||||
<SpinnerIcon class="animate-spin" />
|
||||
Loading backups...
|
||||
{{ formatMessage(messages.loadingBackups) }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<EmptyState
|
||||
type="empty-inbox"
|
||||
heading="No backups yet"
|
||||
description="Create your first backup"
|
||||
:heading="formatMessage(messages.noBackupsHeading)"
|
||||
:description="formatMessage(messages.noBackupsDescription)"
|
||||
>
|
||||
<template #actions>
|
||||
<ButtonStyled color="brand">
|
||||
@@ -72,7 +76,7 @@
|
||||
@click="showCreateModel"
|
||||
>
|
||||
<PlusIcon class="size-5" />
|
||||
Create backup
|
||||
{{ formatMessage(messages.createBackup) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
@@ -81,10 +85,12 @@
|
||||
</div>
|
||||
|
||||
<div v-else key="list" class="flex flex-col gap-1.5">
|
||||
<template v-for="group in groupedBackups" :key="group.label">
|
||||
<template v-for="group in groupedBackups" :key="group.label.id">
|
||||
<div class="flex items-center gap-2">
|
||||
<component :is="group.icon" v-if="group.icon" class="size-6 text-secondary" />
|
||||
<span class="text-lg font-semibold text-secondary">{{ group.label }}</span>
|
||||
<span class="text-lg font-semibold text-secondary">{{
|
||||
formatMessage(group.label)
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
@@ -156,15 +162,83 @@ import BackupDeleteModal from '#ui/components/servers/backups/BackupDeleteModal.
|
||||
import BackupItem from '#ui/components/servers/backups/BackupItem.vue'
|
||||
import BackupRenameModal from '#ui/components/servers/backups/BackupRenameModal.vue'
|
||||
import BackupRestoreModal from '#ui/components/servers/backups/BackupRestoreModal.vue'
|
||||
import { useVIntl } from '#ui/composables/i18n'
|
||||
import { defineMessages, type MessageDescriptor, useVIntl } from '#ui/composables/i18n'
|
||||
import {
|
||||
injectModrinthClient,
|
||||
injectModrinthServerContext,
|
||||
injectNotificationManager,
|
||||
} from '#ui/providers'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
loadFailedTitle: {
|
||||
id: 'servers.manage.backups.load-failed.title',
|
||||
defaultMessage: 'Failed to load backups',
|
||||
},
|
||||
loadFailedDescription: {
|
||||
id: 'servers.manage.backups.load-failed.description',
|
||||
defaultMessage: "We couldn't load your server's backups. Here's what went wrong:",
|
||||
},
|
||||
backupsHeading: {
|
||||
id: 'servers.manage.backups.heading',
|
||||
defaultMessage: 'Backups',
|
||||
},
|
||||
createBackup: {
|
||||
id: 'servers.manage.backups.create-backup',
|
||||
defaultMessage: 'Create backup',
|
||||
},
|
||||
loadingBackups: {
|
||||
id: 'servers.manage.backups.loading',
|
||||
defaultMessage: 'Loading backups...',
|
||||
},
|
||||
noBackupsHeading: {
|
||||
id: 'servers.manage.backups.empty.heading',
|
||||
defaultMessage: 'No backups yet',
|
||||
},
|
||||
noBackupsDescription: {
|
||||
id: 'servers.manage.backups.empty.description',
|
||||
defaultMessage: 'Create your first backup',
|
||||
},
|
||||
groupJustNow: {
|
||||
id: 'servers.manage.backups.group.just-now',
|
||||
defaultMessage: 'Just now',
|
||||
},
|
||||
groupEarlierToday: {
|
||||
id: 'servers.manage.backups.group.earlier-today',
|
||||
defaultMessage: 'Earlier today',
|
||||
},
|
||||
groupYesterday: {
|
||||
id: 'servers.manage.backups.group.yesterday',
|
||||
defaultMessage: 'Yesterday',
|
||||
},
|
||||
groupLastTwoWeeks: {
|
||||
id: 'servers.manage.backups.group.last-two-weeks',
|
||||
defaultMessage: 'Last 2 weeks',
|
||||
},
|
||||
groupOlder: {
|
||||
id: 'servers.manage.backups.group.older',
|
||||
defaultMessage: 'Older',
|
||||
},
|
||||
restoreDisabledRunning: {
|
||||
id: 'servers.manage.backups.tooltip.restore-disabled-running',
|
||||
defaultMessage: 'Cannot restore backup while server is running',
|
||||
},
|
||||
backupSlotsFull: {
|
||||
id: 'servers.manage.backups.tooltip.backup-slots-full',
|
||||
defaultMessage: 'All {quota, number} of your backup slots are in use',
|
||||
},
|
||||
backupInProgressTooltip: {
|
||||
id: 'servers.manage.backups.tooltip.backup-in-progress',
|
||||
defaultMessage: 'A backup is already in progress',
|
||||
},
|
||||
errorDeletingBackupTitle: {
|
||||
id: 'servers.manage.backups.notification.delete-error.title',
|
||||
defaultMessage: 'Error deleting backup',
|
||||
},
|
||||
})
|
||||
const client = injectModrinthClient()
|
||||
const queryClient = useQueryClient()
|
||||
const { server, worldId, backupsState, markBackupCancelled, busyReasons } =
|
||||
@@ -240,7 +314,7 @@ const backups = computed(() => {
|
||||
})
|
||||
|
||||
type BackupGroup = {
|
||||
label: string
|
||||
label: MessageDescriptor
|
||||
icon: Component | null
|
||||
backups: Archon.Backups.v1.Backup[]
|
||||
}
|
||||
@@ -251,8 +325,12 @@ const groupedBackups = computed((): BackupGroup[] => {
|
||||
const now = dayjs()
|
||||
const groups: BackupGroup[] = []
|
||||
|
||||
const addToGroup = (label: string, icon: Component | null, backup: Archon.Backups.v1.Backup) => {
|
||||
let group = groups.find((g) => g.label === label)
|
||||
const addToGroup = (
|
||||
label: MessageDescriptor,
|
||||
icon: Component | null,
|
||||
backup: Archon.Backups.v1.Backup,
|
||||
) => {
|
||||
let group = groups.find((g) => g.label.id === label.id)
|
||||
if (!group) {
|
||||
group = { label, icon, backups: [] }
|
||||
groups.push(group)
|
||||
@@ -268,15 +346,15 @@ const groupedBackups = computed((): BackupGroup[] => {
|
||||
const diffDays = now.diff(created, 'day')
|
||||
|
||||
if (diffMinutes < 30 && isToday) {
|
||||
addToGroup('Just now', CalendarIcon, backup)
|
||||
addToGroup(messages.groupJustNow, CalendarIcon, backup)
|
||||
} else if (isToday) {
|
||||
addToGroup('Earlier today', CalendarIcon, backup)
|
||||
addToGroup(messages.groupEarlierToday, CalendarIcon, backup)
|
||||
} else if (isYesterday) {
|
||||
addToGroup('Yesterday', CalendarIcon, backup)
|
||||
addToGroup(messages.groupYesterday, CalendarIcon, backup)
|
||||
} else if (diffDays <= 14) {
|
||||
addToGroup('Last 2 weeks', CalendarIcon, backup)
|
||||
addToGroup(messages.groupLastTwoWeeks, CalendarIcon, backup)
|
||||
} else {
|
||||
addToGroup('Older', CalendarIcon, backup)
|
||||
addToGroup(messages.groupOlder, CalendarIcon, backup)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,7 +369,7 @@ const deleteBackupModal = ref<InstanceType<typeof BackupDeleteModal>>()
|
||||
|
||||
const backupRestoreDisabled = computed(() => {
|
||||
if (props.isServerRunning) {
|
||||
return 'Cannot restore backup while server is running'
|
||||
return formatMessage(messages.restoreDisabledRunning)
|
||||
}
|
||||
if (busyReasons.value.length > 0) {
|
||||
return formatMessage(busyReasons.value[0].reason)
|
||||
@@ -304,7 +382,7 @@ const backupCreationDisabled = computed(() => {
|
||||
if (quota !== undefined) {
|
||||
const usedCount = backupsData.value?.length ?? server.value.used_backup_quota ?? 0
|
||||
if (usedCount >= quota) {
|
||||
return `All ${quota} of your backup slots are in use`
|
||||
return formatMessage(messages.backupSlotsFull, { quota })
|
||||
}
|
||||
}
|
||||
if (busyReasons.value.length > 0) {
|
||||
@@ -312,7 +390,7 @@ const backupCreationDisabled = computed(() => {
|
||||
}
|
||||
// also check for active backups, combining REST data with WS overlay
|
||||
if (backups.value.some((b) => b.status === 'in_progress' || b.status === 'pending')) {
|
||||
return 'A backup is already in progress'
|
||||
return formatMessage(messages.backupInProgressTooltip)
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
@@ -338,7 +416,7 @@ function deleteBackup(backup?: Archon.Backups.v1.Backup) {
|
||||
if (!backup) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Error deleting backup',
|
||||
title: formatMessage(messages.errorDeletingBackupTitle),
|
||||
text: 'Backup is null',
|
||||
})
|
||||
return
|
||||
@@ -349,7 +427,7 @@ function deleteBackup(backup?: Archon.Backups.v1.Backup) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Error deleting backup',
|
||||
title: formatMessage(messages.errorDeletingBackupTitle),
|
||||
text: message,
|
||||
})
|
||||
},
|
||||
|
||||
@@ -65,10 +65,52 @@ import { useStorage } from '@vueuse/core'
|
||||
import { computed, defineAsyncComponent, ref, shallowRef, watch } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { injectModrinthServerContext, injectPageContext } from '#ui/providers'
|
||||
|
||||
const VueApexCharts = defineAsyncComponent(() => import('vue3-apexcharts'))
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
cpuTitle: {
|
||||
id: 'servers.manage.stats.cpu.title',
|
||||
defaultMessage: 'CPU',
|
||||
},
|
||||
memoryTitle: {
|
||||
id: 'servers.manage.stats.memory.title',
|
||||
defaultMessage: 'Memory',
|
||||
},
|
||||
storageTitle: {
|
||||
id: 'servers.manage.stats.storage.title',
|
||||
defaultMessage: 'Storage',
|
||||
},
|
||||
bytesWithUnit: {
|
||||
id: 'servers.manage.stats.bytes-with-unit',
|
||||
defaultMessage: '{value} {unit}',
|
||||
},
|
||||
unitByte: {
|
||||
id: 'servers.manage.stats.unit.byte',
|
||||
defaultMessage: 'B',
|
||||
},
|
||||
unitKilobyte: {
|
||||
id: 'servers.manage.stats.unit.kilobyte',
|
||||
defaultMessage: 'KB',
|
||||
},
|
||||
unitMegabyte: {
|
||||
id: 'servers.manage.stats.unit.megabyte',
|
||||
defaultMessage: 'MB',
|
||||
},
|
||||
unitGigabyte: {
|
||||
id: 'servers.manage.stats.unit.gigabyte',
|
||||
defaultMessage: 'GB',
|
||||
},
|
||||
ramTotalSecondary: {
|
||||
id: 'servers.manage.stats.ram-total-secondary',
|
||||
defaultMessage: '/ {total}',
|
||||
},
|
||||
})
|
||||
|
||||
const { serverId } = injectModrinthServerContext()
|
||||
const { featureFlags } = injectPageContext()
|
||||
|
||||
@@ -168,24 +210,41 @@ const buildChartOptions = (warning: boolean, index: number, dataMax: number) =>
|
||||
const cpuChartOptions = computed(() => buildChartOptions(cpuWarning.value, 0, cpuDataMax))
|
||||
const ramChartOptions = computed(() => buildChartOptions(ramWarning.value, 1, ramDataMax))
|
||||
|
||||
const cpuSeries = computed(() => [{ name: 'CPU', data: cpuData.value }])
|
||||
const ramSeries = computed(() => [{ name: 'Memory', data: ramData.value }])
|
||||
const byteUnitMessages = [
|
||||
messages.unitByte,
|
||||
messages.unitKilobyte,
|
||||
messages.unitMegabyte,
|
||||
messages.unitGigabyte,
|
||||
] as const
|
||||
|
||||
const cpuSeries = computed(() => [{ name: formatMessage(messages.cpuTitle), data: cpuData.value }])
|
||||
const ramSeries = computed(() => [
|
||||
{ name: formatMessage(messages.memoryTitle), data: ramData.value },
|
||||
])
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
const units = ['B', 'KB', 'MB', 'GB']
|
||||
let value = bytes
|
||||
let unit = 0
|
||||
while (value >= 1024 && unit < units.length - 1) {
|
||||
while (value >= 1024 && unit < byteUnitMessages.length - 1) {
|
||||
value /= 1024
|
||||
unit++
|
||||
}
|
||||
return `${Math.round(value * 10) / 10} ${units[unit]}`
|
||||
const rounded = String(Math.round(value * 10) / 10)
|
||||
return formatMessage(messages.bytesWithUnit, {
|
||||
value: rounded,
|
||||
unit: formatMessage(byteUnitMessages[unit]),
|
||||
})
|
||||
}
|
||||
|
||||
const metrics = computed(() => {
|
||||
const storageMetric = {
|
||||
title: 'Storage',
|
||||
value: props.loading ? '0 B' : formatBytes(stats.value.storage_usage_bytes ?? 0),
|
||||
title: formatMessage(messages.storageTitle),
|
||||
value: props.loading
|
||||
? formatMessage(messages.bytesWithUnit, {
|
||||
value: '0',
|
||||
unit: formatMessage(messages.unitByte),
|
||||
})
|
||||
: formatBytes(stats.value.storage_usage_bytes ?? 0),
|
||||
secondary: null as string | null,
|
||||
icon: FolderOpenIcon,
|
||||
showGraph: false,
|
||||
@@ -197,7 +256,7 @@ const metrics = computed(() => {
|
||||
if (props.loading) {
|
||||
return [
|
||||
{
|
||||
title: 'CPU',
|
||||
title: formatMessage(messages.cpuTitle),
|
||||
value: '0.00%',
|
||||
secondary: null as string | null,
|
||||
icon: CpuIcon,
|
||||
@@ -207,7 +266,7 @@ const metrics = computed(() => {
|
||||
link: null,
|
||||
},
|
||||
{
|
||||
title: 'Memory',
|
||||
title: formatMessage(messages.memoryTitle),
|
||||
value: '0.00%',
|
||||
secondary: null as string | null,
|
||||
icon: DatabaseIcon,
|
||||
@@ -222,7 +281,7 @@ const metrics = computed(() => {
|
||||
|
||||
return [
|
||||
{
|
||||
title: 'CPU',
|
||||
title: formatMessage(messages.cpuTitle),
|
||||
value: `${cpuPercent.value.toFixed(2)}%`,
|
||||
secondary: null as string | null,
|
||||
icon: CpuIcon,
|
||||
@@ -232,12 +291,14 @@ const metrics = computed(() => {
|
||||
link: null,
|
||||
},
|
||||
{
|
||||
title: 'Memory',
|
||||
title: formatMessage(messages.memoryTitle),
|
||||
value: showRamAsBytes.value
|
||||
? formatBytes(stats.value.ram_usage_bytes ?? 0)
|
||||
: `${ramPercent.value.toFixed(2)}%`,
|
||||
secondary: showRamAsBytes.value
|
||||
? `/ ${formatBytes(stats.value.ram_total_bytes ?? 0)}`
|
||||
? ` ${formatMessage(messages.ramTotalSecondary, {
|
||||
total: formatBytes(stats.value.ram_total_bytes ?? 0),
|
||||
})}`
|
||||
: (null as string | null),
|
||||
icon: DatabaseIcon,
|
||||
showGraph: true,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { useVIntl } from '#ui/composables/i18n'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import {
|
||||
injectModrinthClient,
|
||||
injectModrinthServerContext,
|
||||
@@ -27,6 +27,45 @@ const { serverId, fsOps, busyReasons, uploadState, cancelUpload: cancelUploadRef
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
fileDeletedTitle: {
|
||||
id: 'files.manage.delete-success-title',
|
||||
defaultMessage: 'File deleted',
|
||||
},
|
||||
fileDeletedText: {
|
||||
id: 'files.manage.delete-success-text',
|
||||
defaultMessage: 'Your file has been deleted.',
|
||||
},
|
||||
renamedTitle: {
|
||||
id: 'files.manage.rename-success-title',
|
||||
defaultMessage: 'Renamed',
|
||||
},
|
||||
renamedTo: {
|
||||
id: 'files.manage.rename-success-text',
|
||||
defaultMessage: 'Renamed to {name}',
|
||||
},
|
||||
movedTitle: {
|
||||
id: 'files.manage.move-success-title',
|
||||
defaultMessage: 'Moved',
|
||||
},
|
||||
movedTo: {
|
||||
id: 'files.manage.move-success-text',
|
||||
defaultMessage: 'Moved to {path}',
|
||||
},
|
||||
itemCreatedTitle: {
|
||||
id: 'files.manage.create-success-title',
|
||||
defaultMessage: '{type, select, directory {Folder} other {File}} created',
|
||||
},
|
||||
itemCreatedText: {
|
||||
id: 'files.manage.create-success-text',
|
||||
defaultMessage: 'Created {name}',
|
||||
},
|
||||
downloadFailedText: {
|
||||
id: 'files.manage.download-failed-text',
|
||||
defaultMessage: 'Could not download the file.',
|
||||
},
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const queryClient = useQueryClient()
|
||||
@@ -175,8 +214,8 @@ const deleteMutation = useMutation({
|
||||
},
|
||||
onSuccess: () => {
|
||||
addNotification({
|
||||
title: 'File deleted',
|
||||
text: 'Your file has been deleted.',
|
||||
title: formatMessage(messages.fileDeletedTitle),
|
||||
text: formatMessage(messages.fileDeletedText),
|
||||
type: 'success',
|
||||
})
|
||||
},
|
||||
@@ -218,7 +257,11 @@ const renameMutation = useMutation({
|
||||
})
|
||||
},
|
||||
onSuccess: (_, { newName }) => {
|
||||
addNotification({ title: 'Renamed', text: `Renamed to ${newName}`, type: 'success' })
|
||||
addNotification({
|
||||
title: formatMessage(messages.renamedTitle),
|
||||
text: formatMessage(messages.renamedTo, { name: newName }),
|
||||
type: 'success',
|
||||
})
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['files', serverId] })
|
||||
@@ -247,7 +290,11 @@ const moveMutation = useMutation({
|
||||
})
|
||||
},
|
||||
onSuccess: (_, { destination }) => {
|
||||
addNotification({ title: 'Moved', text: `Moved to ${destination}`, type: 'success' })
|
||||
addNotification({
|
||||
title: formatMessage(messages.movedTitle),
|
||||
text: formatMessage(messages.movedTo, { path: destination }),
|
||||
type: 'success',
|
||||
})
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['files', serverId] })
|
||||
@@ -286,10 +333,10 @@ const createMutation = useMutation({
|
||||
})
|
||||
},
|
||||
onSuccess: (_, { path, type }) => {
|
||||
const name = path.split('/').pop()
|
||||
const name = path.split('/').pop() ?? ''
|
||||
addNotification({
|
||||
title: `${type === 'directory' ? 'Folder' : 'File'} created`,
|
||||
text: `Created ${name}`,
|
||||
title: formatMessage(messages.itemCreatedTitle, { type }),
|
||||
text: formatMessage(messages.itemCreatedText, { name }),
|
||||
type: 'success',
|
||||
})
|
||||
},
|
||||
@@ -339,7 +386,7 @@ async function downloadFile(path: string, fileName: string): Promise<void> {
|
||||
} catch {
|
||||
addNotification({
|
||||
title: formatMessage(commonMessages.downloadFailedLabel),
|
||||
text: 'Could not download the file.',
|
||||
text: formatMessage(messages.downloadFailedText),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
@@ -404,7 +451,7 @@ async function uploadFiles(files: File[]) {
|
||||
if (err instanceof Error && err.message === 'Upload cancelled') break
|
||||
addNotification({
|
||||
title: formatMessage(commonMessages.uploadFailedLabel),
|
||||
text: `Failed to upload ${file.name}`,
|
||||
text: formatMessage(commonMessages.uploadFailedFileDetail, { fileName: file.name }),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -61,7 +61,10 @@
|
||||
<li v-if="fetchError" class="text-red">
|
||||
<p>{{ formatMessage(messages.errorDetails) }}</p>
|
||||
<CopyCode
|
||||
:text="(fetchError as ModrinthServersFetchError).message || 'Unknown error'"
|
||||
:text="
|
||||
(fetchError as ModrinthServersFetchError).message ||
|
||||
formatMessage(messages.unknownErrorDetails)
|
||||
"
|
||||
:copyable="false"
|
||||
:selectable="false"
|
||||
:language="'json'"
|
||||
@@ -319,6 +322,30 @@ const messages = defineMessages({
|
||||
id: 'servers.manage.resubscribe-error.text',
|
||||
defaultMessage: 'An error occurred while resubscribing to your Modrinth server.',
|
||||
},
|
||||
unknownErrorDetails: {
|
||||
id: 'servers.manage.error.unknown-details',
|
||||
defaultMessage: 'Unknown error',
|
||||
},
|
||||
planSmall: {
|
||||
id: 'servers.manage.plan.small',
|
||||
defaultMessage: 'Small plan',
|
||||
},
|
||||
planMedium: {
|
||||
id: 'servers.manage.plan.medium',
|
||||
defaultMessage: 'Medium plan',
|
||||
},
|
||||
planLarge: {
|
||||
id: 'servers.manage.plan.large',
|
||||
defaultMessage: 'Large plan',
|
||||
},
|
||||
planCustom: {
|
||||
id: 'servers.manage.plan.custom',
|
||||
defaultMessage: 'Custom plan',
|
||||
},
|
||||
resubscribeFallbackServerName: {
|
||||
id: 'servers.manage.resubscribe.fallback-server-name',
|
||||
defaultMessage: 'this server',
|
||||
},
|
||||
})
|
||||
|
||||
const isPollingForNewServers = ref(false)
|
||||
@@ -778,18 +805,19 @@ function getProductFromPriceId(priceId: string | null | undefined) {
|
||||
}
|
||||
|
||||
function getPlanName(product: Labrinth.Billing.Internal.Product | null): string {
|
||||
if (!product) return 'Medium plan'
|
||||
if (product.metadata.type !== 'pyro' && product.metadata.type !== 'medal') return 'Medium plan'
|
||||
if (!product) return formatMessage(messages.planMedium)
|
||||
if (product.metadata.type !== 'pyro' && product.metadata.type !== 'medal')
|
||||
return formatMessage(messages.planMedium)
|
||||
|
||||
switch (product.metadata.ram) {
|
||||
case 4096:
|
||||
return 'Small plan'
|
||||
return formatMessage(messages.planSmall)
|
||||
case 6144:
|
||||
return 'Medium plan'
|
||||
return formatMessage(messages.planMedium)
|
||||
case 8192:
|
||||
return 'Large plan'
|
||||
return formatMessage(messages.planLarge)
|
||||
default:
|
||||
return 'Custom plan'
|
||||
return formatMessage(messages.planCustom)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -850,7 +878,8 @@ function openResubscribeModal(
|
||||
subscriptionId: subscription.id,
|
||||
wasSuspended: !!charge?.due && dayjs(charge.due).isBefore(dayjs()),
|
||||
serverName:
|
||||
serverList.value.find((server) => server.server_id === serverId)?.name ?? 'this server',
|
||||
serverList.value.find((server) => server.server_id === serverId)?.name ??
|
||||
formatMessage(messages.resubscribeFallbackServerName),
|
||||
planName: getPlanName(product),
|
||||
ramGb: getRamGb(product),
|
||||
storageGb: getStorageGb(product),
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
/>
|
||||
|
||||
<div class="flex min-h-[700px] flex-col gap-2">
|
||||
<span class="text-2xl font-semibold text-contrast">Console</span>
|
||||
<span class="text-2xl font-semibold text-contrast">{{
|
||||
formatMessage(messages.consoleTitle)
|
||||
}}</span>
|
||||
|
||||
<ConsolePageLayout />
|
||||
</div>
|
||||
@@ -17,10 +19,9 @@
|
||||
v-if="isWsAuthIncorrect"
|
||||
class="absolute inset-0 flex flex-col items-center justify-center bg-bg"
|
||||
>
|
||||
<h2>Could not connect to the server.</h2>
|
||||
<h2>{{ formatMessage(messages.wsAuthErrorTitle) }}</h2>
|
||||
<p>
|
||||
An error occurred while attempting to connect to your server. Please try refreshing the
|
||||
page. (WebSocket Authentication Failed)
|
||||
{{ formatMessage(messages.wsAuthErrorDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -29,7 +30,7 @@
|
||||
class="self-start rounded-lg bg-surface-3 px-3 py-1 text-sm text-contrast hover:brightness-125"
|
||||
@click="downloadLog4jDebug"
|
||||
>
|
||||
Download WS debug JSON
|
||||
{{ formatMessage(messages.downloadWsDebugJson) }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -40,11 +41,34 @@ import { useStorage } from '@vueuse/core'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { useModrinthServersConsole } from '#ui/composables'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { ConsolePageLayout, provideConsoleManager } from '#ui/layouts/shared/console'
|
||||
import { injectModrinthClient, injectModrinthServerContext } from '#ui/providers'
|
||||
|
||||
import ServerManageStats from './components/ServerManageStats.vue'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
consoleTitle: {
|
||||
id: 'servers.manage.overview.console.title',
|
||||
defaultMessage: 'Console',
|
||||
},
|
||||
wsAuthErrorTitle: {
|
||||
id: 'servers.manage.overview.ws-auth-error.title',
|
||||
defaultMessage: 'Could not connect to the server.',
|
||||
},
|
||||
wsAuthErrorDescription: {
|
||||
id: 'servers.manage.overview.ws-auth-error.description',
|
||||
defaultMessage:
|
||||
'An error occurred while attempting to connect to your server. Please try refreshing the page. (WebSocket Authentication Failed)',
|
||||
},
|
||||
downloadWsDebugJson: {
|
||||
id: 'servers.manage.overview.download-ws-debug-json',
|
||||
defaultMessage: 'Download WS debug JSON',
|
||||
},
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
showAdvancedDebugInfo?: boolean
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||
>
|
||||
<ErrorInformationCard
|
||||
title="We're getting your server ready"
|
||||
description="Your server's hardware is being prepared and will be available shortly!"
|
||||
:title="formatMessage(serverManageMessages.preparingServerTitle)"
|
||||
:description="formatMessage(serverManageMessages.preparingServerDescription)"
|
||||
:icon="TransferIcon"
|
||||
icon-color="blue"
|
||||
:action="generalErrorAction"
|
||||
@@ -34,8 +34,8 @@
|
||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||
>
|
||||
<ErrorInformationCard
|
||||
title="Server upgrading"
|
||||
description="Your server's hardware is currently being upgraded and will be back online shortly!"
|
||||
:title="formatMessage(serverManageMessages.serverUpgradingTitle)"
|
||||
:description="formatMessage(serverManageMessages.serverUpgradingDescription)"
|
||||
:icon="TransferIcon"
|
||||
icon-color="blue"
|
||||
:action="generalErrorAction"
|
||||
@@ -46,7 +46,7 @@
|
||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||
>
|
||||
<ErrorInformationCard
|
||||
title="Server suspended"
|
||||
:title="formatMessage(serverManageMessages.serverSuspendedTitle)"
|
||||
:description="suspendedDescription"
|
||||
:icon="LockIcon"
|
||||
icon-color="orange"
|
||||
@@ -58,8 +58,8 @@
|
||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||
>
|
||||
<ErrorInformationCard
|
||||
title="An error occured."
|
||||
description="Please contact Modrinth Support."
|
||||
:title="formatMessage(serverManageMessages.forbiddenErrorTitle)"
|
||||
:description="formatMessage(serverManageMessages.forbiddenErrorDescription)"
|
||||
:icon="TransferIcon"
|
||||
icon-color="orange"
|
||||
:error-details="generalErrorDetails"
|
||||
@@ -71,7 +71,7 @@
|
||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||
>
|
||||
<ErrorInformationCard
|
||||
title="Server Node Unavailable"
|
||||
:title="formatMessage(serverManageMessages.nodeUnavailableTitle)"
|
||||
:icon="TriangleAlertIcon"
|
||||
icon-color="red"
|
||||
:action="nodeUnavailableAction"
|
||||
@@ -80,16 +80,13 @@
|
||||
<template #description>
|
||||
<div class="text-md space-y-4">
|
||||
<p class="leading-[170%] text-secondary">
|
||||
Your server's node, where your Modrinth Server is physically hosted, is not accessible
|
||||
at the moment. We are working to resolve the issue as quickly as possible.
|
||||
{{ formatMessage(serverManageMessages.nodeUnavailableP1) }}
|
||||
</p>
|
||||
<p class="leading-[170%] text-secondary">
|
||||
Your data is safe and will not be lost, and your server will be back online as soon as
|
||||
the issue is resolved.
|
||||
{{ formatMessage(serverManageMessages.nodeUnavailableP2) }}
|
||||
</p>
|
||||
<p class="leading-[170%] text-secondary">
|
||||
If reloading does not work initially, please contact Modrinth Support via the chat
|
||||
bubble in the bottom right corner and we'll be happy to help.
|
||||
{{ formatMessage(serverManageMessages.nodeUnavailableP3) }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -149,7 +146,11 @@
|
||||
>
|
||||
<ButtonStyled circular size="large">
|
||||
<button
|
||||
v-tooltip="showSettingsHint ? undefined : 'Server settings'"
|
||||
v-tooltip="
|
||||
showSettingsHint
|
||||
? undefined
|
||||
: formatMessage(serverManageMessages.serverSettingsTooltip)
|
||||
"
|
||||
@click="
|
||||
() => {
|
||||
openServerSettingsModal()
|
||||
@@ -216,7 +217,7 @@
|
||||
<div class="flex flex-col gap-2 leading-[150%]">
|
||||
<div class="flex items-center gap-3">
|
||||
<IssuesIcon class="flex h-8 w-8 shrink-0 text-red sm:hidden" />
|
||||
<div class="flex gap-2 text-2xl font-bold">{{ errorTitle }}</div>
|
||||
<div class="flex gap-2 text-2xl font-bold">{{ formattedInstallErrorTitle }}</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -228,44 +229,36 @@
|
||||
errorMessage.toLocaleLowerCase() === 'the specified version may be incorrect'
|
||||
"
|
||||
>
|
||||
An invalid loader or Minecraft version was specified and could not be installed.
|
||||
{{ formatMessage(serverManageMessages.installErrorInvalidIntro) }}
|
||||
<ul class="m-0 mt-4 p-0 pl-4">
|
||||
<li>
|
||||
If this version of Minecraft was released recently, please check if Modrinth
|
||||
Hosting supports it.
|
||||
{{ formatMessage(serverManageMessages.installErrorBulletRecentMc) }}
|
||||
</li>
|
||||
<li>
|
||||
If you've installed a modpack, it may have been packaged incorrectly or may
|
||||
not be compatible with the loader.
|
||||
{{ formatMessage(serverManageMessages.installErrorBulletModpack) }}
|
||||
</li>
|
||||
<li>
|
||||
Your server may need to be reinstalled with a valid mod loader and version.
|
||||
You can change the loader by clicking the "Change Loader" button.
|
||||
{{ formatMessage(serverManageMessages.installErrorBulletReinstall) }}
|
||||
</li>
|
||||
<li>
|
||||
If you're stuck, please contact Modrinth Support with the information below:
|
||||
{{ formatMessage(serverManageMessages.installErrorBulletSupport) }}
|
||||
</li>
|
||||
</ul>
|
||||
<ButtonStyled>
|
||||
<button class="mt-2" @click="copyServerDebugInfo">
|
||||
<CopyIcon v-if="!copied" />
|
||||
<CheckIcon v-else />
|
||||
Copy Debug Info
|
||||
{{ formatMessage(serverManageMessages.copyDebugInfoButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div v-if="errorMessage.toLocaleLowerCase() === 'internal error'">
|
||||
An internal error occurred while installing your server. Don't fret — try
|
||||
reinstalling your server, and if the problem persists, please contact Modrinth
|
||||
support with your server's debug information.
|
||||
{{ formatMessage(serverManageMessages.installErrorInternalBody) }}
|
||||
</div>
|
||||
<div
|
||||
v-if="errorMessage.toLocaleLowerCase() === 'this version is not yet supported'"
|
||||
>
|
||||
An error occurred while installing your server because Modrinth Hosting does not
|
||||
support the version of Minecraft or the loader you specified. Try reinstalling
|
||||
your server with a different version or loader, and if the problem persists,
|
||||
please contact Modrinth Support with your server's debug information.
|
||||
{{ formatMessage(serverManageMessages.installErrorUnsupportedBody) }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -273,13 +266,17 @@
|
||||
class="mt-2 flex flex-col gap-4 sm:flex-row"
|
||||
>
|
||||
<ButtonStyled v-if="errorLog">
|
||||
<button @click="openInstallLog"><FileIcon />Open Installation Log</button>
|
||||
<button @click="openInstallLog">
|
||||
<FileIcon />{{
|
||||
formatMessage(serverManageMessages.openInstallationLogButton)
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="copyServerDebugInfo">
|
||||
<CopyIcon v-if="!copied" />
|
||||
<CheckIcon v-else />
|
||||
Copy Debug Info
|
||||
{{ formatMessage(serverManageMessages.copyDebugInfoButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="red" type="standard">
|
||||
@@ -288,7 +285,7 @@
|
||||
@click="openServerSettingsModal('installation')"
|
||||
>
|
||||
<RightArrowIcon />
|
||||
Change Loader
|
||||
{{ formatMessage(serverManageMessages.changeLoaderButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
@@ -312,7 +309,7 @@
|
||||
class="mb-4 flex w-full flex-row items-center gap-4 rounded-2xl bg-bg-red p-4 text-contrast"
|
||||
>
|
||||
<IssuesIcon class="size-5 text-red" />
|
||||
Something went wrong...
|
||||
{{ formatMessage(serverManageMessages.wsDisconnectedMessage) }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -321,7 +318,7 @@
|
||||
class="mb-4 flex w-full flex-row items-center gap-4 rounded-2xl bg-bg-orange p-4 text-sm text-contrast"
|
||||
>
|
||||
<LoaderCircleIcon class="h-5 w-5 animate-spin" />
|
||||
Hang on, we're reconnecting to your server.
|
||||
{{ formatMessage(serverManageMessages.wsReconnectingMessage) }}
|
||||
</div>
|
||||
|
||||
<Transition
|
||||
@@ -361,7 +358,12 @@
|
||||
<UploadIcon class="h-6 w-6 flex-none text-brand-blue" />
|
||||
</template>
|
||||
<template #header>
|
||||
Uploading files ({{ uploadState.completedFiles }}/{{ uploadState.totalFiles }})
|
||||
{{
|
||||
formatMessage(serverManageMessages.uploadingFilesHeader, {
|
||||
completed: uploadState.completedFiles,
|
||||
total: uploadState.totalFiles,
|
||||
})
|
||||
}}
|
||||
<span v-if="uploadState.currentFileName" class="font-normal text-secondary">
|
||||
— {{ uploadState.currentFileName }}
|
||||
</span>
|
||||
@@ -374,7 +376,9 @@
|
||||
</span>
|
||||
<template v-if="cancelUpload" #top-right-actions>
|
||||
<ButtonStyled type="outlined" color="blue">
|
||||
<button class="!border" @click="cancelUpload?.()">Cancel</button>
|
||||
<button class="!border" @click="cancelUpload?.()">
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
<template #progress>
|
||||
@@ -393,7 +397,9 @@
|
||||
v-if="showAdvancedDebugInfo"
|
||||
class="experimental-styles-within relative mx-auto mt-6 box-border w-full min-w-0 max-w-[1280px] px-6"
|
||||
>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Server data</h2>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">
|
||||
{{ formatMessage(serverManageMessages.serverDataHeading) }}
|
||||
</h2>
|
||||
<pre class="markdown-body w-full overflow-auto rounded-2xl bg-bg-raised p-4 text-sm">{{
|
||||
safeStringify(serverData)
|
||||
}}</pre>
|
||||
@@ -476,6 +482,7 @@ import {
|
||||
injectNotificationManager,
|
||||
provideServerSettingsModal,
|
||||
} from '#ui/providers'
|
||||
import { commonMessages, serverPanelNavMessages } from '#ui/utils/common-messages'
|
||||
import { formatLoaderLabel } from '#ui/utils/loaders'
|
||||
|
||||
import FileOperationAdmonitions from '../../../shared/files-tab/components/FileOperationAdmonitions.vue'
|
||||
@@ -569,6 +576,194 @@ const settingsHintMessages = defineMessages({
|
||||
},
|
||||
})
|
||||
|
||||
const serverManageMessages = defineMessages({
|
||||
preparingServerTitle: {
|
||||
id: 'servers.manage.error.preparing.title',
|
||||
defaultMessage: "We're getting your server ready",
|
||||
},
|
||||
preparingServerDescription: {
|
||||
id: 'servers.manage.error.preparing.description',
|
||||
defaultMessage: "Your server's hardware is being prepared and will be available shortly!",
|
||||
},
|
||||
serverUpgradingTitle: {
|
||||
id: 'servers.manage.error.upgrading.title',
|
||||
defaultMessage: 'Server upgrading',
|
||||
},
|
||||
serverUpgradingDescription: {
|
||||
id: 'servers.manage.error.upgrading.description',
|
||||
defaultMessage:
|
||||
"Your server's hardware is currently being upgraded and will be back online shortly!",
|
||||
},
|
||||
serverSuspendedTitle: {
|
||||
id: 'servers.manage.error.suspended.title',
|
||||
defaultMessage: 'Server suspended',
|
||||
},
|
||||
suspendedCancelledDescription: {
|
||||
id: 'servers.manage.error.suspended.cancelled',
|
||||
defaultMessage:
|
||||
'Your subscription has been cancelled.\nContact Modrinth Support if you believe this is an error.',
|
||||
},
|
||||
suspendedWithReasonDescription: {
|
||||
id: 'servers.manage.error.suspended.with-reason',
|
||||
defaultMessage:
|
||||
'Your server has been suspended: {reason}\nContact Modrinth Support if you believe this is an error.',
|
||||
},
|
||||
suspendedDefaultDescription: {
|
||||
id: 'servers.manage.error.suspended.default',
|
||||
defaultMessage:
|
||||
'Your server has been suspended.\nContact Modrinth Support if you believe this is an error.',
|
||||
},
|
||||
forbiddenErrorTitle: {
|
||||
id: 'servers.manage.error.forbidden.title',
|
||||
defaultMessage: 'An error occurred.',
|
||||
},
|
||||
forbiddenErrorDescription: {
|
||||
id: 'servers.manage.error.forbidden.description',
|
||||
defaultMessage: 'Please contact Modrinth Support.',
|
||||
},
|
||||
nodeUnavailableTitle: {
|
||||
id: 'servers.manage.error.node-unavailable.title',
|
||||
defaultMessage: 'Server Node Unavailable',
|
||||
},
|
||||
nodeUnavailableP1: {
|
||||
id: 'servers.manage.error.node-unavailable.p1',
|
||||
defaultMessage:
|
||||
"Your server's node, where your Modrinth Server is physically hosted, is not accessible at the moment. We are working to resolve the issue as quickly as possible.",
|
||||
},
|
||||
nodeUnavailableP2: {
|
||||
id: 'servers.manage.error.node-unavailable.p2',
|
||||
defaultMessage:
|
||||
'Your data is safe and will not be lost, and your server will be back online as soon as the issue is resolved.',
|
||||
},
|
||||
nodeUnavailableP3: {
|
||||
id: 'servers.manage.error.node-unavailable.p3',
|
||||
defaultMessage:
|
||||
"If reloading does not work initially, please contact Modrinth Support via the chat bubble in the bottom right corner and we'll be happy to help.",
|
||||
},
|
||||
serverSettingsTooltip: {
|
||||
id: 'servers.manage.tooltip.server-settings',
|
||||
defaultMessage: 'Server settings',
|
||||
},
|
||||
installErrorInvalidIntro: {
|
||||
id: 'servers.manage.install-error.invalid-version.intro',
|
||||
defaultMessage:
|
||||
'An invalid loader or Minecraft version was specified and could not be installed.',
|
||||
},
|
||||
installErrorBulletRecentMc: {
|
||||
id: 'servers.manage.install-error.invalid-version.bullet-recent-mc',
|
||||
defaultMessage:
|
||||
'If this version of Minecraft was released recently, please check if Modrinth Hosting supports it.',
|
||||
},
|
||||
installErrorBulletModpack: {
|
||||
id: 'servers.manage.install-error.invalid-version.bullet-modpack',
|
||||
defaultMessage:
|
||||
"If you've installed a modpack, it may have been packaged incorrectly or may not be compatible with the loader.",
|
||||
},
|
||||
installErrorBulletReinstall: {
|
||||
id: 'servers.manage.install-error.invalid-version.bullet-reinstall',
|
||||
defaultMessage:
|
||||
'Your server may need to be reinstalled with a valid mod loader and version. You can change the loader by clicking the "Change Loader" button.',
|
||||
},
|
||||
installErrorBulletSupport: {
|
||||
id: 'servers.manage.install-error.invalid-version.bullet-support',
|
||||
defaultMessage: "If you're stuck, please contact Modrinth Support with the information below:",
|
||||
},
|
||||
installErrorInternalBody: {
|
||||
id: 'servers.manage.install-error.internal.body',
|
||||
defaultMessage:
|
||||
"An internal error occurred while installing your server. Don't fret — try reinstalling your server, and if the problem persists, please contact Modrinth support with your server's debug information.",
|
||||
},
|
||||
installErrorUnsupportedBody: {
|
||||
id: 'servers.manage.install-error.unsupported-version.body',
|
||||
defaultMessage:
|
||||
"An error occurred while installing your server because Modrinth Hosting does not support the version of Minecraft or the loader you specified. Try reinstalling your server with a different version or loader, and if the problem persists, please contact Modrinth Support with your server's debug information.",
|
||||
},
|
||||
copyDebugInfoButton: {
|
||||
id: 'servers.manage.install-error.copy-debug-info',
|
||||
defaultMessage: 'Copy Debug Info',
|
||||
},
|
||||
openInstallationLogButton: {
|
||||
id: 'servers.manage.install-error.open-installation-log',
|
||||
defaultMessage: 'Open Installation Log',
|
||||
},
|
||||
changeLoaderButton: {
|
||||
id: 'servers.manage.install-error.change-loader',
|
||||
defaultMessage: 'Change Loader',
|
||||
},
|
||||
wsDisconnectedMessage: {
|
||||
id: 'servers.manage.ws.disconnected',
|
||||
defaultMessage: 'Something went wrong...',
|
||||
},
|
||||
wsReconnectingMessage: {
|
||||
id: 'servers.manage.ws.reconnecting',
|
||||
defaultMessage: "Hang on, we're reconnecting to your server.",
|
||||
},
|
||||
uploadingFilesHeader: {
|
||||
id: 'servers.manage.upload.header',
|
||||
defaultMessage: 'Uploading files ({completed}/{total})',
|
||||
},
|
||||
serverDataHeading: {
|
||||
id: 'servers.manage.debug.server-data-heading',
|
||||
defaultMessage: 'Server data',
|
||||
},
|
||||
detailLabelServerId: {
|
||||
id: 'servers.manage.details.server-id',
|
||||
defaultMessage: 'Server ID',
|
||||
},
|
||||
detailLabelNode: {
|
||||
id: 'servers.manage.details.node',
|
||||
defaultMessage: 'Node',
|
||||
},
|
||||
detailLabelErrorMessage: {
|
||||
id: 'servers.manage.details.error-message',
|
||||
defaultMessage: 'Error message',
|
||||
},
|
||||
detailLabelTimestamp: {
|
||||
id: 'servers.manage.details.timestamp',
|
||||
defaultMessage: 'Timestamp',
|
||||
},
|
||||
detailLabelErrorName: {
|
||||
id: 'servers.manage.details.error-name',
|
||||
defaultMessage: 'Error Name',
|
||||
},
|
||||
detailLabelOriginalError: {
|
||||
id: 'servers.manage.details.original-error',
|
||||
defaultMessage: 'Original Error',
|
||||
},
|
||||
detailLabelStackTrace: {
|
||||
id: 'servers.manage.details.stack-trace',
|
||||
defaultMessage: 'Stack Trace',
|
||||
},
|
||||
nodePingFailedMessage: {
|
||||
id: 'servers.manage.details.node-ping-failed',
|
||||
defaultMessage: 'Unable to reach node. Ping test failed.',
|
||||
},
|
||||
goToBillingSettings: {
|
||||
id: 'servers.manage.action.go-to-billing',
|
||||
defaultMessage: 'Go to billing settings',
|
||||
},
|
||||
goBackToAllServers: {
|
||||
id: 'servers.manage.action.go-back-to-servers',
|
||||
defaultMessage: 'Go back to all servers',
|
||||
},
|
||||
reloadPageButton: {
|
||||
id: 'servers.manage.action.reload-page',
|
||||
defaultMessage: 'Reload',
|
||||
},
|
||||
notificationDismissNoticeError: {
|
||||
id: 'servers.manage.notification.dismiss-notice-error',
|
||||
defaultMessage: 'Error dismissing notice',
|
||||
},
|
||||
retryInstallationFailed: {
|
||||
id: 'servers.manage.notification.retry-installation-failed',
|
||||
defaultMessage: 'Failed to retry installation',
|
||||
},
|
||||
installationErrorTitle: {
|
||||
id: 'servers.manage.install-error.title',
|
||||
defaultMessage: 'Installation error',
|
||||
},
|
||||
})
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const client = injectModrinthClient()
|
||||
const isNuxt = computed(() => client instanceof NuxtModrinthClient)
|
||||
@@ -586,6 +781,14 @@ const errorTitle = ref('Error')
|
||||
const errorMessage = ref('An unexpected error occurred.')
|
||||
const errorLog = ref('')
|
||||
const errorLogFile = ref('')
|
||||
|
||||
const formattedInstallErrorTitle = computed(() => {
|
||||
const t = errorTitle.value
|
||||
if (t === 'Installation error') return formatMessage(serverManageMessages.installationErrorTitle)
|
||||
if (t === 'Error') return formatMessage(commonMessages.errorLabel)
|
||||
return t
|
||||
})
|
||||
|
||||
const isOnboarding = computed(() => serverData.value?.flows?.intro)
|
||||
|
||||
const SETTINGS_HINT_KEY = 'server-panel-settings-hint-dismissed'
|
||||
@@ -844,25 +1047,25 @@ watch(serverData, (data) => {
|
||||
|
||||
const navLinks = computed<Tab[]>(() => [
|
||||
{
|
||||
label: 'Overview',
|
||||
label: formatMessage(serverPanelNavMessages.overview),
|
||||
href: `/hosting/manage/${props.serverId}`,
|
||||
icon: LayoutTemplateIcon,
|
||||
subpages: [],
|
||||
},
|
||||
{
|
||||
label: 'Content',
|
||||
label: formatMessage(serverPanelNavMessages.content),
|
||||
href: `/hosting/manage/${props.serverId}/content`,
|
||||
icon: BoxesIcon,
|
||||
subpages: ['mods', 'datapacks'],
|
||||
},
|
||||
{
|
||||
label: 'Files',
|
||||
label: formatMessage(serverPanelNavMessages.files),
|
||||
href: `/hosting/manage/${props.serverId}/files`,
|
||||
icon: FolderOpenIcon,
|
||||
subpages: [],
|
||||
},
|
||||
{
|
||||
label: 'Backups',
|
||||
label: formatMessage(serverPanelNavMessages.backups),
|
||||
href: `/hosting/manage/${props.serverId}/backups`,
|
||||
icon: DatabaseBackupIcon,
|
||||
subpages: [],
|
||||
@@ -878,7 +1081,7 @@ const surveyNotice = computed(() => serverData.value?.notices?.find((n) => n.lev
|
||||
async function dismissNotice(noticeId: number) {
|
||||
await client.archon.servers_v0.dismissNotice(props.serverId, noticeId).catch((err) => {
|
||||
addNotification({
|
||||
title: 'Error dismissing notice',
|
||||
title: formatMessage(serverManageMessages.notificationDismissNoticeError),
|
||||
text: err,
|
||||
type: 'error',
|
||||
})
|
||||
@@ -982,7 +1185,10 @@ async function handleContentRetry() {
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
text: err instanceof Error ? err.message : 'Failed to retry installation',
|
||||
text:
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: formatMessage(serverManageMessages.retryInstallationFailed),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1224,62 +1430,64 @@ const nodeAccessible = ref(true)
|
||||
|
||||
const nodeUnavailableDetails = computed(() => [
|
||||
{
|
||||
label: 'Server ID',
|
||||
label: formatMessage(serverManageMessages.detailLabelServerId),
|
||||
value: props.serverId,
|
||||
type: 'inline' as const,
|
||||
},
|
||||
{
|
||||
label: 'Node',
|
||||
label: formatMessage(serverManageMessages.detailLabelNode),
|
||||
value:
|
||||
(serverError.value?.responseData as { hostname?: string } | undefined)?.hostname ??
|
||||
serverData.value?.datacenter ??
|
||||
'Unknown',
|
||||
formatMessage(commonMessages.unknownLabel),
|
||||
type: 'inline' as const,
|
||||
},
|
||||
{
|
||||
label: 'Error message',
|
||||
label: formatMessage(serverManageMessages.detailLabelErrorMessage),
|
||||
value: nodeAccessible.value
|
||||
? (serverError.value?.message ?? 'Unknown')
|
||||
: 'Unable to reach node. Ping test failed.',
|
||||
? (serverError.value?.message ?? formatMessage(commonMessages.unknownLabel))
|
||||
: formatMessage(serverManageMessages.nodePingFailedMessage),
|
||||
type: 'block' as const,
|
||||
},
|
||||
])
|
||||
|
||||
const suspendedDescription = computed(() => {
|
||||
if (serverData.value?.suspension_reason === 'cancelled') {
|
||||
return 'Your subscription has been cancelled.\nContact Modrinth Support if you believe this is an error.'
|
||||
return formatMessage(serverManageMessages.suspendedCancelledDescription)
|
||||
}
|
||||
if (serverData.value?.suspension_reason) {
|
||||
return `Your server has been suspended: ${serverData.value.suspension_reason}\nContact Modrinth Support if you believe this is an error.`
|
||||
return formatMessage(serverManageMessages.suspendedWithReasonDescription, {
|
||||
reason: serverData.value.suspension_reason,
|
||||
})
|
||||
}
|
||||
return 'Your server has been suspended.\nContact Modrinth Support if you believe this is an error.'
|
||||
return formatMessage(serverManageMessages.suspendedDefaultDescription)
|
||||
})
|
||||
|
||||
const generalErrorDetails = computed(() => [
|
||||
{
|
||||
label: 'Server ID',
|
||||
label: formatMessage(serverManageMessages.detailLabelServerId),
|
||||
value: props.serverId,
|
||||
type: 'inline' as const,
|
||||
},
|
||||
{
|
||||
label: 'Timestamp',
|
||||
label: formatMessage(serverManageMessages.detailLabelTimestamp),
|
||||
value: String(new Date().toISOString()),
|
||||
type: 'inline' as const,
|
||||
},
|
||||
{
|
||||
label: 'Error Name',
|
||||
label: formatMessage(serverManageMessages.detailLabelErrorName),
|
||||
value: serverError.value?.name,
|
||||
type: 'inline' as const,
|
||||
},
|
||||
{
|
||||
label: 'Error Message',
|
||||
label: formatMessage(serverManageMessages.detailLabelErrorMessage),
|
||||
value: serverError.value?.message,
|
||||
type: 'block' as const,
|
||||
},
|
||||
...(serverError.value?.originalError
|
||||
? [
|
||||
{
|
||||
label: 'Original Error',
|
||||
label: formatMessage(serverManageMessages.detailLabelOriginalError),
|
||||
value: String(serverError.value.originalError),
|
||||
type: 'hidden' as const,
|
||||
},
|
||||
@@ -1288,7 +1496,7 @@ const generalErrorDetails = computed(() => [
|
||||
...(serverError.value?.stack
|
||||
? [
|
||||
{
|
||||
label: 'Stack Trace',
|
||||
label: formatMessage(serverManageMessages.detailLabelStackTrace),
|
||||
value: serverError.value.stack,
|
||||
type: 'hidden' as const,
|
||||
},
|
||||
@@ -1297,26 +1505,33 @@ const generalErrorDetails = computed(() => [
|
||||
])
|
||||
|
||||
const suspendedAction = computed(() => ({
|
||||
label: 'Go to billing settings',
|
||||
label: formatMessage(serverManageMessages.goToBillingSettings),
|
||||
onClick: () => props.navigateToBilling?.(),
|
||||
color: 'brand' as const,
|
||||
}))
|
||||
|
||||
const generalErrorAction = computed(() => ({
|
||||
label: 'Go back to all servers',
|
||||
label: formatMessage(serverManageMessages.goBackToAllServers),
|
||||
onClick: () => props.navigateToServers?.(),
|
||||
color: 'brand' as const,
|
||||
}))
|
||||
|
||||
const nodeUnavailableAction = computed(() => ({
|
||||
label: 'Reload',
|
||||
label: formatMessage(serverManageMessages.reloadPageButton),
|
||||
onClick: () => props.reloadPage(),
|
||||
color: 'brand' as const,
|
||||
disabled: false,
|
||||
}))
|
||||
|
||||
const copyServerDebugInfo = () => {
|
||||
const debugInfo = `Server ID: ${serverData.value?.server_id}\nError: ${errorMessage.value}\nKind: ${serverData.value?.upstream?.kind}\nProject ID: ${serverData.value?.upstream?.project_id}\nVersion ID: ${serverData.value?.upstream?.version_id}\nLog: ${errorLog.value}`
|
||||
const debugInfo = [
|
||||
`Server ID: ${String(serverData.value?.server_id ?? '')}`,
|
||||
`Error: ${errorMessage.value}`,
|
||||
`Kind: ${String(serverData.value?.upstream?.kind ?? '')}`,
|
||||
`Project ID: ${String(serverData.value?.upstream?.project_id ?? '')}`,
|
||||
`Version ID: ${String(serverData.value?.upstream?.version_id ?? '')}`,
|
||||
`Log: ${errorLog.value}`,
|
||||
].join('\n')
|
||||
navigator.clipboard.writeText(debugInfo)
|
||||
copied.value = true
|
||||
setTimeout(() => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,165 @@
|
||||
import type { Archon } from '@modrinth/api-client'
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import BackupItem from '../../components/servers/backups/BackupItem.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Servers/BackupItem',
|
||||
component: BackupItem,
|
||||
args: {
|
||||
preview: false,
|
||||
showCopyIdAction: false,
|
||||
showDebugInfo: false,
|
||||
restoreDisabled: undefined,
|
||||
},
|
||||
} satisfies Meta<typeof BackupItem>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
function makeBackup(overrides: Partial<Archon.Backups.v1.Backup> = {}): Archon.Backups.v1.Backup {
|
||||
return {
|
||||
id: 'backup-001',
|
||||
physical_id: 'phys-001',
|
||||
name: 'Backup #5',
|
||||
created_at: new Date(Date.now() - 1000 * 60 * 10).toISOString(),
|
||||
automated: false,
|
||||
status: 'done',
|
||||
interrupted: false,
|
||||
ongoing: false,
|
||||
locked: false,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
export const Default: Story = {
|
||||
name: 'Default (manual)',
|
||||
args: {
|
||||
backup: makeBackup({ name: 'Base finished!!' }),
|
||||
},
|
||||
}
|
||||
|
||||
export const Automated: Story = {
|
||||
name: 'Automated',
|
||||
args: {
|
||||
backup: makeBackup({ automated: true, name: 'Backup #2' }),
|
||||
},
|
||||
}
|
||||
|
||||
export const Creating: Story = {
|
||||
name: 'Creating (in progress)',
|
||||
args: {
|
||||
backup: makeBackup({
|
||||
automated: true,
|
||||
name: 'Backup #3',
|
||||
status: 'in_progress',
|
||||
ongoing: true,
|
||||
task: {
|
||||
create: { progress: 0.4, state: 'ongoing' },
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
export const Restoring: Story = {
|
||||
name: 'Restoring (in progress)',
|
||||
args: {
|
||||
backup: makeBackup({
|
||||
name: 'Backup #5',
|
||||
automated: true,
|
||||
task: {
|
||||
restore: { progress: 0.6, state: 'ongoing' },
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
export const FailedCreate: Story = {
|
||||
name: 'Failed (create)',
|
||||
args: {
|
||||
backup: makeBackup({ status: 'error', name: 'Backup #4' }),
|
||||
},
|
||||
}
|
||||
|
||||
export const FailedRestore: Story = {
|
||||
name: 'Failed (restore)',
|
||||
args: {
|
||||
backup: makeBackup({
|
||||
name: 'Backup #5',
|
||||
task: {
|
||||
restore: { progress: 0, state: 'failed' },
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
export const Preview: Story = {
|
||||
name: 'Preview (compact, used in delete modal)',
|
||||
args: {
|
||||
backup: makeBackup({ name: 'Base finished!!' }),
|
||||
preview: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const RestoreDisabled: Story = {
|
||||
name: 'Restore disabled (server running)',
|
||||
args: {
|
||||
backup: makeBackup({ name: 'Backup #5', automated: true }),
|
||||
restoreDisabled: 'Cannot restore backup while server is running',
|
||||
},
|
||||
}
|
||||
|
||||
export const AllStates: Story = {
|
||||
render: () => ({
|
||||
components: { BackupItem },
|
||||
setup() {
|
||||
const now = new Date(Date.now() - 1000 * 60 * 10).toISOString()
|
||||
|
||||
function makeBackup(overrides: Partial<Archon.Backups.v1.Backup>): Archon.Backups.v1.Backup {
|
||||
return {
|
||||
id: 'backup-001',
|
||||
physical_id: 'phys-001',
|
||||
name: 'Backup #5',
|
||||
created_at: now,
|
||||
automated: false,
|
||||
status: 'done',
|
||||
interrupted: false,
|
||||
ongoing: false,
|
||||
locked: false,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
manual: makeBackup({ name: 'Base finished!!' }),
|
||||
automated: makeBackup({ automated: true, name: 'Backup #2' }),
|
||||
creating: makeBackup({
|
||||
automated: true,
|
||||
name: 'Backup #3',
|
||||
status: 'in_progress',
|
||||
ongoing: true,
|
||||
task: { create: { progress: 0.4, state: 'ongoing' } },
|
||||
}),
|
||||
restoring: makeBackup({
|
||||
automated: true,
|
||||
task: { restore: { progress: 0.6, state: 'ongoing' } },
|
||||
}),
|
||||
failedCreate: makeBackup({ status: 'error', name: 'Backup #4' }),
|
||||
failedRestore: makeBackup({
|
||||
task: { restore: { progress: 0, state: 'failed' } },
|
||||
}),
|
||||
}
|
||||
},
|
||||
template: /* html */ `
|
||||
<div style="display: flex; flex-direction: column; gap: 0.75rem; max-width: 900px;">
|
||||
<BackupItem :backup="manual" />
|
||||
<BackupItem :backup="automated" />
|
||||
<BackupItem :backup="creating" />
|
||||
<BackupItem :backup="restoring" />
|
||||
<BackupItem :backup="failedCreate" />
|
||||
<BackupItem :backup="failedRestore" />
|
||||
<BackupItem :backup="manual" preview />
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
}
|
||||
@@ -567,6 +567,16 @@ export const commonMessages = defineMessages({
|
||||
id: 'label.download-failed',
|
||||
defaultMessage: 'Download failed',
|
||||
},
|
||||
/** Value is pre-formatted numeric string; unit is a stable key for ICU (B, KB, KiB, Bytes, …). */
|
||||
fileSizeFormatted: {
|
||||
id: 'label.file-size.formatted',
|
||||
defaultMessage:
|
||||
'{value} {unit, select, B {B} KB {KB} MB {MB} GB {GB} TB {TB} PB {PB} EB {EB} KiB {KiB} MiB {MiB} GiB {GiB} Bytes {Bytes} other {B}}',
|
||||
},
|
||||
uploadFailedFileDetail: {
|
||||
id: 'notification.upload-failed.file-detail',
|
||||
defaultMessage: 'Failed to upload {fileName}',
|
||||
},
|
||||
projectCreated: {
|
||||
id: 'project.about.details.created',
|
||||
defaultMessage: 'Created {date}',
|
||||
@@ -608,6 +618,18 @@ export const commonMessages = defineMessages({
|
||||
defaultMessage:
|
||||
'{count} {countPlural, plural, one {recent play} other {recent plays}} from Modrinth in the past 2 weeks',
|
||||
},
|
||||
serverSettingsUpdatedTitle: {
|
||||
id: 'server.settings.success.updated.title',
|
||||
defaultMessage: 'Server settings updated',
|
||||
},
|
||||
serverSettingsUpdatedText: {
|
||||
id: 'server.settings.success.updated.text',
|
||||
defaultMessage: 'Your server settings were successfully changed.',
|
||||
},
|
||||
consoleFilterAllLevels: {
|
||||
id: 'servers.console.filter.all-levels',
|
||||
defaultMessage: 'All',
|
||||
},
|
||||
})
|
||||
|
||||
export const formFieldLabels = defineMessages({
|
||||
@@ -1086,3 +1108,53 @@ export const paymentMethodMessages = defineMessages({
|
||||
defaultMessage: 'Charities',
|
||||
},
|
||||
})
|
||||
|
||||
export const serverPanelNavMessages = defineMessages({
|
||||
overview: {
|
||||
id: 'server.panel.nav.overview',
|
||||
defaultMessage: 'Overview',
|
||||
},
|
||||
content: {
|
||||
id: 'server.panel.nav.content',
|
||||
defaultMessage: 'Content',
|
||||
},
|
||||
files: {
|
||||
id: 'server.panel.nav.files',
|
||||
defaultMessage: 'Files',
|
||||
},
|
||||
backups: {
|
||||
id: 'server.panel.nav.backups',
|
||||
defaultMessage: 'Backups',
|
||||
},
|
||||
})
|
||||
|
||||
export const serverSettingsTabMessages = defineMessages({
|
||||
general: {
|
||||
id: 'server.settings.tabs.general',
|
||||
defaultMessage: 'General',
|
||||
},
|
||||
installation: {
|
||||
id: 'server.settings.tabs.installation',
|
||||
defaultMessage: 'Installation',
|
||||
},
|
||||
network: {
|
||||
id: 'server.settings.tabs.network',
|
||||
defaultMessage: 'Network',
|
||||
},
|
||||
properties: {
|
||||
id: 'server.settings.tabs.properties',
|
||||
defaultMessage: 'Properties',
|
||||
},
|
||||
advanced: {
|
||||
id: 'server.settings.tabs.advanced',
|
||||
defaultMessage: 'Advanced',
|
||||
},
|
||||
billing: {
|
||||
id: 'server.settings.tabs.billing',
|
||||
defaultMessage: 'Billing',
|
||||
},
|
||||
'admin-billing': {
|
||||
id: 'server.settings.tabs.admin-billing',
|
||||
defaultMessage: 'Admin Billing',
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user