You've already forked AstralRinth
forked from didirus/AstralRinth
Modrinth Servers February Release: Bug Fix Round 1 (#3267)
* chore(pyroservers): attempt better error propogation Signed-off-by: Evan Song <theevansong@gmail.com> * chore(pyroservers): introduce deferred modules * fix(pyroservers): synchronize server icon processing Signed-off-by: Evan Song <theevansong@gmail.com> * refactor: server action buttons Signed-off-by: Evan Song <theevansong@gmail.com> * chore: bring back skeleton * fix(startup): populate values on refresh Signed-off-by: Evan Song <theevansong@gmail.com> * chore: properly refresh network Signed-off-by: Evan Song <theevansong@gmail.com> * fix: do not open backup settings modal if fetch failed * fix(platform): only clear selected loader version if selecting a different loader Signed-off-by: Evan Song <theevansong@gmail.com> * feat: parse links in console log * fix: attempt to mitigate power button state flash Signed-off-by: Evan Song <theevansong@gmail.com> * Revert "fix: attempt to mitigate power button state flash" This reverts commit 3ba5c0b4f7f5bacf1576aba5efe42785696a5aed. * refactor: error accumulation builder in PyroServersFetch Signed-off-by: Evan Song <theevansong@gmail.com> * fix: sentence case Signed-off-by: Evan Song <theevansong@gmail.com> * fix(files): await deferred fs Signed-off-by: Evan Song <theevansong@gmail.com> * fix: startup border Signed-off-by: Evan Song <theevansong@gmail.com> * fix: prevent suspended server errors from being overwritten Signed-off-by: Evan Song <theevansong@gmail.com> * fix: add server id copy button to suspended server listing Signed-off-by: Evan Song <theevansong@gmail.com> * fix: refresh behavior Signed-off-by: Evan Song <theevansong@gmail.com> * fix: behavior of server icon in options Signed-off-by: Evan Song <theevansong@gmail.com> * chore: clean Signed-off-by: Evan Song <theevansong@gmail.com> * chore: clean Signed-off-by: Evan Song <theevansong@gmail.com> * fix: prevent error inspector failures from destroying the page Signed-off-by: Evan Song <theevansong@gmail.com> * chore: clean Signed-off-by: Evan Song <theevansong@gmail.com> * chore: remove nexttick wrapper Signed-off-by: Evan Song <theevansong@gmail.com> * fix: ensure file edit gets initted due to deferred module Signed-off-by: Evan Song <theevansong@gmail.com> * refactor: prevent module errors from breaking the layout * chore: clean Signed-off-by: Evan Song <theevansong@gmail.com> --------- Signed-off-by: Evan Song <theevansong@gmail.com>
This commit is contained in:
@@ -106,6 +106,7 @@ const fetchSettings = async () => {
|
||||
initialSettings.value = settings as { interval: number; enabled: boolean };
|
||||
autoBackupEnabled.value = settings?.enabled ?? false;
|
||||
autoBackupInterval.value = settings?.interval || 6;
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error fetching backup settings:", error);
|
||||
addNotification({
|
||||
@@ -114,6 +115,7 @@ const fetchSettings = async () => {
|
||||
text: "Failed to load backup settings",
|
||||
type: "error",
|
||||
});
|
||||
return false;
|
||||
} finally {
|
||||
isLoadingSettings.value = false;
|
||||
}
|
||||
@@ -155,8 +157,10 @@ const saveSettings = async () => {
|
||||
|
||||
defineExpose({
|
||||
show: async () => {
|
||||
await fetchSettings();
|
||||
modal.value?.show();
|
||||
const success = await fetchSettings();
|
||||
if (success) {
|
||||
modal.value?.show();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
80
apps/frontend/src/components/ui/servers/OverviewLoading.vue
Normal file
80
apps/frontend/src/components/ui/servers/OverviewLoading.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style="font-variant-numeric: tabular-nums"
|
||||
class="pointer-events-none h-full w-full select-none"
|
||||
>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-row items-center gap-6">
|
||||
<div
|
||||
class="relative max-h-[156px] min-h-[156px] w-full overflow-hidden rounded-2xl bg-bg-raised p-8"
|
||||
>
|
||||
<div class="relative z-10 -ml-3 w-fit rounded-xl px-3 py-1">
|
||||
<div class="-mb-0.5 mt-0.5 flex flex-row items-center gap-2">
|
||||
<h2 class="m-0 -ml-0.5 text-3xl font-extrabold text-contrast">0.00%</h2>
|
||||
<h3 class="relative z-10 text-sm font-normal text-secondary">/ 100%</h3>
|
||||
</div>
|
||||
<h3 class="relative z-10 text-base font-normal text-secondary">CPU usage</h3>
|
||||
</div>
|
||||
<CPUIcon class="absolute right-10 top-10" />
|
||||
</div>
|
||||
<div
|
||||
class="relative max-h-[156px] min-h-[156px] w-full overflow-hidden rounded-2xl bg-bg-raised p-8"
|
||||
>
|
||||
<div class="relative z-10 -ml-3 w-fit rounded-xl px-3 py-1">
|
||||
<div class="-mb-0.5 mt-0.5 flex flex-row items-center gap-2">
|
||||
<h2 class="m-0 -ml-0.5 text-3xl font-extrabold text-contrast">0.00%</h2>
|
||||
<h3 class="relative z-10 text-sm font-normal text-secondary">/ 100%</h3>
|
||||
</div>
|
||||
<h3 class="relative z-10 text-base font-normal text-secondary">Memory usage</h3>
|
||||
</div>
|
||||
<DBIcon class="absolute right-10 top-10" />
|
||||
</div>
|
||||
<div
|
||||
class="relative isolate min-h-[156px] w-full overflow-hidden rounded-2xl bg-bg-raised p-8 transition-transform duration-100 hover:scale-105 active:scale-100"
|
||||
>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<h2 class="m-0 -ml-0.5 mt-1 text-3xl font-extrabold text-contrast">0 B</h2>
|
||||
</div>
|
||||
<h3 class="relative z-10 text-base font-normal text-secondary">Storage usage</h3>
|
||||
<FolderOpenIcon class="absolute right-10 top-10 size-8" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="relative flex h-full w-full flex-col gap-3 overflow-hidden rounded-2xl bg-bg-raised p-8"
|
||||
>
|
||||
<div class="experimental-styles-within flex flex-row items-center">
|
||||
<div class="flex flex-row items-center gap-4">
|
||||
<h2 class="m-0 text-3xl font-extrabold text-contrast">Console</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative w-full">
|
||||
<input type="text" placeholder="Search logs" class="h-12 !w-full !pl-10 !pr-48" />
|
||||
<SearchIcon class="absolute left-4 top-1/2 -translate-y-1/2" />
|
||||
</div>
|
||||
<div
|
||||
class="console relative h-full min-h-[516px] w-full overflow-hidden rounded-xl bg-bg text-sm"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CPUIcon, DBIcon, FolderOpenIcon, SearchIcon } from "@modrinth/assets";
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
html.light-mode .console {
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
html.dark-mode .console {
|
||||
background: black;
|
||||
}
|
||||
|
||||
html.oled-mode .console {
|
||||
background: black;
|
||||
}
|
||||
</style>
|
||||
@@ -1,23 +1,25 @@
|
||||
<template>
|
||||
<div class="contents">
|
||||
<NewModal ref="confirmActionModal" header="Confirming power action" @close="closePowerModal">
|
||||
<NewModal ref="confirmActionModal" header="Confirming power action" @close="resetPowerAction">
|
||||
<div class="flex flex-col gap-4 md:w-[400px]">
|
||||
<p class="m-0">Are you sure you want to {{ currentPendingAction }} the server?</p>
|
||||
|
||||
<p class="m-0">
|
||||
Are you sure you want to <span class="lowercase">{{ confirmActionText }}</span> the
|
||||
server?
|
||||
</p>
|
||||
<UiCheckbox
|
||||
v-model="powerDontAskAgainCheckbox"
|
||||
v-model="dontAskAgain"
|
||||
label="Don't ask me again"
|
||||
class="text-sm"
|
||||
:disabled="!currentPendingAction"
|
||||
:disabled="!powerAction"
|
||||
/>
|
||||
<div class="flex flex-row gap-4">
|
||||
<ButtonStyled type="standard" color="brand" @click="confirmAction">
|
||||
<ButtonStyled type="standard" color="brand" @click="executePowerAction">
|
||||
<button>
|
||||
<CheckIcon class="h-5 w-5" />
|
||||
{{ currentPendingActionFriendly }} server
|
||||
{{ confirmActionText }} server
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled @click="closePowerModal">
|
||||
<ButtonStyled @click="resetPowerAction">
|
||||
<button>
|
||||
<XIcon class="h-5 w-5" />
|
||||
Cancel
|
||||
@@ -29,7 +31,7 @@
|
||||
|
||||
<NewModal
|
||||
ref="detailsModal"
|
||||
:header="`All of ${props.serverName ? props.serverName : 'Server'} info`"
|
||||
:header="`All of ${serverName || 'Server'} info`"
|
||||
@close="closeDetailsModal"
|
||||
>
|
||||
<UiServersServerInfoLabels
|
||||
@@ -51,75 +53,74 @@
|
||||
<UiServersPanelSpinner class="size-5" /> Installing...
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<div v-else class="contents">
|
||||
|
||||
<template v-else>
|
||||
<ButtonStyled v-if="showStopButton" type="transparent">
|
||||
<button :disabled="!canTakeAction || disabled || isStopping" @click="stopServer">
|
||||
<button :disabled="!canTakeAction" @click="initiateAction('stop')">
|
||||
<div class="flex gap-1">
|
||||
<StopCircleIcon class="h-5 w-5" />
|
||||
<span>{{ stopButtonText }}</span>
|
||||
<span>{{ isStoppingState ? "Stopping..." : "Stop" }}</span>
|
||||
</div>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
<ButtonStyled type="standard" color="brand">
|
||||
<button :disabled="!canTakeAction || disabled || isStopping" @click="handleAction">
|
||||
<div v-if="isStartingOrRestarting" class="grid place-content-center">
|
||||
<button :disabled="!canTakeAction" @click="handlePrimaryAction">
|
||||
<div v-if="isTransitionState" class="grid place-content-center">
|
||||
<UiServersIconsLoadingIcon />
|
||||
</div>
|
||||
<div v-else class="contents">
|
||||
<component :is="showRestartIcon ? UpdatedIcon : PlayIcon" />
|
||||
</div>
|
||||
<span>
|
||||
{{ actionButtonText }}
|
||||
</span>
|
||||
<component :is="isRunning ? UpdatedIcon : PlayIcon" v-else />
|
||||
<span>{{ primaryActionText }}</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<!-- Dropdown options -->
|
||||
<ButtonStyled circular type="transparent">
|
||||
<UiServersTeleportOverflowMenu
|
||||
:options="[
|
||||
...(props.isInstalling ? [] : [{ id: 'kill', action: () => killServer() }]),
|
||||
{ id: 'allServers', action: () => router.push('/servers/manage') },
|
||||
{ id: 'details', action: () => showDetailsModal() },
|
||||
]"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #kill>
|
||||
<SlashIcon class="h-5 w-5" />
|
||||
<span>Kill server</span>
|
||||
</template>
|
||||
<template #allServers>
|
||||
<ServerIcon class="h-5 w-5" />
|
||||
<span>All servers</span>
|
||||
</template>
|
||||
<template #details>
|
||||
<InfoIcon class="h-5 w-5" />
|
||||
<span>Details</span>
|
||||
</template>
|
||||
</UiServersTeleportOverflowMenu>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<UiServersTeleportOverflowMenu :options="[...menuOptions]">
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #kill>
|
||||
<SlashIcon class="h-5 w-5" />
|
||||
<span>Kill server</span>
|
||||
</template>
|
||||
<template #allServers>
|
||||
<ServerIcon class="h-5 w-5" />
|
||||
<span>All servers</span>
|
||||
</template>
|
||||
<template #details>
|
||||
<InfoIcon class="h-5 w-5" />
|
||||
<span>Details</span>
|
||||
</template>
|
||||
</UiServersTeleportOverflowMenu>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { ref, computed } from "vue";
|
||||
import {
|
||||
PlayIcon,
|
||||
UpdatedIcon,
|
||||
StopCircleIcon,
|
||||
SlashIcon,
|
||||
MoreVerticalIcon,
|
||||
XIcon,
|
||||
CheckIcon,
|
||||
ServerIcon,
|
||||
InfoIcon,
|
||||
MoreVerticalIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useStorage } from "@vueuse/core";
|
||||
|
||||
type ServerAction = "start" | "stop" | "restart" | "kill";
|
||||
type ServerState = "stopped" | "starting" | "running" | "stopping" | "restarting";
|
||||
|
||||
interface PowerAction {
|
||||
action: ServerAction;
|
||||
nextState: ServerState;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
isOnline: boolean;
|
||||
isActioning: boolean;
|
||||
@@ -130,183 +131,142 @@ const props = defineProps<{
|
||||
uptimeSeconds: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "action", action: ServerAction): void;
|
||||
}>();
|
||||
|
||||
const router = useRouter();
|
||||
const serverId = router.currentRoute.value.params.id;
|
||||
const confirmActionModal = ref<InstanceType<typeof NewModal> | null>(null);
|
||||
const detailsModal = ref<InstanceType<typeof NewModal> | null>(null);
|
||||
|
||||
const userPreferences = useStorage(`pyro-server-${serverId}-preferences`, {
|
||||
powerDontAskAgain: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "action", action: "start" | "restart" | "stop" | "kill"): void;
|
||||
}>();
|
||||
const serverState = ref<ServerState>(props.isOnline ? "running" : "stopped");
|
||||
const powerAction = ref<PowerAction | null>(null);
|
||||
const dontAskAgain = ref(false);
|
||||
const startingDelay = ref(false);
|
||||
|
||||
const confirmActionModal = ref<InstanceType<typeof NewModal> | null>(null);
|
||||
const detailsModal = ref<InstanceType<typeof NewModal> | null>(null);
|
||||
|
||||
const ServerState = {
|
||||
Stopped: "Stopped",
|
||||
Starting: "Starting",
|
||||
Running: "Running",
|
||||
Stopping: "Stopping",
|
||||
Restarting: "Restarting",
|
||||
} as const;
|
||||
|
||||
type ServerStateType = (typeof ServerState)[keyof typeof ServerState];
|
||||
|
||||
const currentPendingAction = ref<string | null>(null);
|
||||
const currentPendingState = ref<ServerStateType | null>(null);
|
||||
const powerDontAskAgainCheckbox = ref(false);
|
||||
|
||||
const currentState = ref<ServerStateType>(
|
||||
props.isOnline ? ServerState.Running : ServerState.Stopped,
|
||||
);
|
||||
|
||||
const isStartingDelay = ref(false);
|
||||
const showStopButton = computed(
|
||||
() => currentState.value === ServerState.Running || currentState.value === ServerState.Stopping,
|
||||
);
|
||||
const showRestartIcon = computed(() => currentState.value === ServerState.Running);
|
||||
const canTakeAction = computed(
|
||||
() =>
|
||||
!props.isActioning &&
|
||||
!isStartingDelay.value &&
|
||||
currentState.value !== ServerState.Starting &&
|
||||
currentState.value !== ServerState.Stopping,
|
||||
() => !props.isActioning && !startingDelay.value && !isTransitionState.value,
|
||||
);
|
||||
|
||||
const isStartingOrRestarting = computed(
|
||||
() =>
|
||||
currentState.value === ServerState.Starting || currentState.value === ServerState.Restarting,
|
||||
const isRunning = computed(() => serverState.value === "running");
|
||||
const isTransitionState = computed(() =>
|
||||
["starting", "stopping", "restarting"].includes(serverState.value),
|
||||
);
|
||||
const isStoppingState = computed(() => serverState.value === "stopping");
|
||||
const showStopButton = computed(() => isRunning.value || isStoppingState.value);
|
||||
|
||||
const isStopping = computed(() => currentState.value === ServerState.Stopping);
|
||||
|
||||
const actionButtonText = computed(() => {
|
||||
switch (currentState.value) {
|
||||
case ServerState.Starting:
|
||||
return "Starting...";
|
||||
case ServerState.Restarting:
|
||||
return "Restarting...";
|
||||
case ServerState.Running:
|
||||
return "Restart";
|
||||
case ServerState.Stopping:
|
||||
return "Stopping...";
|
||||
default:
|
||||
return "Start";
|
||||
}
|
||||
const primaryActionText = computed(() => {
|
||||
const states: Record<ServerState, string> = {
|
||||
starting: "Starting...",
|
||||
restarting: "Restarting...",
|
||||
running: "Restart",
|
||||
stopping: "Stopping...",
|
||||
stopped: "Start",
|
||||
};
|
||||
return states[serverState.value];
|
||||
});
|
||||
|
||||
const currentPendingActionFriendly = computed(() => {
|
||||
switch (currentPendingAction.value) {
|
||||
case "start":
|
||||
return "Start";
|
||||
case "restart":
|
||||
return "Restart";
|
||||
case "stop":
|
||||
return "Stop";
|
||||
case "kill":
|
||||
return "Kill";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
const confirmActionText = computed(() => {
|
||||
if (!powerAction.value) return "";
|
||||
return powerAction.value.action.charAt(0).toUpperCase() + powerAction.value.action.slice(1);
|
||||
});
|
||||
|
||||
const stopButtonText = computed(() =>
|
||||
currentState.value === ServerState.Stopping ? "Stopping..." : "Stop",
|
||||
);
|
||||
const menuOptions = computed(() => [
|
||||
...(props.isInstalling
|
||||
? []
|
||||
: [
|
||||
{
|
||||
id: "kill",
|
||||
label: "Kill server",
|
||||
icon: SlashIcon,
|
||||
action: () => initiateAction("kill"),
|
||||
},
|
||||
]),
|
||||
{
|
||||
id: "allServers",
|
||||
label: "All servers",
|
||||
icon: ServerIcon,
|
||||
action: () => router.push("/servers/manage"),
|
||||
},
|
||||
{
|
||||
id: "details",
|
||||
label: "Details",
|
||||
icon: InfoIcon,
|
||||
action: () => detailsModal.value?.show(),
|
||||
},
|
||||
]);
|
||||
|
||||
const createPendingAction = () => {
|
||||
function initiateAction(action: ServerAction) {
|
||||
if (!canTakeAction.value) return;
|
||||
if (currentState.value === ServerState.Running) {
|
||||
currentPendingAction.value = "restart";
|
||||
currentPendingState.value = ServerState.Restarting;
|
||||
showPowerModal();
|
||||
} else {
|
||||
runAction("start", ServerState.Starting);
|
||||
|
||||
const stateMap: Record<ServerAction, ServerState> = {
|
||||
start: "starting",
|
||||
stop: "stopping",
|
||||
restart: "restarting",
|
||||
kill: "stopping",
|
||||
};
|
||||
|
||||
if (action === "start") {
|
||||
emit("action", action);
|
||||
serverState.value = stateMap[action];
|
||||
startingDelay.value = true;
|
||||
setTimeout(() => (startingDelay.value = false), 5000);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const handleAction = () => {
|
||||
createPendingAction();
|
||||
};
|
||||
powerAction.value = { action, nextState: stateMap[action] };
|
||||
|
||||
const showPowerModal = () => {
|
||||
if (userPreferences.value.powerDontAskAgain) {
|
||||
runAction(
|
||||
currentPendingAction.value as "start" | "restart" | "stop" | "kill",
|
||||
currentPendingState.value!,
|
||||
);
|
||||
executePowerAction();
|
||||
} else {
|
||||
confirmActionModal.value?.show();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const confirmAction = () => {
|
||||
if (powerDontAskAgainCheckbox.value) {
|
||||
function handlePrimaryAction() {
|
||||
initiateAction(isRunning.value ? "restart" : "start");
|
||||
}
|
||||
|
||||
function executePowerAction() {
|
||||
if (!powerAction.value) return;
|
||||
|
||||
const { action, nextState } = powerAction.value;
|
||||
emit("action", action);
|
||||
serverState.value = nextState;
|
||||
|
||||
if (dontAskAgain.value) {
|
||||
userPreferences.value.powerDontAskAgain = true;
|
||||
}
|
||||
runAction(
|
||||
currentPendingAction.value as "start" | "restart" | "stop" | "kill",
|
||||
currentPendingState.value!,
|
||||
);
|
||||
closePowerModal();
|
||||
};
|
||||
|
||||
const runAction = (action: "start" | "restart" | "stop" | "kill", serverState: ServerStateType) => {
|
||||
emit("action", action);
|
||||
currentState.value = serverState;
|
||||
|
||||
if (action === "start") {
|
||||
isStartingDelay.value = true;
|
||||
setTimeout(() => {
|
||||
isStartingDelay.value = false;
|
||||
}, 5000);
|
||||
startingDelay.value = true;
|
||||
setTimeout(() => (startingDelay.value = false), 5000);
|
||||
}
|
||||
};
|
||||
|
||||
const stopServer = () => {
|
||||
if (!canTakeAction.value) return;
|
||||
currentPendingAction.value = "stop";
|
||||
currentPendingState.value = ServerState.Stopping;
|
||||
showPowerModal();
|
||||
};
|
||||
resetPowerAction();
|
||||
}
|
||||
|
||||
const killServer = () => {
|
||||
currentPendingAction.value = "kill";
|
||||
currentPendingState.value = ServerState.Stopping;
|
||||
showPowerModal();
|
||||
};
|
||||
|
||||
const closePowerModal = () => {
|
||||
function resetPowerAction() {
|
||||
confirmActionModal.value?.hide();
|
||||
currentPendingAction.value = null;
|
||||
powerDontAskAgainCheckbox.value = false;
|
||||
};
|
||||
powerAction.value = null;
|
||||
dontAskAgain.value = false;
|
||||
}
|
||||
|
||||
const closeDetailsModal = () => {
|
||||
function closeDetailsModal() {
|
||||
detailsModal.value?.hide();
|
||||
};
|
||||
|
||||
const showDetailsModal = () => {
|
||||
detailsModal.value?.show();
|
||||
};
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.isOnline,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
currentState.value = ServerState.Running;
|
||||
} else {
|
||||
currentState.value = ServerState.Stopped;
|
||||
}
|
||||
},
|
||||
(online) => (serverState.value = online ? "running" : "stopped"),
|
||||
);
|
||||
|
||||
watch(
|
||||
() => router.currentRoute.value.fullPath,
|
||||
() => {
|
||||
closeDetailsModal();
|
||||
},
|
||||
() => closeDetailsModal(),
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -1,66 +1,72 @@
|
||||
<template>
|
||||
<div
|
||||
:aria-label="`Server is ${getStatusText}`"
|
||||
:aria-label="`Server is ${getStatusText(state)}`"
|
||||
class="relative inline-flex select-none items-center"
|
||||
@mouseenter="isExpanded = true"
|
||||
@mouseleave="isExpanded = false"
|
||||
>
|
||||
<div
|
||||
:class="`h-4 w-4 rounded-full transition-all duration-300 ease-in-out ${getStatusClass.main}`"
|
||||
:class="[
|
||||
'h-4 w-4 rounded-full transition-all duration-300 ease-in-out',
|
||||
getStatusClass(state).main,
|
||||
]"
|
||||
>
|
||||
<div
|
||||
:class="`absolute inline-flex h-4 w-4 animate-ping rounded-full ${getStatusClass.bg}`"
|
||||
:class="[
|
||||
'absolute inline-flex h-4 w-4 animate-ping rounded-full',
|
||||
getStatusClass(state).bg,
|
||||
]"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
:class="`absolute -left-4 flex w-auto items-center gap-2 rounded-full px-2 py-1 transition-all duration-150 ease-in-out ${getStatusClass.bg} ${
|
||||
isExpanded ? 'translate-x-2 scale-100 opacity-100' : 'translate-x-0 scale-90 opacity-0'
|
||||
}`"
|
||||
:class="[
|
||||
'absolute -left-4 flex w-auto items-center gap-2 rounded-full px-2 py-1 transition-all duration-150 ease-in-out',
|
||||
getStatusClass(state).bg,
|
||||
isExpanded ? 'translate-x-2 scale-100 opacity-100' : 'translate-x-0 scale-90 opacity-0',
|
||||
]"
|
||||
>
|
||||
<div class="h-3 w-3 rounded-full"></div>
|
||||
<span
|
||||
class="origin-left whitespace-nowrap text-sm font-semibold text-contrast transition-all duration-[200ms] ease-in-out"
|
||||
:class="`${isExpanded ? 'translate-x-0 scale-100' : '-translate-x-1 scale-x-75'}`"
|
||||
:class="[
|
||||
'origin-left whitespace-nowrap text-sm font-semibold text-contrast transition-all duration-[200ms] ease-in-out',
|
||||
isExpanded ? 'translate-x-0 scale-100' : '-translate-x-1 scale-x-75',
|
||||
]"
|
||||
>
|
||||
{{ getStatusText }}
|
||||
{{ getStatusText(state) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { ref } from "vue";
|
||||
import type { ServerState } from "~/types/servers";
|
||||
|
||||
const props = defineProps<{
|
||||
const STATUS_CLASSES = {
|
||||
running: { main: "bg-brand", bg: "bg-bg-green" },
|
||||
stopped: { main: "", bg: "" },
|
||||
crashed: { main: "bg-brand-red", bg: "bg-bg-red" },
|
||||
unknown: { main: "", bg: "" },
|
||||
} as const;
|
||||
|
||||
const STATUS_TEXTS = {
|
||||
running: "Running",
|
||||
stopped: "",
|
||||
crashed: "Crashed",
|
||||
unknown: "Unknown",
|
||||
} as const;
|
||||
|
||||
defineProps<{
|
||||
state: ServerState;
|
||||
}>();
|
||||
|
||||
const isExpanded = ref(false);
|
||||
|
||||
const getStatusClass = computed(() => {
|
||||
switch (props.state) {
|
||||
case "running":
|
||||
return { main: "bg-brand", bg: "bg-bg-green" };
|
||||
case "stopped":
|
||||
return { main: "", bg: "" };
|
||||
case "crashed":
|
||||
return { main: "bg-brand-red", bg: "bg-bg-red" };
|
||||
default:
|
||||
return { main: "", bg: "" };
|
||||
}
|
||||
});
|
||||
function getStatusClass(state: ServerState) {
|
||||
return STATUS_CLASSES[state] ?? STATUS_CLASSES.unknown;
|
||||
}
|
||||
|
||||
const getStatusText = computed(() => {
|
||||
switch (props.state) {
|
||||
case "running":
|
||||
return "Running";
|
||||
case "stopped":
|
||||
return "";
|
||||
case "crashed":
|
||||
return "Crashed";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
});
|
||||
function getStatusText(state: ServerState) {
|
||||
return STATUS_TEXTS[state] ?? STATUS_TEXTS.unknown;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -260,7 +260,25 @@
|
||||
</div>
|
||||
<NewModal ref="viewLogModal" class="z-[9999]" header="Viewing selected logs">
|
||||
<div class="text-contrast">
|
||||
<pre class="select-text overflow-x-auto whitespace-pre font-mono">{{ selectedLog }}</pre>
|
||||
<pre
|
||||
class="select-text overflow-x-auto whitespace-pre rounded-lg bg-bg font-mono"
|
||||
v-html="processedLogWithLinks"
|
||||
></pre>
|
||||
<div v-if="detectedLinks.length" class="border-contrast/20 mt-4 border-t pt-4">
|
||||
<h2>Detected Links</h2>
|
||||
<ul class="flex flex-col gap-2">
|
||||
<li v-for="(link, index) in detectedLinks" :key="index">
|
||||
<a
|
||||
:href="link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-sm text-blue hover:underline"
|
||||
>
|
||||
{{ link }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</NewModal>
|
||||
</div>
|
||||
@@ -272,6 +290,7 @@ import { ref, computed, onMounted, onUnmounted, watch, nextTick } from "vue";
|
||||
import { useDebounceFn } from "@vueuse/core";
|
||||
import { NewModal } from "@modrinth/ui";
|
||||
import ButtonStyled from "@modrinth/ui/src/components/base/ButtonStyled.vue";
|
||||
import DOMPurify from "dompurify";
|
||||
import { usePyroConsole } from "~/store/console.ts";
|
||||
|
||||
const { $cosmetics } = useNuxtApp();
|
||||
@@ -984,6 +1003,38 @@ const jumpToLine = (line: string, event?: MouseEvent) => {
|
||||
});
|
||||
};
|
||||
|
||||
const sanitizeUrl = (url: string): string => {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
if (!["http:", "https:"].includes(parsed.protocol)) {
|
||||
return "#";
|
||||
}
|
||||
return parsed.toString();
|
||||
} catch {
|
||||
return "#";
|
||||
}
|
||||
};
|
||||
|
||||
const detectedLinks = computed(() => {
|
||||
const urlRegex = /(https?:\/\/[^\s,<]+(?=[,\s<]|$))/g;
|
||||
const matches = [...selectedLog.value.matchAll(urlRegex)].map((match) => match[0]);
|
||||
return matches.filter((url) => sanitizeUrl(url) !== "#");
|
||||
});
|
||||
|
||||
const processedLogWithLinks = computed(() => {
|
||||
const urlRegex = /(https?:\/\/[^\s,<]+(?=[,\s<]|$))/g;
|
||||
const sanitizedLog = DOMPurify.sanitize(selectedLog.value, {
|
||||
ALLOWED_TAGS: [],
|
||||
ALLOWED_ATTR: [],
|
||||
});
|
||||
|
||||
return sanitizedLog.replace(urlRegex, (url) => {
|
||||
const safeUrl = sanitizeUrl(url);
|
||||
if (safeUrl === "#") return url;
|
||||
return `<a href="${safeUrl}" target="_blank" rel="noopener noreferrer nofollow" class="text-blue hover:underline">${url}</a>`;
|
||||
});
|
||||
});
|
||||
|
||||
watch(
|
||||
() => pyroConsole.filteredOutput.value,
|
||||
() => {
|
||||
|
||||
@@ -337,7 +337,7 @@ watch(
|
||||
selectedLoaderVersions,
|
||||
(newVersions) => {
|
||||
if (newVersions.length > 0 && !selectedLoaderVersion.value) {
|
||||
selectedLoaderVersion.value = String(newVersions[0]); // Ensure string type
|
||||
selectedLoaderVersion.value = String(newVersions[0]);
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
@@ -516,8 +516,6 @@ const handleReinstall = async () => {
|
||||
|
||||
const onShow = () => {
|
||||
selectedMCVersion.value = props.server.general?.mc_version || "";
|
||||
selectedLoaderVersion.value = "";
|
||||
hardReset.value = false;
|
||||
};
|
||||
|
||||
const onHide = () => {
|
||||
@@ -528,13 +526,15 @@ const onHide = () => {
|
||||
loadingServerCheck.value = false;
|
||||
isLoading.value = false;
|
||||
selectedMCVersion.value = "";
|
||||
selectedLoaderVersion.value = "";
|
||||
serverCheckError.value = "";
|
||||
paperVersions.value = {};
|
||||
purpurVersions.value = {};
|
||||
};
|
||||
|
||||
const show = (loader: Loaders) => {
|
||||
if (selectedLoader.value !== loader) {
|
||||
selectedLoaderVersion.value = "";
|
||||
}
|
||||
selectedLoader.value = loader;
|
||||
selectedMCVersion.value = props.server.general?.mc_version || "";
|
||||
versionSelectModal.value?.show();
|
||||
|
||||
@@ -69,11 +69,13 @@
|
||||
</div>
|
||||
<div
|
||||
v-else-if="status === 'suspended' && suspension_reason !== 'upgrading'"
|
||||
class="relative -mt-4 flex w-full flex-row items-center gap-2 rounded-b-3xl bg-bg-red p-4 text-sm font-bold text-contrast"
|
||||
class="relative -mt-4 flex w-full flex-col gap-2 rounded-b-3xl bg-bg-red p-4 text-sm font-bold text-contrast"
|
||||
>
|
||||
<UiServersIconsPanelErrorIcon class="!size-5" />
|
||||
Your server has been suspended due to a billing issue. Please visit your billing settings or
|
||||
contact Modrinth Support for more information.
|
||||
<div class="flex flex-row gap-2">
|
||||
<UiServersIconsPanelErrorIcon class="!size-5" /> Your server has been suspended. Please
|
||||
update your billing information or contact Modrinth Support for more information.
|
||||
</div>
|
||||
<UiCopyCode :text="`${props.server_id}`" class="ml-auto" />
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div
|
||||
v-if="uptimeSeconds || uptimeSeconds !== 0"
|
||||
v-tooltip="`Online for ${verboseUptime}`"
|
||||
class="flex min-w-0 flex-row items-center gap-4"
|
||||
class="server-action-buttons-anim flex min-w-0 flex-row items-center gap-4"
|
||||
data-pyro-uptime
|
||||
>
|
||||
<div v-if="!noSeparator" class="experimental-styles-within h-6 w-0.5 bg-button-border"></div>
|
||||
|
||||
Reference in New Issue
Block a user