mirror of
https://github.com/modrinth/code.git
synced 2026-07-31 21:26:40 +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:
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div class="rounded-lg border border-divider bg-bg-raised p-3">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<p class="m-0 break-words font-semibold text-contrast">
|
||||
{{ trace.project_name }}
|
||||
</p>
|
||||
<p class="m-0 mt-1 break-all text-sm text-secondary">
|
||||
Project {{ trace.project_slug ?? trace.project_id }} / Version
|
||||
{{ trace.version_number }} / File {{ trace.file_name }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm text-secondary">Local</span>
|
||||
<Badge :type="trace.local_status" />
|
||||
<span class="text-sm text-secondary">Effective</span>
|
||||
<Badge :type="trace.effective_status" />
|
||||
<ButtonStyled>
|
||||
<NuxtLink :to="localTraceLink">
|
||||
<ExternalIcon aria-hidden="true" />
|
||||
View
|
||||
</NuxtLink>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 grid gap-2 text-sm text-secondary md:grid-cols-2">
|
||||
<p class="m-0 break-all">
|
||||
<span class="font-semibold text-contrast">Issue</span>
|
||||
{{ trace.issue_type }}
|
||||
</p>
|
||||
<p class="m-0 break-all">
|
||||
<span class="font-semibold text-contrast">Severity</span>
|
||||
{{ trace.severity }}
|
||||
</p>
|
||||
<p class="m-0 break-all">
|
||||
<span class="font-semibold text-contrast">Path</span>
|
||||
{{ trace.file_path }}
|
||||
</p>
|
||||
<p v-if="trace.jar" class="m-0 break-all">
|
||||
<span class="font-semibold text-contrast">JAR</span>
|
||||
{{ trace.jar }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { ExternalIcon } from '@modrinth/assets'
|
||||
import { Badge, ButtonStyled } from '@modrinth/ui'
|
||||
|
||||
const props = defineProps<{
|
||||
trace: Labrinth.TechReview.Internal.GlobalIssueDetailTrace
|
||||
}>()
|
||||
|
||||
const localTraceLink = computed(
|
||||
() =>
|
||||
`/moderation/technical-review/${props.trace.project_id}?detail=${encodeURIComponent(
|
||||
props.trace.detail_id,
|
||||
)}`,
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<div>
|
||||
<form class="flex flex-col gap-2 sm:flex-row" @submit.prevent="executeSearch">
|
||||
<StyledInput
|
||||
v-model="query"
|
||||
:icon="SearchIcon"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
placeholder="Search global trace keys..."
|
||||
clearable
|
||||
wrapper-class="flex-1 w-full"
|
||||
/>
|
||||
<ButtonStyled color="brand">
|
||||
<button type="submit" :disabled="isLoading">
|
||||
<SearchIcon aria-hidden="true" />
|
||||
Search
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</form>
|
||||
|
||||
<div
|
||||
v-if="!isLoading && !loadError && total > 0"
|
||||
class="mt-4 flex flex-wrap items-center justify-between gap-3"
|
||||
>
|
||||
<p class="m-0 text-sm text-secondary">Showing {{ pageStart }}-{{ pageEnd }} of {{ total }}</p>
|
||||
<Pagination :page="currentPage" :count="pageCount" @switch-page="switchPage" />
|
||||
</div>
|
||||
|
||||
<EmptyState
|
||||
v-if="isLoading"
|
||||
type="no-search-result"
|
||||
heading="Loading global detail traces..."
|
||||
/>
|
||||
<EmptyState
|
||||
v-else-if="loadError"
|
||||
type="no-search-result"
|
||||
heading="Failed to load global detail traces"
|
||||
/>
|
||||
<div v-else-if="traces.length > 0" class="mt-4 flex flex-col gap-3">
|
||||
<article
|
||||
v-for="trace in traces"
|
||||
:key="trace.detail_key"
|
||||
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">
|
||||
{{ formatTraceCount(trace.local_trace_count) }}
|
||||
</p>
|
||||
</div>
|
||||
<Badge :type="trace.verdict" />
|
||||
</div>
|
||||
|
||||
<div v-if="getPreviewLocalTraces(trace).length > 0" class="flex flex-col gap-2">
|
||||
<div
|
||||
v-if="getVisibleLocalTraceTotal(trace) > getPreviewLocalTraces(trace).length"
|
||||
class="flex flex-wrap items-center justify-between gap-2"
|
||||
>
|
||||
<p class="m-0 text-sm text-secondary">
|
||||
Showing first {{ getPreviewLocalTraces(trace).length }} of
|
||||
{{ getVisibleLocalTraceTotal(trace) }} local traces
|
||||
</p>
|
||||
<ButtonStyled>
|
||||
<NuxtLink :to="getGlobalTraceLink(trace)">
|
||||
<ListIcon aria-hidden="true" />
|
||||
View all
|
||||
</NuxtLink>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<GlobalDetailLocalTraceCard
|
||||
v-for="localTrace in getPreviewLocalTraces(trace)"
|
||||
:key="localTrace.detail_id"
|
||||
:trace="localTrace"
|
||||
/>
|
||||
</div>
|
||||
<EmptyState
|
||||
v-else
|
||||
type="no-search-result"
|
||||
heading="No local traces currently match this key"
|
||||
/>
|
||||
</article>
|
||||
</div>
|
||||
<EmptyState v-else type="no-search-result" heading="No global detail traces found" />
|
||||
|
||||
<div v-if="!isLoading && !loadError && total > 0" class="mt-4 flex justify-end">
|
||||
<Pagination :page="currentPage" :count="pageCount" @switch-page="switchPage" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { HashIcon, ListIcon, SearchIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Badge,
|
||||
ButtonStyled,
|
||||
EmptyState,
|
||||
injectModrinthClient,
|
||||
Pagination,
|
||||
StyledInput,
|
||||
} from '@modrinth/ui'
|
||||
|
||||
import GlobalDetailLocalTraceCard from '~/components/ui/moderation/GlobalDetailLocalTraceCard.vue'
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const query = ref('')
|
||||
const activeQuery = ref<string | null>(null)
|
||||
const isLoading = ref(false)
|
||||
const loadError = ref(false)
|
||||
const currentPage = ref(1)
|
||||
const itemsPerPage = 20
|
||||
const localTracePreviewLimit = 10
|
||||
const total = ref(0)
|
||||
const traces = ref<Labrinth.TechReview.Internal.GlobalIssueDetail[]>([])
|
||||
|
||||
const pageCount = computed(() => Math.max(Math.ceil(total.value / itemsPerPage), 1))
|
||||
const pageStart = computed(() =>
|
||||
total.value === 0 ? 0 : (currentPage.value - 1) * itemsPerPage + 1,
|
||||
)
|
||||
const pageEnd = computed(() => Math.min(currentPage.value * itemsPerPage, total.value))
|
||||
|
||||
function formatTraceCount(count: number) {
|
||||
return `${count} local ${count === 1 ? 'trace' : 'traces'}`
|
||||
}
|
||||
|
||||
function getPreviewLocalTraces(trace: Labrinth.TechReview.Internal.GlobalIssueDetail) {
|
||||
return trace.local_traces.slice(0, localTracePreviewLimit)
|
||||
}
|
||||
|
||||
function getVisibleLocalTraceTotal(trace: Labrinth.TechReview.Internal.GlobalIssueDetail) {
|
||||
return Math.max(trace.local_trace_count, trace.local_traces.length)
|
||||
}
|
||||
|
||||
function getGlobalTraceLink(trace: Labrinth.TechReview.Internal.GlobalIssueDetail) {
|
||||
return `/moderation/global-traces/${encodeURIComponent(trace.detail_key)}`
|
||||
}
|
||||
|
||||
async function loadTraces() {
|
||||
isLoading.value = true
|
||||
loadError.value = false
|
||||
|
||||
try {
|
||||
const response = await client.labrinth.tech_review_internal.searchGlobalIssueDetails({
|
||||
query: activeQuery.value,
|
||||
limit: itemsPerPage,
|
||||
page: currentPage.value - 1,
|
||||
})
|
||||
|
||||
traces.value = response.traces
|
||||
total.value = response.total
|
||||
} catch (error) {
|
||||
console.error('Failed to load global detail traces', error)
|
||||
traces.value = []
|
||||
total.value = 0
|
||||
loadError.value = true
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function executeSearch() {
|
||||
activeQuery.value = query.value.trim() || null
|
||||
currentPage.value = 1
|
||||
await loadTraces()
|
||||
}
|
||||
|
||||
async function switchPage(page: number) {
|
||||
currentPage.value = page
|
||||
await loadTraces()
|
||||
}
|
||||
|
||||
onMounted(loadTraces)
|
||||
</script>
|
||||
@@ -45,7 +45,7 @@ import {
|
||||
type User,
|
||||
} from '@modrinth/utils'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { computed, nextTick, reactive, ref, watch } from 'vue'
|
||||
|
||||
import type { UnsafeFile } from '~/components/ui/moderation/MaliciousSummaryModal.vue'
|
||||
import ThreadView from '~/components/ui/thread/ThreadView.vue'
|
||||
@@ -89,6 +89,7 @@ const props = defineProps<{
|
||||
thread: Labrinth.TechReview.Internal.Thread
|
||||
reports: FlattenedFileReport[]
|
||||
}
|
||||
focusedDetailId?: string | null
|
||||
loadingIssues: Set<string>
|
||||
decompiledSources: Map<string, string>
|
||||
}>()
|
||||
@@ -192,7 +193,12 @@ watch(selectedFile, (newFile) => {
|
||||
|
||||
const client = injectModrinthClient()
|
||||
|
||||
async function updateIssueDetails(data: { detail_id: string; verdict: 'safe' | 'unsafe' }[]) {
|
||||
async function updateIssueDetails(
|
||||
data: {
|
||||
detail_id: string
|
||||
verdict: Labrinth.TechReview.Internal.DelphiReportIssueStatus
|
||||
}[],
|
||||
) {
|
||||
await client.request('/moderation/tech-review/issue-detail', {
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
@@ -381,6 +387,49 @@ function viewFileFlags(file: FlattenedFileReport) {
|
||||
currentTab.value = 'File'
|
||||
}
|
||||
|
||||
function getDetailElementId(detailId: string) {
|
||||
return `tech-review-detail-${detailId}`
|
||||
}
|
||||
|
||||
function findFileForDetail(detailId: string): FlattenedFileReport | null {
|
||||
for (const report of props.item.reports) {
|
||||
for (const issue of report.issues) {
|
||||
if (issue.details.some((detail) => detail.id === detailId)) {
|
||||
return report
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async function focusDetail(detailId: string) {
|
||||
const file = findFileForDetail(detailId)
|
||||
if (!file) return
|
||||
|
||||
viewFileFlags(file)
|
||||
await nextTick()
|
||||
|
||||
const classItem = groupedByClass.value.find((group) =>
|
||||
group.flags.some((flag) => flag.detail.id === detailId),
|
||||
)
|
||||
|
||||
if (classItem) {
|
||||
expandClass(classItem)
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
if (!import.meta.client) return
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
document.getElementById(getDetailElementId(detailId))?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function backToFileList() {
|
||||
selectedFileId.value = null
|
||||
if (currentTab.value === 'File') {
|
||||
@@ -691,6 +740,16 @@ const groupedByJar = computed<JarGroup[]>(() => {
|
||||
})
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.focusedDetailId,
|
||||
(detailId) => {
|
||||
if (detailId) {
|
||||
focusDetail(detailId)
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// Auto-expand/load source for small files; keep larger files lazy.
|
||||
watch(
|
||||
[selectedFileId, groupedByClass],
|
||||
@@ -1384,8 +1443,12 @@ function copyId() {
|
||||
>
|
||||
<div
|
||||
v-for="flag in classItem.flags"
|
||||
:id="getDetailElementId(flag.detail.id)"
|
||||
:key="`${flag.issueId}-${flag.detail.id}`"
|
||||
class="flex flex-col gap-2 rounded-lg border-[1px] border-b border-solid border-surface-5 bg-surface-3 py-2 pl-4 last:border-b-0"
|
||||
:class="{
|
||||
'!border-brand bg-brand-highlight': props.focusedDetailId === flag.detail.id,
|
||||
}"
|
||||
>
|
||||
<div class="grid grid-cols-[1fr_auto] items-center">
|
||||
<div
|
||||
|
||||
@@ -2681,6 +2681,9 @@
|
||||
"moderation.page.external-projects": {
|
||||
"message": "External projects"
|
||||
},
|
||||
"moderation.page.global-detail-traces": {
|
||||
"message": "Global traces"
|
||||
},
|
||||
"moderation.page.projects": {
|
||||
"message": "Projects"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
|
||||
Generated
-34
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n m.id AS \"project_id: DBProjectId\",\n MIN(t.id) AS \"thread_id!: DBThreadId\"\n FROM mods m\n INNER JOIN threads t ON t.mod_id = m.id\n INNER JOIN versions v ON v.mod_id = m.id\n INNER JOIN files f ON f.version_id = v.id\n INNER JOIN delphi_reports dr ON dr.file_id = f.id\n INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id\n INNER JOIN delphi_report_issue_details drid\n ON drid.issue_id = dri.id\n LEFT JOIN delphi_issue_detail_verdicts didv\n ON m.id = didv.project_id AND drid.key = didv.detail_key\n LEFT JOIN threads_messages tm_last\n ON tm_last.thread_id = t.id\n AND tm_last.id = (\n SELECT id FROM threads_messages\n WHERE thread_id = t.id\n ORDER BY created DESC\n LIMIT 1\n )\n LEFT JOIN users u_last\n ON u_last.id = tm_last.author_id\n WHERE\n (\n cardinality($4::text[]) = 0\n OR (\n 'minecraft_java_server' = ANY($4::text[])\n AND (\n m.components ? 'minecraft_server'\n OR m.components ? 'minecraft_java_server'\n )\n )\n OR EXISTS (\n SELECT 1\n FROM versions type_v\n INNER JOIN loaders_versions type_lv\n ON type_lv.version_id = type_v.id\n INNER JOIN loaders_project_types type_lpt\n ON type_lpt.joining_loader_id = type_lv.loader_id\n INNER JOIN project_types type_pt\n ON type_pt.id = type_lpt.joining_project_type_id\n WHERE\n type_v.mod_id = m.id\n AND type_pt.name = ANY($4::text[])\n AND (\n type_pt.name != 'modpack'\n OR NOT (\n m.components ? 'minecraft_server'\n OR m.components ? 'minecraft_java_server'\n )\n )\n )\n )\n AND m.status NOT IN ('draft', 'rejected', 'withheld')\n AND (cardinality($6::text[]) = 0 OR m.status = ANY($6::text[]))\n AND (cardinality($7::text[]) = 0 OR dri.issue_type = ANY($7::text[]))\n AND (didv.verdict IS NULL OR didv.verdict = 'pending'::delphi_report_issue_status)\n AND (\n $5::text IS NULL\n OR ($5::text = 'unreplied' AND (tm_last.id IS NULL OR u_last.role IS NULL OR u_last.role NOT IN ('moderator', 'admin')))\n OR ($5::text = 'replied' AND tm_last.id IS NOT NULL AND u_last.role IS NOT NULL AND u_last.role IN ('moderator', 'admin'))\n )\n GROUP BY m.id\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN MIN(dr.created) ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN MIN(dr.created) ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'severity_asc' THEN MAX(dr.severity) ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN MAX(dr.severity) ELSE 'low'::delphi_severity END DESC,\n -- tie-breaker: oldest reports\n MIN(dr.created) ASC\n LIMIT $1 OFFSET $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "project_id: DBProjectId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "thread_id!: DBThreadId",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Text",
|
||||
"TextArray",
|
||||
"Text",
|
||||
"TextArray",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "0545adc0340800b9fb4c23eb5ec2b30d5bba824f80cd25dbf08ca9f86c32ea1f"
|
||||
}
|
||||
Generated
+148
@@ -0,0 +1,148 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n didws.key AS \"detail_key!\",\n didws.id AS \"detail_id!: DelphiReportIssueDetailsId\",\n didws.issue_id AS \"issue_id!: DelphiReportIssueId\",\n dri.issue_type,\n m.id AS \"project_id!: DBProjectId\",\n m.slug AS \"project_slug?\",\n m.name AS \"project_name!\",\n v.id AS \"version_id!: DBVersionId\",\n v.version_number,\n f.id AS \"file_id!: DBFileId\",\n f.filename AS \"file_name!\",\n didws.jar AS \"jar?\",\n didws.file_path AS \"file_path!\",\n didws.severity AS \"severity!: DelphiSeverity\",\n COALESCE(didv.verdict, 'pending'::delphi_report_issue_status)\n AS \"local_status!: DelphiStatus\",\n didws.status AS \"effective_status!: DelphiStatus\"\n FROM delphi_issue_details_with_statuses didws\n INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id\n INNER JOIN delphi_reports dr ON dr.id = dri.report_id\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n LEFT JOIN delphi_issue_detail_verdicts didv\n ON didv.project_id = didws.project_id\n AND didv.detail_key = didws.key\n WHERE\n didws.key = $1\n AND ($2::bigint IS NULL OR didws.id > $2)\n AND dri.issue_type != '__dummy'\n ORDER BY didws.id\n LIMIT $3\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "detail_key!",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "detail_id!: DelphiReportIssueDetailsId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "issue_id!: DelphiReportIssueId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "issue_type",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "project_id!: DBProjectId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "project_slug?",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "project_name!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "version_id!: DBVersionId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "version_number",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "file_id!: DBFileId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "file_name!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "jar?",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "file_path!",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "severity!: DelphiSeverity",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "delphi_severity",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"severe"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "local_status!: DelphiStatus",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "delphi_report_issue_status",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"pending",
|
||||
"safe",
|
||||
"unsafe"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "effective_status!: DelphiStatus",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "delphi_report_issue_status",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"pending",
|
||||
"safe",
|
||||
"unsafe"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
null,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "055f71ec6d193c5f5259cafc989a04f3f791205e6c2df15c5bf9d5afcf71a76a"
|
||||
}
|
||||
Generated
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n m.id AS \"project_id: DBProjectId\",\n MIN(t.id) AS \"thread_id!: DBThreadId\"\n FROM mods m\n INNER JOIN threads t ON t.mod_id = m.id\n INNER JOIN versions v ON v.mod_id = m.id\n INNER JOIN files f ON f.version_id = v.id\n INNER JOIN delphi_reports dr ON dr.file_id = f.id\n INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id\n INNER JOIN delphi_issue_details_with_statuses didws\n ON didws.issue_id = dri.id\n LEFT JOIN threads_messages tm_last\n ON tm_last.thread_id = t.id\n AND tm_last.id = (\n SELECT id FROM threads_messages\n WHERE thread_id = t.id\n ORDER BY created DESC\n LIMIT 1\n )\n LEFT JOIN users u_last\n ON u_last.id = tm_last.author_id\n WHERE\n (\n cardinality($4::text[]) = 0\n OR (\n 'minecraft_java_server' = ANY($4::text[])\n AND (\n m.components ? 'minecraft_server'\n OR m.components ? 'minecraft_java_server'\n )\n )\n OR EXISTS (\n SELECT 1\n FROM versions type_v\n INNER JOIN loaders_versions type_lv\n ON type_lv.version_id = type_v.id\n INNER JOIN loaders_project_types type_lpt\n ON type_lpt.joining_loader_id = type_lv.loader_id\n INNER JOIN project_types type_pt\n ON type_pt.id = type_lpt.joining_project_type_id\n WHERE\n type_v.mod_id = m.id\n AND type_pt.name = ANY($4::text[])\n AND (\n type_pt.name != 'modpack'\n OR NOT (\n m.components ? 'minecraft_server'\n OR m.components ? 'minecraft_java_server'\n )\n )\n )\n )\n AND m.status NOT IN ('draft', 'rejected', 'withheld')\n AND (cardinality($6::text[]) = 0 OR m.status = ANY($6::text[]))\n AND (cardinality($7::text[]) = 0 OR dri.issue_type = ANY($7::text[]))\n AND didws.status = 'pending'\n AND (\n $5::text IS NULL\n OR ($5::text = 'unreplied' AND (tm_last.id IS NULL OR u_last.role IS NULL OR u_last.role NOT IN ('moderator', 'admin')))\n OR ($5::text = 'replied' AND tm_last.id IS NOT NULL AND u_last.role IS NOT NULL AND u_last.role IN ('moderator', 'admin'))\n )\n GROUP BY m.id\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN MIN(dr.created) ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN MIN(dr.created) ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'severity_asc' THEN MAX(dr.severity) ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN MAX(dr.severity) ELSE 'low'::delphi_severity END DESC,\n -- tie-breaker: oldest reports\n MIN(dr.created) ASC\n LIMIT $1 OFFSET $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "project_id: DBProjectId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "thread_id!: DBThreadId",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Text",
|
||||
"TextArray",
|
||||
"Text",
|
||||
"TextArray",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "1a6d4ac11af078439cff5c772f4dc2392729f99ba1f8c7892831235f341fb276"
|
||||
}
|
||||
Generated
+47
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n dgdv.detail_key,\n dgdv.verdict AS \"verdict!: DelphiStatus\",\n COUNT(dri.id) AS \"local_trace_count!\"\n FROM delphi_global_detail_verdicts dgdv\n LEFT JOIN delphi_issue_details_with_statuses didws\n ON didws.key = dgdv.detail_key\n LEFT JOIN delphi_report_issues dri\n ON dri.id = didws.issue_id\n AND dri.issue_type != '__dummy'\n WHERE (\n $1::text IS NULL\n OR dgdv.detail_key ILIKE '%' || $1 || '%'\n )\n GROUP BY dgdv.detail_key, dgdv.verdict\n ORDER BY dgdv.detail_key\n LIMIT $2 OFFSET $3\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "detail_key",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "verdict!: DelphiStatus",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "delphi_report_issue_status",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"pending",
|
||||
"safe",
|
||||
"unsafe"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "local_trace_count!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "4680c4a59c6679f90e3b9e1a33ed1cb1fb60b93ffb79ba5b99e01ee0c14c991a"
|
||||
}
|
||||
Generated
+45
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n dgdv.detail_key,\n dgdv.verdict AS \"verdict!: DelphiStatus\",\n COUNT(dri.id) AS \"local_trace_count!\"\n FROM delphi_global_detail_verdicts dgdv\n LEFT JOIN delphi_issue_details_with_statuses didws\n ON didws.key = dgdv.detail_key\n LEFT JOIN delphi_report_issues dri\n ON dri.id = didws.issue_id\n AND dri.issue_type != '__dummy'\n WHERE dgdv.detail_key = $1\n GROUP BY dgdv.detail_key, dgdv.verdict\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "detail_key",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "verdict!: DelphiStatus",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "delphi_report_issue_status",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"pending",
|
||||
"safe",
|
||||
"unsafe"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "local_trace_count!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "5e5fcfa6ac68f296ba5238e8211f8e206473f11fe1796fc2feda15f8d0ee7f41"
|
||||
}
|
||||
Generated
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n WITH incoming AS (\n SELECT *\n FROM unnest($1::text[], $2::text[]) WITH ORDINALITY\n AS u(detail_key, verdict, ord)\n )\n INSERT INTO delphi_global_detail_verdicts (\n detail_key,\n verdict\n )\n SELECT DISTINCT ON (detail_key)\n detail_key,\n verdict::delphi_report_issue_status\n FROM incoming\n ORDER BY detail_key, ord DESC\n ON CONFLICT (detail_key)\n DO UPDATE SET verdict = EXCLUDED.verdict\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6402f359bbf733a9a2173f23c2ea222611fde49f692406123145dc6acd3dcd6a"
|
||||
}
|
||||
Generated
+147
@@ -0,0 +1,147 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n WITH ranked_local_traces AS (\n SELECT\n didws.key AS detail_key,\n didws.id AS detail_id,\n didws.issue_id,\n dri.issue_type,\n m.id AS project_id,\n m.slug AS project_slug,\n m.name AS project_name,\n v.id AS version_id,\n v.version_number,\n f.id AS file_id,\n f.filename AS file_name,\n didws.jar,\n didws.file_path,\n didws.severity,\n COALESCE(didv.verdict, 'pending'::delphi_report_issue_status)\n AS local_status,\n didws.status AS effective_status,\n ROW_NUMBER() OVER (\n PARTITION BY didws.key\n ORDER BY didws.id\n ) AS row_num\n FROM delphi_issue_details_with_statuses didws\n INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id\n INNER JOIN delphi_reports dr ON dr.id = dri.report_id\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n LEFT JOIN delphi_issue_detail_verdicts didv\n ON didv.project_id = didws.project_id\n AND didv.detail_key = didws.key\n WHERE\n didws.key = ANY($1::text[])\n AND dri.issue_type != '__dummy'\n )\n SELECT\n detail_key AS \"detail_key!\",\n detail_id AS \"detail_id!: DelphiReportIssueDetailsId\",\n issue_id AS \"issue_id!: DelphiReportIssueId\",\n issue_type,\n project_id AS \"project_id!: DBProjectId\",\n project_slug AS \"project_slug?\",\n project_name AS \"project_name!\",\n version_id AS \"version_id!: DBVersionId\",\n v.version_number,\n file_id AS \"file_id!: DBFileId\",\n file_name AS \"file_name!\",\n jar AS \"jar?\",\n file_path AS \"file_path!\",\n severity AS \"severity!: DelphiSeverity\",\n local_status AS \"local_status!: DelphiStatus\",\n effective_status AS \"effective_status!: DelphiStatus\"\n FROM ranked_local_traces v\n WHERE row_num <= $2\n ORDER BY detail_key, detail_id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "detail_key!",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "detail_id!: DelphiReportIssueDetailsId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "issue_id!: DelphiReportIssueId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "issue_type",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "project_id!: DBProjectId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "project_slug?",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "project_name!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "version_id!: DBVersionId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "version_number",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "file_id!: DBFileId",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "file_name!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "jar?",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "file_path!",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "severity!: DelphiSeverity",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "delphi_severity",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"severe"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "local_status!: DelphiStatus",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "delphi_report_issue_status",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"pending",
|
||||
"safe",
|
||||
"unsafe"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "effective_status!: DelphiStatus",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "delphi_report_issue_status",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"pending",
|
||||
"safe",
|
||||
"unsafe"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
null,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "87d30e8802ebe69858141d0d918cafb5286f984cfe55ad1a7ac2219ec50539a4"
|
||||
}
|
||||
+9
-9
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n drid.id AS \"id!: DelphiReportIssueDetailsId\",\n drid.issue_id AS \"issue_id!: DelphiReportIssueId\",\n drid.key AS \"key!: String\",\n drid.jar AS \"jar?: String\",\n drid.file_path AS \"file_path!: String\",\n drid.data AS \"data!: sqlx::types::Json<HashMap<String, serde_json::Value>>\",\n drid.severity AS \"severity!: DelphiSeverity\",\n COALESCE(didv.verdict, 'pending'::delphi_report_issue_status) AS \"status!: DelphiStatus\"\n FROM delphi_report_issue_details drid\n INNER JOIN delphi_report_issues dri ON dri.id = drid.issue_id\n INNER JOIN delphi_reports dr ON dr.id = dri.report_id\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n LEFT JOIN delphi_issue_detail_verdicts didv\n ON m.id = didv.project_id AND drid.key = didv.detail_key\n WHERE drid.issue_id = ANY($1::bigint[])\n ",
|
||||
"query": "\n SELECT\n didws.id AS \"id!: DelphiReportIssueDetailsId\",\n didws.issue_id AS \"issue_id!: DelphiReportIssueId\",\n didws.key AS \"key!: String\",\n didws.jar AS \"jar?: String\",\n didws.file_path AS \"file_path!: String\",\n didws.data AS \"data!: sqlx::types::Json<HashMap<String, serde_json::Value>>\",\n didws.severity AS \"severity!: DelphiSeverity\",\n didws.status AS \"status!: DelphiStatus\"\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.issue_id = ANY($1::bigint[])\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -73,15 +73,15 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "263ad3654f544ffb6061c839d49dada47fb382a76fdcabad2077fb1ef6d1010a"
|
||||
"hash": "951700c659d040068dae8e2f91b82b6a3af676439b47ff76d72000ddeca0e334"
|
||||
}
|
||||
Generated
-23
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT EXISTS(\n SELECT 1\n FROM unnest($2::text[]) AS incoming(detail_key)\n LEFT JOIN delphi_issue_detail_verdicts didv\n ON didv.project_id = $1 AND didv.detail_key = incoming.detail_key\n WHERE didv.project_id IS NULL\n ) AS \"has_unflagged_issue_details!\"\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "has_unflagged_issue_details!",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "997944b328b628792d84b21747f9e9c670ad40d0f89a175aedece93df1169195"
|
||||
}
|
||||
Generated
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT EXISTS(\n SELECT 1\n FROM unnest($2::text[]) AS incoming(detail_key)\n LEFT JOIN delphi_global_detail_verdicts dgdv\n ON dgdv.detail_key = incoming.detail_key\n LEFT JOIN delphi_issue_detail_verdicts didv\n ON didv.project_id = $1 AND didv.detail_key = incoming.detail_key\n WHERE dgdv.detail_key IS NULL AND didv.project_id IS NULL\n ) AS \"has_unflagged_issue_details!\"\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "has_unflagged_issue_details!",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "bab0f71c84902bac7589a30923e95cbcd76d340c21ac6116d9bd27878786bd26"
|
||||
}
|
||||
Generated
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT COUNT(*) AS \"total!\"\n FROM delphi_global_detail_verdicts dgdv\n WHERE (\n $1::text IS NULL\n OR dgdv.detail_key ILIKE '%' || $1 || '%'\n )\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "total!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "c9d5796b2c98dedd9f2f237a1a2cf7a059a49387c0c564b730733bda7358f7fe"
|
||||
}
|
||||
Generated
-29
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n WITH incoming AS (\n SELECT *\n FROM unnest($1::bigint[], $2::text[]) WITH ORDINALITY\n AS u(detail_id, verdict, ord)\n ),\n resolved AS (\n SELECT\n i.ord,\n didws.project_id,\n didws.key AS detail_key,\n i.verdict::delphi_report_issue_status AS verdict\n FROM incoming i\n INNER JOIN delphi_issue_details_with_statuses didws ON didws.id = i.detail_id\n INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id\n WHERE\n -- see delphi.rs todo comment\n dri.issue_type != '__dummy'\n ),\n validated AS (\n SELECT\n (SELECT COUNT(*) FROM incoming) AS incoming_count,\n (SELECT COUNT(*) FROM resolved) AS resolved_count\n ),\n upserted AS (\n INSERT INTO delphi_issue_detail_verdicts (\n project_id,\n detail_key,\n verdict\n )\n SELECT DISTINCT ON (project_id, detail_key)\n project_id,\n detail_key,\n verdict\n FROM resolved\n ORDER BY project_id, detail_key, ord DESC\n ON CONFLICT (project_id, detail_key)\n DO UPDATE SET verdict = EXCLUDED.verdict\n RETURNING 1\n )\n SELECT\n (v.incoming_count = v.resolved_count) AS \"all_found!\",\n (SELECT COUNT(*) FROM upserted) AS \"upserted_count!\"\n FROM validated v\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "all_found!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "upserted_count!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8Array",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "ccedb120b05ff47ddc15bb4570025a8e8249050c12f7036d936f9a01f939db1f"
|
||||
}
|
||||
Generated
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n WITH incoming AS (\n SELECT *\n FROM unnest($1::bigint[], $2::text[]) WITH ORDINALITY\n AS u(detail_id, verdict, ord)\n ),\n resolved AS (\n SELECT\n i.ord,\n didws.project_id,\n didws.key AS detail_key,\n i.verdict\n FROM incoming i\n INNER JOIN delphi_issue_details_with_statuses didws ON didws.id = i.detail_id\n INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id\n WHERE\n -- see delphi.rs todo comment\n dri.issue_type != '__dummy'\n ),\n validated AS (\n SELECT\n (SELECT COUNT(*) FROM incoming) AS incoming_count,\n (SELECT COUNT(*) FROM resolved) AS resolved_count\n ),\n latest AS (\n SELECT DISTINCT ON (project_id, detail_key)\n project_id,\n detail_key,\n verdict\n FROM resolved\n ORDER BY project_id, detail_key, ord DESC\n ),\n deleted AS (\n DELETE FROM delphi_issue_detail_verdicts didv\n USING latest\n WHERE\n didv.project_id = latest.project_id\n AND didv.detail_key = latest.detail_key\n AND latest.verdict = 'pending'\n RETURNING 1\n ),\n upserted AS (\n INSERT INTO delphi_issue_detail_verdicts (\n project_id,\n detail_key,\n verdict\n )\n SELECT\n project_id,\n detail_key,\n verdict::delphi_report_issue_status\n FROM latest\n WHERE verdict != 'pending'\n ON CONFLICT (project_id, detail_key)\n DO UPDATE SET verdict = EXCLUDED.verdict\n RETURNING 1\n )\n SELECT\n (v.incoming_count = v.resolved_count) AS \"all_found!\",\n (SELECT COUNT(*) FROM upserted) AS \"upserted_count!\"\n FROM validated v\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "all_found!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "upserted_count!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8Array",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "e8b6e423aaf62046afd19fb1e177a3ef1f18f4814df8924926c47d4b0c7145c7"
|
||||
}
|
||||
@@ -33,3 +33,4 @@
|
||||
- To seed the database locally: `psql postgresql://labrinth:labrinth@localhost/labrinth -f apps/labrinth/fixtures/labrinth-seed-data-202508052143.sql`
|
||||
- When writing `sqlx` queries, prefer `r#` raw strings over escaping quotes
|
||||
- When interacting with the Postgres database, prefer using a `ro_pool: ReadOnlyPgPool` when performing read-only operations
|
||||
- After writing a SQL query, run `EXPLAIN ANALYZE` to see how long the query would take to run. Report to the user the estimated performance impact of the query.
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
CREATE TABLE delphi_global_detail_verdicts (
|
||||
detail_key TEXT PRIMARY KEY,
|
||||
verdict delphi_report_issue_status NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX delphi_report_issue_details_key
|
||||
ON delphi_report_issue_details (key);
|
||||
|
||||
CREATE INDEX delphi_issue_detail_verdicts_detail_key
|
||||
ON delphi_issue_detail_verdicts (detail_key);
|
||||
|
||||
DROP VIEW delphi_issue_details_with_statuses;
|
||||
|
||||
CREATE VIEW delphi_issue_details_with_statuses AS
|
||||
SELECT
|
||||
drid.*,
|
||||
m.id AS project_id,
|
||||
COALESCE(dgdv.verdict, didv.verdict, 'pending') AS status
|
||||
FROM delphi_report_issue_details drid
|
||||
INNER JOIN delphi_report_issues dri ON dri.id = drid.issue_id
|
||||
INNER JOIN delphi_reports dr ON dr.id = dri.report_id
|
||||
INNER JOIN files f ON f.id = dr.file_id
|
||||
INNER JOIN versions v ON v.id = f.version_id
|
||||
INNER JOIN mods m ON m.id = v.mod_id
|
||||
LEFT JOIN delphi_global_detail_verdicts dgdv
|
||||
ON drid.key = dgdv.detail_key
|
||||
LEFT JOIN delphi_issue_detail_verdicts didv
|
||||
ON m.id = didv.project_id
|
||||
AND drid.key = didv.detail_key;
|
||||
@@ -240,9 +240,11 @@ async fn ingest_report_deserialized(
|
||||
SELECT EXISTS(
|
||||
SELECT 1
|
||||
FROM unnest($2::text[]) AS incoming(detail_key)
|
||||
LEFT JOIN delphi_global_detail_verdicts dgdv
|
||||
ON dgdv.detail_key = incoming.detail_key
|
||||
LEFT JOIN delphi_issue_detail_verdicts didv
|
||||
ON didv.project_id = $1 AND didv.detail_key = incoming.detail_key
|
||||
WHERE didv.project_id IS NULL
|
||||
WHERE dgdv.detail_key IS NULL AND didv.project_id IS NULL
|
||||
) AS "has_unflagged_issue_details!"
|
||||
"#,
|
||||
DBProjectId::from(report.project_id) as _,
|
||||
|
||||
@@ -113,9 +113,12 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
moderation::tech_review::get_issue,
|
||||
moderation::tech_review::get_report,
|
||||
moderation::tech_review::search_projects,
|
||||
moderation::tech_review::global::search_global_issue_details,
|
||||
moderation::tech_review::global::get_global_issue_detail,
|
||||
moderation::tech_review::get_project_report,
|
||||
moderation::tech_review::submit_report,
|
||||
moderation::tech_review::update_issue_details,
|
||||
moderation::tech_review::update_global_issue_details,
|
||||
moderation::tech_review::add_report,
|
||||
moderation::external_license::search,
|
||||
moderation::external_license::lookup,
|
||||
|
||||
@@ -37,13 +37,17 @@ use crate::{
|
||||
};
|
||||
use eyre::eyre;
|
||||
|
||||
pub mod global;
|
||||
|
||||
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
|
||||
cfg.service(search_projects)
|
||||
.configure(global::config)
|
||||
.service(get_project_report)
|
||||
.service(get_report)
|
||||
.service(get_issue)
|
||||
.service(submit_report)
|
||||
.service(update_issue_details)
|
||||
.service(update_global_issue_details)
|
||||
.service(add_report);
|
||||
}
|
||||
|
||||
@@ -502,23 +506,16 @@ async fn fetch_project_reports(
|
||||
let detail_rows = sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
drid.id AS "id!: DelphiReportIssueDetailsId",
|
||||
drid.issue_id AS "issue_id!: DelphiReportIssueId",
|
||||
drid.key AS "key!: String",
|
||||
drid.jar AS "jar?: String",
|
||||
drid.file_path AS "file_path!: String",
|
||||
drid.data AS "data!: sqlx::types::Json<HashMap<String, serde_json::Value>>",
|
||||
drid.severity AS "severity!: DelphiSeverity",
|
||||
COALESCE(didv.verdict, 'pending'::delphi_report_issue_status) AS "status!: DelphiStatus"
|
||||
FROM delphi_report_issue_details drid
|
||||
INNER JOIN delphi_report_issues dri ON dri.id = drid.issue_id
|
||||
INNER JOIN delphi_reports dr ON dr.id = dri.report_id
|
||||
INNER JOIN files f ON f.id = dr.file_id
|
||||
INNER JOIN versions v ON v.id = f.version_id
|
||||
INNER JOIN mods m ON m.id = v.mod_id
|
||||
LEFT JOIN delphi_issue_detail_verdicts didv
|
||||
ON m.id = didv.project_id AND drid.key = didv.detail_key
|
||||
WHERE drid.issue_id = ANY($1::bigint[])
|
||||
didws.id AS "id!: DelphiReportIssueDetailsId",
|
||||
didws.issue_id AS "issue_id!: DelphiReportIssueId",
|
||||
didws.key AS "key!: String",
|
||||
didws.jar AS "jar?: String",
|
||||
didws.file_path AS "file_path!: String",
|
||||
didws.data AS "data!: sqlx::types::Json<HashMap<String, serde_json::Value>>",
|
||||
didws.severity AS "severity!: DelphiSeverity",
|
||||
didws.status AS "status!: DelphiStatus"
|
||||
FROM delphi_issue_details_with_statuses didws
|
||||
WHERE didws.issue_id = ANY($1::bigint[])
|
||||
"#,
|
||||
&issue_ids.iter().map(|i| i.0).collect::<Vec<_>>()
|
||||
)
|
||||
@@ -718,10 +715,8 @@ pub async fn search_projects(
|
||||
INNER JOIN files f ON f.version_id = v.id
|
||||
INNER JOIN delphi_reports dr ON dr.file_id = f.id
|
||||
INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id
|
||||
INNER JOIN delphi_report_issue_details drid
|
||||
ON drid.issue_id = dri.id
|
||||
LEFT JOIN delphi_issue_detail_verdicts didv
|
||||
ON m.id = didv.project_id AND drid.key = didv.detail_key
|
||||
INNER JOIN delphi_issue_details_with_statuses didws
|
||||
ON didws.issue_id = dri.id
|
||||
LEFT JOIN threads_messages tm_last
|
||||
ON tm_last.thread_id = t.id
|
||||
AND tm_last.id = (
|
||||
@@ -766,7 +761,7 @@ pub async fn search_projects(
|
||||
AND m.status NOT IN ('draft', 'rejected', 'withheld')
|
||||
AND (cardinality($6::text[]) = 0 OR m.status = ANY($6::text[]))
|
||||
AND (cardinality($7::text[]) = 0 OR dri.issue_type = ANY($7::text[]))
|
||||
AND (didv.verdict IS NULL OR didv.verdict = 'pending'::delphi_report_issue_status)
|
||||
AND didws.status = 'pending'
|
||||
AND (
|
||||
$5::text IS NULL
|
||||
OR ($5::text = 'unreplied' AND (tm_last.id IS NULL OR u_last.role IS NULL OR u_last.role NOT IN ('moderator', 'admin')))
|
||||
@@ -1170,6 +1165,17 @@ pub struct UpdateIssue {
|
||||
/// ID of the issue detail to update.
|
||||
pub detail_id: DelphiReportIssueDetailsId,
|
||||
/// What the moderator has decided the outcome of this issue is.
|
||||
///
|
||||
/// `pending` removes the project-local verdict for this issue detail.
|
||||
pub verdict: DelphiStatus,
|
||||
}
|
||||
|
||||
/// See [`update_global_issue_details`].
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct UpdateGlobalIssue {
|
||||
/// Key of the issue detail to update globally.
|
||||
pub detail_key: String,
|
||||
/// What the moderator has decided the outcome of this issue is globally.
|
||||
pub verdict: DelphiVerdict,
|
||||
}
|
||||
|
||||
@@ -1210,8 +1216,9 @@ pub async fn update_issue_details(
|
||||
let verdicts = updates
|
||||
.iter()
|
||||
.map(|u| match u.verdict {
|
||||
DelphiVerdict::Safe => "safe".to_string(),
|
||||
DelphiVerdict::Unsafe => "unsafe".to_string(),
|
||||
DelphiStatus::Safe => "safe".to_string(),
|
||||
DelphiStatus::Unsafe => "unsafe".to_string(),
|
||||
DelphiStatus::Pending => "pending".to_string(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -1227,7 +1234,7 @@ pub async fn update_issue_details(
|
||||
i.ord,
|
||||
didws.project_id,
|
||||
didws.key AS detail_key,
|
||||
i.verdict::delphi_report_issue_status AS verdict
|
||||
i.verdict
|
||||
FROM incoming i
|
||||
INNER JOIN delphi_issue_details_with_statuses didws ON didws.id = i.detail_id
|
||||
INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id
|
||||
@@ -1240,18 +1247,35 @@ pub async fn update_issue_details(
|
||||
(SELECT COUNT(*) FROM incoming) AS incoming_count,
|
||||
(SELECT COUNT(*) FROM resolved) AS resolved_count
|
||||
),
|
||||
upserted AS (
|
||||
INSERT INTO delphi_issue_detail_verdicts (
|
||||
project_id,
|
||||
detail_key,
|
||||
verdict
|
||||
)
|
||||
latest AS (
|
||||
SELECT DISTINCT ON (project_id, detail_key)
|
||||
project_id,
|
||||
detail_key,
|
||||
verdict
|
||||
FROM resolved
|
||||
ORDER BY project_id, detail_key, ord DESC
|
||||
),
|
||||
deleted AS (
|
||||
DELETE FROM delphi_issue_detail_verdicts didv
|
||||
USING latest
|
||||
WHERE
|
||||
didv.project_id = latest.project_id
|
||||
AND didv.detail_key = latest.detail_key
|
||||
AND latest.verdict = 'pending'
|
||||
RETURNING 1
|
||||
),
|
||||
upserted AS (
|
||||
INSERT INTO delphi_issue_detail_verdicts (
|
||||
project_id,
|
||||
detail_key,
|
||||
verdict
|
||||
)
|
||||
SELECT
|
||||
project_id,
|
||||
detail_key,
|
||||
verdict::delphi_report_issue_status
|
||||
FROM latest
|
||||
WHERE verdict != 'pending'
|
||||
ON CONFLICT (project_id, detail_key)
|
||||
DO UPDATE SET verdict = EXCLUDED.verdict
|
||||
RETURNING 1
|
||||
@@ -1279,6 +1303,92 @@ pub async fn update_issue_details(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update global technical review issue detail verdicts.
|
||||
///
|
||||
/// This marks every issue detail with a matching key as safe or unsafe.
|
||||
#[utoipa::path(
|
||||
context_path = "/moderation/tech-review",
|
||||
tag = "moderation",
|
||||
security(("bearer_auth" = [])),
|
||||
responses((status = NO_CONTENT))
|
||||
)]
|
||||
#[post("/global-issue-detail")]
|
||||
pub async fn update_global_issue_details(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
update_reqs: web::Json<Vec<UpdateGlobalIssue>>,
|
||||
) -> Result<(), ApiError> {
|
||||
check_is_moderator_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Scopes::PROJECT_WRITE,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let updates = update_reqs.into_inner();
|
||||
|
||||
if updates.iter().any(|u| {
|
||||
u.detail_key.is_empty() || u.detail_key.starts_with("<no-key-")
|
||||
}) {
|
||||
return Err(ApiError::Request(eyre!(
|
||||
"detail key cannot be empty or generated fallback key"
|
||||
)));
|
||||
}
|
||||
|
||||
let detail_keys = updates
|
||||
.iter()
|
||||
.map(|u| u.detail_key.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let verdicts = updates
|
||||
.iter()
|
||||
.map(|u| match u.verdict {
|
||||
DelphiVerdict::Safe => "safe".to_string(),
|
||||
DelphiVerdict::Unsafe => "unsafe".to_string(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut txn = pool
|
||||
.begin()
|
||||
.await
|
||||
.wrap_internal_err("failed to start transaction")?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
WITH incoming AS (
|
||||
SELECT *
|
||||
FROM unnest($1::text[], $2::text[]) WITH ORDINALITY
|
||||
AS u(detail_key, verdict, ord)
|
||||
)
|
||||
INSERT INTO delphi_global_detail_verdicts (
|
||||
detail_key,
|
||||
verdict
|
||||
)
|
||||
SELECT DISTINCT ON (detail_key)
|
||||
detail_key,
|
||||
verdict::delphi_report_issue_status
|
||||
FROM incoming
|
||||
ORDER BY detail_key, ord DESC
|
||||
ON CONFLICT (detail_key)
|
||||
DO UPDATE SET verdict = EXCLUDED.verdict
|
||||
"#,
|
||||
&detail_keys,
|
||||
&verdicts,
|
||||
)
|
||||
.execute(&mut txn)
|
||||
.await
|
||||
.wrap_internal_err("failed to update global issue details")?;
|
||||
|
||||
txn.commit()
|
||||
.await
|
||||
.wrap_internal_err("failed to commit transaction")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// See [`add_report`].
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct AddReport {
|
||||
|
||||
@@ -0,0 +1,465 @@
|
||||
use actix_web::{HttpRequest, post, web};
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
auth::check_is_moderator_from_headers,
|
||||
database::{
|
||||
PgPool,
|
||||
models::{
|
||||
DBFileId, DBProjectId, DBVersionId, DelphiReportIssueDetailsId,
|
||||
DelphiReportIssueId,
|
||||
delphi_report_item::{DelphiSeverity, DelphiStatus},
|
||||
},
|
||||
redis::RedisPool,
|
||||
},
|
||||
models::{
|
||||
ids::{FileId, ProjectId, VersionId},
|
||||
pats::Scopes,
|
||||
},
|
||||
queue::session::AuthQueue,
|
||||
routes::ApiError,
|
||||
util::error::Context,
|
||||
};
|
||||
use eyre::eyre;
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(search_global_issue_details)
|
||||
.service(get_global_issue_detail);
|
||||
}
|
||||
|
||||
fn default_limit() -> u64 {
|
||||
20
|
||||
}
|
||||
|
||||
const LOCAL_TRACE_PREVIEW_LIMIT: i64 = 10;
|
||||
|
||||
/// Arguments for searching globally-verdict'ed Delphi detail traces.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct SearchGlobalIssueDetails {
|
||||
#[serde(default = "default_limit")]
|
||||
#[schema(default = 20)]
|
||||
pub limit: u64,
|
||||
#[serde(default)]
|
||||
#[schema(default = 0)]
|
||||
pub page: u64,
|
||||
#[serde(default)]
|
||||
pub query: Option<String>,
|
||||
}
|
||||
|
||||
/// Response for searching globally-verdict'ed Delphi detail traces.
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct SearchGlobalIssueDetailsResponse {
|
||||
/// Total number of matching global verdicts.
|
||||
pub total: i64,
|
||||
/// Globally-verdict'ed detail keys and their local matches.
|
||||
pub traces: Vec<GlobalIssueDetail>,
|
||||
}
|
||||
|
||||
/// Arguments for fetching one globally-verdict'ed Delphi detail trace.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct GetGlobalIssueDetail {
|
||||
/// Key used by Delphi to identify matching detail traces.
|
||||
pub detail_key: String,
|
||||
#[serde(default = "default_limit")]
|
||||
#[schema(default = 20)]
|
||||
pub limit: u64,
|
||||
/// Return local traces with IDs greater than this detail ID.
|
||||
#[serde(default)]
|
||||
pub after_detail_id: Option<DelphiReportIssueDetailsId>,
|
||||
}
|
||||
|
||||
/// Response for one globally-verdict'ed Delphi detail trace.
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct GetGlobalIssueDetailResponse {
|
||||
/// Globally-verdict'ed detail key and a page of local matches.
|
||||
pub trace: GlobalIssueDetail,
|
||||
/// Pass this as `after_detail_id` to fetch the next page.
|
||||
pub next_after_detail_id: Option<DelphiReportIssueDetailsId>,
|
||||
}
|
||||
|
||||
/// A globally-verdict'ed Delphi detail key.
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct GlobalIssueDetail {
|
||||
/// Key used by Delphi to identify matching detail traces.
|
||||
pub detail_key: String,
|
||||
/// Verdict applied to every matching detail trace.
|
||||
pub verdict: DelphiStatus,
|
||||
/// Number of local detail traces with this key.
|
||||
pub local_trace_count: i64,
|
||||
/// Local detail traces matching this key.
|
||||
pub local_traces: Vec<GlobalIssueDetailTrace>,
|
||||
}
|
||||
|
||||
/// A local Delphi detail trace matching a globally-verdict'ed key.
|
||||
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct GlobalIssueDetailTrace {
|
||||
/// ID of the local detail trace.
|
||||
pub detail_id: DelphiReportIssueDetailsId,
|
||||
/// ID of the issue containing this detail trace.
|
||||
pub issue_id: DelphiReportIssueId,
|
||||
/// Delphi issue kind.
|
||||
pub issue_type: String,
|
||||
/// ID of the project containing this detail trace.
|
||||
pub project_id: ProjectId,
|
||||
/// Project slug, when one is set.
|
||||
pub project_slug: Option<String>,
|
||||
/// Project name.
|
||||
pub project_name: String,
|
||||
/// ID of the version containing this detail trace.
|
||||
pub version_id: VersionId,
|
||||
/// Version number.
|
||||
pub version_number: String,
|
||||
/// ID of the file containing this detail trace.
|
||||
pub file_id: FileId,
|
||||
/// File name.
|
||||
pub file_name: String,
|
||||
/// JAR containing the detail trace, when Delphi reported one.
|
||||
pub jar: Option<String>,
|
||||
/// File path containing the detail trace.
|
||||
pub file_path: String,
|
||||
/// Delphi severity for this detail trace.
|
||||
pub severity: DelphiSeverity,
|
||||
/// Project-local verdict for this key.
|
||||
pub local_status: DelphiStatus,
|
||||
/// Effective verdict after applying global fallback rules.
|
||||
pub effective_status: DelphiStatus,
|
||||
}
|
||||
|
||||
/// Search globally-verdict'ed Delphi detail traces.
|
||||
#[utoipa::path(
|
||||
context_path = "/moderation/tech-review",
|
||||
tag = "moderation",
|
||||
security(("bearer_auth" = [])),
|
||||
responses((status = OK, body = SearchGlobalIssueDetailsResponse))
|
||||
)]
|
||||
#[post("/global-issue-detail/search")]
|
||||
pub async fn search_global_issue_details(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
search_req: web::Json<SearchGlobalIssueDetails>,
|
||||
) -> Result<web::Json<SearchGlobalIssueDetailsResponse>, ApiError> {
|
||||
check_is_moderator_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let query = search_req
|
||||
.query
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|q| !q.is_empty());
|
||||
let limit = search_req.limit.clamp(1, 100);
|
||||
let offset = limit.saturating_mul(search_req.page);
|
||||
|
||||
let limit =
|
||||
i64::try_from(limit).wrap_request_err("limit cannot fit into `i64`")?;
|
||||
let offset = i64::try_from(offset)
|
||||
.wrap_request_err("offset cannot fit into `i64`")?;
|
||||
|
||||
let total = sqlx::query!(
|
||||
r#"
|
||||
SELECT COUNT(*) AS "total!"
|
||||
FROM delphi_global_detail_verdicts dgdv
|
||||
WHERE (
|
||||
$1::text IS NULL
|
||||
OR dgdv.detail_key ILIKE '%' || $1 || '%'
|
||||
)
|
||||
"#,
|
||||
query,
|
||||
)
|
||||
.fetch_one(&**pool)
|
||||
.await
|
||||
.wrap_internal_err("failed to count global issue details")?
|
||||
.total;
|
||||
|
||||
let global_rows = sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
dgdv.detail_key,
|
||||
dgdv.verdict AS "verdict!: DelphiStatus",
|
||||
COUNT(dri.id) AS "local_trace_count!"
|
||||
FROM delphi_global_detail_verdicts dgdv
|
||||
LEFT JOIN delphi_issue_details_with_statuses didws
|
||||
ON didws.key = dgdv.detail_key
|
||||
LEFT JOIN delphi_report_issues dri
|
||||
ON dri.id = didws.issue_id
|
||||
AND dri.issue_type != '__dummy'
|
||||
WHERE (
|
||||
$1::text IS NULL
|
||||
OR dgdv.detail_key ILIKE '%' || $1 || '%'
|
||||
)
|
||||
GROUP BY dgdv.detail_key, dgdv.verdict
|
||||
ORDER BY dgdv.detail_key
|
||||
LIMIT $2 OFFSET $3
|
||||
"#,
|
||||
query,
|
||||
limit,
|
||||
offset,
|
||||
)
|
||||
.fetch_all(&**pool)
|
||||
.await
|
||||
.wrap_internal_err("failed to fetch global issue details")?;
|
||||
|
||||
let detail_keys = global_rows
|
||||
.iter()
|
||||
.map(|row| row.detail_key.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let local_rows = sqlx::query!(
|
||||
r#"
|
||||
WITH ranked_local_traces AS (
|
||||
SELECT
|
||||
didws.key AS detail_key,
|
||||
didws.id AS detail_id,
|
||||
didws.issue_id,
|
||||
dri.issue_type,
|
||||
m.id AS project_id,
|
||||
m.slug AS project_slug,
|
||||
m.name AS project_name,
|
||||
v.id AS version_id,
|
||||
v.version_number,
|
||||
f.id AS file_id,
|
||||
f.filename AS file_name,
|
||||
didws.jar,
|
||||
didws.file_path,
|
||||
didws.severity,
|
||||
COALESCE(didv.verdict, 'pending'::delphi_report_issue_status)
|
||||
AS local_status,
|
||||
didws.status AS effective_status,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY didws.key
|
||||
ORDER BY didws.id
|
||||
) AS row_num
|
||||
FROM delphi_issue_details_with_statuses didws
|
||||
INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id
|
||||
INNER JOIN delphi_reports dr ON dr.id = dri.report_id
|
||||
INNER JOIN files f ON f.id = dr.file_id
|
||||
INNER JOIN versions v ON v.id = f.version_id
|
||||
INNER JOIN mods m ON m.id = v.mod_id
|
||||
LEFT JOIN delphi_issue_detail_verdicts didv
|
||||
ON didv.project_id = didws.project_id
|
||||
AND didv.detail_key = didws.key
|
||||
WHERE
|
||||
didws.key = ANY($1::text[])
|
||||
AND dri.issue_type != '__dummy'
|
||||
)
|
||||
SELECT
|
||||
detail_key AS "detail_key!",
|
||||
detail_id AS "detail_id!: DelphiReportIssueDetailsId",
|
||||
issue_id AS "issue_id!: DelphiReportIssueId",
|
||||
issue_type,
|
||||
project_id AS "project_id!: DBProjectId",
|
||||
project_slug AS "project_slug?",
|
||||
project_name AS "project_name!",
|
||||
version_id AS "version_id!: DBVersionId",
|
||||
v.version_number,
|
||||
file_id AS "file_id!: DBFileId",
|
||||
file_name AS "file_name!",
|
||||
jar AS "jar?",
|
||||
file_path AS "file_path!",
|
||||
severity AS "severity!: DelphiSeverity",
|
||||
local_status AS "local_status!: DelphiStatus",
|
||||
effective_status AS "effective_status!: DelphiStatus"
|
||||
FROM ranked_local_traces v
|
||||
WHERE row_num <= $2
|
||||
ORDER BY detail_key, detail_id
|
||||
"#,
|
||||
&detail_keys,
|
||||
LOCAL_TRACE_PREVIEW_LIMIT,
|
||||
)
|
||||
.fetch_all(&**pool)
|
||||
.await
|
||||
.wrap_internal_err("failed to fetch local issue detail traces")?;
|
||||
|
||||
let mut traces_by_key = local_rows
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
(
|
||||
row.detail_key,
|
||||
GlobalIssueDetailTrace {
|
||||
detail_id: row.detail_id,
|
||||
issue_id: row.issue_id,
|
||||
issue_type: row.issue_type,
|
||||
project_id: ProjectId::from(row.project_id),
|
||||
project_slug: row.project_slug,
|
||||
project_name: row.project_name,
|
||||
version_id: VersionId::from(row.version_id),
|
||||
version_number: row.version_number,
|
||||
file_id: FileId::from(row.file_id),
|
||||
file_name: row.file_name,
|
||||
jar: row.jar,
|
||||
file_path: row.file_path,
|
||||
severity: row.severity,
|
||||
local_status: row.local_status,
|
||||
effective_status: row.effective_status,
|
||||
},
|
||||
)
|
||||
})
|
||||
.into_group_map();
|
||||
|
||||
let traces = global_rows
|
||||
.into_iter()
|
||||
.map(|row| GlobalIssueDetail {
|
||||
local_traces: traces_by_key
|
||||
.remove(&row.detail_key)
|
||||
.unwrap_or_default(),
|
||||
detail_key: row.detail_key,
|
||||
verdict: row.verdict,
|
||||
local_trace_count: row.local_trace_count,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(web::Json(SearchGlobalIssueDetailsResponse {
|
||||
total,
|
||||
traces,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Fetch one globally-verdict'ed Delphi detail trace with paginated local matches.
|
||||
#[utoipa::path(
|
||||
context_path = "/moderation/tech-review",
|
||||
tag = "moderation",
|
||||
security(("bearer_auth" = [])),
|
||||
responses((status = OK, body = GetGlobalIssueDetailResponse))
|
||||
)]
|
||||
#[post("/global-issue-detail/local-traces")]
|
||||
pub async fn get_global_issue_detail(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
get_req: web::Json<GetGlobalIssueDetail>,
|
||||
) -> Result<web::Json<GetGlobalIssueDetailResponse>, ApiError> {
|
||||
check_is_moderator_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let detail_key = get_req.detail_key.trim();
|
||||
if detail_key.is_empty() {
|
||||
return Err(ApiError::Request(eyre!("detail key cannot be empty")));
|
||||
}
|
||||
|
||||
let limit = get_req.limit.clamp(1, 100);
|
||||
let query_limit = i64::try_from(limit + 1)
|
||||
.wrap_request_err("limit cannot fit into `i64`")?;
|
||||
let limit = usize::try_from(limit)
|
||||
.wrap_request_err("limit cannot fit into `usize`")?;
|
||||
let after_detail_id = get_req.after_detail_id.map(|id| id.0);
|
||||
|
||||
let global_row = sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
dgdv.detail_key,
|
||||
dgdv.verdict AS "verdict!: DelphiStatus",
|
||||
COUNT(dri.id) AS "local_trace_count!"
|
||||
FROM delphi_global_detail_verdicts dgdv
|
||||
LEFT JOIN delphi_issue_details_with_statuses didws
|
||||
ON didws.key = dgdv.detail_key
|
||||
LEFT JOIN delphi_report_issues dri
|
||||
ON dri.id = didws.issue_id
|
||||
AND dri.issue_type != '__dummy'
|
||||
WHERE dgdv.detail_key = $1
|
||||
GROUP BY dgdv.detail_key, dgdv.verdict
|
||||
"#,
|
||||
detail_key,
|
||||
)
|
||||
.fetch_optional(&**pool)
|
||||
.await
|
||||
.wrap_internal_err("failed to fetch global issue detail")?
|
||||
.ok_or(ApiError::NotFound)?;
|
||||
|
||||
let local_rows = sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
didws.key AS "detail_key!",
|
||||
didws.id AS "detail_id!: DelphiReportIssueDetailsId",
|
||||
didws.issue_id AS "issue_id!: DelphiReportIssueId",
|
||||
dri.issue_type,
|
||||
m.id AS "project_id!: DBProjectId",
|
||||
m.slug AS "project_slug?",
|
||||
m.name AS "project_name!",
|
||||
v.id AS "version_id!: DBVersionId",
|
||||
v.version_number,
|
||||
f.id AS "file_id!: DBFileId",
|
||||
f.filename AS "file_name!",
|
||||
didws.jar AS "jar?",
|
||||
didws.file_path AS "file_path!",
|
||||
didws.severity AS "severity!: DelphiSeverity",
|
||||
COALESCE(didv.verdict, 'pending'::delphi_report_issue_status)
|
||||
AS "local_status!: DelphiStatus",
|
||||
didws.status AS "effective_status!: DelphiStatus"
|
||||
FROM delphi_issue_details_with_statuses didws
|
||||
INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id
|
||||
INNER JOIN delphi_reports dr ON dr.id = dri.report_id
|
||||
INNER JOIN files f ON f.id = dr.file_id
|
||||
INNER JOIN versions v ON v.id = f.version_id
|
||||
INNER JOIN mods m ON m.id = v.mod_id
|
||||
LEFT JOIN delphi_issue_detail_verdicts didv
|
||||
ON didv.project_id = didws.project_id
|
||||
AND didv.detail_key = didws.key
|
||||
WHERE
|
||||
didws.key = $1
|
||||
AND ($2::bigint IS NULL OR didws.id > $2)
|
||||
AND dri.issue_type != '__dummy'
|
||||
ORDER BY didws.id
|
||||
LIMIT $3
|
||||
"#,
|
||||
detail_key,
|
||||
after_detail_id,
|
||||
query_limit,
|
||||
)
|
||||
.fetch_all(&**pool)
|
||||
.await
|
||||
.wrap_internal_err("failed to fetch local issue detail traces")?;
|
||||
|
||||
let mut local_traces = local_rows
|
||||
.into_iter()
|
||||
.map(|row| GlobalIssueDetailTrace {
|
||||
detail_id: row.detail_id,
|
||||
issue_id: row.issue_id,
|
||||
issue_type: row.issue_type,
|
||||
project_id: ProjectId::from(row.project_id),
|
||||
project_slug: row.project_slug,
|
||||
project_name: row.project_name,
|
||||
version_id: VersionId::from(row.version_id),
|
||||
version_number: row.version_number,
|
||||
file_id: FileId::from(row.file_id),
|
||||
file_name: row.file_name,
|
||||
jar: row.jar,
|
||||
file_path: row.file_path,
|
||||
severity: row.severity,
|
||||
local_status: row.local_status,
|
||||
effective_status: row.effective_status,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let has_more = local_traces.len() > limit;
|
||||
if has_more {
|
||||
local_traces.pop();
|
||||
}
|
||||
let next_after_detail_id = has_more
|
||||
.then(|| local_traces.last().map(|trace| trace.detail_id))
|
||||
.flatten();
|
||||
|
||||
Ok(web::Json(GetGlobalIssueDetailResponse {
|
||||
trace: GlobalIssueDetail {
|
||||
detail_key: global_row.detail_key,
|
||||
verdict: global_row.verdict,
|
||||
local_trace_count: global_row.local_trace_count,
|
||||
local_traces,
|
||||
},
|
||||
next_after_detail_id,
|
||||
}))
|
||||
}
|
||||
@@ -100,9 +100,15 @@ export class LabrinthTechReviewInternalModule extends AbstractModule {
|
||||
*/
|
||||
public async updateIssueDetail(
|
||||
detailId: string,
|
||||
data: Labrinth.TechReview.Internal.UpdateIssueRequest,
|
||||
data: Labrinth.TechReview.Internal.UpdateIssueDetailRequest,
|
||||
): Promise<void> {
|
||||
return this.client.request<void>(`/moderation/tech-review/issue-detail/${detailId}`, {
|
||||
return this.updateIssueDetails([{ detail_id: detailId, verdict: data.verdict }])
|
||||
}
|
||||
|
||||
public async updateIssueDetails(
|
||||
data: Labrinth.TechReview.Internal.UpdateIssueRequest[],
|
||||
): Promise<void> {
|
||||
return this.client.request<void>('/moderation/tech-review/issue-detail', {
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'PATCH',
|
||||
@@ -110,6 +116,45 @@ export class LabrinthTechReviewInternalModule extends AbstractModule {
|
||||
})
|
||||
}
|
||||
|
||||
public async updateGlobalIssueDetails(
|
||||
data: Labrinth.TechReview.Internal.UpdateGlobalIssueRequest[],
|
||||
): Promise<void> {
|
||||
return this.client.request<void>('/moderation/tech-review/global-issue-detail', {
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'POST',
|
||||
body: data,
|
||||
})
|
||||
}
|
||||
|
||||
public async searchGlobalIssueDetails(
|
||||
params: Labrinth.TechReview.Internal.SearchGlobalIssueDetailsRequest,
|
||||
): Promise<Labrinth.TechReview.Internal.SearchGlobalIssueDetailsResponse> {
|
||||
return this.client.request<Labrinth.TechReview.Internal.SearchGlobalIssueDetailsResponse>(
|
||||
'/moderation/tech-review/global-issue-detail/search',
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'POST',
|
||||
body: params,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
public async getGlobalIssueDetail(
|
||||
params: Labrinth.TechReview.Internal.GetGlobalIssueDetailRequest,
|
||||
): Promise<Labrinth.TechReview.Internal.GetGlobalIssueDetailResponse> {
|
||||
return this.client.request<Labrinth.TechReview.Internal.GetGlobalIssueDetailResponse>(
|
||||
'/moderation/tech-review/global-issue-detail/local-traces',
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'POST',
|
||||
body: params,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
public async submitProject(
|
||||
projectId: string,
|
||||
data: Labrinth.TechReview.Internal.SubmitProjectRequest,
|
||||
|
||||
@@ -2224,9 +2224,66 @@ export namespace Labrinth {
|
||||
| 'severity_desc'
|
||||
|
||||
export type UpdateIssueRequest = {
|
||||
detail_id: string
|
||||
verdict: DelphiReportIssueStatus
|
||||
}
|
||||
|
||||
export type UpdateIssueDetailRequest = {
|
||||
verdict: DelphiReportIssueStatus
|
||||
}
|
||||
|
||||
export type UpdateGlobalIssueRequest = {
|
||||
detail_key: string
|
||||
verdict: 'safe' | 'unsafe'
|
||||
}
|
||||
|
||||
export type SearchGlobalIssueDetailsRequest = {
|
||||
limit?: number
|
||||
page?: number
|
||||
query?: string | null
|
||||
}
|
||||
|
||||
export type SearchGlobalIssueDetailsResponse = {
|
||||
total: number
|
||||
traces: GlobalIssueDetail[]
|
||||
}
|
||||
|
||||
export type GetGlobalIssueDetailRequest = {
|
||||
detail_key: string
|
||||
limit?: number
|
||||
after_detail_id?: string | null
|
||||
}
|
||||
|
||||
export type GetGlobalIssueDetailResponse = {
|
||||
trace: GlobalIssueDetail
|
||||
next_after_detail_id: string | null
|
||||
}
|
||||
|
||||
export type GlobalIssueDetail = {
|
||||
detail_key: string
|
||||
verdict: DelphiReportIssueStatus
|
||||
local_trace_count: number
|
||||
local_traces: GlobalIssueDetailTrace[]
|
||||
}
|
||||
|
||||
export type GlobalIssueDetailTrace = {
|
||||
detail_id: string
|
||||
issue_id: string
|
||||
issue_type: string
|
||||
project_id: string
|
||||
project_slug: string | null
|
||||
project_name: string
|
||||
version_id: string
|
||||
version_number: string
|
||||
file_id: string
|
||||
file_name: string
|
||||
jar: string | null
|
||||
file_path: string
|
||||
severity: DelphiSeverity
|
||||
local_status: DelphiReportIssueStatus
|
||||
effective_status: DelphiReportIssueStatus
|
||||
}
|
||||
|
||||
export type SubmitProjectRequest = {
|
||||
verdict: 'safe' | 'unsafe'
|
||||
message?: string
|
||||
|
||||
Reference in New Issue
Block a user