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 15:58:01 +00:00
committed by GitHub
co-authored by Prospector
parent 58ad58f958
commit bd97ace974
227 changed files with 15578 additions and 2153 deletions
+6 -2
View File
@@ -23,6 +23,10 @@ function copy(id: string) {
navigator.clipboard?.writeText(`/_internal/templates/email/${id}`).catch(() => {})
}
function uncachedPreviewUrl(id: string) {
return `/_internal/templates/email/${id}?preview=${Date.now()}`
}
const previewModal = ref<{ hide: () => void; show: () => void } | null>(null)
const previewTemplate = ref<string | null>(null)
const previewLoading = ref(false)
@@ -73,7 +77,7 @@ async function openPreview(id: string, event?: MouseEvent) {
variableValues.value = {}
try {
const response = await fetch(`/_internal/templates/email/${id}`)
const response = await fetch(uncachedPreviewUrl(id), { cache: 'no-store' })
previewHtml.value = await response.text()
if (!response.ok) {
@@ -103,7 +107,7 @@ function openPopupPreview(id: string, offset = 0) {
const left = window.screenX + (window.outerWidth - width) / 2 + ((offset * 28) % 320)
const top = window.screenY + (window.outerHeight - height) / 2 + ((offset * 28) % 320)
window.open(
`/_internal/templates/email/${id}`,
uncachedPreviewUrl(id),
`email-${id}`,
`popup=yes,width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes,menubar=no,toolbar=no,location=no,status=no`,
)
@@ -218,7 +218,7 @@
:level="notice.level"
:message="notice.message"
:dismissable="notice.dismissable"
:title="notice.title"
:title="notice.title ?? undefined"
preview
/>
<div class="mt-4 flex items-center gap-2">
@@ -260,6 +260,7 @@
</div>
</template>
<script setup lang="ts">
import type { Archon } from '@modrinth/api-client'
import { EditIcon, PlusIcon, SaveIcon, SettingsIcon, TrashIcon, XIcon } from '@modrinth/assets'
import {
ButtonStyled,
@@ -267,6 +268,7 @@ import {
commonMessages,
CopyCode,
defineMessages,
injectModrinthClient,
injectNotificationManager,
NewModal,
ServerNotice,
@@ -278,14 +280,13 @@ import {
useVIntl,
} from '@modrinth/ui'
import { NOTICE_LEVELS } from '@modrinth/ui/src/utils/notices.ts'
import type { ServerNotice as ServerNoticeType } from '@modrinth/utils'
import dayjs from 'dayjs'
import { computed } from 'vue'
import AssignNoticeModal from '~/components/ui/admin/AssignNoticeModal.vue'
import { useServersFetch } from '~/composables/servers/servers-fetch.ts'
const { addNotification } = injectNotificationManager()
const client = injectModrinthClient()
const { formatMessage } = useVIntl()
const formatRelativeTime = useRelativeTime()
const formatDateTime = useFormatDateTime({
@@ -297,6 +298,8 @@ const formatDateTimeShortMonth = useFormatDateTime({
dateStyle: 'medium',
})
type ServerNoticeType = Archon.Notices.v0.ListedNotice
const notices = ref<ServerNoticeType[]>([])
const createNoticeModal = ref<InstanceType<typeof NewModal>>()
const assignNoticeModal = ref<InstanceType<typeof AssignNoticeModal>>()
@@ -304,16 +307,14 @@ const assignNoticeModal = ref<InstanceType<typeof AssignNoticeModal>>()
await refreshNotices()
async function refreshNotices() {
await useServersFetch('notices').then((res) => {
notices.value = res as ServerNoticeType[]
notices.value.sort((a, b) => {
const dateDiff = dayjs(b.announce_at).diff(dayjs(a.announce_at))
if (dateDiff === 0) {
return b.id - a.id
}
notices.value = await client.archon.notices_v0.list()
notices.value.sort((a, b) => {
const dateDiff = dayjs(b.announce_at).diff(dayjs(a.announce_at))
if (dateDiff === 0) {
return b.id - a.id
}
return dateDiff
})
return dateDiff
})
}
@@ -347,7 +348,7 @@ function startEditing(notice: ServerNoticeType, assignments: boolean = false) {
newNoticeLevel.value = levelOptions.find((x) => x.id === notice.level) ?? levelOptions[0]
newNoticeDismissable.value = notice.dismissable
newNoticeMessage.value = notice.message
newNoticeTitle.value = notice.title
newNoticeTitle.value = notice.title ?? undefined
newNoticeScheduledDate.value = dayjs(notice.announce_at).format(DATE_TIME_FORMAT)
newNoticeExpiresDate.value = notice.expires
? dayjs(notice.expires).format(DATE_TIME_FORMAT)
@@ -361,9 +362,8 @@ function startEditing(notice: ServerNoticeType, assignments: boolean = false) {
}
async function deleteNotice(notice: ServerNoticeType) {
await useServersFetch(`notices/${notice.id}`, {
method: 'DELETE',
})
await client.archon.notices_v0
.delete(notice.id)
.then(() => {
addNotification({
title: `Successfully deleted notice #${notice.id}`,
@@ -412,9 +412,10 @@ async function saveChanges() {
return
}
await useServersFetch(`notices/${editingNotice.value?.id}`, {
method: 'PATCH',
body: {
if (!editingNotice.value) return
await client.archon.notices_v0
.update(editingNotice.value.id, {
message: newNoticeMessage.value,
title: newNoticeSurvey.value ? undefined : trimmedTitle.value,
level: newNoticeLevel.value.id,
@@ -425,14 +426,14 @@ async function saveChanges() {
expires: newNoticeExpiresDate.value
? dayjs(newNoticeExpiresDate.value).toISOString()
: undefined,
},
}).catch((err) => {
addNotification({
title: 'Error saving changes to notice',
text: err,
type: 'error',
})
})
.catch((err) => {
addNotification({
title: 'Error saving changes to notice',
text: err,
type: 'error',
})
})
await refreshNotices()
createNoticeModal.value?.hide()
}
@@ -442,9 +443,8 @@ async function createNotice() {
return
}
await useServersFetch('notices', {
method: 'POST',
body: {
await client.archon.notices_v0
.create({
message: newNoticeMessage.value,
title: newNoticeSurvey.value ? undefined : trimmedTitle.value,
level: newNoticeLevel.value.id,
@@ -455,14 +455,14 @@ async function createNotice() {
expires: newNoticeExpiresDate.value
? dayjs(newNoticeExpiresDate.value).toISOString()
: undefined,
},
}).catch((err) => {
addNotification({
title: 'Error creating notice',
text: err,
type: 'error',
})
})
.catch((err) => {
addNotification({
title: 'Error creating notice',
text: err,
type: 'error',
})
})
await refreshNotices()
createNoticeModal.value?.hide()
}
@@ -108,11 +108,13 @@
</template>
<script setup lang="ts">
import type { Archon } from '@modrinth/api-client'
import { PlusIcon, XCircleIcon } from '@modrinth/assets'
import {
Avatar,
ButtonStyled,
ConfirmModal,
injectModrinthClient,
injectNotificationManager,
Pagination,
TagItem,
@@ -124,38 +126,16 @@ import dayjs from 'dayjs'
import { computed, ref } from 'vue'
import TransferModal from '~/components/ui/admin/TransferModal.vue'
import { useServersFetch } from '~/composables/servers/servers-fetch.ts'
const { addNotification } = injectNotificationManager()
const client = injectModrinthClient()
const formatRelativeTime = useRelativeTime()
const formatDateTime = useFormatDateTime({
timeStyle: 'short',
dateStyle: 'long',
})
// Types
interface ProvisionOptions {
region?: string | null
node_tags?: string[]
}
interface TransferBatch {
id: number
created_by: string
created_at: string
reason: string | null
scheduled_at: string
cancelled: boolean
log_count: number
provision_options: ProvisionOptions
}
interface HistoryResponse {
batches: TransferBatch[]
total: number
page: number
page_size: number
}
type TransferBatch = Archon.Transfers.Internal.TransferLogBatchEntry
const transferModal = ref<InstanceType<typeof TransferModal>>()
const cancelModal = ref<InstanceType<typeof ConfirmModal>>()
@@ -178,10 +158,10 @@ async function refreshHistory() {
loading.value = true
error.value = null
try {
const data = await useServersFetch<HistoryResponse>(
`/transfers/history?page=${currentPage.value}&page_size=${pageSize}`,
{ version: 'internal' },
)
const data = await client.archon.transfers_internal.history({
page: currentPage.value,
page_size: pageSize,
})
batches.value = data.batches || []
total.value = data.total || 0
@@ -283,12 +263,8 @@ function showCancelModal(batchId: number) {
async function confirmCancel() {
if (!cancellingBatchId.value) return
try {
await useServersFetch('/transfers/cancel', {
version: 'internal',
method: 'POST',
body: {
batch_ids: [cancellingBatchId.value],
},
await client.archon.transfers_internal.cancel({
batch_ids: [cancellingBatchId.value],
})
addNotification({
title: 'Transfer cancelled',