feat: hosting access tab (#5995)

* feat: implement access tab with dummy data

* fix: spacing

* feat: qa

* feat: implement backend

* qa: qa pass

* feat: fix user "search"

* fix: lint

* feat: change to bitfield

* feat: fix fields

* fix: lint

* fix: lint

* feat: hook up api

* feat: fix permissions

* feat: audit log table event start

* feat: better mobile mode for audit log table

* feat: i18n

* feat: qa

* feat: enforce permissions

* feat: email template start

* feat: qa

* fix: tooltip bug

* feat: qa

* impl: sse support in api-client

* feat: sse impl

* fix: desync path

* feat: time frame picker from analytics

* feat: QA

* fix: spacing

* fix: permisison audit log entries

* fix: hosting manage page shared server detection

* fix: lint

* feat: qa + lint

* feat: audit log table sort by time

* feat: finish frontend panel stuff

* fix: lint

* fix: backend alignment

* fix: lint

* fix: supress friend errors

* feat: qa

* fix: qa

* fix: lint

* fix: utils barrel

* fix: safari cookies in dev

* fix: pin nuxt

* feat: fixes + notif fix

* fix: notifications

* feat: qa

* fix: notification sync not happening immediately

* fix: qa

* fix: qa

* feat: qa

* blog + prepr

* feat: toast shit

* blog images

* thumbnail update one last time

* prepr

* feat: use reinvite route

* update images

* fix: reinvite stuff

* fix: lint

* fix: alignment of save bar

* fix: notif sizing

* fix: split up access

* fix: lint

* fix: lint

* fix: link

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
Calum H.
2026-06-04 16:58:01 +01:00
committed by GitHub
parent 58ad58f958
commit bd97ace974
227 changed files with 15578 additions and 2153 deletions
@@ -1,244 +1,284 @@
<template>
<div
class="notification"
:class="{
'has-body': hasBody,
compact: compact,
read: notification.read,
}"
:class="
type === 'server_invite'
? { read: notification.read }
: {
notification: true,
'has-body': hasBody,
compact: compact,
read: notification.read,
}
"
>
<nuxt-link
v-if="!type"
:to="notification.link"
class="notification__icon backed-svg"
:class="{ raised: raised }"
>
<BellIcon />
</nuxt-link>
<DoubleIcon v-else class="notification__icon">
<template #primary>
<nuxt-link v-if="project" :to="getProjectLink(project)" tabindex="-1">
<Avatar size="xs" :src="project.icon_url" :raised="raised" no-shadow />
</nuxt-link>
<nuxt-link
v-else-if="organization"
:to="`/organization/${organization.slug}`"
tabindex="-1"
>
<Avatar size="xs" :src="organization.icon_url" :raised="raised" no-shadow />
</nuxt-link>
<nuxt-link v-else-if="user" :to="getUserLink(user)" tabindex="-1">
<Avatar size="xs" :src="user.avatar_url" :raised="raised" no-shadow />
</nuxt-link>
<Avatar v-else size="xs" :raised="raised" no-shadow />
</template>
<template #secondary>
<ScaleIcon
v-if="type === 'moderator_message' || type === 'status_change'"
class="moderation-color"
/>
<UserPlusIcon v-else-if="type === 'team_invite' && project" class="creator-color" />
<UserPlusIcon
v-else-if="type === 'organization_invite' && organization"
class="creator-color"
/>
<VersionIcon v-else-if="type === 'project_update' && project && version" />
<BellIcon v-else />
</template>
</DoubleIcon>
<div class="notification__title">
<template v-if="type === 'project_update' && project && version">
A project you follow,
<nuxt-link :to="getProjectLink(project)" class="title-link">{{ project.title }}</nuxt-link>
, has been updated:
</template>
<template v-else-if="type === 'team_invite' && project">
<nuxt-link
:to="`/user/${invitedBy.username}`"
class="iconified-link title-link inline-flex"
>
<Avatar
:src="invitedBy.avatar_url"
circle
size="xxs"
no-shadow
:raised="raised"
class="inline-flex"
/>
<span class="space">&nbsp;</span>
<span>{{ invitedBy.username }}</span>
</nuxt-link>
<span>
has invited you to join
<nuxt-link :to="getProjectLink(project)" class="title-link">
{{ project.title }} </nuxt-link
>.
</span>
</template>
<template v-else-if="type === 'organization_invite' && organization">
<nuxt-link
:to="`/user/${invitedBy.username}`"
class="iconified-link title-link inline-flex"
>
<Avatar
:src="invitedBy.avatar_url"
circle
size="xxs"
no-shadow
:raised="raised"
class="inline-flex"
/>
<span class="space">&nbsp;</span>
<span>{{ invitedBy.username }}</span>
</nuxt-link>
<span>
has invited you to join
<nuxt-link :to="`/organization/${organization.slug}`" class="title-link">
{{ organization.name }} </nuxt-link
>.
</span>
</template>
<template v-else-if="type === 'status_change' && project">
<nuxt-link :to="getProjectLink(project)" class="title-link">
{{ project.title }}
</nuxt-link>
<template v-if="tags.rejectedStatuses.includes(notification.body.new_status)">
has been
<ProjectStatusBadge :status="notification.body.new_status" />
</template>
<template v-else>
updated from
<ProjectStatusBadge :status="notification.body.old_status" />
to
<ProjectStatusBadge :status="notification.body.new_status" />
</template>
by the moderators.
</template>
<template v-else-if="type === 'moderator_message' && thread && project && !report">
Your project,
<nuxt-link :to="getProjectLink(project)" class="title-link">{{ project.title }}</nuxt-link>
, has received
<template v-if="notification.grouped_notifs"> messages</template>
<template v-else>a message</template>
from the moderators.
</template>
<template v-else-if="type === 'moderator_message' && thread && report">
A moderator replied to your report of
<template v-if="version">
version
<nuxt-link :to="getVersionLink(project, version)" class="title-link">
{{ version.name }}
</nuxt-link>
of project
</template>
<nuxt-link v-if="project" :to="getProjectLink(project)" class="title-link">
{{ project.title }}
</nuxt-link>
<nuxt-link v-else-if="user" :to="getUserLink(user)" class="title-link">
{{ user.username }}
</nuxt-link>
.
</template>
<nuxt-link v-else :to="notification.link" class="title-link">
<span v-html="renderString(notification.title)" />
</nuxt-link>
<!-- <span v-else class="known-errors">Error reading notification.</span>-->
</div>
<div v-if="hasBody" class="notification__body">
<ThreadSummary
v-if="type === 'moderator_message' && thread"
:thread="thread"
:link="threadLink"
:raised="raised"
:messages="getMessages()"
class="thread-summary"
:auth="auth"
/>
<div v-else-if="type === 'project_update'" class="version-list">
<template v-if="type === 'server_invite'">
<div class="flex flex-col gap-4">
<ModrinthServersIcon class="h-auto w-56 max-w-full text-[var(--color-heading)]" />
<div
v-for="notif in (notification.grouped_notifs
? [notification, ...notification.grouped_notifs]
: [notification]
).filter((x) => x.extra_data.version)"
:key="notif.id"
class="version-link"
class="flex flex-wrap items-center gap-x-1.5 gap-y-2 text-lg leading-tight text-[var(--color-heading)]"
>
<VersionIcon />
<nuxt-link
:to="getVersionLink(notif.extra_data.project, notif.extra_data.version)"
class="text-link"
v-if="invitedBy"
:to="`/user/${invitedBy.username}`"
class="inline-flex items-center font-bold text-[var(--color-heading)] hover:underline"
>
{{ notif.extra_data.version.name }}
</nuxt-link>
<span class="version-info">
for
<Categories
:categories="getLoaderCategories(notif.extra_data.version)"
:type="notif.extra_data.project.project_type"
class="categories"
<Avatar
:src="invitedBy.avatar_url"
circle
size="xxs"
no-shadow
:raised="raised"
class="mr-1.5 inline-flex"
/>
{{ $formatVersion(notif.extra_data.version.game_versions) }}
<span v-tooltip="formatDateTime(notif.extra_data.version.date_published)" class="date">
{{ formatRelativeTime(notif.extra_data.version.date_published) }}
</span>
</span>
<span>{{ invitedBy.username }}</span>
</nuxt-link>
<span v-if="invitedBy">has invited you to manage</span>
<span v-else>You have been invited to manage</span>
<span
><strong class="font-bold text-[var(--color-heading)]">{{
notification.body.server_name
}}</strong
>.</span
>
</div>
</div>
<template v-else>
{{ notification.text }}
</template>
</div>
<span class="notification__date">
<span v-if="notification.read" class="read-badge inline-flex">
<CheckCircleIcon /> Read
</span>
<span v-tooltip="formatDateTime(notification.created)" class="inline-flex">
<CalendarIcon class="mr-1" /> Received
{{ formatRelativeTime(notification.created) }}
</span>
</span>
<div v-if="compact" class="notification__actions">
<template v-if="type === 'team_invite' || type === 'organization_invite'">
<ButtonStyled circular color="brand" type="transparent">
<button
v-tooltip="`Accept`"
@click="
() => {
acceptTeamInvite(notification.body.team_id)
read()
}
"
>
<CheckIcon />
</button>
</ButtonStyled>
<ButtonStyled circular color="red" type="transparent">
<button
v-tooltip="`Decline`"
@click="
() => {
removeSelfFromTeam(notification.body.team_id)
read()
}
"
>
<XIcon />
</button>
</ButtonStyled>
</template>
<ButtonStyled v-else-if="!notification.read" circular type="transparent">
<button v-tooltip="`Mark as read`" @click="read()">
<XIcon />
</button>
</ButtonStyled>
</div>
<div v-else class="notification__actions">
<div v-if="type !== null" class="input-group">
<template
v-if="(type === 'team_invite' || type === 'organization_invite') && !notification.read"
<div
v-if="!notification.read"
class="flex flex-wrap items-center gap-3"
:class="{ 'gap-2': compact }"
>
<ButtonStyled color="brand">
<button @click="performActionByTitle(notification, 'Accept')">
<CheckIcon />
Accept
</button>
</ButtonStyled>
<ButtonStyled color="red">
<button @click="performActionByTitle(notification, 'Deny')">
<XIcon />
Decline
</button>
</ButtonStyled>
</div>
<div
class="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-[var(--color-text-secondary)]"
>
<span
v-if="notification.read"
class="inline-flex items-center font-bold text-[var(--color-text)]"
>
<CheckCircleIcon /> Read
</span>
<span v-tooltip="formatDateTime(notification.created)" class="inline-flex items-center">
<CalendarIcon class="mr-1" /> Received
{{ formatRelativeTime(notification.created) }}
</span>
<CopyCode v-if="flags.developerMode" :text="notification.id" />
</div>
</div>
</template>
<template v-else>
<nuxt-link
v-if="!type"
:to="notification.link"
class="notification__icon backed-svg"
:class="{ raised: raised }"
>
<BellIcon />
</nuxt-link>
<DoubleIcon v-else class="notification__icon">
<template #primary>
<nuxt-link v-if="project" :to="getProjectLink(project)" tabindex="-1">
<Avatar size="xs" :src="project.icon_url" :raised="raised" no-shadow />
</nuxt-link>
<nuxt-link
v-else-if="organization"
:to="`/organization/${organization.slug}`"
tabindex="-1"
>
<Avatar size="xs" :src="organization.icon_url" :raised="raised" no-shadow />
</nuxt-link>
<nuxt-link v-else-if="user" :to="getUserLink(user)" tabindex="-1">
<Avatar size="xs" :src="user.avatar_url" :raised="raised" no-shadow />
</nuxt-link>
<Avatar v-else size="xs" :raised="raised" no-shadow />
</template>
<template #secondary>
<ScaleIcon
v-if="type === 'moderator_message' || type === 'status_change'"
class="moderation-color"
/>
<UserPlusIcon v-else-if="type === 'team_invite' && project" class="creator-color" />
<UserPlusIcon
v-else-if="type === 'organization_invite' && organization"
class="creator-color"
/>
<VersionIcon v-else-if="type === 'project_update' && project && version" />
<BellIcon v-else />
</template>
</DoubleIcon>
<div class="notification__title">
<template v-if="type === 'project_update' && project && version">
A project you follow,
<nuxt-link :to="getProjectLink(project)" class="title-link">{{
project.title
}}</nuxt-link>
, has been updated:
</template>
<template v-else-if="type === 'team_invite' && project">
<nuxt-link
:to="`/user/${invitedBy.username}`"
class="iconified-link title-link inline-flex"
>
<Avatar
:src="invitedBy.avatar_url"
circle
size="xxs"
no-shadow
:raised="raised"
class="inline-flex"
/>
<span class="space">&nbsp;</span>
<span>{{ invitedBy.username }}</span>
</nuxt-link>
<span>
has invited you to join
<nuxt-link :to="getProjectLink(project)" class="title-link">
{{ project.title }} </nuxt-link
>.
</span>
</template>
<template v-else-if="type === 'organization_invite' && organization">
<nuxt-link
:to="`/user/${invitedBy.username}`"
class="iconified-link title-link inline-flex"
>
<Avatar
:src="invitedBy.avatar_url"
circle
size="xxs"
no-shadow
:raised="raised"
class="inline-flex"
/>
<span class="space">&nbsp;</span>
<span>{{ invitedBy.username }}</span>
</nuxt-link>
<span>
has invited you to join
<nuxt-link :to="`/organization/${organization.slug}`" class="title-link">
{{ organization.name }} </nuxt-link
>.
</span>
</template>
<template v-else-if="type === 'status_change' && project">
<nuxt-link :to="getProjectLink(project)" class="title-link">
{{ project.title }}
</nuxt-link>
<template v-if="tags.rejectedStatuses.includes(notification.body.new_status)">
has been
<ProjectStatusBadge :status="notification.body.new_status" />
</template>
<template v-else>
updated from
<ProjectStatusBadge :status="notification.body.old_status" />
to
<ProjectStatusBadge :status="notification.body.new_status" />
</template>
by the moderators.
</template>
<template v-else-if="type === 'moderator_message' && thread && project && !report">
Your project,
<nuxt-link :to="getProjectLink(project)" class="title-link">{{
project.title
}}</nuxt-link>
, has received
<template v-if="notification.grouped_notifs"> messages</template>
<template v-else>a message</template>
from the moderators.
</template>
<template v-else-if="type === 'moderator_message' && thread && report">
A moderator replied to your report of
<template v-if="version">
version
<nuxt-link :to="getVersionLink(project, version)" class="title-link">
{{ version.name }}
</nuxt-link>
of project
</template>
<nuxt-link v-if="project" :to="getProjectLink(project)" class="title-link">
{{ project.title }}
</nuxt-link>
<nuxt-link v-else-if="user" :to="getUserLink(user)" class="title-link">
{{ user.username }}
</nuxt-link>
.
</template>
<nuxt-link v-else :to="notification.link" class="title-link">
<span v-html="renderString(notification.title)" />
</nuxt-link>
<!-- <span v-else class="known-errors">Error reading notification.</span>-->
</div>
<div v-if="hasBody" class="notification__body">
<ThreadSummary
v-if="type === 'moderator_message' && thread"
:thread="thread"
:link="threadLink"
:raised="raised"
:messages="getMessages()"
class="thread-summary"
:auth="auth"
/>
<div v-else-if="type === 'project_update'" class="version-list">
<div
v-for="notif in (notification.grouped_notifs
? [notification, ...notification.grouped_notifs]
: [notification]
).filter((x) => x.extra_data.version)"
:key="notif.id"
class="version-link"
>
<VersionIcon />
<nuxt-link
:to="getVersionLink(notif.extra_data.project, notif.extra_data.version)"
class="text-link"
>
{{ notif.extra_data.version.name }}
</nuxt-link>
<span class="version-info">
for
<Categories
:categories="getLoaderCategories(notif.extra_data.version)"
:type="notif.extra_data.project.project_type"
class="categories"
/>
{{ $formatVersion(notif.extra_data.version.game_versions) }}
<span
v-tooltip="formatDateTime(notif.extra_data.version.date_published)"
class="date"
>
{{ formatRelativeTime(notif.extra_data.version.date_published) }}
</span>
</span>
</div>
</div>
<template v-else>
{{ notification.text }}
</template>
</div>
<span class="notification__date">
<span v-if="notification.read" class="read-badge inline-flex">
<CheckCircleIcon /> Read
</span>
<span v-tooltip="formatDateTime(notification.created)" class="inline-flex">
<CalendarIcon class="mr-1" /> Received
{{ formatRelativeTime(notification.created) }}
</span>
</span>
<div v-if="compact" class="notification__actions">
<template v-if="type === 'team_invite' || type === 'organization_invite'">
<ButtonStyled circular color="brand" type="transparent">
<button
v-tooltip="`Accept`"
@click="
() => {
acceptTeamInvite(notification.body.team_id)
@@ -247,11 +287,11 @@
"
>
<CheckIcon />
Accept
</button>
</ButtonStyled>
<ButtonStyled color="red">
<ButtonStyled circular color="red" type="transparent">
<button
v-tooltip="`Decline`"
@click="
() => {
removeSelfFromTeam(notification.body.team_id)
@@ -260,41 +300,79 @@
"
>
<XIcon />
Decline
</button>
</ButtonStyled>
</template>
<ButtonStyled v-else-if="!notification.read">
<button @click="read()">
<CheckIcon />
Mark as read
<ButtonStyled v-else-if="!notification.read" circular type="transparent">
<button v-tooltip="`Mark as read`" @click="read()">
<XIcon />
</button>
</ButtonStyled>
<CopyCode v-if="flags.developerMode" :text="notification.id" />
</div>
<div v-else class="input-group">
<ButtonStyled v-if="notification.link && notification.link !== '#'">
<nuxt-link :to="notification.link" target="_blank">
<ExternalIcon />
Open link
</nuxt-link>
</ButtonStyled>
<ButtonStyled v-for="(action, actionIndex) in notification.actions" :key="actionIndex">
<button @click="performAction(notification, actionIndex)">
<CheckIcon v-if="action.title === 'Accept'" />
<XIcon v-else-if="action.title === 'Deny'" />
{{ action.title }}
</button>
</ButtonStyled>
<ButtonStyled v-if="notification.actions.length === 0 && !notification.read">
<button @click="performAction(notification, null)">
<CheckIcon />
Mark as read
</button>
</ButtonStyled>
<CopyCode v-if="flags.developerMode" :text="notification.id" />
<div v-else class="notification__actions">
<div v-if="type !== null" class="input-group">
<template
v-if="(type === 'team_invite' || type === 'organization_invite') && !notification.read"
>
<ButtonStyled color="brand">
<button
@click="
() => {
acceptTeamInvite(notification.body.team_id)
read()
}
"
>
<CheckIcon />
Accept
</button>
</ButtonStyled>
<ButtonStyled color="red">
<button
@click="
() => {
removeSelfFromTeam(notification.body.team_id)
read()
}
"
>
<XIcon />
Decline
</button>
</ButtonStyled>
</template>
<ButtonStyled v-else-if="!notification.read">
<button @click="read()">
<CheckIcon />
Mark as read
</button>
</ButtonStyled>
<CopyCode v-if="flags.developerMode" :text="notification.id" />
</div>
<div v-else class="input-group">
<ButtonStyled v-if="notification.link && notification.link !== '#'">
<nuxt-link :to="notification.link" target="_blank">
<ExternalIcon />
Open link
</nuxt-link>
</ButtonStyled>
<ButtonStyled v-for="(action, actionIndex) in notification.actions" :key="actionIndex">
<button @click="performAction(notification, actionIndex)">
<CheckIcon v-if="action.title === 'Accept'" />
<XIcon v-else-if="action.title === 'Deny'" />
{{ action.title }}
</button>
</ButtonStyled>
<ButtonStyled v-if="notification.actions.length === 0 && !notification.read">
<button @click="performAction(notification, null)">
<CheckIcon />
Mark as read
</button>
</ButtonStyled>
<CopyCode v-if="flags.developerMode" :text="notification.id" />
</div>
</div>
</div>
</template>
</div>
</template>
@@ -328,11 +406,13 @@ import { markAsRead } from '~/helpers/platform-notifications'
import { getProjectLink, getVersionLink } from '~/helpers/projects'
import { acceptTeamInvite, removeSelfFromTeam } from '~/helpers/teams'
import ModrinthServersIcon from '../brand/ModrinthServersIcon.vue'
import ThreadSummary from './thread/ThreadSummary.vue'
const client = injectModrinthClient()
const { addNotification } = injectNotificationManager()
const emit = defineEmits(['update:notifications'])
const router = useRouter()
const formatRelativeTime = useRelativeTime()
const formatDateTime = useFormatDateTime({
timeStyle: 'short',
@@ -415,9 +495,29 @@ async function performAction(notification, actionIndex) {
await read()
if (actionIndex !== null) {
await useBaseFetch(`${notification.actions[actionIndex].action_route[1]}`, {
method: notification.actions[actionIndex].action_route[0].toUpperCase(),
})
const action = notification.actions[actionIndex]
if (type.value === 'server_invite') {
const actionName = action.title.toLowerCase()
const inviteAction = actionName === 'accept' ? 'accept' : 'decline'
const serverId = notification.body.server_id
await client.request(`/servers/${serverId}/invites/${inviteAction}`, {
api: 'archon',
version: 1,
method: 'POST',
})
if (inviteAction === 'accept') {
await router.push(`/hosting/manage/${encodeURIComponent(serverId)}`)
}
} else {
const [method, route] = action.action_route
await useBaseFetch(route, {
method: method.toUpperCase(),
})
}
}
} catch (err) {
addNotification({
@@ -429,6 +529,20 @@ async function performAction(notification, actionIndex) {
stopLoading()
}
function performActionByTitle(notification, title) {
const actionIndex = notification.actions.findIndex((action) => action.title === title)
if (actionIndex === -1) {
addNotification({
title: 'An error occurred',
text: `Missing ${title.toLowerCase()} action for notification.`,
type: 'error',
})
return
}
return performAction(notification, actionIndex)
}
function getMessages() {
const messages = []
if (props.notification.body.message_id) {
@@ -1,20 +1,22 @@
<script setup lang="ts">
import type { Archon } from '@modrinth/api-client'
import { PlusIcon, XIcon } from '@modrinth/assets'
import {
Accordion,
ButtonStyled,
injectModrinthClient,
injectNotificationManager,
NewModal,
ServerNotice,
StyledInput,
TagItem,
} from '@modrinth/ui'
import type { ServerNotice as ServerNoticeType } from '@modrinth/utils'
import { ref } from 'vue'
import { useServersFetch } from '~/composables/servers/servers-fetch.ts'
const { addNotification } = injectNotificationManager()
const client = injectModrinthClient()
type ServerNoticeType = Archon.Notices.v0.ListedNotice
const modal = ref<InstanceType<typeof NewModal>>()
@@ -32,28 +34,23 @@ const assignedNodes = computed(() => assigned.value.filter((n) => n.kind === 'no
const inputField = ref('')
async function refresh() {
await useServersFetch('notices').then((res) => {
const notices = res as ServerNoticeType[]
assigned.value = notices.find((n) => n.id === notice.value?.id)?.assigned ?? []
})
const notices = await client.archon.notices_v0.list()
assigned.value = notices.find((n) => n.id === notice.value?.id)?.assigned ?? []
}
async function assign(server: boolean = true) {
const input = inputField.value.trim()
if (input !== '' && notice.value) {
await useServersFetch(
`notices/${notice.value.id}/assign?${server ? 'server' : 'node'}=${input}`,
{
method: 'PUT',
},
).catch((err) => {
addNotification({
title: 'Error assigning notice',
text: err,
type: 'error',
await client.archon.notices_v0
.assign(notice.value.id, server ? { server: input } : { node: input })
.catch((err) => {
addNotification({
title: 'Error assigning notice',
text: err,
type: 'error',
})
})
})
} else {
addNotification({
title: 'Error assigning notice',
@@ -84,18 +81,15 @@ async function unassignDetect() {
async function unassign(id: string, server: boolean = true) {
if (notice.value) {
await useServersFetch(
`notices/${notice.value.id}/unassign?${server ? 'server' : 'node'}=${id}`,
{
method: 'PUT',
},
).catch((err) => {
addNotification({
title: 'Error unassigning notice',
text: err,
type: 'error',
await client.archon.notices_v0
.unassign(notice.value.id, server ? { server: id } : { node: id })
.catch((err) => {
addNotification({
title: 'Error unassigning notice',
text: err,
type: 'error',
})
})
})
}
await refresh()
}
@@ -125,7 +119,7 @@ defineExpose({ show, hide })
:level="notice.level"
:message="notice.message"
:dismissable="notice.dismissable"
:title="notice.title"
:title="notice.title ?? undefined"
preview
/>
<div class="flex flex-col gap-2">
@@ -133,6 +133,7 @@ import { CheckIcon, PlusIcon, XIcon } from '@modrinth/assets'
import {
ButtonStyled,
Combobox,
injectModrinthClient,
injectNotificationManager,
NewModal,
StyledInput,
@@ -143,9 +144,9 @@ import { DEFAULT_CREDIT_EMAIL_MESSAGE } from '@modrinth/utils/utils.ts'
import { computed, ref } from 'vue'
import { useBaseFetch } from '#imports'
import { useServersFetch } from '~/composables/servers/servers-fetch.ts'
const { addNotification } = injectNotificationManager()
const client = injectModrinthClient()
const modal = ref<InstanceType<typeof NewModal>>()
@@ -205,12 +206,12 @@ const applyDisabled = computed(() => {
async function ensureOverview() {
if (regions.value.length || nodeHostnames.value.length) return
try {
const data = await useServersFetch<any>('/nodes/overview', { version: 'internal' })
regions.value = (data.regions || []).map((r: any) => ({
const data = await client.archon.nodes_internal.overview()
regions.value = data.regions.map((r) => ({
value: r.key,
label: `${r.display_name} (${r.key})`,
}))
nodeHostnames.value = data.node_hostnames || []
nodeHostnames.value = data.node_hostnames
if (!selectedRegion.value && regions.value.length) selectedRegion.value = regions.value[0].value
} catch (err) {
addNotification({ title: 'Failed to load nodes overview', text: String(err), type: 'error' })
@@ -198,6 +198,7 @@ import {
ButtonStyled,
Chips,
Combobox,
injectModrinthClient,
injectNotificationManager,
NewModal,
StyledInput,
@@ -207,13 +208,12 @@ import {
import dayjs from 'dayjs'
import { computed, ref } from 'vue'
import { useServersFetch } from '~/composables/servers/servers-fetch.ts'
const emit = defineEmits<{
success: []
}>()
const { addNotification } = injectNotificationManager()
const client = injectModrinthClient()
const modal = ref<InstanceType<typeof NewModal>>()
@@ -341,12 +341,12 @@ const submitDisabled = computed(() => {
async function ensureOverview() {
if (regions.value.length || nodeHostnames.value.length) return
try {
const data = await useServersFetch<any>('/nodes/overview', { version: 'internal' })
regions.value = (data.regions || []).map((r: any) => ({
const data = await client.archon.nodes_internal.overview()
regions.value = data.regions.map((r) => ({
value: r.key,
label: `${r.display_name} (${r.key})`,
}))
nodeHostnames.value = data.node_hostnames || []
nodeHostnames.value = data.node_hostnames
if (!selectedRegion.value && regions.value.length) {
selectedRegion.value = regions.value[0].value
}
@@ -364,30 +364,22 @@ async function submit() {
scheduleOption.value === 'now' ? undefined : dayjs(scheduledDate.value).toISOString()
if (mode.value === 'servers') {
await useServersFetch('/transfers/schedule/servers', {
version: 'internal',
method: 'POST',
body: {
server_ids: parsedServerIds.value,
scheduled_at: scheduledAt,
target_region: selectedRegion.value || undefined,
node_tags: selectedTags.value.length > 0 ? selectedTags.value : undefined,
reason: reason.value.trim(),
},
await client.archon.transfers_internal.scheduleServers({
server_ids: parsedServerIds.value,
scheduled_at: scheduledAt,
target_region: selectedRegion.value || undefined,
node_tags: selectedTags.value.length > 0 ? selectedTags.value : undefined,
reason: reason.value.trim(),
})
} else {
await useServersFetch('/transfers/schedule/nodes', {
version: 'internal',
method: 'POST',
body: {
node_hostnames: selectedNodes.value.slice(),
scheduled_at: scheduledAt,
target_region: selectedRegion.value || undefined,
node_tags: selectedTags.value.length > 0 ? selectedTags.value : undefined,
reason: reason.value.trim(),
cordon_nodes: cordonNodes.value,
tag_nodes: tagNodes.value.trim() || undefined,
},
await client.archon.transfers_internal.scheduleNodes({
node_hostnames: selectedNodes.value.slice(),
scheduled_at: scheduledAt,
target_region: selectedRegion.value || undefined,
node_tags: selectedTags.value.length > 0 ? selectedTags.value : undefined,
reason: reason.value.trim(),
cordon_nodes: cordonNodes.value,
tag_nodes: tagNodes.value.trim() || undefined,
})
}
@@ -48,7 +48,12 @@
>{{ isIncome ? '' : '-' }}{{ formatMoney(transaction.amount) }}</span
>
<template v-if="transaction.type === 'withdrawal' && transaction.status === 'in-transit'">
<Tooltip theme="dismissable-prompt" :triggers="['hover', 'focus']" no-auto-focus>
<Tooltip
theme="dismissable-prompt"
class="inline-flex shrink-0"
:triggers="['hover', 'focus']"
no-auto-focus
>
<span class="my-auto align-middle"
><ButtonStyled circular type="outlined" size="small">
<button class="align-middle" @click="cancelPayout">