feat: add moderation checklist back to project page (#5814)
* fix: billing page server plan heading * fix: matching server page spacing with instance page * feat: update server header buttons * feat: add show ram as bytes always on * fix: revert to large buttons * feat: add hostname and server states in info card * feat: add publishing checklist to project page * fix: markdown table style and max width * fix: teleport overflow menu bad anchoring
This commit is contained in:
@@ -157,6 +157,11 @@ provideModrinthClient(tauriApiClient)
|
|||||||
providePageContext({
|
providePageContext({
|
||||||
hierarchicalSidebarAvailable: ref(true),
|
hierarchicalSidebarAvailable: ref(true),
|
||||||
showAds: ref(false),
|
showAds: ref(false),
|
||||||
|
featureFlags: {
|
||||||
|
serverRamAsBytesAlwaysOn: computed(() =>
|
||||||
|
themeStore.getFeatureFlag('server_ram_as_bytes_always_on'),
|
||||||
|
),
|
||||||
|
},
|
||||||
openExternalUrl: (url) => openUrl(url),
|
openExternalUrl: (url) => openUrl(url),
|
||||||
})
|
})
|
||||||
provideModalBehavior({
|
provideModalBehavior({
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const DEFAULT_FEATURE_FLAGS = {
|
|||||||
worlds_tab: false,
|
worlds_tab: false,
|
||||||
worlds_in_home: true,
|
worlds_in_home: true,
|
||||||
server_project_qa: false,
|
server_project_qa: false,
|
||||||
|
server_ram_as_bytes_always_on: false,
|
||||||
i18n_debug: false,
|
i18n_debug: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,8 @@
|
|||||||
|
|
||||||
.normal-page__content {
|
.normal-page__content {
|
||||||
grid-area: content;
|
grid-area: content;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.normal-page__header {
|
.normal-page__header {
|
||||||
@@ -116,6 +118,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.normal-page__content {
|
.normal-page__content {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
max-width: calc(80rem - 18.75rem - 1.5rem);
|
max-width: calc(80rem - 18.75rem - 1.5rem);
|
||||||
//overflow-x: hidden;
|
//overflow-x: hidden;
|
||||||
}
|
}
|
||||||
@@ -164,6 +168,8 @@
|
|||||||
|
|
||||||
.normal-page__content {
|
.normal-page__content {
|
||||||
grid-area: content;
|
grid-area: content;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
max-width: calc(80rem - 18.75rem - 1.5rem);
|
max-width: calc(80rem - 18.75rem - 1.5rem);
|
||||||
//overflow-x: hidden;
|
//overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export const DEFAULT_FEATURE_FLAGS = validateValues({
|
|||||||
showProjectPageQuickServerButton: false,
|
showProjectPageQuickServerButton: false,
|
||||||
newProjectGeneralSettings: false,
|
newProjectGeneralSettings: false,
|
||||||
newProjectEnvironmentSettings: true,
|
newProjectEnvironmentSettings: true,
|
||||||
|
serverRamAsBytesAlwaysOn: false,
|
||||||
archonSentryCapture: false,
|
archonSentryCapture: false,
|
||||||
hideRussiaCensorshipBanner: false,
|
hideRussiaCensorshipBanner: false,
|
||||||
disablePrettyProjectUrlRedirects: false,
|
disablePrettyProjectUrlRedirects: false,
|
||||||
|
|||||||
@@ -439,6 +439,24 @@
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div class="normal-page__header relative my-4">
|
<div class="normal-page__header relative my-4">
|
||||||
|
<div class="mb-6">
|
||||||
|
<ModerationProjectNags
|
||||||
|
v-if="
|
||||||
|
projectV3 &&
|
||||||
|
((currentMember && project.status === 'draft') ||
|
||||||
|
tags.rejectedStatuses.includes(project.status))
|
||||||
|
"
|
||||||
|
:project="project"
|
||||||
|
:project-v3="projectV3"
|
||||||
|
:versions="versions ?? undefined"
|
||||||
|
:current-member="currentMember"
|
||||||
|
:collapsed="collapsedChecklist"
|
||||||
|
:route-name="route.name"
|
||||||
|
:tags="tags"
|
||||||
|
@toggle-collapsed="() => (collapsedChecklist = !collapsedChecklist)"
|
||||||
|
@set-processing="setProcessing"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<ProjectHeader
|
<ProjectHeader
|
||||||
v-if="projectV3Loaded"
|
v-if="projectV3Loaded"
|
||||||
:project="project"
|
:project="project"
|
||||||
@@ -1120,6 +1138,7 @@ import AutomaticAccordion from '~/components/ui/AutomaticAccordion.vue'
|
|||||||
import CollectionCreateModal from '~/components/ui/create/CollectionCreateModal.vue'
|
import CollectionCreateModal from '~/components/ui/create/CollectionCreateModal.vue'
|
||||||
import MessageBanner from '~/components/ui/MessageBanner.vue'
|
import MessageBanner from '~/components/ui/MessageBanner.vue'
|
||||||
import ModerationChecklist from '~/components/ui/moderation/checklist/ModerationChecklist.vue'
|
import ModerationChecklist from '~/components/ui/moderation/checklist/ModerationChecklist.vue'
|
||||||
|
import ModerationProjectNags from '~/components/ui/moderation/ModerationProjectNags.vue'
|
||||||
import ProjectMemberHeader from '~/components/ui/ProjectMemberHeader.vue'
|
import ProjectMemberHeader from '~/components/ui/ProjectMemberHeader.vue'
|
||||||
import { getSignInRouteObj } from '~/composables/auth.js'
|
import { getSignInRouteObj } from '~/composables/auth.js'
|
||||||
import { saveFeatureFlags } from '~/composables/featureFlags.ts'
|
import { saveFeatureFlags } from '~/composables/featureFlags.ts'
|
||||||
|
|||||||
@@ -280,7 +280,7 @@
|
|||||||
<div class="flex flex-col justify-between gap-4">
|
<div class="flex flex-col justify-between gap-4">
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<ModrinthServersIcon class="flex h-8 w-fit" />
|
<ModrinthServersIcon class="flex h-8 w-fit" />
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-6">
|
||||||
<ServerListing
|
<ServerListing
|
||||||
v-if="subscription.serverInfo"
|
v-if="subscription.serverInfo"
|
||||||
v-bind="subscription.serverInfo"
|
v-bind="subscription.serverInfo"
|
||||||
@@ -311,15 +311,16 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="m-0 mt-4 text-xl font-semibold leading-none text-contrast">
|
|
||||||
{{
|
|
||||||
formatMessage(messages.planTitle, {
|
|
||||||
size: getProductSize(getPyroProduct(subscription)),
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
</h3>
|
|
||||||
<div class="flex flex-row justify-between">
|
<div class="flex flex-row justify-between">
|
||||||
<div class="mt-2 flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
|
<h3 class="m-0 mb-1 text-xl font-semibold leading-none">
|
||||||
|
{{
|
||||||
|
formatMessage(messages.planTitle, {
|
||||||
|
size: getProductSize(getPyroProduct(subscription)),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</h3>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<CheckCircleIcon class="h-5 w-5 text-brand" />
|
<CheckCircleIcon class="h-5 w-5 text-brand" />
|
||||||
<span>
|
<span>
|
||||||
@@ -370,8 +371,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col items-end justify-between">
|
<div class="flex flex-col items-end justify-between">
|
||||||
<div class="flex flex-col items-end gap-2">
|
<div class="flex flex-col items-end gap-2">
|
||||||
<div class="flex text-2xl font-bold text-contrast">
|
<h3 class="m-0 flex text-lg font-semibold text-contrast">
|
||||||
<span class="text-contrast">
|
<span class="leading-none text-contrast">
|
||||||
{{
|
{{
|
||||||
getProductPrice(getPyroProduct(subscription), subscription.interval)
|
getProductPrice(getPyroProduct(subscription), subscription.interval)
|
||||||
? formatPrice(
|
? formatPrice(
|
||||||
@@ -383,14 +384,14 @@
|
|||||||
: ''
|
: ''
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span class="leading-none">
|
||||||
{{
|
{{
|
||||||
formatMessage(messages.slashInterval, {
|
formatMessage(messages.slashInterval, {
|
||||||
interval: getIntervalNounLabel(subscription.interval),
|
interval: getIntervalNounLabel(subscription.interval),
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</h3>
|
||||||
<div
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
getPyroCharge(subscription) &&
|
getPyroCharge(subscription) &&
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import { provideModalBehavior, providePageContext } from '@modrinth/ui'
|
import { provideModalBehavior, providePageContext } from '@modrinth/ui'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
import { useFeatureFlags } from '~/composables/featureFlags.ts'
|
||||||
|
|
||||||
export function setupPageContextProvider() {
|
export function setupPageContextProvider() {
|
||||||
const cosmetics = useCosmetics()
|
const cosmetics = useCosmetics()
|
||||||
|
const featureFlags = useFeatureFlags()
|
||||||
|
|
||||||
providePageContext({
|
providePageContext({
|
||||||
hierarchicalSidebarAvailable: ref(false),
|
hierarchicalSidebarAvailable: ref(false),
|
||||||
showAds: ref(false),
|
showAds: ref(false),
|
||||||
|
featureFlags: {
|
||||||
|
serverRamAsBytesAlwaysOn: computed(() => featureFlags.value.serverRamAsBytesAlwaysOn),
|
||||||
|
},
|
||||||
openExternalUrl: (url) => window.open(url, '_blank'),
|
openExternalUrl: (url) => window.open(url, '_blank'),
|
||||||
})
|
})
|
||||||
provideModalBehavior({
|
provideModalBehavior({
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ pub enum FeatureFlag {
|
|||||||
ProjectBackground,
|
ProjectBackground,
|
||||||
WorldsTab,
|
WorldsTab,
|
||||||
WorldsInHome,
|
WorldsInHome,
|
||||||
|
ServerRamAsBytesAlwaysOn,
|
||||||
ServersInApp,
|
ServersInApp,
|
||||||
ServerProjectQa,
|
ServerProjectQa,
|
||||||
I18nDebug,
|
I18nDebug,
|
||||||
|
|||||||
@@ -906,10 +906,15 @@ a:not(.no-click-animation),
|
|||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
border: 0.1rem solid var(--color-button-bg);
|
border: 0.1rem solid var(--color-button-bg);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-xl);
|
||||||
|
|
||||||
th {
|
th {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
background-color: var(--surface-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
tr {
|
||||||
|
background-color: var(--surface-1-5);
|
||||||
}
|
}
|
||||||
|
|
||||||
td,
|
td,
|
||||||
@@ -918,7 +923,7 @@ a:not(.no-click-animation),
|
|||||||
}
|
}
|
||||||
|
|
||||||
tr:nth-child(2n) {
|
tr:nth-child(2n) {
|
||||||
background-color: var(--color-accent-contrast);
|
background-color: var(--surface-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
td:not(:last-of-type),
|
td:not(:last-of-type),
|
||||||
|
|||||||
@@ -162,9 +162,8 @@ const calculateMenuPosition = () => {
|
|||||||
if (!triggerRef.value || !menuRef.value) return { top: '0px', left: '0px' }
|
if (!triggerRef.value || !menuRef.value) return { top: '0px', left: '0px' }
|
||||||
|
|
||||||
const triggerRect = triggerRef.value.getBoundingClientRect()
|
const triggerRect = triggerRef.value.getBoundingClientRect()
|
||||||
const menuRect = menuRef.value.getBoundingClientRect()
|
const menuWidth = menuRef.value.offsetWidth
|
||||||
const menuWidth = menuRect.width + 16
|
const menuHeight = menuRef.value.offsetHeight
|
||||||
const menuHeight = menuRect.height
|
|
||||||
const margin = 8
|
const margin = 8
|
||||||
|
|
||||||
let top: number
|
let top: number
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<ButtonStyled v-if="showStopButton" type="transparent" size="large">
|
<ButtonStyled v-if="showStopButton" type="standard" color="red" size="large">
|
||||||
<button
|
<button
|
||||||
v-tooltip="busyTooltip"
|
v-tooltip="busyTooltip"
|
||||||
:disabled="!canTakeAction"
|
:disabled="!canTakeAction"
|
||||||
@@ -21,9 +21,38 @@
|
|||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
|
|
||||||
<ButtonStyled type="standard" color="brand" size="large">
|
<div v-if="showRestartDropdown" class="joined-buttons">
|
||||||
|
<ButtonStyled type="standard" color="orange" size="large">
|
||||||
|
<button v-tooltip="busyTooltip" :disabled="!canTakeAction" @click="handlePrimaryAction">
|
||||||
|
<UpdatedIcon />
|
||||||
|
<span>{{ primaryActionText }}</span>
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled type="standard" color="orange" size="large">
|
||||||
|
<OverflowMenu
|
||||||
|
v-tooltip="busyTooltip"
|
||||||
|
:disabled="!canTakeAction"
|
||||||
|
:options="[
|
||||||
|
{
|
||||||
|
id: 'kill_server',
|
||||||
|
action: () => initiateAction('Kill'),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div class="w-0 text-xl relative top-0.5 right-2.5">
|
||||||
|
<DropdownIcon />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #kill_server>
|
||||||
|
<SlashIcon class="h-5 w-5" />
|
||||||
|
Kill server
|
||||||
|
</template>
|
||||||
|
</OverflowMenu>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
<ButtonStyled v-else type="standard" color="brand" size="large">
|
||||||
<button v-tooltip="busyTooltip" :disabled="!canTakeAction" @click="handlePrimaryAction">
|
<button v-tooltip="busyTooltip" :disabled="!canTakeAction" @click="handlePrimaryAction">
|
||||||
<component :is="isRunning || showStopButton ? UpdatedIcon : PlayIcon" />
|
<PlayIcon />
|
||||||
<span>{{ primaryActionText }}</span>
|
<span>{{ primaryActionText }}</span>
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
@@ -33,10 +62,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { LoaderCircleIcon, PlayIcon, StopCircleIcon, UpdatedIcon } from '@modrinth/assets'
|
import {
|
||||||
|
DropdownIcon,
|
||||||
|
LoaderCircleIcon,
|
||||||
|
PlayIcon,
|
||||||
|
SlashIcon,
|
||||||
|
StopCircleIcon,
|
||||||
|
UpdatedIcon,
|
||||||
|
} from '@modrinth/assets'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import { ButtonStyled } from '#ui/components'
|
import { ButtonStyled, OverflowMenu } from '#ui/components'
|
||||||
|
|
||||||
import { useServerPowerAction } from './use-server-power-action'
|
import { useServerPowerAction } from './use-server-power-action'
|
||||||
|
|
||||||
@@ -51,7 +87,6 @@ const props = withDefaults(
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
isInstalling,
|
isInstalling,
|
||||||
isRunning,
|
|
||||||
showStopButton,
|
showStopButton,
|
||||||
busyTooltip,
|
busyTooltip,
|
||||||
canTakeAction,
|
canTakeAction,
|
||||||
@@ -61,4 +96,32 @@ const {
|
|||||||
} = useServerPowerAction({
|
} = useServerPowerAction({
|
||||||
disabled: computed(() => props.disabled),
|
disabled: computed(() => props.disabled),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const showRestartDropdown = computed(() => primaryActionText.value === 'Restart')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.joined-buttons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.joined-buttons > :deep(.btn) {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.joined-buttons > :deep(.btn:first-child) {
|
||||||
|
border-top-left-radius: var(--radius-md);
|
||||||
|
border-bottom-left-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.joined-buttons > :deep(.btn:last-child) {
|
||||||
|
border-top-right-radius: var(--radius-md);
|
||||||
|
border-bottom-right-radius: var(--radius-md);
|
||||||
|
margin-left: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.joined-buttons > :deep(.btn:not(:last-child)) {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -3,10 +3,6 @@
|
|||||||
<ButtonStyled circular type="transparent" size="large">
|
<ButtonStyled circular type="transparent" size="large">
|
||||||
<TeleportOverflowMenu :options="menuOptions">
|
<TeleportOverflowMenu :options="menuOptions">
|
||||||
<MoreVerticalIcon aria-hidden="true" />
|
<MoreVerticalIcon aria-hidden="true" />
|
||||||
<template #kill>
|
|
||||||
<SlashIcon class="h-5 w-5" />
|
|
||||||
<span>Kill server</span>
|
|
||||||
</template>
|
|
||||||
<template #allServers>
|
<template #allServers>
|
||||||
<ServerIcon class="h-5 w-5" />
|
<ServerIcon class="h-5 w-5" />
|
||||||
<span>All servers</span>
|
<span>All servers</span>
|
||||||
@@ -21,7 +17,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ClipboardCopyIcon, MoreVerticalIcon, ServerIcon, SlashIcon } from '@modrinth/assets'
|
import { ClipboardCopyIcon, MoreVerticalIcon, ServerIcon } from '@modrinth/assets'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
@@ -29,8 +25,6 @@ import { ButtonStyled } from '#ui/components'
|
|||||||
import TeleportOverflowMenu from '#ui/components/base/TeleportOverflowMenu.vue'
|
import TeleportOverflowMenu from '#ui/components/base/TeleportOverflowMenu.vue'
|
||||||
import { injectModrinthServerContext } from '#ui/providers'
|
import { injectModrinthServerContext } from '#ui/providers'
|
||||||
|
|
||||||
import { useServerPowerAction } from './use-server-power-action'
|
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
@@ -49,21 +43,7 @@ const props = withDefaults(
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { serverId } = injectModrinthServerContext()
|
const { serverId } = injectModrinthServerContext()
|
||||||
|
|
||||||
const { isInstalling, initiateAction } = useServerPowerAction({
|
|
||||||
disabled: computed(() => props.disabled),
|
|
||||||
})
|
|
||||||
|
|
||||||
const menuOptions = computed(() => [
|
const menuOptions = computed(() => [
|
||||||
...(isInstalling.value
|
|
||||||
? []
|
|
||||||
: [
|
|
||||||
{
|
|
||||||
id: 'kill',
|
|
||||||
label: 'Kill server',
|
|
||||||
icon: SlashIcon,
|
|
||||||
action: () => initiateAction('Kill'),
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
{
|
{
|
||||||
id: 'allServers',
|
id: 'allServers',
|
||||||
label: 'All servers',
|
label: 'All servers',
|
||||||
|
|||||||
@@ -89,26 +89,36 @@
|
|||||||
</div>
|
</div>
|
||||||
<span>{{ prefConfig.description }}</span>
|
<span>{{ prefConfig.description }}</span>
|
||||||
</label>
|
</label>
|
||||||
<Toggle
|
<div v-tooltip="getPreferenceTooltip(key)">
|
||||||
:id="`pref-${key}`"
|
<Toggle
|
||||||
v-model="newUserPreferences[key]"
|
:id="`pref-${key}`"
|
||||||
class="flex-none"
|
:model-value="getPreferenceValue(key)"
|
||||||
:disabled="!prefConfig.implemented"
|
class="flex-none"
|
||||||
/>
|
:disabled="!prefConfig.implemented || isPreferenceForcedByFeatureFlag(key)"
|
||||||
|
@update:model-value="(value) => setPreferenceValue(key, !!value)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Info -->
|
<!-- Info -->
|
||||||
<div class="flex flex-col gap-2.5">
|
<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">Info</div>
|
||||||
<div class="flex flex-col gap-2.5 rounded-xl bg-surface-2 p-4">
|
<div class="flex flex-col gap-2.5 rounded-xl bg-surface-2 p-4">
|
||||||
<div
|
<div
|
||||||
v-for="property in infoProperties"
|
v-for="property in infoProperties"
|
||||||
:key="property.name"
|
:key="property.name"
|
||||||
class="flex items-center justify-between gap-4"
|
class="flex items-start justify-between gap-4"
|
||||||
>
|
>
|
||||||
<template v-if="property.value !== 'Unknown'">
|
<template v-if="property.value !== 'Unknown'">
|
||||||
<span>{{ property.name }}</span>
|
<span class="mt-1">{{ property.name }}</span>
|
||||||
<CopyCode :text="property.value" />
|
<CopyCode v-if="property.type === 'copy'" :text="property.value" />
|
||||||
|
<div
|
||||||
|
v-else-if="property.type === 'specs'"
|
||||||
|
class="flex flex-col items-end text-right text-sm leading-5 break-words"
|
||||||
|
>
|
||||||
|
<span v-for="line in property.lines" :key="line">{{ line }}</span>
|
||||||
|
</div>
|
||||||
|
<span v-else class="text-right text-sm break-words">{{ property.value }}</span>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -127,7 +137,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
import type { Labrinth } from '@modrinth/api-client'
|
||||||
|
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
||||||
@@ -138,11 +149,13 @@ import {
|
|||||||
injectModrinthClient,
|
injectModrinthClient,
|
||||||
injectModrinthServerContext,
|
injectModrinthServerContext,
|
||||||
injectNotificationManager,
|
injectNotificationManager,
|
||||||
|
injectPageContext,
|
||||||
} from '#ui/providers'
|
} from '#ui/providers'
|
||||||
|
|
||||||
const { addNotification } = injectNotificationManager()
|
const { addNotification } = injectNotificationManager()
|
||||||
const client = injectModrinthClient()
|
const client = injectModrinthClient()
|
||||||
const { server: data, serverId, busyReasons } = injectModrinthServerContext()
|
const { server: data, serverId, busyReasons } = injectModrinthServerContext()
|
||||||
|
const { featureFlags } = injectPageContext()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
const serverName = ref(data.value?.name)
|
const serverName = ref(data.value?.name)
|
||||||
@@ -207,11 +220,118 @@ const userPreferences = useStorage<UserPreferences>(
|
|||||||
|
|
||||||
const newUserPreferences = ref<UserPreferences>(JSON.parse(JSON.stringify(userPreferences.value)))
|
const newUserPreferences = ref<UserPreferences>(JSON.parse(JSON.stringify(userPreferences.value)))
|
||||||
|
|
||||||
|
const isRamAsBytesForcedByFeatureFlag = computed(
|
||||||
|
() => featureFlags?.serverRamAsBytesAlwaysOn?.value ?? false,
|
||||||
|
)
|
||||||
|
|
||||||
|
const isPreferenceForcedByFeatureFlag = (key: string) =>
|
||||||
|
key === 'ramAsNumber' && isRamAsBytesForcedByFeatureFlag.value
|
||||||
|
|
||||||
|
const getPreferenceTooltip = (key: string) =>
|
||||||
|
isPreferenceForcedByFeatureFlag(key)
|
||||||
|
? 'Feature flag enabled to always show RAM as bytes.'
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
const getPreferenceValue = (key: string) =>
|
||||||
|
isPreferenceForcedByFeatureFlag(key) ? true : newUserPreferences.value[key as PreferenceKeys]
|
||||||
|
|
||||||
|
const setPreferenceValue = (key: string, value: boolean) => {
|
||||||
|
if (isPreferenceForcedByFeatureFlag(key)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newUserPreferences.value[key as PreferenceKeys] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: subscriptions } = useQuery({
|
||||||
|
queryKey: ['billing', 'subscriptions'],
|
||||||
|
queryFn: () => client.labrinth.billing_internal.getSubscriptions(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data: products } = useQuery({
|
||||||
|
queryKey: ['billing', 'products'],
|
||||||
|
queryFn: () => client.labrinth.billing_internal.getProducts(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const serverSubscription = computed(() =>
|
||||||
|
subscriptions.value?.find(
|
||||||
|
(subscription) =>
|
||||||
|
subscription.metadata?.type === 'pyro' && subscription.metadata.id === serverId,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
const serverProduct = computed(() =>
|
||||||
|
products.value?.find((product) =>
|
||||||
|
product.prices.some((price) => price.id === serverSubscription.value?.price_id),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
const formatSpecNumber = (value: number) =>
|
||||||
|
Number.isInteger(value) ? String(value) : value.toFixed(1)
|
||||||
|
|
||||||
|
const getServerSpecs = (product?: Labrinth.Billing.Internal.Product | null) => {
|
||||||
|
const metadata = product?.metadata
|
||||||
|
if (!metadata || (metadata.type !== 'pyro' && metadata.type !== 'medal')) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const sharedCpus = formatSpecNumber(metadata.cpu / 2)
|
||||||
|
const burstCpus = formatSpecNumber(metadata.cpu)
|
||||||
|
const ramGb = formatSpecNumber(metadata.ram / 1024)
|
||||||
|
const swapGb = formatSpecNumber(metadata.swap / 1024)
|
||||||
|
const storageGb = formatSpecNumber(metadata.storage / 1024)
|
||||||
|
|
||||||
|
return {
|
||||||
|
sharedCpus,
|
||||||
|
burstCpus,
|
||||||
|
ramGb,
|
||||||
|
swapGb,
|
||||||
|
storageGb,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverHostname = computed(() =>
|
||||||
|
serverSubdomain.value ? `${serverSubdomain.value}.modrinth.gg` : 'Unknown',
|
||||||
|
)
|
||||||
|
|
||||||
|
const serverSpecs = computed(() => getServerSpecs(serverProduct.value))
|
||||||
|
|
||||||
|
type InfoProperty =
|
||||||
|
| {
|
||||||
|
name: string
|
||||||
|
value: string
|
||||||
|
type: 'copy'
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
name: string
|
||||||
|
value: string
|
||||||
|
type: 'text'
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
name: string
|
||||||
|
value: string
|
||||||
|
type: 'specs'
|
||||||
|
lines: string[]
|
||||||
|
}
|
||||||
|
|
||||||
// Info properties
|
// Info properties
|
||||||
const infoProperties = [
|
const infoProperties = computed<InfoProperty[]>(() => [
|
||||||
{ name: 'Server ID', value: serverId ?? 'Unknown' },
|
{ name: 'Server ID', value: serverId ?? 'Unknown', type: 'copy' },
|
||||||
{ name: 'Node', value: data.value?.node?.instance ?? 'Unknown' },
|
{ 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`,
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
// Unsaved changes tracking (API fields + preferences)
|
// Unsaved changes tracking (API fields + preferences)
|
||||||
const hasUnsavedChanges = computed(
|
const hasUnsavedChanges = computed(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div
|
<div
|
||||||
data-pyro-server-stats
|
data-pyro-server-stats
|
||||||
style="font-variant-numeric: tabular-nums"
|
style="font-variant-numeric: tabular-nums"
|
||||||
class="flex select-none flex-col items-center gap-4 md:flex-row"
|
class="flex select-none flex-col items-center gap-3 md:flex-row"
|
||||||
:class="{ 'pointer-events-none': loading }"
|
:class="{ 'pointer-events-none': loading }"
|
||||||
:aria-hidden="loading"
|
:aria-hidden="loading"
|
||||||
>
|
>
|
||||||
@@ -60,11 +60,12 @@ import { useStorage } from '@vueuse/core'
|
|||||||
import { computed, defineAsyncComponent, ref, shallowRef, watch } from 'vue'
|
import { computed, defineAsyncComponent, ref, shallowRef, watch } from 'vue'
|
||||||
import { RouterLink } from 'vue-router'
|
import { RouterLink } from 'vue-router'
|
||||||
|
|
||||||
import { injectModrinthServerContext } from '#ui/providers'
|
import { injectModrinthServerContext, injectPageContext } from '#ui/providers'
|
||||||
|
|
||||||
const VueApexCharts = defineAsyncComponent(() => import('vue3-apexcharts'))
|
const VueApexCharts = defineAsyncComponent(() => import('vue3-apexcharts'))
|
||||||
|
|
||||||
const { serverId } = injectModrinthServerContext()
|
const { serverId } = injectModrinthServerContext()
|
||||||
|
const { featureFlags } = injectPageContext()
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -83,6 +84,9 @@ const chartsReady = ref(new Set<number>())
|
|||||||
const userPreferences = useStorage(`pyro-server-${serverId || 'unknown'}-preferences`, {
|
const userPreferences = useStorage(`pyro-server-${serverId || 'unknown'}-preferences`, {
|
||||||
ramAsNumber: false,
|
ramAsNumber: false,
|
||||||
})
|
})
|
||||||
|
const isRamAsBytesForcedByFeatureFlag = computed(
|
||||||
|
() => featureFlags?.serverRamAsBytesAlwaysOn?.value ?? false,
|
||||||
|
)
|
||||||
|
|
||||||
const stats = shallowRef(
|
const stats = shallowRef(
|
||||||
props.data?.current || {
|
props.data?.current || {
|
||||||
@@ -214,7 +218,9 @@ const metrics = computed(() => {
|
|||||||
{
|
{
|
||||||
title: 'Memory',
|
title: 'Memory',
|
||||||
value:
|
value:
|
||||||
props.showMemoryAsBytes || userPreferences.value.ramAsNumber
|
props.showMemoryAsBytes ||
|
||||||
|
isRamAsBytesForcedByFeatureFlag.value ||
|
||||||
|
userPreferences.value.ramAsNumber
|
||||||
? formatBytes(stats.value.ram_usage_bytes ?? 0)
|
? formatBytes(stats.value.ram_usage_bytes ?? 0)
|
||||||
: `${ramPercent.value.toFixed(2)}%`,
|
: `${ramPercent.value.toFixed(2)}%`,
|
||||||
icon: DatabaseIcon,
|
icon: DatabaseIcon,
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative flex select-none flex-col gap-6" data-pyro-server-manager-root>
|
<div class="relative flex select-none flex-col gap-6" data-pyro-server-manager-root>
|
||||||
<div class="flex flex-col gap-6">
|
<div class="flex flex-col gap-4">
|
||||||
<ServerManageStats
|
<ServerManageStats
|
||||||
:data="!isWsAuthIncorrect ? stats : undefined"
|
:data="!isWsAuthIncorrect ? stats : undefined"
|
||||||
:loading="isWsAuthIncorrect"
|
:loading="isWsAuthIncorrect"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex min-h-[700px] flex-col gap-4">
|
<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">Console</span>
|
||||||
|
|
||||||
<ConsolePageLayout />
|
<ConsolePageLayout />
|
||||||
|
|||||||
@@ -107,7 +107,7 @@
|
|||||||
<div
|
<div
|
||||||
v-else-if="serverData"
|
v-else-if="serverData"
|
||||||
data-pyro-server-manager-root
|
data-pyro-server-manager-root
|
||||||
class="experimental-styles-within relative mx-auto pb-12 box-border flex min-h-[calc(100svh-100px)] w-full min-w-0 flex-col gap-6 px-6 transition-all duration-300"
|
class="experimental-styles-within relative mx-auto pb-12 box-border flex min-h-[calc(100svh-100px)] w-full min-w-0 flex-col gap-4 px-6 transition-all duration-300"
|
||||||
:style="{
|
:style="{
|
||||||
'--server-bg-image': serverImage
|
'--server-bg-image': serverImage
|
||||||
? `url(${serverImage})`
|
? `url(${serverImage})`
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ export interface PageContext {
|
|||||||
// pages may render sidebar content in #sidebar-teleport-target instead of in the main layout when true
|
// pages may render sidebar content in #sidebar-teleport-target instead of in the main layout when true
|
||||||
hierarchicalSidebarAvailable: Ref<boolean>
|
hierarchicalSidebarAvailable: Ref<boolean>
|
||||||
showAds: Ref<boolean>
|
showAds: Ref<boolean>
|
||||||
|
featureFlags?: {
|
||||||
|
serverRamAsBytesAlwaysOn?: Ref<boolean>
|
||||||
|
}
|
||||||
openExternalUrl: (url: string) => void
|
openExternalUrl: (url: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user