You've already forked AstralRinth
forked from didirus/AstralRinth
backup page fixes and new impls for new apis (#3437)
* wip: backup page fixes and new impls for new apis * wip: more progress on backup fixes, almost done * lint * Backups cleanup * Don't show create warning if creating * Fix ongoing state * Download support * Support ready * Disable auto backup button * Use auth param for download of backups * Disable install buttons when backup is in progress, add retrying * Make prepare button have immediate feedback, don't refresh backups in all cases * Intl:extract & rebase fixes * Updated changelog and fix lint --------- Co-authored-by: Prospector <prospectordev@gmail.com>
This commit is contained in:
@@ -1,401 +1,391 @@
|
||||
<template>
|
||||
<div class="contents">
|
||||
<div
|
||||
v-if="filteredNotices.length > 0"
|
||||
class="experimental-styles-within relative mx-auto flex w-full min-w-0 max-w-[1280px] flex-col gap-3 px-6"
|
||||
>
|
||||
<ServerNotice
|
||||
v-for="notice in filteredNotices"
|
||||
:key="`notice-${notice.id}`"
|
||||
:level="notice.level"
|
||||
:message="notice.message"
|
||||
:dismissable="notice.dismissable"
|
||||
:title="notice.title"
|
||||
class="w-full"
|
||||
@dismiss="() => dismissNotice(notice.id)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="serverData?.status === 'suspended' && serverData.suspension_reason === 'upgrading'"
|
||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||
>
|
||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="grid place-content-center rounded-full bg-bg-blue p-4">
|
||||
<TransferIcon class="size-12 text-blue" />
|
||||
</div>
|
||||
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">Server upgrading</h1>
|
||||
<div
|
||||
v-if="filteredNotices.length > 0"
|
||||
class="experimental-styles-within relative mx-auto flex w-full min-w-0 max-w-[1280px] flex-col gap-3 px-6"
|
||||
>
|
||||
<ServerNotice
|
||||
v-for="notice in filteredNotices"
|
||||
:key="`notice-${notice.id}`"
|
||||
:level="notice.level"
|
||||
:message="notice.message"
|
||||
:dismissable="notice.dismissable"
|
||||
:title="notice.title"
|
||||
class="w-full"
|
||||
@dismiss="() => dismissNotice(notice.id)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="serverData?.status === 'suspended' && serverData.suspension_reason === 'upgrading'"
|
||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||
>
|
||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="grid place-content-center rounded-full bg-bg-blue p-4">
|
||||
<TransferIcon class="size-12 text-blue" />
|
||||
</div>
|
||||
<p class="text-lg text-secondary">
|
||||
Your server's hardware is currently being upgraded and will be back online shortly!
|
||||
</p>
|
||||
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">Server upgrading</h1>
|
||||
</div>
|
||||
<p class="text-lg text-secondary">
|
||||
Your server's hardware is currently being upgraded and will be back online shortly!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="serverData?.status === 'suspended' && serverData.suspension_reason === 'support'"
|
||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||
>
|
||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="grid place-content-center rounded-full bg-bg-blue p-4">
|
||||
<TransferIcon class="size-12 text-blue" />
|
||||
</div>
|
||||
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">We're working on your server</h1>
|
||||
</div>
|
||||
<div
|
||||
v-if="serverData?.status === 'suspended' && serverData.suspension_reason === 'support'"
|
||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||
>
|
||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="grid place-content-center rounded-full bg-bg-blue p-4">
|
||||
<TransferIcon class="size-12 text-blue" />
|
||||
</div>
|
||||
<p class="text-lg text-secondary">
|
||||
You recently contacted Modrinth Support, and we're actively working on your server. It
|
||||
will be back online shortly.
|
||||
</p>
|
||||
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">We're working on your server</h1>
|
||||
</div>
|
||||
<p class="text-lg text-secondary">
|
||||
You recently contacted Modrinth Support, and we're actively working on your server. It
|
||||
will be back online shortly.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="serverData?.status === 'suspended' && serverData.suspension_reason !== 'upgrading'"
|
||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||
>
|
||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="grid place-content-center rounded-full bg-bg-orange p-4">
|
||||
<LockIcon class="size-12 text-orange" />
|
||||
</div>
|
||||
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">Server suspended</h1>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="serverData?.status === 'suspended' && serverData.suspension_reason !== 'upgrading'"
|
||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||
>
|
||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="grid place-content-center rounded-full bg-bg-orange p-4">
|
||||
<LockIcon class="size-12 text-orange" />
|
||||
</div>
|
||||
<p class="text-lg text-secondary">
|
||||
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">Server suspended</h1>
|
||||
</div>
|
||||
<p class="text-lg text-secondary">
|
||||
{{
|
||||
serverData.suspension_reason === "cancelled"
|
||||
? "Your subscription has been cancelled."
|
||||
: serverData.suspension_reason
|
||||
? `Your server has been suspended: ${serverData.suspension_reason}`
|
||||
: "Your server has been suspended."
|
||||
}}
|
||||
<br />
|
||||
Contact Modrinth support if you believe this is an error.
|
||||
</p>
|
||||
</div>
|
||||
<ButtonStyled size="large" color="brand" @click="() => router.push('/settings/billing')">
|
||||
<button class="mt-6 !w-full">Go to billing settings</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
server.general?.error?.error.statusCode === 403 ||
|
||||
server.general?.error?.error.statusCode === 404
|
||||
"
|
||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||
>
|
||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="grid place-content-center rounded-full bg-bg-orange p-4">
|
||||
<TransferIcon class="size-12 text-orange" />
|
||||
</div>
|
||||
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">Server not found</h1>
|
||||
</div>
|
||||
<p class="text-lg text-secondary">
|
||||
You don't have permission to view this server or it no longer exists. If you believe this
|
||||
is an error, please contact Modrinth support.
|
||||
</p>
|
||||
</div>
|
||||
<UiCopyCode :text="JSON.stringify(server.general?.error)" />
|
||||
|
||||
<ButtonStyled size="large" color="brand" @click="() => router.push('/servers/manage')">
|
||||
<button class="mt-6 !w-full">Go back to all servers</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="server.general?.error?.error.statusCode === 503"
|
||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||
>
|
||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="grid place-content-center rounded-full bg-bg-red p-4">
|
||||
<UiServersIconsPanelErrorIcon class="size-12 text-red" />
|
||||
</div>
|
||||
<h1 class="m-0 mb-4 w-fit text-4xl font-bold">Server Node Unavailable</h1>
|
||||
</div>
|
||||
<p class="m-0 mb-4 leading-[170%] text-secondary">
|
||||
Your server's node, where your Modrinth Server is physically hosted, is experiencing
|
||||
issues. We are working with our datacenter to resolve the issue as quickly as possible.
|
||||
</p>
|
||||
<p class="m-0 mb-4 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.
|
||||
</p>
|
||||
<p class="m-0 mb-4 leading-[170%] text-secondary">
|
||||
For updates, please join the Modrinth Discord or contact Modrinth Support via the chat
|
||||
bubble in the bottom right corner and we'll be happy to help.
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<UiCopyCode :text="'Server ID: ' + server.serverId" />
|
||||
<UiCopyCode :text="'Node: ' + server.general?.datacenter" />
|
||||
</div>
|
||||
</div>
|
||||
<ButtonStyled
|
||||
size="large"
|
||||
color="standard"
|
||||
@click="
|
||||
() =>
|
||||
navigateTo('https://discord.modrinth.com', {
|
||||
external: true,
|
||||
})
|
||||
"
|
||||
>
|
||||
<button class="mt-6 !w-full">Join Modrinth Discord</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
:disabled="formattedTime !== '00'"
|
||||
size="large"
|
||||
color="standard"
|
||||
@click="() => reloadNuxtApp()"
|
||||
>
|
||||
<button class="mt-3 !w-full">Reload</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="server.general?.error"
|
||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||
>
|
||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="grid place-content-center rounded-full bg-bg-orange p-4">
|
||||
<TransferIcon class="size-12 text-orange" />
|
||||
</div>
|
||||
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">Connection lost</h1>
|
||||
<div class="text-center text-secondary">
|
||||
{{
|
||||
serverData.suspension_reason === "cancelled"
|
||||
? "Your subscription has been cancelled."
|
||||
: serverData.suspension_reason
|
||||
? `Your server has been suspended: ${serverData.suspension_reason}`
|
||||
: "Your server has been suspended."
|
||||
formattedTime == "00" ? "Reconnecting..." : `Retrying in ${formattedTime} seconds...`
|
||||
}}
|
||||
<br />
|
||||
Contact Modrinth support if you believe this is an error.
|
||||
</p>
|
||||
</div>
|
||||
<ButtonStyled size="large" color="brand" @click="() => router.push('/settings/billing')">
|
||||
<button class="mt-6 !w-full">Go to billing settings</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
server.general?.error?.error.statusCode === 403 ||
|
||||
server.general?.error?.error.statusCode === 404
|
||||
"
|
||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||
>
|
||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="grid place-content-center rounded-full bg-bg-orange p-4">
|
||||
<TransferIcon class="size-12 text-orange" />
|
||||
</div>
|
||||
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">Server not found</h1>
|
||||
</div>
|
||||
<p class="text-lg text-secondary">
|
||||
You don't have permission to view this server or it no longer exists. If you believe
|
||||
this is an error, please contact Modrinth support.
|
||||
</p>
|
||||
</div>
|
||||
<UiCopyCode :text="JSON.stringify(server.general?.error)" />
|
||||
<p class="text-lg text-secondary">
|
||||
Something went wrong, and we couldn't connect to your server. This is likely due to a
|
||||
temporary network issue. You'll be reconnected automatically.
|
||||
</p>
|
||||
</div>
|
||||
<UiCopyCode :text="JSON.stringify(server.general?.error)" />
|
||||
<ButtonStyled
|
||||
:disabled="formattedTime !== '00'"
|
||||
size="large"
|
||||
color="brand"
|
||||
@click="() => reloadNuxtApp()"
|
||||
>
|
||||
<button class="mt-6 !w-full">Reload</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<!-- SERVER START -->
|
||||
<div
|
||||
v-else-if="serverData"
|
||||
data-pyro-server-manager-root
|
||||
class="experimental-styles-within mobile-blurred-servericon relative mx-auto mb-6 box-border flex min-h-screen w-full min-w-0 max-w-[1280px] flex-col gap-6 px-6 transition-all duration-300"
|
||||
:style="{
|
||||
'--server-bg-image': serverData.image
|
||||
? `url(${serverData.image})`
|
||||
: `linear-gradient(180deg, rgba(153,153,153,1) 0%, rgba(87,87,87,1) 100%)`,
|
||||
}"
|
||||
>
|
||||
<div class="flex w-full min-w-0 select-none flex-col items-center gap-6 pt-4 sm:flex-row">
|
||||
<UiServersServerIcon :image="serverData.image" class="drop-shadow-lg sm:drop-shadow-none" />
|
||||
<div
|
||||
class="flex min-w-0 flex-1 flex-col-reverse items-center gap-2 sm:flex-col sm:items-start"
|
||||
>
|
||||
<div class="hidden shrink-0 flex-row items-center gap-1 sm:flex">
|
||||
<NuxtLink to="/servers/manage" class="breadcrumb goto-link flex w-fit items-center">
|
||||
<LeftArrowIcon />
|
||||
All servers
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<div class="flex w-full flex-col items-center gap-4 sm:flex-row">
|
||||
<h1
|
||||
class="m-0 w-screen flex-shrink gap-3 truncate px-3 text-center text-4xl font-bold text-contrast sm:w-full sm:p-0 sm:text-left"
|
||||
>
|
||||
{{ serverData.name }}
|
||||
</h1>
|
||||
<div
|
||||
v-if="isConnected"
|
||||
data-pyro-server-action-buttons
|
||||
class="server-action-buttons-anim flex w-fit flex-shrink-0"
|
||||
>
|
||||
<UiServersPanelServerActionButton
|
||||
class="flex-shrink-0"
|
||||
:is-online="isServerRunning"
|
||||
:is-actioning="isActioning"
|
||||
:is-installing="serverData.status === 'installing'"
|
||||
:disabled="isActioning || !!error"
|
||||
:server-name="serverData.name"
|
||||
:server-data="serverData"
|
||||
:uptime-seconds="uptimeSeconds"
|
||||
@action="sendPowerAction"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ButtonStyled size="large" color="brand" @click="() => router.push('/servers/manage')">
|
||||
<button class="mt-6 !w-full">Go back to all servers</button>
|
||||
</ButtonStyled>
|
||||
<UiServersServerInfoLabels
|
||||
:server-data="serverData"
|
||||
:show-game-label="showGameLabel"
|
||||
:show-loader-label="showLoaderLabel"
|
||||
:uptime-seconds="uptimeSeconds"
|
||||
:linked="true"
|
||||
class="server-action-buttons-anim flex min-w-0 flex-col flex-wrap items-center gap-4 text-secondary *:hidden sm:flex-row sm:*:flex"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="server.general?.error?.error.statusCode === 503"
|
||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||
>
|
||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="grid place-content-center rounded-full bg-bg-red p-4">
|
||||
<UiServersIconsPanelErrorIcon class="size-12 text-red" />
|
||||
</div>
|
||||
<h1 class="m-0 mb-4 w-fit text-4xl font-bold">Server Node Unavailable</h1>
|
||||
</div>
|
||||
<p class="m-0 mb-4 leading-[170%] text-secondary">
|
||||
Your server's node, where your Modrinth Server is physically hosted, is experiencing
|
||||
issues. We are working with our datacenter to resolve the issue as quickly as possible.
|
||||
</p>
|
||||
<p class="m-0 mb-4 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.
|
||||
</p>
|
||||
<p class="m-0 mb-4 leading-[170%] text-secondary">
|
||||
For updates, please join the Modrinth Discord or contact Modrinth Support via the chat
|
||||
bubble in the bottom right corner and we'll be happy to help.
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<UiCopyCode :text="'Server ID: ' + server.serverId" />
|
||||
<UiCopyCode :text="'Node: ' + server.general?.datacenter" />
|
||||
</div>
|
||||
</div>
|
||||
<ButtonStyled
|
||||
size="large"
|
||||
color="standard"
|
||||
@click="
|
||||
() =>
|
||||
navigateTo('https://discord.modrinth.com', {
|
||||
external: true,
|
||||
})
|
||||
"
|
||||
>
|
||||
<button class="mt-6 !w-full">Join Modrinth Discord</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
:disabled="formattedTime !== '00'"
|
||||
size="large"
|
||||
color="standard"
|
||||
@click="() => reloadNuxtApp()"
|
||||
>
|
||||
<button class="mt-3 !w-full">Reload</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="server.general?.error"
|
||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||
data-pyro-navigation
|
||||
class="isolate flex w-full select-none flex-col justify-between gap-4 overflow-auto md:flex-row md:items-center"
|
||||
>
|
||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="grid place-content-center rounded-full bg-bg-orange p-4">
|
||||
<TransferIcon class="size-12 text-orange" />
|
||||
</div>
|
||||
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">Connection lost</h1>
|
||||
<div class="text-center text-secondary">
|
||||
{{
|
||||
formattedTime == "00"
|
||||
? "Reconnecting..."
|
||||
: `Retrying in ${formattedTime} seconds...`
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-lg text-secondary">
|
||||
Something went wrong, and we couldn't connect to your server. This is likely due to a
|
||||
temporary network issue. You'll be reconnected automatically.
|
||||
</p>
|
||||
</div>
|
||||
<UiCopyCode :text="JSON.stringify(server.general?.error)" />
|
||||
<ButtonStyled
|
||||
:disabled="formattedTime !== '00'"
|
||||
size="large"
|
||||
color="brand"
|
||||
@click="() => reloadNuxtApp()"
|
||||
>
|
||||
<button class="mt-6 !w-full">Reload</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<UiNavTabs :links="navLinks" />
|
||||
</div>
|
||||
<!-- SERVER START -->
|
||||
<div
|
||||
v-else-if="serverData"
|
||||
data-pyro-server-manager-root
|
||||
class="experimental-styles-within mobile-blurred-servericon relative mx-auto box-border flex min-h-screen w-full min-w-0 max-w-[1280px] flex-col gap-6 px-6 transition-all duration-300"
|
||||
:style="{
|
||||
'--server-bg-image': serverData.image
|
||||
? `url(${serverData.image})`
|
||||
: `linear-gradient(180deg, rgba(153,153,153,1) 0%, rgba(87,87,87,1) 100%)`,
|
||||
}"
|
||||
>
|
||||
<div class="flex w-full min-w-0 select-none flex-col items-center gap-6 pt-4 sm:flex-row">
|
||||
<UiServersServerIcon :image="serverData.image" class="drop-shadow-lg sm:drop-shadow-none" />
|
||||
<div
|
||||
class="flex min-w-0 flex-1 flex-col-reverse items-center gap-2 sm:flex-col sm:items-start"
|
||||
>
|
||||
<div class="hidden shrink-0 flex-row items-center gap-1 sm:flex">
|
||||
<NuxtLink to="/servers/manage" class="breadcrumb goto-link flex w-fit items-center">
|
||||
<LeftArrowIcon />
|
||||
All servers
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<div class="flex w-full flex-col items-center gap-4 sm:flex-row">
|
||||
<h1
|
||||
class="m-0 w-screen flex-shrink gap-3 truncate px-3 text-center text-4xl font-bold text-contrast sm:w-full sm:p-0 sm:text-left"
|
||||
>
|
||||
{{ serverData.name }}
|
||||
</h1>
|
||||
<div
|
||||
v-if="isConnected"
|
||||
data-pyro-server-action-buttons
|
||||
class="server-action-buttons-anim flex w-fit flex-shrink-0"
|
||||
>
|
||||
<UiServersPanelServerActionButton
|
||||
class="flex-shrink-0"
|
||||
:is-online="isServerRunning"
|
||||
:is-actioning="isActioning"
|
||||
:is-installing="serverData.status === 'installing'"
|
||||
:disabled="isActioning || !!error"
|
||||
:server-name="serverData.name"
|
||||
:server-data="serverData"
|
||||
:uptime-seconds="uptimeSeconds"
|
||||
@action="sendPowerAction"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UiServersServerInfoLabels
|
||||
:server-data="serverData"
|
||||
:show-game-label="showGameLabel"
|
||||
:show-loader-label="showLoaderLabel"
|
||||
:uptime-seconds="uptimeSeconds"
|
||||
:linked="true"
|
||||
class="server-action-buttons-anim flex min-w-0 flex-col flex-wrap items-center gap-4 text-secondary *:hidden sm:flex-row sm:*:flex"
|
||||
/>
|
||||
<div data-pyro-mount class="h-full w-full flex-1">
|
||||
<div
|
||||
v-if="error"
|
||||
class="mx-auto mb-4 flex justify-between gap-2 rounded-2xl border-2 border-solid border-red bg-bg-red p-4 font-semibold text-contrast"
|
||||
>
|
||||
<div class="flex flex-row gap-4">
|
||||
<IssuesIcon class="hidden h-8 w-8 shrink-0 text-red sm:block" />
|
||||
<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>
|
||||
|
||||
<div v-if="errorTitle.toLocaleLowerCase() === 'installation error'" class="font-normal">
|
||||
<div
|
||||
v-if="errorMessage.toLocaleLowerCase() === 'the specified version may be incorrect'"
|
||||
>
|
||||
An invalid loader or Minecraft version was specified and could not be installed.
|
||||
<ul class="m-0 mt-4 p-0 pl-4">
|
||||
<li>
|
||||
If this version of Minecraft was released recently, please check if Modrinth
|
||||
Servers supports it.
|
||||
</li>
|
||||
<li>
|
||||
If you've installed a modpack, it may have been packaged incorrectly or may not
|
||||
be compatible with the loader.
|
||||
</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.
|
||||
</li>
|
||||
<li>
|
||||
If you're stuck, please contact Modrinth support with the information below:
|
||||
</li>
|
||||
</ul>
|
||||
<ButtonStyled>
|
||||
<button class="mt-2" @click="copyServerDebugInfo">
|
||||
<CopyIcon v-if="!copied" />
|
||||
<CheckIcon v-else />
|
||||
Copy Debug Info
|
||||
</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.
|
||||
</div>
|
||||
<div v-if="errorMessage.toLocaleLowerCase() === 'this version is not yet supported'">
|
||||
An error occurred while installing your server because Modrinth Servers 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.
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="errorTitle === 'Installation error'"
|
||||
class="mt-2 flex flex-col gap-4 sm:flex-row"
|
||||
>
|
||||
<ButtonStyled v-if="errorLog">
|
||||
<button @click="openInstallLog"><FileIcon />Open Installation Log</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="copyServerDebugInfo">
|
||||
<CopyIcon v-if="!copied" />
|
||||
<CheckIcon v-else />
|
||||
Copy Debug Info
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="red" type="standard">
|
||||
<NuxtLink
|
||||
class="whitespace-pre"
|
||||
:to="`/servers/manage/${serverId}/options/loader`"
|
||||
>
|
||||
<RightArrowIcon />
|
||||
Change Loader
|
||||
</NuxtLink>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-pyro-navigation
|
||||
class="isolate flex w-full select-none flex-col justify-between gap-4 overflow-auto md:flex-row md:items-center"
|
||||
v-if="!isConnected && !isReconnecting && !isLoading"
|
||||
data-pyro-server-ws-error
|
||||
class="mb-4 flex w-full flex-row items-center gap-4 rounded-2xl bg-bg-red p-4 text-contrast"
|
||||
>
|
||||
<UiNavTabs :links="navLinks" />
|
||||
<IssuesIcon class="size-5 text-red" />
|
||||
Something went wrong...
|
||||
</div>
|
||||
|
||||
<div data-pyro-mount class="h-full w-full flex-1">
|
||||
<div
|
||||
v-if="error"
|
||||
class="mx-auto mb-4 flex justify-between gap-2 rounded-2xl border-2 border-solid border-red bg-bg-red p-4 font-semibold text-contrast"
|
||||
>
|
||||
<div class="flex flex-row gap-4">
|
||||
<IssuesIcon class="hidden h-8 w-8 shrink-0 text-red sm:block" />
|
||||
<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>
|
||||
|
||||
<div
|
||||
v-if="errorTitle.toLocaleLowerCase() === 'installation error'"
|
||||
class="font-normal"
|
||||
>
|
||||
<div
|
||||
v-if="
|
||||
errorMessage.toLocaleLowerCase() === 'the specified version may be incorrect'
|
||||
"
|
||||
>
|
||||
An invalid loader or Minecraft version was specified and could not be installed.
|
||||
<ul class="m-0 mt-4 p-0 pl-4">
|
||||
<li>
|
||||
If this version of Minecraft was released recently, please check if Modrinth
|
||||
Servers supports it.
|
||||
</li>
|
||||
<li>
|
||||
If you've installed a modpack, it may have been packaged incorrectly or may
|
||||
not be compatible with the loader.
|
||||
</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.
|
||||
</li>
|
||||
<li>
|
||||
If you're stuck, please contact Modrinth support with the information below:
|
||||
</li>
|
||||
</ul>
|
||||
<ButtonStyled>
|
||||
<button class="mt-2" @click="copyServerDebugInfo">
|
||||
<CopyIcon v-if="!copied" />
|
||||
<CheckIcon v-else />
|
||||
Copy Debug Info
|
||||
</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.
|
||||
</div>
|
||||
<div
|
||||
v-if="errorMessage.toLocaleLowerCase() === 'this version is not yet supported'"
|
||||
>
|
||||
An error occurred while installing your server because Modrinth Servers 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.
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="errorTitle === 'Installation error'"
|
||||
class="mt-2 flex flex-col gap-4 sm:flex-row"
|
||||
>
|
||||
<ButtonStyled v-if="errorLog">
|
||||
<button @click="openInstallLog"><FileIcon />Open Installation Log</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="copyServerDebugInfo">
|
||||
<CopyIcon v-if="!copied" />
|
||||
<CheckIcon v-else />
|
||||
Copy Debug Info
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="red" type="standard">
|
||||
<NuxtLink
|
||||
class="whitespace-pre"
|
||||
:to="`/servers/manage/${serverId}/options/loader`"
|
||||
>
|
||||
<RightArrowIcon />
|
||||
Change Loader
|
||||
</NuxtLink>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!isConnected && !isReconnecting && !isLoading"
|
||||
data-pyro-server-ws-error
|
||||
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...
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isReconnecting"
|
||||
data-pyro-server-ws-reconnecting
|
||||
class="mb-4 flex w-full flex-row items-center gap-4 rounded-2xl bg-bg-orange p-4 text-sm text-contrast"
|
||||
>
|
||||
<UiServersPanelSpinner />
|
||||
Hang on, we're reconnecting to your server.
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="serverData.status === 'installing'"
|
||||
data-pyro-server-installing
|
||||
class="mb-4 flex w-full flex-row items-center gap-4 rounded-2xl bg-bg-blue p-4 text-sm text-contrast"
|
||||
>
|
||||
<UiServersServerIcon :image="serverData.image" class="!h-10 !w-10" />
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-lg font-bold"> We're preparing your server! </span>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<UiServersPanelSpinner class="!h-3 !w-3" /> <LazyUiServersInstallingTicker />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<NuxtPage
|
||||
:route="route"
|
||||
:is-connected="isConnected"
|
||||
:is-ws-auth-incorrect="isWSAuthIncorrect"
|
||||
:is-server-running="isServerRunning"
|
||||
:stats="stats"
|
||||
:server-power-state="serverPowerState"
|
||||
:power-state-details="powerStateDetails"
|
||||
:socket="socket"
|
||||
:server="server"
|
||||
@reinstall="onReinstall"
|
||||
/>
|
||||
<div
|
||||
v-if="isReconnecting"
|
||||
data-pyro-server-ws-reconnecting
|
||||
class="mb-4 flex w-full flex-row items-center gap-4 rounded-2xl bg-bg-orange p-4 text-sm text-contrast"
|
||||
>
|
||||
<UiServersPanelSpinner />
|
||||
Hang on, we're reconnecting to your server.
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="serverData.status === 'installing'"
|
||||
data-pyro-server-installing
|
||||
class="mb-4 flex w-full flex-row items-center gap-4 rounded-2xl bg-bg-blue p-4 text-sm text-contrast"
|
||||
>
|
||||
<UiServersServerIcon :image="serverData.image" class="!h-10 !w-10" />
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-lg font-bold"> We're preparing your server! </span>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<UiServersPanelSpinner class="!h-3 !w-3" /> <LazyUiServersInstallingTicker />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<NuxtPage
|
||||
:route="route"
|
||||
:is-connected="isConnected"
|
||||
:is-ws-auth-incorrect="isWSAuthIncorrect"
|
||||
:is-server-running="isServerRunning"
|
||||
:stats="stats"
|
||||
:server-power-state="serverPowerState"
|
||||
:power-state-details="powerStateDetails"
|
||||
:socket="socket"
|
||||
:server="server"
|
||||
:backup-in-progress="backupInProgress"
|
||||
@reinstall="onReinstall"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -416,8 +406,10 @@ import DOMPurify from "dompurify";
|
||||
import { ButtonStyled, ServerNotice } from "@modrinth/ui";
|
||||
import { Intercom, shutdown } from "@intercom/messenger-js-sdk";
|
||||
import { reloadNuxtApp, navigateTo } from "#app";
|
||||
import type { MessageDescriptor } from "@vintl/vintl";
|
||||
import type { ServerState, Stats, WSEvent, WSInstallationResultEvent } from "~/types/servers";
|
||||
import { usePyroConsole } from "~/store/console.ts";
|
||||
import { type Backup } from "~/composables/pyroServers.ts";
|
||||
import { usePyroFetch } from "~/composables/pyroFetch.ts";
|
||||
|
||||
const app = useNuxtApp() as unknown as { $notify: any };
|
||||
@@ -770,6 +762,31 @@ const handleWebSocketMessage = (data: WSEvent) => {
|
||||
uptimeSeconds.value = data.uptime;
|
||||
startUptimeUpdates();
|
||||
break;
|
||||
case "backup-progress": {
|
||||
// Update a backup's state
|
||||
const curBackup = server.backups?.data.find((backup) => backup.id === data.id);
|
||||
|
||||
if (!curBackup) {
|
||||
console.log(`Ignoring backup-progress event for unknown backup: ${data.id}`);
|
||||
} else {
|
||||
console.log(
|
||||
`Handling backup progress for ${curBackup.name} (${data.id}) task: ${data.task} state: ${data.state} progress: ${data.progress}`,
|
||||
);
|
||||
|
||||
if (!curBackup.task) {
|
||||
curBackup.task = {};
|
||||
}
|
||||
|
||||
curBackup.task[data.task] = {
|
||||
progress: data.progress,
|
||||
state: data.state,
|
||||
};
|
||||
|
||||
curBackup.ongoing = data.task === "create" && data.state === "ongoing";
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
console.warn("Unhandled WebSocket event:", data);
|
||||
}
|
||||
@@ -968,6 +985,41 @@ const formattedTime = computed(() => {
|
||||
return `${seconds.toString().padStart(2, "0")}`;
|
||||
});
|
||||
|
||||
export type BackupInProgressReason = {
|
||||
type: string;
|
||||
tooltip: MessageDescriptor;
|
||||
};
|
||||
|
||||
const RestoreInProgressReason = {
|
||||
type: "restore",
|
||||
tooltip: defineMessage({
|
||||
id: "servers.backup.restore.in-progress.tooltip",
|
||||
defaultMessage: "Backup restore in progress",
|
||||
}),
|
||||
} satisfies BackupInProgressReason;
|
||||
|
||||
const CreateInProgressReason = {
|
||||
type: "create",
|
||||
tooltip: defineMessage({
|
||||
id: "servers.backup.create.in-progress.tooltip",
|
||||
defaultMessage: "Backup creation in progress",
|
||||
}),
|
||||
} satisfies BackupInProgressReason;
|
||||
|
||||
const backupInProgress = computed(() => {
|
||||
const backups = server.backups?.data;
|
||||
if (!backups) {
|
||||
return undefined;
|
||||
}
|
||||
if (backups.find((backup: Backup) => backup?.task?.create?.state === "ongoing")) {
|
||||
return CreateInProgressReason;
|
||||
}
|
||||
if (backups.find((backup: Backup) => backup?.task?.restore?.state === "ongoing")) {
|
||||
return RestoreInProgressReason;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const stopPolling = () => {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
|
||||
@@ -1,248 +1,147 @@
|
||||
<template>
|
||||
<div class="contents">
|
||||
<div
|
||||
v-if="server.backups?.error"
|
||||
class="flex w-full flex-col items-center justify-center gap-4 p-4"
|
||||
>
|
||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<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>
|
||||
<div
|
||||
v-if="server.backups?.error"
|
||||
class="flex w-full flex-col items-center justify-center gap-4 p-4"
|
||||
>
|
||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="grid place-content-center rounded-full bg-bg-orange p-4">
|
||||
<IssuesIcon class="size-12 text-orange" />
|
||||
</div>
|
||||
<p class="text-lg text-secondary">
|
||||
We couldn't load your server's backups. Here's what went wrong:
|
||||
</p>
|
||||
<p>
|
||||
<span class="break-all font-mono">{{ JSON.stringify(server.backups.error) }}</span>
|
||||
</p>
|
||||
<ButtonStyled size="large" color="brand" @click="() => server.refresh(['backups'])">
|
||||
<button class="mt-6 !w-full">Retry</button>
|
||||
</ButtonStyled>
|
||||
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">Failed to load backups</h1>
|
||||
</div>
|
||||
<p class="text-lg text-secondary">
|
||||
We couldn't load your server's backups. Here's what went wrong:
|
||||
</p>
|
||||
<p>
|
||||
<span class="break-all font-mono">{{ JSON.stringify(server.backups.error) }}</span>
|
||||
</p>
|
||||
<ButtonStyled size="large" color="brand" @click="() => server.refresh(['backups'])">
|
||||
<button class="mt-6 !w-full">Retry</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="data" class="contents">
|
||||
<LazyUiServersBackupCreateModal
|
||||
ref="createBackupModal"
|
||||
:server="server"
|
||||
@backup-created="handleBackupCreated"
|
||||
/>
|
||||
<LazyUiServersBackupRenameModal
|
||||
ref="renameBackupModal"
|
||||
:server="server"
|
||||
:current-backup-id="currentBackup"
|
||||
:backup-name="renameBackupName"
|
||||
@backup-renamed="handleBackupRenamed"
|
||||
/>
|
||||
<LazyUiServersBackupRestoreModal
|
||||
ref="restoreBackupModal"
|
||||
:server="server"
|
||||
:backup-id="currentBackup"
|
||||
:backup-name="currentBackupDetails?.name ?? ''"
|
||||
:backup-created-at="currentBackupDetails?.created_at ?? ''"
|
||||
@backup-restored="handleBackupRestored"
|
||||
/>
|
||||
<LazyUiServersBackupDeleteModal
|
||||
ref="deleteBackupModal"
|
||||
:server="server"
|
||||
:backup-id="currentBackup"
|
||||
:backup-name="currentBackupDetails?.name ?? ''"
|
||||
:backup-created-at="currentBackupDetails?.created_at ?? ''"
|
||||
@backup-deleted="handleBackupDeleted"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="data" class="contents">
|
||||
<BackupCreateModal ref="createBackupModal" :server="server" />
|
||||
<BackupRenameModal ref="renameBackupModal" :server="server" />
|
||||
<BackupRestoreModal ref="restoreBackupModal" :server="server" />
|
||||
<BackupDeleteModal ref="deleteBackupModal" :server="server" @delete="deleteBackup" />
|
||||
<BackupSettingsModal ref="backupSettingsModal" :server="server" />
|
||||
|
||||
<LazyUiServersBackupSettingsModal ref="backupSettingsModal" :server="server" />
|
||||
|
||||
<ul class="m-0 flex list-none flex-col gap-4 p-0">
|
||||
<div class="relative w-full overflow-hidden rounded-2xl bg-bg-raised p-6 shadow-md">
|
||||
<div class="flex flex-col items-center justify-between gap-4 sm:flex-row sm:gap-0">
|
||||
<div class="flex flex-col items-baseline gap-2">
|
||||
<div class="text-2xl font-bold text-contrast">
|
||||
{{
|
||||
data.used_backup_quota === 0
|
||||
? "No backups"
|
||||
: `You've created ${data.used_backup_quota} backup${data.used_backup_quota === 1 ? "" : "s"}`
|
||||
}}
|
||||
</div>
|
||||
<div>
|
||||
{{
|
||||
data.backup_quota - data.used_backup_quota === 0
|
||||
? "You have reached your backup limit. Consider removing old backups to create new ones."
|
||||
: `You can create ${data.backup_quota - data.used_backup_quota} more backups for your server.`
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full flex-col gap-2 sm:w-fit sm:flex-row">
|
||||
<ButtonStyled type="standard">
|
||||
<button
|
||||
:disabled="server.general?.status === 'installing'"
|
||||
@click="showbackupSettingsModal"
|
||||
>
|
||||
<SettingsIcon class="h-5 w-5" />
|
||||
Auto backups
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled type="standard" color="brand">
|
||||
<button
|
||||
v-tooltip="
|
||||
isServerRunning && !userPreferences.backupWhileRunning
|
||||
? 'Cannot create backup while server is running. You can disable this from your server Options > Preferences.'
|
||||
: server.general?.status === 'installing'
|
||||
? 'Cannot create backups while server is being installed'
|
||||
: ''
|
||||
"
|
||||
class="w-full sm:w-fit"
|
||||
:disabled="
|
||||
(isServerRunning && !userPreferences.backupWhileRunning) ||
|
||||
data.used_backup_quota >= data.backup_quota ||
|
||||
backups.some((backup) => backup.ongoing) ||
|
||||
server.general?.status === 'installing'
|
||||
"
|
||||
@click="showCreateModel"
|
||||
>
|
||||
<PlusIcon class="h-5 w-5" />
|
||||
Create backup
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="backups.some((backup) => backup.ongoing)"
|
||||
data-pyro-server-backup-ongoing
|
||||
class="flex w-full flex-row items-center gap-4 rounded-2xl bg-bg-orange p-4 text-contrast"
|
||||
>
|
||||
A backup is currently being created. This may take a few minutes. This page will
|
||||
automatically refresh when the backup is complete.
|
||||
</div>
|
||||
|
||||
<div class="flex w-full flex-col gap-2">
|
||||
<li
|
||||
v-for="(backup, index) in backups"
|
||||
:key="backup.id"
|
||||
class="relative m-0 w-full list-none rounded-2xl bg-bg-raised p-2 shadow-md"
|
||||
<div class="mb-6 flex flex-col items-center justify-between gap-4 sm:flex-row">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<h1 class="m-0 text-2xl font-extrabold text-contrast">Backups</h1>
|
||||
<TagItem
|
||||
v-tooltip="`${data.backup_quota - data.used_backup_quota} backup slots remaining`"
|
||||
class="cursor-help"
|
||||
:style="{
|
||||
'--_color':
|
||||
data.backup_quota <= data.used_backup_quota
|
||||
? 'var(--color-red)'
|
||||
: data.backup_quota - data.used_backup_quota <= 3
|
||||
? 'var(--color-orange)'
|
||||
: undefined,
|
||||
'--_bg-color':
|
||||
data.backup_quota <= data.used_backup_quota
|
||||
? 'var(--color-red-bg)'
|
||||
: data.backup_quota - data.used_backup_quota <= 3
|
||||
? 'var(--color-orange-bg)'
|
||||
: undefined,
|
||||
}"
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex min-w-0 flex-row items-center gap-4">
|
||||
<div
|
||||
class="grid size-14 shrink-0 place-content-center overflow-hidden rounded-xl border-[1px] border-solid border-button-border shadow-sm"
|
||||
:class="backup.ongoing ? 'text-green [&&]:bg-bg-green' : 'bg-button-bg'"
|
||||
>
|
||||
<UiServersIconsLoadingIcon
|
||||
v-if="backup.ongoing"
|
||||
v-tooltip="'Backup in progress'"
|
||||
class="size-6 animate-spin"
|
||||
/>
|
||||
<LockIcon v-else-if="backup.locked" class="size-8" />
|
||||
<BoxIcon v-else class="size-8" />
|
||||
</div>
|
||||
<div class="flex min-w-0 flex-col gap-2">
|
||||
<div class="flex min-w-0 flex-col gap-2 sm:flex-row sm:items-center">
|
||||
<div class="max-w-full truncate font-bold text-contrast">
|
||||
{{ backup.name }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="index == 0"
|
||||
class="hidden items-center gap-1 rounded-full bg-bg-green p-1 px-1.5 text-xs font-semibold text-brand sm:flex"
|
||||
>
|
||||
<CheckIcon class="size-4" /> Latest
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 text-xs">
|
||||
<CalendarIcon class="size-4" />
|
||||
{{
|
||||
new Date(backup.created_at).toLocaleString("en-US", {
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
year: "2-digit",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
hour12: true,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ButtonStyled v-if="!backup.ongoing" circular type="transparent">
|
||||
<UiServersTeleportOverflowMenu
|
||||
direction="left"
|
||||
position="bottom"
|
||||
class="bg-transparent"
|
||||
:disabled="backups.some((b) => b.ongoing)"
|
||||
:options="[
|
||||
{
|
||||
id: 'rename',
|
||||
action: () => {
|
||||
renameBackupName = backup.name;
|
||||
currentBackup = backup.id;
|
||||
renameBackupModal?.show();
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'restore',
|
||||
action: () => {
|
||||
currentBackup = backup.id;
|
||||
restoreBackupModal?.show();
|
||||
},
|
||||
},
|
||||
{ id: 'download', action: () => initiateDownload(backup.id) },
|
||||
{
|
||||
id: 'lock',
|
||||
action: () => {
|
||||
if (backup.locked) {
|
||||
unlockBackup(backup.id);
|
||||
} else {
|
||||
lockBackup(backup.id);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'delete',
|
||||
action: () => {
|
||||
currentBackup = backup.id;
|
||||
deleteBackupModal?.show();
|
||||
},
|
||||
color: 'red',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<MoreHorizontalIcon class="h-5 w-5 bg-transparent" />
|
||||
<template #rename> <EditIcon /> Rename </template>
|
||||
<template #restore> <ClipboardCopyIcon /> Restore </template>
|
||||
<template v-if="backup.locked" #lock> <LockOpenIcon /> Unlock </template>
|
||||
<template v-else #lock> <LockIcon /> Lock </template>
|
||||
<template #download> <DownloadIcon /> Download </template>
|
||||
<template #delete> <TrashIcon /> Delete </template>
|
||||
</UiServersTeleportOverflowMenu>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{{ data.used_backup_quota }} / {{ data.backup_quota }}
|
||||
</TagItem>
|
||||
</div>
|
||||
</ul>
|
||||
|
||||
<p class="m-0">
|
||||
You can have up to {{ data.backup_quota }} backups at once, securely off-site with
|
||||
Backblaze.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="over-the-top-download-animation"
|
||||
:class="{ 'animation-hidden': !overTheTopDownloadAnimation }"
|
||||
class="grid w-full grid-cols-[repeat(auto-fit,_minmax(180px,1fr))] gap-2 sm:flex sm:w-fit sm:flex-row"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="animation-ring-3 flex items-center justify-center rounded-full border-4 border-solid border-brand bg-brand-highlight opacity-40"
|
||||
></div>
|
||||
<div
|
||||
class="animation-ring-2 flex items-center justify-center rounded-full border-4 border-solid border-brand bg-brand-highlight opacity-60"
|
||||
></div>
|
||||
<div
|
||||
class="animation-ring-1 flex items-center justify-center rounded-full border-4 border-solid border-brand bg-brand-highlight"
|
||||
<ButtonStyled type="standard">
|
||||
<button
|
||||
v-tooltip="
|
||||
'Auto backups are currently unavailable; we apologize for the inconvenience.'
|
||||
"
|
||||
:disabled="true || server.general?.status === 'installing'"
|
||||
@click="showbackupSettingsModal"
|
||||
>
|
||||
<DownloadIcon class="h-20 w-20 text-contrast" />
|
||||
</div>
|
||||
<SettingsIcon class="h-5 w-5" />
|
||||
Auto backups
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled type="standard" color="brand">
|
||||
<button
|
||||
v-tooltip="backupCreationDisabled"
|
||||
class="w-full sm:w-fit"
|
||||
:disabled="!!backupCreationDisabled"
|
||||
@click="showCreateModel"
|
||||
>
|
||||
<PlusIcon class="h-5 w-5" />
|
||||
Create backup
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full flex-col gap-2">
|
||||
<div
|
||||
v-if="backups.length === 0"
|
||||
class="mt-6 flex items-center justify-center gap-2 text-center text-secondary"
|
||||
>
|
||||
<template v-if="data.used_backup_quota">
|
||||
<SpinnerIcon class="animate-spin" />
|
||||
Loading backups...
|
||||
</template>
|
||||
<template v-else> You don't have any backups yet. </template>
|
||||
</div>
|
||||
<BackupItem
|
||||
v-for="backup in backups"
|
||||
:key="`backup-${backup.id}`"
|
||||
:backup="backup"
|
||||
:kyros-url="props.server.general?.node.instance"
|
||||
:jwt="props.server.general?.node.token"
|
||||
@prepare="() => prepareDownload(backup.id)"
|
||||
@download="() => triggerDownloadAnimation()"
|
||||
@rename="() => renameBackupModal?.show(backup)"
|
||||
@restore="() => restoreBackupModal?.show(backup)"
|
||||
@lock="
|
||||
() => {
|
||||
if (backup.locked) {
|
||||
unlockBackup(backup.id);
|
||||
} else {
|
||||
lockBackup(backup.id);
|
||||
}
|
||||
}
|
||||
"
|
||||
@delete="
|
||||
(skipConfirmation?: boolean) =>
|
||||
!skipConfirmation ? deleteBackup(backup) : deleteBackupModal?.show(backup)
|
||||
"
|
||||
@retry="() => retryBackup(backup.id)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="over-the-top-download-animation"
|
||||
:class="{ 'animation-hidden': !overTheTopDownloadAnimation }"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="animation-ring-3 flex items-center justify-center rounded-full border-4 border-solid border-brand bg-brand-highlight opacity-40"
|
||||
></div>
|
||||
<div
|
||||
class="animation-ring-2 flex items-center justify-center rounded-full border-4 border-solid border-brand bg-brand-highlight opacity-60"
|
||||
></div>
|
||||
<div
|
||||
class="animation-ring-1 flex items-center justify-center rounded-full border-4 border-solid border-brand bg-brand-highlight"
|
||||
>
|
||||
<DownloadIcon class="h-20 w-20 text-contrast" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -250,25 +149,17 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import { ButtonStyled, TagItem } from "@modrinth/ui";
|
||||
import { useStorage } from "@vueuse/core";
|
||||
import {
|
||||
PlusIcon,
|
||||
CheckIcon,
|
||||
CalendarIcon,
|
||||
MoreHorizontalIcon,
|
||||
EditIcon,
|
||||
ClipboardCopyIcon,
|
||||
DownloadIcon,
|
||||
TrashIcon,
|
||||
SettingsIcon,
|
||||
BoxIcon,
|
||||
LockIcon,
|
||||
LockOpenIcon,
|
||||
IssuesIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { SpinnerIcon, PlusIcon, DownloadIcon, SettingsIcon, IssuesIcon } from "@modrinth/assets";
|
||||
import { ref, computed } from "vue";
|
||||
import type { Server } from "~/composables/pyroServers";
|
||||
import BackupItem from "~/components/ui/servers/BackupItem.vue";
|
||||
import BackupRenameModal from "~/components/ui/servers/BackupRenameModal.vue";
|
||||
import BackupCreateModal from "~/components/ui/servers/BackupCreateModal.vue";
|
||||
import BackupRestoreModal from "~/components/ui/servers/BackupRestoreModal.vue";
|
||||
import BackupDeleteModal from "~/components/ui/servers/BackupDeleteModal.vue";
|
||||
import BackupSettingsModal from "~/components/ui/servers/BackupSettingsModal.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
@@ -299,19 +190,30 @@ useHead({
|
||||
|
||||
const overTheTopDownloadAnimation = ref();
|
||||
|
||||
const createBackupModal = ref<typeof NewModal>();
|
||||
const renameBackupModal = ref<typeof NewModal>();
|
||||
const restoreBackupModal = ref<typeof NewModal>();
|
||||
const deleteBackupModal = ref<typeof NewModal>();
|
||||
const backupSettingsModal = ref<typeof NewModal>();
|
||||
const createBackupModal = ref<InstanceType<typeof BackupCreateModal>>();
|
||||
const renameBackupModal = ref<InstanceType<typeof BackupRenameModal>>();
|
||||
const restoreBackupModal = ref<InstanceType<typeof BackupRestoreModal>>();
|
||||
const deleteBackupModal = ref<InstanceType<typeof BackupDeleteModal>>();
|
||||
const backupSettingsModal = ref<InstanceType<typeof BackupSettingsModal>>();
|
||||
|
||||
const renameBackupName = ref("");
|
||||
const currentBackup = ref("");
|
||||
|
||||
const refreshInterval = ref<ReturnType<typeof setInterval>>();
|
||||
|
||||
const currentBackupDetails = computed(() => {
|
||||
return backups.value.find((backup) => backup.id === currentBackup.value);
|
||||
const backupCreationDisabled = computed(() => {
|
||||
if (props.isServerRunning && !userPreferences.value.backupWhileRunning) {
|
||||
return "Cannot create backup while server is running";
|
||||
}
|
||||
if (
|
||||
data.value?.used_backup_quota !== undefined &&
|
||||
data.value?.backup_quota !== undefined &&
|
||||
data.value?.used_backup_quota >= data.value?.backup_quota
|
||||
) {
|
||||
return `All ${data.value.backup_quota} of your backup slots are in use`;
|
||||
}
|
||||
if (backups.value.some((backup) => backup.task?.create?.state === "ongoing")) {
|
||||
return "A backup is already in progress";
|
||||
}
|
||||
if (props.server.general?.status === "installing") {
|
||||
return "Cannot create backup while server is installing";
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const showCreateModel = () => {
|
||||
@@ -322,69 +224,17 @@ const showbackupSettingsModal = () => {
|
||||
backupSettingsModal.value?.show();
|
||||
};
|
||||
|
||||
const handleBackupCreated = async (payload: { success: boolean; message: string }) => {
|
||||
if (payload.success) {
|
||||
addNotification({ type: "success", text: payload.message });
|
||||
await props.server.refresh(["backups"]);
|
||||
} else {
|
||||
addNotification({ type: "error", text: payload.message });
|
||||
}
|
||||
};
|
||||
|
||||
const handleBackupRenamed = async (payload: { success: boolean; message: string }) => {
|
||||
if (payload.success) {
|
||||
addNotification({ type: "success", text: payload.message });
|
||||
await props.server.refresh(["backups"]);
|
||||
} else {
|
||||
addNotification({ type: "error", text: payload.message });
|
||||
}
|
||||
};
|
||||
|
||||
const handleBackupRestored = async (payload: { success: boolean; message: string }) => {
|
||||
if (payload.success) {
|
||||
addNotification({ type: "success", text: payload.message });
|
||||
await props.server.refresh(["backups"]);
|
||||
} else {
|
||||
addNotification({ type: "error", text: payload.message });
|
||||
}
|
||||
};
|
||||
|
||||
const handleBackupDeleted = async (payload: { success: boolean; message: string }) => {
|
||||
if (payload.success) {
|
||||
addNotification({ type: "success", text: payload.message });
|
||||
await props.server.refresh(["backups"]);
|
||||
} else {
|
||||
addNotification({ type: "error", text: payload.message });
|
||||
}
|
||||
};
|
||||
|
||||
function triggerDownloadAnimation() {
|
||||
overTheTopDownloadAnimation.value = true;
|
||||
setTimeout(() => (overTheTopDownloadAnimation.value = false), 500);
|
||||
}
|
||||
|
||||
const initiateDownload = async (backupId: string) => {
|
||||
triggerDownloadAnimation();
|
||||
|
||||
const prepareDownload = async (backupId: string) => {
|
||||
try {
|
||||
const downloadurl: any = await props.server.backups?.download(backupId);
|
||||
if (!downloadurl || !downloadurl.download_url) {
|
||||
throw new Error("Invalid download URL.");
|
||||
}
|
||||
|
||||
let finalDownloadUrl: string = downloadurl.download_url;
|
||||
|
||||
if (!/^https?:\/\//i.test(finalDownloadUrl)) {
|
||||
finalDownloadUrl = `https://${finalDownloadUrl.startsWith("/") ? finalDownloadUrl.substring(1) : finalDownloadUrl}`;
|
||||
}
|
||||
|
||||
const a = document.createElement("a");
|
||||
a.href = finalDownloadUrl;
|
||||
a.setAttribute("download", "");
|
||||
a.click();
|
||||
a.remove();
|
||||
await props.server.backups?.prepare(backupId);
|
||||
} catch (error) {
|
||||
console.error("Download failed:", error);
|
||||
console.error("Failed to prepare download:", error);
|
||||
addNotification({ type: "error", title: "Failed to prepare backup for download", text: error });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -406,28 +256,37 @@ const unlockBackup = async (backupId: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
watchEffect(() => {
|
||||
const hasOngoingBackups = backups.value.some((backup) => backup.ongoing);
|
||||
|
||||
if (refreshInterval.value) {
|
||||
clearInterval(refreshInterval.value);
|
||||
refreshInterval.value = undefined;
|
||||
}
|
||||
|
||||
if (hasOngoingBackups) {
|
||||
refreshInterval.value = setInterval(async () => {
|
||||
await props.server.refresh(["backups"]);
|
||||
}, 10000);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (refreshInterval.value) {
|
||||
clearInterval(refreshInterval.value);
|
||||
const retryBackup = async (backupId: string) => {
|
||||
try {
|
||||
await props.server.backups?.retry(backupId);
|
||||
await props.server.refresh(["backups"]);
|
||||
} catch (error) {
|
||||
console.error("Failed to retry backup:", error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
async function deleteBackup(backup?: Backup) {
|
||||
if (!backup) {
|
||||
addNotification({
|
||||
type: "error",
|
||||
title: "Error deleting backup",
|
||||
text: "Backup is null",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await props.server.backups?.delete(backup.id);
|
||||
await props.server.refresh();
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
addNotification({
|
||||
type: "error",
|
||||
title: "Error deleting backup",
|
||||
text: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<UiServersServerSidebar :route="route" :nav-links="navLinks" :server="props.server" />
|
||||
<UiServersServerSidebar
|
||||
:route="route"
|
||||
:nav-links="navLinks"
|
||||
:server="server"
|
||||
:backup-in-progress="backupInProgress"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
InfoIcon,
|
||||
@@ -14,12 +18,14 @@ import {
|
||||
WrenchIcon,
|
||||
} from "@modrinth/assets";
|
||||
import type { Server } from "~/composables/pyroServers";
|
||||
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
||||
|
||||
const route = useRoute();
|
||||
const serverId = route.params.id as string;
|
||||
|
||||
const props = defineProps<{
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
backupInProgress?: BackupInProgressReason;
|
||||
}>();
|
||||
|
||||
useHead({
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
ref="versionSelectModal"
|
||||
:server="props.server"
|
||||
:current-loader="data?.loader as Loaders"
|
||||
:backup-in-progress="backupInProgress"
|
||||
@reinstall="emit('reinstall', $event)"
|
||||
/>
|
||||
|
||||
@@ -93,13 +94,23 @@
|
||||
</div>
|
||||
<div v-else class="flex w-full flex-col items-center gap-2 sm:w-fit sm:flex-row">
|
||||
<ButtonStyled>
|
||||
<nuxt-link class="!w-full sm:!w-auto" :to="`/modpacks?sid=${props.server.serverId}`">
|
||||
<nuxt-link
|
||||
v-tooltip="backupInProgress ? formatMessage(backupInProgress.tooltip) : undefined"
|
||||
:class="{ disabled: backupInProgress }"
|
||||
class="!w-full sm:!w-auto"
|
||||
:to="`/modpacks?sid=${props.server.serverId}`"
|
||||
>
|
||||
<CompassIcon class="size-4" /> Find a modpack
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
<span class="hidden sm:block">or</span>
|
||||
<ButtonStyled>
|
||||
<button class="!w-full sm:!w-auto" @click="mrpackModal.show()">
|
||||
<button
|
||||
v-tooltip="backupInProgress ? formatMessage(backupInProgress.tooltip) : undefined"
|
||||
:disabled="!!backupInProgress"
|
||||
class="!w-full sm:!w-auto"
|
||||
@click="mrpackModal.show()"
|
||||
>
|
||||
<UploadIcon class="size-4" /> Upload .mrpack file
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
@@ -143,9 +154,13 @@ import { ButtonStyled, NewProjectCard } from "@modrinth/ui";
|
||||
import { TransferIcon, UploadIcon, InfoIcon, CompassIcon, SettingsIcon } from "@modrinth/assets";
|
||||
import type { Server } from "~/composables/pyroServers";
|
||||
import type { Loaders } from "~/types/servers";
|
||||
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
||||
|
||||
const { formatMessage } = useVIntl();
|
||||
|
||||
const props = defineProps<{
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
backupInProgress?: BackupInProgressReason;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
Reference in New Issue
Block a user