mirror of
https://github.com/modrinth/code.git
synced 2026-09-02 04:56:52 +00:00
fix: header power state
This commit is contained in:
@@ -155,56 +155,138 @@
|
|||||||
|
|
||||||
<div v-if="props.actions.length" class="flex flex-wrap gap-2 items-center">
|
<div v-if="props.actions.length" class="flex flex-wrap gap-2 items-center">
|
||||||
<template v-for="action in props.actions" :key="action.id">
|
<template v-for="action in props.actions" :key="action.id">
|
||||||
<JoinedButtons
|
<Tooltip
|
||||||
v-if="action.joinedActions?.length"
|
v-if="action.prompt"
|
||||||
:actions="action.joinedActions"
|
theme="dismissable-prompt"
|
||||||
:color="joinedActionColor(action.color)"
|
:triggers="[]"
|
||||||
:size="action.size ?? 'large'"
|
:shown="action.prompt.shown"
|
||||||
:disabled="action.disabled"
|
:auto-hide="false"
|
||||||
:primary-disabled="action.primaryDisabled"
|
:placement="action.prompt.placement ?? 'bottom'"
|
||||||
:dropdown-disabled="action.dropdownDisabled"
|
|
||||||
:primary-muted="action.primaryMuted"
|
|
||||||
/>
|
|
||||||
<ButtonStyled
|
|
||||||
v-else
|
|
||||||
:color="action.color ?? 'standard'"
|
|
||||||
:size="action.size ?? 'large'"
|
|
||||||
:type="action.type ?? 'standard'"
|
|
||||||
:circular="action.circular ?? action.labelHidden ?? false"
|
|
||||||
>
|
>
|
||||||
<AutoLink
|
<JoinedButtons
|
||||||
v-if="action.to"
|
v-if="action.joinedActions?.length"
|
||||||
v-tooltip="action.tooltip"
|
:actions="action.joinedActions"
|
||||||
:to="action.to"
|
:color="joinedActionColor(action.color)"
|
||||||
:aria-label="actionLabel(action)"
|
:size="action.size ?? 'large'"
|
||||||
>
|
|
||||||
<component
|
|
||||||
:is="action.icon"
|
|
||||||
v-if="action.icon"
|
|
||||||
:class="action.iconClass"
|
|
||||||
aria-hidden="true"
|
|
||||||
v-bind="action.iconProps"
|
|
||||||
/>
|
|
||||||
<span v-if="!action.labelHidden && !action.circular">{{ action.label }}</span>
|
|
||||||
</AutoLink>
|
|
||||||
<button
|
|
||||||
v-else
|
|
||||||
v-tooltip="action.tooltip"
|
|
||||||
type="button"
|
|
||||||
:disabled="action.disabled"
|
:disabled="action.disabled"
|
||||||
:aria-label="actionLabel(action)"
|
:primary-disabled="action.primaryDisabled"
|
||||||
@click="action.onClick"
|
:dropdown-disabled="action.dropdownDisabled"
|
||||||
|
:primary-muted="action.primaryMuted"
|
||||||
|
/>
|
||||||
|
<ButtonStyled
|
||||||
|
v-else
|
||||||
|
:color="action.color ?? 'standard'"
|
||||||
|
:size="action.size ?? 'large'"
|
||||||
|
:type="action.type ?? 'standard'"
|
||||||
|
:circular="action.circular ?? action.labelHidden ?? false"
|
||||||
>
|
>
|
||||||
<component
|
<AutoLink
|
||||||
:is="action.icon"
|
v-if="action.to"
|
||||||
v-if="action.icon"
|
v-tooltip="action.tooltip"
|
||||||
:class="action.iconClass"
|
:to="action.to"
|
||||||
aria-hidden="true"
|
:aria-label="actionLabel(action)"
|
||||||
v-bind="action.iconProps"
|
@click="dismissActionPrompt(action)"
|
||||||
/>
|
>
|
||||||
<span v-if="!action.labelHidden && !action.circular">{{ action.label }}</span>
|
<component
|
||||||
</button>
|
:is="action.icon"
|
||||||
</ButtonStyled>
|
v-if="action.icon"
|
||||||
|
:class="action.iconClass"
|
||||||
|
aria-hidden="true"
|
||||||
|
v-bind="action.iconProps"
|
||||||
|
/>
|
||||||
|
<span v-if="!action.labelHidden && !action.circular">{{ action.label }}</span>
|
||||||
|
</AutoLink>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
v-tooltip="action.tooltip"
|
||||||
|
type="button"
|
||||||
|
:disabled="action.disabled"
|
||||||
|
:aria-label="actionLabel(action)"
|
||||||
|
@click="handleActionClick(action)"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="action.icon"
|
||||||
|
v-if="action.icon"
|
||||||
|
:class="action.iconClass"
|
||||||
|
aria-hidden="true"
|
||||||
|
v-bind="action.iconProps"
|
||||||
|
/>
|
||||||
|
<span v-if="!action.labelHidden && !action.circular">{{ action.label }}</span>
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<template #popper>
|
||||||
|
<div class="grid grid-cols-[min-content] gap-1">
|
||||||
|
<div class="flex min-w-48 items-center justify-between gap-8">
|
||||||
|
<h3 class="m-0 whitespace-nowrap text-base font-bold text-contrast">
|
||||||
|
{{ action.prompt.title }}
|
||||||
|
</h3>
|
||||||
|
<ButtonStyled size="small" circular>
|
||||||
|
<button
|
||||||
|
v-tooltip="action.prompt.dismissLabel"
|
||||||
|
@click="action.prompt.onDismiss?.()"
|
||||||
|
>
|
||||||
|
<XIcon aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
<p class="m-0 text-wrap text-sm font-medium leading-tight text-secondary">
|
||||||
|
{{ action.prompt.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Tooltip>
|
||||||
|
<template v-else>
|
||||||
|
<JoinedButtons
|
||||||
|
v-if="action.joinedActions?.length"
|
||||||
|
:actions="action.joinedActions"
|
||||||
|
:color="joinedActionColor(action.color)"
|
||||||
|
:size="action.size ?? 'large'"
|
||||||
|
:disabled="action.disabled"
|
||||||
|
:primary-disabled="action.primaryDisabled"
|
||||||
|
:dropdown-disabled="action.dropdownDisabled"
|
||||||
|
:primary-muted="action.primaryMuted"
|
||||||
|
/>
|
||||||
|
<ButtonStyled
|
||||||
|
v-else
|
||||||
|
:color="action.color ?? 'standard'"
|
||||||
|
:size="action.size ?? 'large'"
|
||||||
|
:type="action.type ?? 'standard'"
|
||||||
|
:circular="action.circular ?? action.labelHidden ?? false"
|
||||||
|
>
|
||||||
|
<AutoLink
|
||||||
|
v-if="action.to"
|
||||||
|
v-tooltip="action.tooltip"
|
||||||
|
:to="action.to"
|
||||||
|
:aria-label="actionLabel(action)"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="action.icon"
|
||||||
|
v-if="action.icon"
|
||||||
|
:class="action.iconClass"
|
||||||
|
aria-hidden="true"
|
||||||
|
v-bind="action.iconProps"
|
||||||
|
/>
|
||||||
|
<span v-if="!action.labelHidden && !action.circular">{{ action.label }}</span>
|
||||||
|
</AutoLink>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
v-tooltip="action.tooltip"
|
||||||
|
type="button"
|
||||||
|
:disabled="action.disabled"
|
||||||
|
:aria-label="actionLabel(action)"
|
||||||
|
@click="handleActionClick(action)"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="action.icon"
|
||||||
|
v-if="action.icon"
|
||||||
|
:class="action.iconClass"
|
||||||
|
aria-hidden="true"
|
||||||
|
v-bind="action.iconProps"
|
||||||
|
/>
|
||||||
|
<span v-if="!action.labelHidden && !action.circular">{{ action.label }}</span>
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -270,6 +352,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { XIcon } from '@modrinth/assets'
|
||||||
|
import { Tooltip } from 'floating-vue'
|
||||||
import type { Component } from 'vue'
|
import type { Component } from 'vue'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import type { RouteLocationRaw } from 'vue-router'
|
import type { RouteLocationRaw } from 'vue-router'
|
||||||
@@ -337,6 +421,15 @@ type PageHeaderMetadataItem = {
|
|||||||
class?: string
|
class?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PageHeaderActionPrompt = {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
dismissLabel?: string
|
||||||
|
shown?: boolean
|
||||||
|
placement?: string
|
||||||
|
onDismiss?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
type PageHeaderAction = {
|
type PageHeaderAction = {
|
||||||
id: string
|
id: string
|
||||||
label: string
|
label: string
|
||||||
@@ -357,6 +450,7 @@ type PageHeaderAction = {
|
|||||||
primaryDisabled?: boolean
|
primaryDisabled?: boolean
|
||||||
dropdownDisabled?: boolean
|
dropdownDisabled?: boolean
|
||||||
primaryMuted?: boolean
|
primaryMuted?: boolean
|
||||||
|
prompt?: PageHeaderActionPrompt
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
@@ -416,6 +510,17 @@ function actionLabel(action: PageHeaderAction) {
|
|||||||
return action.ariaLabel ?? action.tooltip ?? action.label
|
return action.ariaLabel ?? action.tooltip ?? action.label
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function dismissActionPrompt(action: PageHeaderAction) {
|
||||||
|
if (action.prompt?.shown) {
|
||||||
|
action.prompt.onDismiss?.()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleActionClick(action: PageHeaderAction) {
|
||||||
|
dismissActionPrompt(action)
|
||||||
|
void action.onClick?.()
|
||||||
|
}
|
||||||
|
|
||||||
function joinedActionColor(color?: ButtonColor) {
|
function joinedActionColor(color?: ButtonColor) {
|
||||||
return color === 'medal-promo' ? 'standard' : color
|
return color === 'medal-promo' ? 'standard' : color
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,77 +1,39 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full flex flex-col gap-4" :class="{ 'mt-4': isNuxt }">
|
<div class="w-full flex flex-col gap-4" :class="{ 'mt-4': isNuxt }">
|
||||||
<ContentPageHeader :class="props.headerClass">
|
<PageHeader
|
||||||
<template #icon>
|
:header="props.server?.name || 'Server'"
|
||||||
<ServerIcon
|
:leading="leadingItems"
|
||||||
:image="headerImage"
|
:metadata="metadataItems"
|
||||||
:class="isNuxt ? 'size-15 !rounded-2xl' : 'size-15 !rounded-xl'"
|
:actions="headerActions"
|
||||||
/>
|
:header-class="props.headerClass"
|
||||||
</template>
|
/>
|
||||||
<template #title>
|
|
||||||
{{ props.server?.name || 'Server' }}
|
|
||||||
</template>
|
|
||||||
<template #stats>
|
|
||||||
<div
|
|
||||||
v-if="props.server?.flows?.intro"
|
|
||||||
class="flex items-center gap-2 font-semibold text-secondary"
|
|
||||||
>
|
|
||||||
<SettingsIcon />
|
|
||||||
Configuring server...
|
|
||||||
</div>
|
|
||||||
<div v-else class="flex min-w-0 flex-wrap items-center gap-2">
|
|
||||||
<template v-for="(item, index) in headerStats" :key="item.id">
|
|
||||||
<div v-if="index > 0" class="h-1.5 w-1.5 rounded-full bg-surface-5" />
|
|
||||||
<button
|
|
||||||
v-if="item.copyable"
|
|
||||||
v-tooltip="'Copy server address'"
|
|
||||||
class="m-0 flex min-w-0 cursor-pointer items-center gap-2 border-0 bg-transparent p-0 font-medium text-secondary hover:underline text-nowrap"
|
|
||||||
type="button"
|
|
||||||
@click="copyServerAddress"
|
|
||||||
>
|
|
||||||
<component :is="item.icon" class="flex size-5 shrink-0" />
|
|
||||||
<span class="truncate">{{ item.label }}</span>
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="flex min-w-0 items-center gap-2 font-medium text-secondary text-nowrap"
|
|
||||||
>
|
|
||||||
<component :is="item.icon" class="flex size-5 shrink-0" />
|
|
||||||
<span class="truncate">{{ item.label }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div v-if="showProject && headerStats.length > 0" class="h-1.5 w-1.5 rounded-full bg-surface-5" />
|
|
||||||
<div
|
|
||||||
v-if="showProject"
|
|
||||||
class="flex min-w-0 items-center gap-1.5 font-medium text-primary text-nowrap"
|
|
||||||
>
|
|
||||||
Linked to
|
|
||||||
<Avatar
|
|
||||||
:src="props.serverProject?.icon_url ?? undefined"
|
|
||||||
:alt="props.serverProject?.title ?? ''"
|
|
||||||
size="24px"
|
|
||||||
/>
|
|
||||||
<AutoLink :to="serverProjectLink" class="truncate text-primary hover:underline">
|
|
||||||
{{ props.serverProject?.title }}
|
|
||||||
</AutoLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #actions>
|
|
||||||
<slot name="actions" />
|
|
||||||
</template>
|
|
||||||
</ContentPageHeader>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Archon } from '@modrinth/api-client'
|
import type { Archon } from '@modrinth/api-client'
|
||||||
import { NuxtModrinthClient } from '@modrinth/api-client'
|
import { NuxtModrinthClient } from '@modrinth/api-client'
|
||||||
import { GlobeIcon, LinkIcon, SettingsIcon, TimerIcon } from '@modrinth/assets'
|
import {
|
||||||
|
GlobeIcon,
|
||||||
|
LinkIcon,
|
||||||
|
LoaderCircleIcon,
|
||||||
|
PlayIcon,
|
||||||
|
SettingsIcon,
|
||||||
|
SlashIcon,
|
||||||
|
StopCircleIcon,
|
||||||
|
TimerIcon,
|
||||||
|
UpdatedIcon,
|
||||||
|
} from '@modrinth/assets'
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
import type { Component } from 'vue'
|
import type { Component } from 'vue'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
import type { RouteLocationRaw } from 'vue-router'
|
||||||
|
|
||||||
import { AutoLink, Avatar, ContentPageHeader, ServerIcon } from '#ui/components'
|
import Avatar from '#ui/components/base/Avatar.vue'
|
||||||
|
import PageHeader from '#ui/components/base/PageHeader.vue'
|
||||||
|
import type { JoinedButtonAction } from '#ui/components/base/JoinedButtons.vue'
|
||||||
|
import ServerIcon from '#ui/components/servers/icons/ServerIcon.vue'
|
||||||
|
import LoaderIcon from '#ui/components/servers/icons/LoaderIcon.vue'
|
||||||
import {
|
import {
|
||||||
injectModrinthClient,
|
injectModrinthClient,
|
||||||
injectModrinthServerContext,
|
injectModrinthServerContext,
|
||||||
@@ -79,7 +41,7 @@ import {
|
|||||||
} from '#ui/providers'
|
} from '#ui/providers'
|
||||||
import { formatLoaderLabel } from '#ui/utils/loaders'
|
import { formatLoaderLabel } from '#ui/utils/loaders'
|
||||||
|
|
||||||
import LoaderIcon from '../icons/LoaderIcon.vue'
|
import { useServerPowerAction } from './use-server-power-action'
|
||||||
|
|
||||||
type ServerProjectSummary = {
|
type ServerProjectSummary = {
|
||||||
id: string
|
id: string
|
||||||
@@ -88,11 +50,51 @@ type ServerProjectSummary = {
|
|||||||
icon_url?: string | null
|
icon_url?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
type HeaderStat = {
|
type HeaderWorld = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type HeaderMetadataItem = {
|
||||||
id: string
|
id: string
|
||||||
label: string
|
label: string
|
||||||
icon: Component
|
icon: Component
|
||||||
copyable?: boolean
|
iconProps?: Record<string, unknown>
|
||||||
|
tooltip?: string
|
||||||
|
ariaLabel?: string
|
||||||
|
to?: string | RouteLocationRaw
|
||||||
|
onClick?: () => void | Promise<void>
|
||||||
|
class?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type HeaderAction = {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
icon?: Component
|
||||||
|
iconProps?: Record<string, unknown>
|
||||||
|
iconClass?: string
|
||||||
|
tooltip?: string
|
||||||
|
ariaLabel?: string
|
||||||
|
to?: string | RouteLocationRaw
|
||||||
|
onClick?: () => void | Promise<void>
|
||||||
|
disabled?: boolean
|
||||||
|
labelHidden?: boolean
|
||||||
|
circular?: boolean
|
||||||
|
color?: 'standard' | 'brand' | 'red' | 'orange' | 'green' | 'blue' | 'purple'
|
||||||
|
size?: 'standard' | 'large' | 'small'
|
||||||
|
type?: 'standard' | 'outlined' | 'transparent' | 'highlight' | 'highlight-colored-text' | 'chip'
|
||||||
|
joinedActions?: JoinedButtonAction[]
|
||||||
|
primaryDisabled?: boolean
|
||||||
|
dropdownDisabled?: boolean
|
||||||
|
primaryMuted?: boolean
|
||||||
|
prompt?: {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
dismissLabel?: string
|
||||||
|
shown?: boolean
|
||||||
|
placement?: string
|
||||||
|
onDismiss?: () => void
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
@@ -107,6 +109,14 @@ const props = withDefaults(
|
|||||||
backHref?: string
|
backHref?: string
|
||||||
breadcrumbClass?: string
|
breadcrumbClass?: string
|
||||||
headerClass?: string
|
headerClass?: string
|
||||||
|
worlds?: HeaderWorld[]
|
||||||
|
powerDisabled?: boolean
|
||||||
|
settingsLabel?: string
|
||||||
|
showSettingsHint?: boolean
|
||||||
|
settingsHintTitle?: string
|
||||||
|
settingsHintDescription?: string
|
||||||
|
settingsHintDismissLabel?: string
|
||||||
|
actions?: HeaderAction[]
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
serverImage: null,
|
serverImage: null,
|
||||||
@@ -118,13 +128,39 @@ const props = withDefaults(
|
|||||||
backHref: '/hosting/manage',
|
backHref: '/hosting/manage',
|
||||||
breadcrumbClass: 'breadcrumb goto-link flex w-fit items-center',
|
breadcrumbClass: 'breadcrumb goto-link flex w-fit items-center',
|
||||||
headerClass: '',
|
headerClass: '',
|
||||||
|
worlds: () => [],
|
||||||
|
powerDisabled: false,
|
||||||
|
settingsLabel: 'Server settings',
|
||||||
|
showSettingsHint: false,
|
||||||
|
settingsHintTitle: '',
|
||||||
|
settingsHintDescription: '',
|
||||||
|
settingsHintDismissLabel: "Don't show again",
|
||||||
|
actions: () => [],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
openSettings: []
|
||||||
|
dismissSettingsHint: []
|
||||||
|
}>()
|
||||||
|
|
||||||
const client = injectModrinthClient()
|
const client = injectModrinthClient()
|
||||||
const { addNotification } = injectNotificationManager()
|
const { addNotification } = injectNotificationManager()
|
||||||
const { serverId } = injectModrinthServerContext()
|
const { serverId } = injectModrinthServerContext()
|
||||||
const isNuxt = computed(() => client instanceof NuxtModrinthClient)
|
const isNuxt = computed(() => client instanceof NuxtModrinthClient)
|
||||||
|
const {
|
||||||
|
isInstalling,
|
||||||
|
isStopping,
|
||||||
|
showRestartButton,
|
||||||
|
busyTooltip,
|
||||||
|
canTakeAction,
|
||||||
|
canKill,
|
||||||
|
primaryActionText,
|
||||||
|
initiateAction,
|
||||||
|
handlePrimaryAction,
|
||||||
|
} = useServerPowerAction({
|
||||||
|
disabled: computed(() => props.powerDisabled),
|
||||||
|
})
|
||||||
|
|
||||||
const userPreferences = useStorage(`pyro-server-${serverId}-preferences`, {
|
const userPreferences = useStorage(`pyro-server-${serverId}-preferences`, {
|
||||||
hideSubdomainLabel: false,
|
hideSubdomainLabel: false,
|
||||||
@@ -137,6 +173,18 @@ const headerImage = computed(() => {
|
|||||||
return props.serverImage ?? undefined
|
return props.serverImage ?? undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const leadingItems = computed(() => [
|
||||||
|
{
|
||||||
|
id: 'server-icon',
|
||||||
|
type: 'component' as const,
|
||||||
|
component: ServerIcon,
|
||||||
|
componentProps: {
|
||||||
|
image: headerImage.value,
|
||||||
|
},
|
||||||
|
class: isNuxt.value ? 'size-15 !rounded-2xl' : 'size-15 !rounded-xl',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
const showUptime = computed(() => props.showUptime && (props.uptimeSeconds ?? 0) > 0)
|
const showUptime = computed(() => props.showUptime && (props.uptimeSeconds ?? 0) > 0)
|
||||||
|
|
||||||
const formattedUptime = computed(() => {
|
const formattedUptime = computed(() => {
|
||||||
@@ -167,45 +215,71 @@ const showAddress = computed(
|
|||||||
() => !!serverAddress.value && (!props.server?.net?.domain || !userPreferences.value.hideSubdomainLabel),
|
() => !!serverAddress.value && (!props.server?.net?.domain || !userPreferences.value.hideSubdomainLabel),
|
||||||
)
|
)
|
||||||
|
|
||||||
const headerStats = computed<HeaderStat[]>(() => {
|
const metadataItems = computed<HeaderMetadataItem[]>(() => {
|
||||||
const stats: HeaderStat[] = []
|
if (props.server?.flows?.intro) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'configuring',
|
||||||
|
label: 'Configuring server...',
|
||||||
|
icon: SettingsIcon,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const items: HeaderMetadataItem[] = []
|
||||||
const worldName = props.activeWorldName
|
const worldName = props.activeWorldName
|
||||||
if (worldName) {
|
if (worldName) {
|
||||||
stats.push({
|
items.push({
|
||||||
id: 'world',
|
id: 'world',
|
||||||
label: worldName,
|
label: worldName,
|
||||||
icon: GlobeIcon,
|
icon: GlobeIcon,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (props.server?.loader) {
|
if (props.server?.loader) {
|
||||||
stats.push({
|
items.push({
|
||||||
id: 'loader',
|
id: 'loader',
|
||||||
label: props.server.mc_version
|
label: props.server.mc_version
|
||||||
? `${formatLoaderLabel(props.server.loader)} ${props.server.mc_version}`
|
? `${formatLoaderLabel(props.server.loader)} ${props.server.mc_version}`
|
||||||
: formatLoaderLabel(props.server.loader),
|
: formatLoaderLabel(props.server.loader),
|
||||||
icon: LoaderIcon,
|
icon: LoaderIcon,
|
||||||
|
iconProps: {
|
||||||
|
loader: props.server.loader,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (showAddress.value && serverAddress.value) {
|
if (showAddress.value && serverAddress.value) {
|
||||||
stats.push({
|
items.push({
|
||||||
id: 'address',
|
id: 'address',
|
||||||
label: serverAddress.value,
|
label: serverAddress.value,
|
||||||
icon: LinkIcon,
|
icon: LinkIcon,
|
||||||
copyable: true,
|
tooltip: 'Copy server address',
|
||||||
|
onClick: copyServerAddress,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (showUptime.value) {
|
if (showUptime.value) {
|
||||||
stats.push({
|
items.push({
|
||||||
id: 'uptime',
|
id: 'uptime',
|
||||||
label: formattedUptime.value,
|
label: formattedUptime.value,
|
||||||
icon: TimerIcon,
|
icon: TimerIcon,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return stats
|
if (props.serverProject) {
|
||||||
|
items.push({
|
||||||
|
id: 'project',
|
||||||
|
label: `Linked to ${props.serverProject.title}`,
|
||||||
|
icon: Avatar,
|
||||||
|
iconProps: {
|
||||||
|
src: props.serverProject.icon_url ?? undefined,
|
||||||
|
alt: props.serverProject.title,
|
||||||
|
size: '24px',
|
||||||
|
},
|
||||||
|
to: serverProjectLink.value,
|
||||||
|
class: 'text-primary',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return items
|
||||||
})
|
})
|
||||||
|
|
||||||
const showProject = computed(() => !!props.serverProject)
|
|
||||||
|
|
||||||
const serverProjectLink = computed(() => {
|
const serverProjectLink = computed(() => {
|
||||||
if (props.serverProjectLink) {
|
if (props.serverProjectLink) {
|
||||||
return props.serverProjectLink
|
return props.serverProjectLink
|
||||||
@@ -216,6 +290,134 @@ const serverProjectLink = computed(() => {
|
|||||||
return `/project/${props.serverProject.slug ?? props.serverProject.id}`
|
return `/project/${props.serverProject.slug ?? props.serverProject.id}`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const startActionText = computed(() =>
|
||||||
|
primaryActionText.value === 'Start' ? 'Start server' : primaryActionText.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
const startableWorlds = computed(() => {
|
||||||
|
if (props.worlds.length > 0) return props.worlds
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'current-world',
|
||||||
|
name: props.activeWorldName ?? 'current instance',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const startSplitActions = computed<JoinedButtonAction[]>(() => [
|
||||||
|
{
|
||||||
|
id: 'start',
|
||||||
|
label: startActionText.value,
|
||||||
|
icon: PlayIcon,
|
||||||
|
action: handlePrimaryAction,
|
||||||
|
},
|
||||||
|
...startableWorlds.value.map((world) => ({
|
||||||
|
id: `start-${world.id}`,
|
||||||
|
label: `Start with ${world.name}`,
|
||||||
|
icon: GlobeIcon,
|
||||||
|
action: handlePrimaryAction,
|
||||||
|
})),
|
||||||
|
])
|
||||||
|
|
||||||
|
const stopSplitActions = computed<JoinedButtonAction[]>(() => [
|
||||||
|
{
|
||||||
|
id: 'stop',
|
||||||
|
label: isStopping.value ? 'Stopping' : 'Stop',
|
||||||
|
icon: StopCircleIcon,
|
||||||
|
action: () => initiateAction('Stop'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'kill_server',
|
||||||
|
label: 'Kill server',
|
||||||
|
icon: SlashIcon,
|
||||||
|
action: () => initiateAction('Kill'),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const powerActions = computed<HeaderAction[]>(() => {
|
||||||
|
if (isInstalling.value) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'installing',
|
||||||
|
label: 'Installing...',
|
||||||
|
icon: LoaderCircleIcon,
|
||||||
|
iconClass: 'animate-spin',
|
||||||
|
color: 'brand',
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
if (showRestartButton.value) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'restart',
|
||||||
|
label: primaryActionText.value,
|
||||||
|
icon: UpdatedIcon,
|
||||||
|
color: 'orange',
|
||||||
|
tooltip: busyTooltip.value,
|
||||||
|
disabled: !canTakeAction.value,
|
||||||
|
onClick: handlePrimaryAction,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'stop',
|
||||||
|
label: 'Stop server',
|
||||||
|
color: 'red',
|
||||||
|
joinedActions: stopSplitActions.value,
|
||||||
|
primaryDisabled: !canTakeAction.value,
|
||||||
|
dropdownDisabled: !canKill.value,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
if (isStopping.value) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'stop',
|
||||||
|
label: 'Stop server',
|
||||||
|
color: 'red',
|
||||||
|
joinedActions: stopSplitActions.value,
|
||||||
|
primaryDisabled: true,
|
||||||
|
dropdownDisabled: !canKill.value,
|
||||||
|
primaryMuted: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'start',
|
||||||
|
label: startActionText.value,
|
||||||
|
color: 'brand',
|
||||||
|
tooltip: busyTooltip.value,
|
||||||
|
joinedActions: startSplitActions.value,
|
||||||
|
primaryDisabled: !canTakeAction.value,
|
||||||
|
dropdownDisabled: !canTakeAction.value,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const settingsAction = computed<HeaderAction>(() => ({
|
||||||
|
id: 'settings',
|
||||||
|
label: props.settingsLabel,
|
||||||
|
icon: SettingsIcon,
|
||||||
|
labelHidden: true,
|
||||||
|
tooltip: props.showSettingsHint ? undefined : props.settingsLabel,
|
||||||
|
onClick: () => emit('openSettings'),
|
||||||
|
prompt: {
|
||||||
|
title: props.settingsHintTitle,
|
||||||
|
description: props.settingsHintDescription,
|
||||||
|
dismissLabel: props.settingsHintDismissLabel,
|
||||||
|
shown: props.showSettingsHint,
|
||||||
|
placement: 'bottom-end',
|
||||||
|
onDismiss: () => emit('dismissSettingsHint'),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const headerActions = computed<HeaderAction[]>(() => [
|
||||||
|
...powerActions.value,
|
||||||
|
settingsAction.value,
|
||||||
|
...props.actions,
|
||||||
|
])
|
||||||
|
|
||||||
function copyServerAddress() {
|
function copyServerAddress() {
|
||||||
if (!serverAddress.value) return
|
if (!serverAddress.value) return
|
||||||
navigator.clipboard.writeText(serverAddress.value)
|
navigator.clipboard.writeText(serverAddress.value)
|
||||||
|
|||||||
@@ -127,54 +127,16 @@
|
|||||||
:server-project="serverProject"
|
:server-project="serverProject"
|
||||||
:active-world-name="activeWorldName"
|
:active-world-name="activeWorldName"
|
||||||
:uptime-seconds="showUptime ? uptimeSeconds : undefined"
|
:uptime-seconds="showUptime ? uptimeSeconds : undefined"
|
||||||
>
|
:worlds="serverFull?.worlds ?? []"
|
||||||
<template #actions>
|
:power-disabled="!!installError"
|
||||||
<div class="flex gap-2">
|
:settings-label="formatMessage(messages.serverSettings)"
|
||||||
<PanelServerActionButton :disabled="!!installError" />
|
:show-settings-hint="showSettingsHint"
|
||||||
<Tooltip
|
:settings-hint-title="formatMessage(settingsHintMessages.title)"
|
||||||
theme="dismissable-prompt"
|
:settings-hint-description="formatMessage(settingsHintMessages.description)"
|
||||||
:triggers="[]"
|
:settings-hint-dismiss-label="formatMessage(settingsHintMessages.dismiss)"
|
||||||
:shown="showSettingsHint"
|
@open-settings="openServerSettingsModal()"
|
||||||
:auto-hide="false"
|
@dismiss-settings-hint="dismissSettingsHint"
|
||||||
placement="bottom-end"
|
/>
|
||||||
>
|
|
||||||
<ButtonStyled circular size="large">
|
|
||||||
<button
|
|
||||||
v-tooltip="showSettingsHint ? undefined : formatMessage(messages.serverSettings)"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
openServerSettingsModal()
|
|
||||||
dismissSettingsHint()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<SettingsIcon />
|
|
||||||
</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
<template #popper>
|
|
||||||
<div class="grid grid-cols-[min-content] gap-1">
|
|
||||||
<div class="flex min-w-48 items-center justify-between gap-8">
|
|
||||||
<h3 class="m-0 whitespace-nowrap text-base font-bold text-contrast">
|
|
||||||
{{ formatMessage(settingsHintMessages.title) }}
|
|
||||||
</h3>
|
|
||||||
<ButtonStyled size="small" circular>
|
|
||||||
<button
|
|
||||||
v-tooltip="formatMessage(settingsHintMessages.dismiss)"
|
|
||||||
@click="dismissSettingsHint"
|
|
||||||
>
|
|
||||||
<XIcon aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
|
||||||
<p class="m-0 text-wrap text-sm font-medium leading-tight text-secondary">
|
|
||||||
{{ formatMessage(settingsHintMessages.description) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</ServerManageHeader>
|
|
||||||
|
|
||||||
<ServerOnboardingPanelPage v-if="isOnboarding" :browse-modpacks="handleBrowseModpacks" />
|
<ServerOnboardingPanelPage v-if="isOnboarding" :browse-modpacks="handleBrowseModpacks" />
|
||||||
|
|
||||||
@@ -348,16 +310,13 @@ import {
|
|||||||
LoaderCircleIcon,
|
LoaderCircleIcon,
|
||||||
LockIcon,
|
LockIcon,
|
||||||
RightArrowIcon,
|
RightArrowIcon,
|
||||||
SettingsIcon,
|
|
||||||
TransferIcon,
|
TransferIcon,
|
||||||
TriangleAlertIcon,
|
TriangleAlertIcon,
|
||||||
UsersIcon,
|
UsersIcon,
|
||||||
XIcon,
|
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||||
import { useStorage, useTimeoutFn } from '@vueuse/core'
|
import { useStorage, useTimeoutFn } from '@vueuse/core'
|
||||||
import DOMPurify from 'dompurify'
|
import DOMPurify from 'dompurify'
|
||||||
import { Tooltip } from 'floating-vue'
|
|
||||||
import { computed, nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, watch } from 'vue'
|
import { computed, nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
|
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
@@ -368,7 +327,7 @@ import ServerNotice from '#ui/components/base/ServerNotice.vue'
|
|||||||
import ConfirmLeaveModal from '#ui/components/modal/ConfirmLeaveModal.vue'
|
import ConfirmLeaveModal from '#ui/components/modal/ConfirmLeaveModal.vue'
|
||||||
import ServerPanelAdmonitions from '#ui/components/servers/admonitions/ServerPanelAdmonitions.vue'
|
import ServerPanelAdmonitions from '#ui/components/servers/admonitions/ServerPanelAdmonitions.vue'
|
||||||
import MedalServerCountdown from '#ui/components/servers/marketing/MedalServerCountdown.vue'
|
import MedalServerCountdown from '#ui/components/servers/marketing/MedalServerCountdown.vue'
|
||||||
import { PanelServerActionButton, ServerManageHeader } from '#ui/components/servers/server-header'
|
import { ServerManageHeader } from '#ui/components/servers/server-header'
|
||||||
import ServerSettingsModal from '#ui/components/servers/ServerSettingsModal.vue'
|
import ServerSettingsModal from '#ui/components/servers/ServerSettingsModal.vue'
|
||||||
import {
|
import {
|
||||||
hasServerPermission,
|
hasServerPermission,
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
|
<Admonition
|
||||||
|
v-if="!instanceInfoAdmonitionDismissed"
|
||||||
|
type="info"
|
||||||
|
:header="formatMessage(messages.instanceInfoHeader)"
|
||||||
|
dismissible
|
||||||
|
@dismiss="dismissInstanceInfoAdmonition"
|
||||||
|
>
|
||||||
|
<ul class="m-0 pl-4">
|
||||||
|
<li>{{ formatMessage(messages.instanceInfoDefinition) }}</li>
|
||||||
|
<li>{{ formatMessage(messages.instanceInfoSwitching) }}</li>
|
||||||
|
<li>{{ formatMessage(messages.instanceInfoFiles) }}</li>
|
||||||
|
</ul>
|
||||||
|
</Admonition>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="worldsPending"
|
v-if="worldsPending"
|
||||||
class="grid grid-cols-[repeat(auto-fit,minmax(min(100%,20.25rem),1fr))] gap-6"
|
class="grid grid-cols-[repeat(auto-fit,minmax(min(100%,20.25rem),1fr))] gap-6"
|
||||||
@@ -26,9 +40,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Archon } from '@modrinth/api-client'
|
import type { Archon } from '@modrinth/api-client'
|
||||||
import { useQuery } from '@tanstack/vue-query'
|
import { useQuery } from '@tanstack/vue-query'
|
||||||
|
import { useStorage } from '@vueuse/core'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import Admonition from '#ui/components/base/Admonition.vue'
|
||||||
import InstanceCard from '#ui/components/servers/instances/InstanceCard.vue'
|
import InstanceCard from '#ui/components/servers/instances/InstanceCard.vue'
|
||||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||||
import {
|
import {
|
||||||
@@ -43,6 +59,22 @@ const messages = defineMessages({
|
|||||||
id: 'servers.manage.instances.slot-name',
|
id: 'servers.manage.instances.slot-name',
|
||||||
defaultMessage: 'Instance #{index}',
|
defaultMessage: 'Instance #{index}',
|
||||||
},
|
},
|
||||||
|
instanceInfoHeader: {
|
||||||
|
id: 'servers.manage.instances.info.header',
|
||||||
|
defaultMessage: 'What is an instance?',
|
||||||
|
},
|
||||||
|
instanceInfoDefinition: {
|
||||||
|
id: 'servers.manage.instances.info.definition',
|
||||||
|
defaultMessage: 'An instance is a separate server setup.',
|
||||||
|
},
|
||||||
|
instanceInfoSwitching: {
|
||||||
|
id: 'servers.manage.instances.info.switching',
|
||||||
|
defaultMessage: 'You can switch which instance your server runs.',
|
||||||
|
},
|
||||||
|
instanceInfoFiles: {
|
||||||
|
id: 'servers.manage.instances.info.files',
|
||||||
|
defaultMessage: 'Each instance has its own server files, worlds, installed content, and settings.',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
type LinkedModpack = {
|
type LinkedModpack = {
|
||||||
@@ -79,12 +111,14 @@ type ContentSummary = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const WORLD_SLOT_COUNT = 3
|
const WORLD_SLOT_COUNT = 3
|
||||||
|
const INSTANCE_INFO_ADMONITION_KEY = 'server-instances-info-admonition-dismissed'
|
||||||
|
|
||||||
const client = injectModrinthClient()
|
const client = injectModrinthClient()
|
||||||
const { serverId, server, isServerRunning } = injectModrinthServerContext()
|
const { serverId, server, isServerRunning } = injectModrinthServerContext()
|
||||||
const { openServerSettings } = injectServerSettingsModal()
|
const { openServerSettings } = injectServerSettingsModal()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const instanceInfoAdmonitionDismissed = useStorage(INSTANCE_INFO_ADMONITION_KEY, false)
|
||||||
|
|
||||||
const worldsQuery = useQuery({
|
const worldsQuery = useQuery({
|
||||||
queryKey: computed(() => ['servers', 'worlds', 'summary', 'v1', serverId]),
|
queryKey: computed(() => ['servers', 'worlds', 'summary', 'v1', serverId]),
|
||||||
@@ -286,4 +320,8 @@ function handleWorldSettings() {
|
|||||||
function handleCreateWorld() {
|
function handleCreateWorld() {
|
||||||
openServerSettings({ tabId: 'installation' })
|
openServerSettings({ tabId: 'installation' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function dismissInstanceInfoAdmonition() {
|
||||||
|
instanceInfoAdmonitionDismissed.value = true
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user