feat: server management in app (#5628)

* start new server settings tabs

* update properties tab to match design

* better stying in general tab

* feat: add suffix input for hostname field

* implement tables for allocations and DNS records

* add tags for dns record type

* small gap adjustment

* polish advanced page

* adjust properties page hierarchy

* fix searching properties, empty state and projection radius appearing

* pnpm prepr

* update copy to match designs

* fix suffix input component

* style fixes and match heading size

* small fix

* fix search allocations placeholder

* adjust table styles

* move all installation settings helper text to below input

* update icon to use overflow menu buttons

* fix modal to be consistent

* open advanced properties when search

* remove other and custom properties, and update styles

* remove hide/show all java versions

* handle mc 26

* refactor: move server settings pages into /ui and add app ServerSettingsModal

* hook up server pages for app

* add server page header to app

* hook up server settings modal

* use large size

* fix card box shadow style

* fix hostname input for app

* fix app/website card containers

* implement external tabs for billing and admin billing

* fix save banner fixed to parent instead of page body

* remove unused prop to FriendsList causing warning in app

* fix client-only not available for app

* fix bottom cut off

* wire node auth

* implement full copy buttons

* dedup copy button tailwind styles

* fix hover class not working in @apply

* fix spacing

* fix error validation styles

* apply consistent styles and spacing

* feat: update hosting server card (#5609)

* fix type errors

* fix some stylesheets not imported for storybook

* add server listing stories

* add fix for frontend stylesheet imports

* remove props.

* convert copy code to use tailwind

* update server listing component styles

* update server info label styles

* start status/player count info label, more style updates and fixes

* add new server card buttons

* hook up server cards and implement updated styles

* hook up on download button

* fix tauri throwing error when api returns 204 No Content

* hook up purchase server modal in app

* fix upgrading state loading icon

* pnpm prepr

* filter out servers past 30 days after cancellation

* do not apply opacity on lock or spiner icons

* fix disabled server icon background

* update pending change stage

* handle known suspension states

* refactor: reduce code duplication for server listing

* update disabled state text color

* fix loading icon color

* clean up copy

* fix disabled opacity for server card

* update server listing files kept to be countdown

* implement resubscribe modal

* implement proper provisioning state for resubscribe

* fix duplicate attribute and pnpm prepr

* feat: add shared UI package auth DI

* feat: update purchase server flow (#5714)

* implement server list empty state component

* fix stories and adjust spacing

* implement select plan design refresh

* implement auth for empty server list

* use refs instead of reactive

* pnpm prepr

* fix auth usage for empty servers list

* move app auth provider setup to src/providers/setup

* pnpm prepr

* fix max height

* style fix

* fix getCreds no auth is blocking api client

* implement servers guest plan modal and signin which redirects back to modal's next step

* refactor guest plan select logic into provider

* implement sign in or create account popup

* remove force empty serverList

* add download button for suspended mod and generic

* add handling for when user logs out

* QA pass style fixes

* more consistent page styles

* fix duplicate export

* refactor: remove all fallback stuff from resubscribe modal

* implement shared download latest backup util

* i18n pass

* pnpm prepr

* fix region being selected if ping failed

* pnpm prepr

* feat: servers in app finalization (#5744)

* feat: start on shared console implementation into logs and overview pages

* fix: terminal gap issues

* feat: swap word wrap for full screen

* fix: stats cards alignment

* fix: stats

* feat: fix console clear + remove copy

* fix: lint

* fix: use reset not clear

* feat: shared server header & overview page for app and website (#5736)

* feat: implement shared server header for app and website

* feat: implement wrapped overview page with shared composable and hook it up

* pnpm prepr

* fix: bugs

* qa: cleanup

* feat: root.vue shared layout

* feat: delete old options pages + fix discovery frontend

* fix: discovery

* fix: misc style/layout issues

* fix page padding

* fix: modal height jankiness

* feat: implement server install content in app and server setup modal with DI

* fix: spacing

* remove servers in app feature flag

* Revert "remove servers in app feature flag"

This reverts commit 86e284c4bdd6fa42c3c8fbaf1efbec41f4d1c6d2.

* fix: qa

* feat: remove legacy components from apps/frontend/src/components/ui/servers

---------

Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>

* qa pass (#5738)

* fix: qa

* feat: qa

* fix: server icon fetch fails due to global node auth race condition overriding each other

* fix: lint

* fix: server icon upload/sync and centralize logic

* fix: server settings modal not closing for server reset

* fix: better server sorting

* feat: copy address in server listing card

* fix: notification panel in modal and when overlapping with action bar

* fix: empty server list empty state flashing when refresh, fixed by adding isReady auth flag

* feat: use floating action bar for save banner

* fix: saving state in save bar

* fix: edit server icon styling

* fix: confirm modal to have consistent buttons

* feat: loading animation for server panel + caching improvements for app

* pnpm prepr

* feat: search page deduplication (#5754)

* fix: action bar behind modal

* fix: remove warning modal for stopping

* fix: server cards states

* we hate webkit we hate webkit

* fix: update allocation creation to not use modal

* fix: properties tab spacing and styles

* feat: add files tab copy

* fix: advanced properties icon

* fix: remove back to all servers link

* feat: add files tab link in copy

* fix: server header styles to be consistent with instance

* fix: add header icons back

* feat: update instance settings icon to be consistent

* fix: icon container

* feat: upload state persistence across tabs

* fix: server labels text wrapping

* fix: use surface-5 border

* fix: loading spinner showing with onboarding below

* feat: new server button shows purchase modal in website

* fix: billing page not showing quarterly interval

* fix: server downgrade not showing updated subscription notification

* fix: server settings invalidate saved state and remove server context provider since its already provided in the page

* pnpm prepr

* add stripe publishable key to app build

* feat: console highlighting

* fix: rename servers title to modrinth hosting

* feat: search fix

* fix: qa/styles

* fix: ip click active and remove power dont ask again

* fix: qa

* feat: highlighting fix console

* fix: disable conflicts action

* fix: error dismiss bug

* feat: modal clarification

* fix: files perms issue

* fix: lint

* feat: modal fix

* enable show uptime

* fix: add loading state to edit server icon

* fix: notification panel take in has sidebar from settings

* fix: consistency pass on app settings

* fix: consistency pass on instance settings

* pnpm prepr

* fix: nagivate to billing button in app to go to website

* fix: stripe return url in app causing app to open modrinth.com in tauri

* refactor: better show polling UI code

* fix: new server polling comparison to use server ids instead of length

* fix: buttonstyled story

* fix: button styling

* fix: content.vue regression

* feat: project url redirects

* fix: breadcrumbs

* fix: purchase with newly added card

* fix: console ordering problems

* fix: app-frontend missing env config and staging environment

* fix: log syncing for instances and server panel accidentally

* fix: QA issues

* fix: server page loading state

* fix: stats card logic

* fix: lint

* fix: qa

* fix: console height padding

* fix: terminal padding + loading indicator

* feat: update medal server listing styling

* fix: no upgrade button for medal server listing in app

* fix: go to overview instead of content tab after onboarding

* fix: qa

* fix: teleport modals to body

* fix: logs tab + qa

* fix: local storage for user preferences

* fix: qa loading indic

* feat: considitonal debug and trace

* fix: jump to top on install bug

* feat: swap out server hard drive icon to server stack icon

* feat: servers in app feature flag default true

* fix: highlight row ufll

* fix: webkit thing onto a tag

* fix: input field

* fix: clear fix

* fix: lint

* fix: fmt

* feat: improve share modal and bring it back for sharing log

* pnpm prepr

* fix: menu overflowing

* feat: remove servers in app feature flag

* fix: server stat charts no longer showing color

* fix: library nav no primary state

* fix: better modal height and width

* fix: highlighting bugs

* fix: empty states

* fix: delay import to fix overview page slow load on MacOS

* fix: medal server listing too bright on light mode

* fix: admon analysis + fix logs

* fix: bug

* fix: clear purchase intent from sign-in after closing modal

* performance: improve server manage stats loading by splitting reactivity

* fix: deploy + admon + disable highlighting

* fix: clippy

---------

Co-authored-by: tdgao <mr.trumgao@gmail.com>
Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com>

* feat: temp wrangler

* fix: lint

* fix: logs upload

* fix: console empty state and admon regressions

* fix: fields

* feat: log deleting + prefetch for Logs.vue

* feat: move delete before share

* feat: clear endpoint

* feat: we ball!

---------

Co-authored-by: Calum H. <calum@modrinth.com>
Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
This commit is contained in:
Truman Gao
2026-04-12 15:38:08 -06:00
committed by GitHub
parent a2a97d1313
commit 693a371d61
278 changed files with 15974 additions and 12608 deletions

View File

@@ -1,5 +1,5 @@
<template>
<NewModal ref="modal" :noblur="noblur" :danger="danger" :on-hide="onHide">
<NewModal ref="modal" :noblur="noblur" :danger="danger" :on-hide="onHide" max-width="550px">
<template #title>
<slot name="title">
<span class="font-extrabold text-contrast text-lg">{{ title }}</span>
@@ -30,19 +30,19 @@
placeholder="Type here..."
wrapper-class="max-w-[20rem]"
/>
<div class="flex gap-2">
<div class="flex gap-2 justify-end">
<ButtonStyled>
<button class="!shadow-none" @click="hide()">
<XIcon />
Cancel
</button>
</ButtonStyled>
<ButtonStyled :color="danger ? 'red' : 'brand'">
<button :disabled="action_disabled" @click="proceed">
<component :is="proceedIcon" />
{{ proceedLabel }}
</button>
</ButtonStyled>
<ButtonStyled>
<button @click="hide()">
<XIcon />
Cancel
</button>
</ButtonStyled>
</div>
</div>
</NewModal>

View File

@@ -11,7 +11,9 @@ import {
import QrcodeVue from 'qrcode.vue'
import { computed, nextTick, ref } from 'vue'
import { Button, Modal, StyledInput } from '../index'
import { injectNotificationManager } from '#ui/providers'
import { Button, ButtonStyled, NewModal, StyledInput } from '../index'
const props = defineProps({
header: {
@@ -38,6 +40,10 @@ const props = defineProps({
type: Boolean,
default: false,
},
socialButtons: {
type: Boolean,
default: true,
},
onHide: {
type: Function,
default() {
@@ -47,6 +53,7 @@ const props = defineProps({
})
const shareModal = ref(null)
const { addNotification } = injectNotificationManager()
const qrCode = ref(null)
const qrImage = ref(null)
@@ -94,7 +101,21 @@ const copyImage = async () => {
}
const copyText = async () => {
await navigator.clipboard.writeText(url.value ?? content.value)
try {
await navigator.clipboard.writeText(url.value ?? content.value)
addNotification({
type: 'success',
title: 'Link copied',
text: 'The link has been copied to your clipboard.',
})
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
addNotification({
type: 'error',
title: 'Failed to copy text',
text: message,
})
}
}
const sendEmail = computed(
@@ -125,50 +146,59 @@ defineExpose({
</script>
<template>
<Modal ref="shareModal" :header="header" :noblur="noblur" :on-hide="onHide">
<div class="share-body">
<div v-if="link" class="qr-wrapper">
<NewModal ref="shareModal" :header="header" :noblur="noblur" :on-hide="onHide">
<div class="flex flex-row flex-wrap items-center gap-2">
<div v-if="link" class="group relative mx-auto">
<div ref="qrCode">
<QrcodeVue :value="url" class="qr-code" margin="3" />
<QrcodeVue :value="url" class="!bg-white rounded-[var(--radius-md)]" margin="3" />
</div>
<Button
v-tooltip="'Copy QR code'"
icon-only
class="copy-button"
aria-label="Copy QR code"
@click="copyImage"
>
<ClipboardCopyIcon aria-hidden="true" />
</Button>
<ButtonStyled circular>
<button
v-tooltip="'Copy QR code'"
class="absolute top-0 right-0 m-2 opacity-0 transition-all duration-200 ease-in-out group-hover:opacity-100 group-focus-within:opacity-100 motion-reduce:transition-none"
aria-label="Copy QR code"
@click="copyImage"
>
<ClipboardCopyIcon class="h-5 w-5" aria-hidden="true" />
</button>
</ButtonStyled>
</div>
<StyledInput v-else v-model="content" multiline resize="vertical" wrapper-class="h-full">
<template #right>
<Button
<button
v-tooltip="'Copy Text'"
icon-only
type="button"
aria-label="Copy Text"
class="copy-button transparent"
class="absolute top-0 right-0 m-2 grid h-10 w-10 cursor-pointer place-content-center rounded-lg border-none bg-button-bg text-primary transition-all hover:bg-button-bg-hover hover:brightness-125 active:scale-95"
@click="copyText"
>
<ClipboardCopyIcon aria-hidden="true" />
</Button>
<ClipboardCopyIcon class="h-5 w-5" aria-hidden="true" />
</button>
</template>
</StyledInput>
<div class="all-buttons">
<StyledInput v-if="link" type="text" :model-value="url" readonly wrapper-class="w-full">
<template #right>
<Button v-tooltip="'Copy Text'" aria-label="Copy Text" class="r-btn" @click="copyText">
<ClipboardCopyIcon aria-hidden="true" />
</Button>
</template>
</StyledInput>
<div class="button-row">
<div class="flex flex-grow flex-col justify-center gap-2">
<button
v-if="link"
v-tooltip="'Copy Link'"
type="button"
aria-label="Copy Link"
class="flex h-10 w-full cursor-pointer items-center justify-between gap-2 rounded-lg border-none bg-button-bg px-3 pr-1.5 text-primary transition-all hover:bg-button-bg-hover hover:brightness-125 active:scale-95"
@click="copyText"
>
<span class="cursor-pointer truncate text-left font-semibold text-primary">
{{ url }}
</span>
<div class="grid h-10 w-10 place-content-center">
<ClipboardCopyIcon class="h-5 w-5" aria-hidden="true" />
</div>
</button>
<div v-if="socialButtons" class="flex flex-row gap-2">
<Button v-if="canShare" v-tooltip="'Share'" aria-label="Share" icon-only @click="share">
<ShareIcon aria-hidden="true" />
</Button>
<a
v-tooltip="'Send as an email'"
class="btn icon-only"
class="btn icon-only fill-contrast text-contrast"
:href="sendEmail"
:target="targetParameter"
aria-label="Send as an email"
@@ -178,7 +208,7 @@ defineExpose({
<a
v-if="link"
v-tooltip="'Open link in browser'"
class="btn icon-only"
class="btn icon-only fill-contrast text-contrast"
:target="targetParameter"
:href="url"
aria-label="Open link in browser"
@@ -187,7 +217,7 @@ defineExpose({
</a>
<a
v-tooltip="'Toot about it'"
class="btn mastodon icon-only"
class="btn icon-only fill-contrast text-contrast bg-[#563acc]"
:target="targetParameter"
:href="sendToot"
aria-label="Toot about it"
@@ -196,7 +226,7 @@ defineExpose({
</a>
<a
v-tooltip="'Tweet about it'"
class="btn twitter icon-only"
class="btn icon-only fill-contrast text-contrast bg-[#1da1f2]"
:target="targetParameter"
:href="sendTweet"
aria-label="Tweet about it"
@@ -205,7 +235,7 @@ defineExpose({
</a>
<a
v-tooltip="'Share on Reddit'"
class="btn reddit icon-only"
class="btn icon-only fill-contrast text-contrast bg-[#ff4500]"
:target="targetParameter"
:href="postOnReddit"
aria-label="Share on Reddit"
@@ -215,76 +245,5 @@ defineExpose({
</div>
</div>
</div>
</Modal>
</NewModal>
</template>
<style scoped lang="scss">
.share-body {
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
gap: var(--gap-sm);
padding: var(--gap-lg);
}
.all-buttons {
display: flex;
flex-direction: column;
gap: var(--gap-sm);
flex-grow: 1;
justify-content: center;
}
.button-row {
display: flex;
flex-direction: row;
gap: var(--gap-sm);
.btn {
fill: var(--color-contrast);
color: var(--color-contrast);
&.reddit {
background-color: #ff4500;
}
&.mastodon {
background-color: #563acc;
}
&.twitter {
background-color: #1da1f2;
}
}
}
.qr-wrapper {
position: relative;
margin: 0 auto;
&:hover {
.copy-button {
opacity: 1;
}
}
}
.qr-code {
background-color: white !important;
border-radius: var(--radius-md);
}
.copy-button {
position: absolute;
top: 0;
right: 0;
margin: var(--gap-sm);
transition: all 0.2s ease-in-out;
opacity: 0;
@media (prefers-reduced-motion) {
transition: none !important;
}
}
</style>

View File

@@ -1,6 +1,7 @@
<script lang="ts"></script>
<script setup lang="ts">
import { RightArrowIcon } from '@modrinth/assets'
import { type Component, computed, nextTick, ref } from 'vue'
import { type MessageDescriptor, useVIntl } from '../../composables/i18n'
@@ -9,7 +10,8 @@ import NewModal from './NewModal.vue'
export interface Tab {
name: MessageDescriptor
icon: Component
content: Component
content?: Component
href?: string
badge?: MessageDescriptor
shown?: boolean
}
@@ -75,15 +77,19 @@ defineExpose({ show, hide, selectedTab, setTab })
<template v-if="$slots.title" #title>
<slot name="title" />
</template>
<div class="grid grid-cols-[auto_1fr] p-4">
<div class="grid grid-cols-[auto_1fr] p-6 pb-3 pr-0">
<div
class="flex flex-col gap-1 border-solid pr-4 border-0 border-r-[1px] border-divider min-w-[200px]"
>
<button
<component
:is="tab.href ? 'a' : 'button'"
v-for="(tab, index) in visibleTabs"
:key="index"
:class="`flex gap-2 items-center text-left rounded-xl px-4 py-2 border-none text-nowrap font-semibold cursor-pointer active:scale-[0.97] transition-all ${selectedTab === index ? 'bg-button-bgSelected text-button-textSelected' : 'bg-transparent text-button-text hover:bg-button-bg hover:text-contrast'}`"
@click="() => setTab(index)"
:href="tab.href ?? undefined"
:target="tab.href ? '_blank' : undefined"
:rel="tab.href ? 'noopener noreferrer' : undefined"
:class="`flex gap-2 items-center text-left rounded-xl px-4 py-2 border-none text-nowrap font-semibold cursor-pointer active:scale-[0.97] transition-all no-underline ${!tab.href && selectedTab === index ? 'bg-button-bgSelected text-button-textSelected' : 'bg-transparent text-button-text hover:bg-button-bg hover:text-contrast'}`"
@click="!tab.href && setTab(index)"
>
<component :is="tab.icon" class="w-4 h-4 flex-shrink-0" />
<span>{{ formatMessage(tab.name) }}</span>
@@ -93,7 +99,8 @@ defineExpose({ show, hide, selectedTab, setTab })
>
{{ formatMessage(tab.badge) }}
</span>
</button>
<RightArrowIcon v-if="tab.href" class="size-4 ml-auto" />
</component>
<slot name="footer" />
</div>
@@ -101,38 +108,41 @@ defineExpose({ show, hide, selectedTab, setTab })
<Transition
enter-active-class="transition-all duration-200 ease-out"
enter-from-class="opacity-0 max-h-0"
enter-to-class="opacity-100 max-h-10"
enter-to-class="opacity-100 max-h-4"
leave-active-class="transition-all duration-200 ease-in"
leave-from-class="opacity-100 max-h-10"
leave-from-class="opacity-100 max-h-4"
leave-to-class="opacity-0 max-h-0"
>
<div
v-if="showTopFade"
class="pointer-events-none absolute left-0 right-0 top-0 z-10 h-10 bg-gradient-to-b from-bg-raised to-transparent"
class="pointer-events-none absolute left-0 right-0 top-0 z-10 h-4 bg-gradient-to-b from-bg-raised to-transparent"
/>
</Transition>
<div
ref="scrollContainer"
class="min-w-[400px] h-[500px] overflow-y-auto px-4"
class="overflow-y-auto px-6 pb-6 h-screen max-h-[min(65vh,600px)]"
@scroll="checkScrollState"
>
<Suspense>
<component :is="visibleTabs[selectedTab].content" />
<component
:is="visibleTabs[selectedTab]?.content"
v-if="visibleTabs[selectedTab]?.content"
/>
</Suspense>
</div>
<Transition
enter-active-class="transition-all duration-200 ease-out"
enter-from-class="opacity-0 max-h-0"
enter-to-class="opacity-100 max-h-10"
enter-to-class="opacity-100 max-h-16"
leave-active-class="transition-all duration-200 ease-in"
leave-from-class="opacity-100 max-h-10"
leave-from-class="opacity-100 max-h-16"
leave-to-class="opacity-0 max-h-0"
>
<div
v-if="showBottomFade"
class="pointer-events-none absolute bottom-0 left-0 right-0 z-10 h-10 bg-gradient-to-t from-bg-raised to-transparent"
class="pointer-events-none absolute bottom-0 left-0 right-0 z-10 h-16 bg-gradient-to-t from-bg-raised to-transparent"
/>
</Transition>
</div>

View File

@@ -0,0 +1,50 @@
<template>
<NewModal ref="modal" :header="formatMessage(messages.header)" :closable="false">
<div class="flex flex-col gap-4 md:w-[400px]">
<AppearingProgressBar :max-value="totalBytes" :current-value="uploadedBytes" />
<p class="m-0 text-sm text-secondary">{{ formatMessage(messages.warningText) }}</p>
</div>
</NewModal>
</template>
<script setup lang="ts">
import type { UploadHandle } from '@modrinth/api-client'
import { ref } from 'vue'
import { AppearingProgressBar } from '#ui/components/base'
import { defineMessages, useVIntl } from '#ui/composables/i18n'
import NewModal from './NewModal.vue'
const { formatMessage } = useVIntl()
const messages = defineMessages({
header: {
id: 'servers.setup.uploading-modpack.header',
defaultMessage: 'Uploading modpack',
},
warningText: {
id: 'servers.setup.upload-warning',
defaultMessage: "Please don't close this page while uploading.",
},
})
const modal = ref<InstanceType<typeof NewModal>>()
const uploadedBytes = ref(0)
const totalBytes = ref(0)
function track<T>(handle: UploadHandle<T>): Promise<T> {
uploadedBytes.value = 0
totalBytes.value = 0
modal.value?.show()
handle.onProgress(({ loaded, total }) => {
uploadedBytes.value = loaded
totalBytes.value = total
})
return handle.promise.finally(() => {
modal.value?.hide()
})
}
defineExpose({ track })
</script>

View File

@@ -8,3 +8,4 @@ export { default as OpenInAppModal } from './OpenInAppModal.vue'
export { default as ShareModal } from './ShareModal.vue'
export type { Tab as TabbedModalTab } from './TabbedModal.vue'
export { default as TabbedModal } from './TabbedModal.vue'
export { default as UploadProgressModal } from './UploadProgressModal.vue'