mirror of
https://github.com/modrinth/code.git
synced 2026-08-26 17:44:50 +00:00
feat: tech review global trace database (#6648)
* global detail verdicts system * global traces frontend * split global routes into own mod * tweak migration to only do no-key check at app level * prepr
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FolderIcon, GlobeIcon, ReportIcon, ShieldCheckIcon } from '@modrinth/assets'
|
||||
import { FolderIcon, GlobeIcon, HashIcon, ReportIcon, ShieldCheckIcon } from '@modrinth/assets'
|
||||
import { Chips, defineMessages, NavTabs, useVIntl } from '@modrinth/ui'
|
||||
|
||||
definePageMeta({
|
||||
@@ -47,6 +47,10 @@ const messages = defineMessages({
|
||||
id: 'moderation.page.external-projects',
|
||||
defaultMessage: 'External projects',
|
||||
},
|
||||
globalDetailTracesTitle: {
|
||||
id: 'moderation.page.global-detail-traces',
|
||||
defaultMessage: 'Global traces',
|
||||
},
|
||||
})
|
||||
|
||||
const moderationLinks = [
|
||||
@@ -62,6 +66,11 @@ const moderationLinks = [
|
||||
href: '/moderation/external-projects',
|
||||
icon: GlobeIcon,
|
||||
},
|
||||
{
|
||||
label: formatMessage(messages.globalDetailTracesTitle),
|
||||
href: '/moderation/global-traces',
|
||||
icon: HashIcon,
|
||||
},
|
||||
]
|
||||
|
||||
const mobileNavOptions = [
|
||||
@@ -69,15 +78,20 @@ const mobileNavOptions = [
|
||||
formatMessage(messages.technicalReviewTitle),
|
||||
formatMessage(messages.reportsTitle),
|
||||
formatMessage(messages.externalFilesTitle),
|
||||
formatMessage(messages.globalDetailTracesTitle),
|
||||
]
|
||||
|
||||
const selectedChip = computed({
|
||||
get() {
|
||||
const path = route.path
|
||||
if (path === '/moderation/technical-review') {
|
||||
if (path.startsWith('/moderation/technical-review')) {
|
||||
return formatMessage(messages.technicalReviewTitle)
|
||||
} else if (path.startsWith('/moderation/reports/')) {
|
||||
} else if (path.startsWith('/moderation/reports')) {
|
||||
return formatMessage(messages.reportsTitle)
|
||||
} else if (path.startsWith('/moderation/external-projects')) {
|
||||
return formatMessage(messages.externalFilesTitle)
|
||||
} else if (path.startsWith('/moderation/global-traces')) {
|
||||
return formatMessage(messages.globalDetailTracesTitle)
|
||||
} else {
|
||||
return formatMessage(messages.projectsTitle)
|
||||
}
|
||||
@@ -92,6 +106,10 @@ function navigateToPage(selectedOption: string) {
|
||||
router.push('/moderation/technical-review')
|
||||
} else if (selectedOption === formatMessage(messages.reportsTitle)) {
|
||||
router.push('/moderation/reports')
|
||||
} else if (selectedOption === formatMessage(messages.externalFilesTitle)) {
|
||||
router.push('/moderation/external-projects')
|
||||
} else if (selectedOption === formatMessage(messages.globalDetailTracesTitle)) {
|
||||
router.push('/moderation/global-traces')
|
||||
} else {
|
||||
router.push('/moderation')
|
||||
}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<ButtonStyled>
|
||||
<NuxtLink to="/moderation/global-traces">
|
||||
<ArrowLeftIcon aria-hidden="true" />
|
||||
Back to global traces
|
||||
</NuxtLink>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<EmptyState
|
||||
v-if="isLoading && !trace"
|
||||
type="no-search-result"
|
||||
heading="Loading global detail trace..."
|
||||
/>
|
||||
<EmptyState
|
||||
v-else-if="loadError"
|
||||
type="no-search-result"
|
||||
heading="Failed to load global detail trace"
|
||||
/>
|
||||
<article v-else-if="trace" class="universal-card flex flex-col gap-3">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<HashIcon class="shrink-0 text-secondary" aria-hidden="true" />
|
||||
<h2 class="m-0 min-w-0 text-lg font-semibold text-contrast">
|
||||
Trace
|
||||
<span class="break-all font-mono text-base">{{ trace.detail_key }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
<p class="m-0 mt-1 text-sm text-secondary">
|
||||
{{ pageStart }}-{{ pageEnd }} of {{ trace.local_trace_count }} local traces
|
||||
</p>
|
||||
</div>
|
||||
<Badge :type="trace.verdict" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="trace.local_trace_count > localTracePageSize"
|
||||
class="flex flex-wrap items-center justify-between gap-3"
|
||||
>
|
||||
<Pagination :page="currentPage" :count="pageCount" @switch-page="switchPage" />
|
||||
<p v-if="isLoading" class="m-0 text-sm text-secondary">Loading page...</p>
|
||||
</div>
|
||||
|
||||
<div v-if="trace.local_traces.length > 0" class="flex flex-col gap-2">
|
||||
<GlobalDetailLocalTraceCard
|
||||
v-for="localTrace in trace.local_traces"
|
||||
:key="localTrace.detail_id"
|
||||
:trace="localTrace"
|
||||
/>
|
||||
</div>
|
||||
<EmptyState v-else type="no-search-result" heading="No local traces match this key" />
|
||||
|
||||
<div v-if="trace.local_trace_count > localTracePageSize" class="mt-1 flex justify-end">
|
||||
<Pagination :page="currentPage" :count="pageCount" @switch-page="switchPage" />
|
||||
</div>
|
||||
</article>
|
||||
<EmptyState v-else type="no-search-result" heading="Global detail trace not found" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { ArrowLeftIcon, HashIcon } from '@modrinth/assets'
|
||||
import { Badge, ButtonStyled, EmptyState, injectModrinthClient, Pagination } from '@modrinth/ui'
|
||||
|
||||
import GlobalDetailLocalTraceCard from '~/components/ui/moderation/GlobalDetailLocalTraceCard.vue'
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const route = useRoute()
|
||||
|
||||
const detailKey = computed(() => {
|
||||
const key = route.params.key
|
||||
return Array.isArray(key) ? key.join('/') : String(key)
|
||||
})
|
||||
|
||||
useHead({ title: () => `Global trace - ${detailKey.value} - Modrinth` })
|
||||
|
||||
const localTracePageSize = 20
|
||||
const isLoading = ref(false)
|
||||
const loadError = ref(false)
|
||||
const currentPage = ref(1)
|
||||
const pageStartCursors = ref<(string | null)[]>([null])
|
||||
const trace = ref<Labrinth.TechReview.Internal.GlobalIssueDetail | null>(null)
|
||||
|
||||
const pageCount = computed(() =>
|
||||
Math.max(Math.ceil((trace.value?.local_trace_count ?? 0) / localTracePageSize), 1),
|
||||
)
|
||||
const pageStart = computed(() =>
|
||||
trace.value && trace.value.local_trace_count > 0
|
||||
? (currentPage.value - 1) * localTracePageSize + 1
|
||||
: 0,
|
||||
)
|
||||
const pageEnd = computed(() =>
|
||||
Math.min(currentPage.value * localTracePageSize, trace.value?.local_trace_count ?? 0),
|
||||
)
|
||||
|
||||
async function fetchTracePage(afterDetailId: string | null) {
|
||||
return await client.labrinth.tech_review_internal.getGlobalIssueDetail({
|
||||
detail_key: detailKey.value,
|
||||
limit: localTracePageSize,
|
||||
after_detail_id: afterDetailId,
|
||||
})
|
||||
}
|
||||
|
||||
async function loadPage(page: number) {
|
||||
if (page < 1 || isLoading.value) return
|
||||
|
||||
isLoading.value = true
|
||||
loadError.value = false
|
||||
|
||||
try {
|
||||
while (pageStartCursors.value.length < page) {
|
||||
const cursor = pageStartCursors.value[pageStartCursors.value.length - 1]
|
||||
const response = await fetchTracePage(cursor)
|
||||
|
||||
if (!response.next_after_detail_id) {
|
||||
trace.value = response.trace
|
||||
currentPage.value = pageStartCursors.value.length
|
||||
return
|
||||
}
|
||||
|
||||
pageStartCursors.value.push(response.next_after_detail_id)
|
||||
}
|
||||
|
||||
const response = await fetchTracePage(pageStartCursors.value[page - 1])
|
||||
trace.value = response.trace
|
||||
currentPage.value = page
|
||||
} catch (error) {
|
||||
console.error('Failed to load global detail trace', error)
|
||||
loadError.value = true
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function switchPage(page: number) {
|
||||
await loadPage(page)
|
||||
}
|
||||
|
||||
watch(
|
||||
detailKey,
|
||||
() => {
|
||||
currentPage.value = 1
|
||||
pageStartCursors.value = [null]
|
||||
trace.value = null
|
||||
loadPage(1)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<GlobalDetailTracesList />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import GlobalDetailTracesList from '~/components/ui/moderation/GlobalDetailTracesList.vue'
|
||||
|
||||
useHead({ title: 'Global detail traces - Modrinth' })
|
||||
</script>
|
||||
@@ -11,6 +11,7 @@ import ModerationTechRevCard from '~/components/ui/moderation/ModerationTechRevC
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const queryClient = useQueryClient()
|
||||
const route = useRoute()
|
||||
|
||||
const projectId = String(useRouteId('project'))
|
||||
|
||||
@@ -245,6 +246,8 @@ const reviewItem = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const focusedDetailId = computed(() => route.query.detail?.toString() ?? null)
|
||||
|
||||
async function handleMarkComplete(projectId: string) {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ['tech-reviews'] }),
|
||||
@@ -299,6 +302,7 @@ function refetch() {
|
||||
<ModerationTechRevCard
|
||||
v-else
|
||||
:item="reviewItem"
|
||||
:focused-detail-id="focusedDetailId"
|
||||
:loading-issues="loadingIssues"
|
||||
:decompiled-sources="decompiledSources"
|
||||
@refetch="refetch"
|
||||
|
||||
Reference in New Issue
Block a user