mirror of
https://github.com/modrinth/code.git
synced 2026-08-25 00:55:25 +00:00
fix: global trace and tech review improvements (#6699)
* remaining globally safe/unsafe buttons * tweak global traces layout * deterministic local trace ordering * fix preview * ignore severity for ordering * show globally resolved traces toggle * minor frontend changes * prepr --------- Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
@@ -5,16 +5,21 @@
|
|||||||
<p class="m-0 break-words font-semibold text-contrast">
|
<p class="m-0 break-words font-semibold text-contrast">
|
||||||
{{ trace.project_name }}
|
{{ trace.project_name }}
|
||||||
</p>
|
</p>
|
||||||
<p class="m-0 mt-1 break-all text-sm text-secondary">
|
<p class="m-0 mt-1 flex flex-wrap items-center gap-1 text-sm text-secondary">
|
||||||
Project {{ trace.project_slug ?? trace.project_id }} / Version
|
<span class="break-all">{{ trace.version_number }}</span>
|
||||||
{{ trace.version_number }} / File {{ trace.file_name }}
|
<ChevronRightIcon class="size-4 shrink-0" aria-hidden="true" />
|
||||||
|
<span class="break-all">{{ decodeTracePath(trace.file_name) }}</span>
|
||||||
|
<template v-if="trace.jar">
|
||||||
|
<ChevronRightIcon class="size-4 shrink-0" aria-hidden="true" />
|
||||||
|
<span class="break-all">{{ decodeTracePath(trace.jar) }}</span>
|
||||||
|
</template>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<span class="text-sm text-secondary">Local</span>
|
<template v-if="trace.local_status !== 'pending'">
|
||||||
<Badge :type="trace.local_status" />
|
<span class="text-sm text-secondary">Local</span>
|
||||||
<span class="text-sm text-secondary">Effective</span>
|
<Badge :type="trace.local_status" />
|
||||||
<Badge :type="trace.effective_status" />
|
</template>
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<NuxtLink :to="localTraceLink">
|
<NuxtLink :to="localTraceLink">
|
||||||
<ExternalIcon aria-hidden="true" />
|
<ExternalIcon aria-hidden="true" />
|
||||||
@@ -23,31 +28,12 @@
|
|||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Labrinth } from '@modrinth/api-client'
|
import type { Labrinth } from '@modrinth/api-client'
|
||||||
import { ExternalIcon } from '@modrinth/assets'
|
import { ChevronRightIcon, ExternalIcon } from '@modrinth/assets'
|
||||||
import { Badge, ButtonStyled } from '@modrinth/ui'
|
import { Badge, ButtonStyled } from '@modrinth/ui'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -60,4 +46,12 @@ const localTraceLink = computed(
|
|||||||
props.trace.detail_id,
|
props.trace.detail_id,
|
||||||
)}`,
|
)}`,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
function decodeTracePath(path: string): string {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(path)
|
||||||
|
} catch {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -42,20 +42,34 @@
|
|||||||
:key="trace.detail_key"
|
:key="trace.detail_key"
|
||||||
class="universal-card flex flex-col gap-3"
|
class="universal-card flex flex-col gap-3"
|
||||||
>
|
>
|
||||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
<div class="grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<div class="flex min-w-0 items-center gap-2">
|
<div class="flex min-w-0 flex-wrap items-center gap-2">
|
||||||
<HashIcon class="shrink-0 text-secondary" aria-hidden="true" />
|
<HashIcon class="shrink-0 text-secondary" aria-hidden="true" />
|
||||||
<h2 class="m-0 min-w-0 text-lg font-semibold text-contrast">
|
<h2 class="m-0 min-w-0 text-lg font-semibold text-contrast">
|
||||||
Trace
|
Trace
|
||||||
<span class="break-all font-mono text-base">{{ trace.detail_key }}</span>
|
<span class="break-all font-mono text-base">{{ trace.detail_key }}</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
<span
|
||||||
|
v-if="getLatestLocalTrace(trace)"
|
||||||
|
class="rounded-full border border-solid px-2.5 py-1 text-sm font-medium capitalize"
|
||||||
|
:class="getSeverityBadgeColor(getLatestLocalTrace(trace)?.severity)"
|
||||||
|
>
|
||||||
|
{{ getLatestLocalTrace(trace)?.severity }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="getLatestLocalTrace(trace)" class="mt-1 flex flex-wrap gap-x-3 text-sm">
|
||||||
|
<p class="m-0 break-all text-secondary">
|
||||||
|
<span class="font-semibold text-contrast">Issue</span>
|
||||||
|
{{ getLatestLocalTrace(trace)?.issue_type }}
|
||||||
|
</p>
|
||||||
|
<p class="m-0 break-all text-secondary">
|
||||||
|
<span class="font-semibold text-contrast">Path</span>
|
||||||
|
{{ decodeTracePath(getLatestLocalTrace(trace)?.file_path ?? '') }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="m-0 mt-1 text-sm text-secondary">
|
|
||||||
{{ formatTraceCount(trace.local_trace_count) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex shrink-0 flex-wrap items-center gap-2">
|
<div class="flex shrink-0 flex-nowrap items-center gap-2">
|
||||||
<Badge :type="trace.verdict" />
|
<Badge :type="trace.verdict" />
|
||||||
<ButtonStyled color="red">
|
<ButtonStyled color="red">
|
||||||
<button
|
<button
|
||||||
@@ -70,15 +84,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="getPreviewLocalTraces(trace).length > 0" class="flex flex-col gap-2">
|
<div v-if="getPreviewLocalTraces(trace).length > 0" class="flex flex-col gap-2">
|
||||||
<div
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
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">
|
<p class="m-0 text-sm text-secondary">
|
||||||
Showing first {{ getPreviewLocalTraces(trace).length }} of
|
Showing {{ getPreviewLocalTraces(trace).length }} of
|
||||||
{{ getVisibleLocalTraceTotal(trace) }} local traces
|
{{ formatTraceCount(getVisibleLocalTraceTotal(trace)) }}
|
||||||
</p>
|
</p>
|
||||||
<ButtonStyled>
|
<ButtonStyled
|
||||||
|
v-if="getVisibleLocalTraceTotal(trace) > getPreviewLocalTraces(trace).length"
|
||||||
|
>
|
||||||
<NuxtLink :to="getGlobalTraceLink(trace)">
|
<NuxtLink :to="getGlobalTraceLink(trace)">
|
||||||
<ListIcon aria-hidden="true" />
|
<ListIcon aria-hidden="true" />
|
||||||
View all
|
View all
|
||||||
@@ -129,7 +142,7 @@ const isLoading = ref(false)
|
|||||||
const loadError = ref(false)
|
const loadError = ref(false)
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const itemsPerPage = 20
|
const itemsPerPage = 20
|
||||||
const localTracePreviewLimit = 10
|
const localTracePreviewLimit = 3
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const traces = ref<Labrinth.TechReview.Internal.GlobalIssueDetail[]>([])
|
const traces = ref<Labrinth.TechReview.Internal.GlobalIssueDetail[]>([])
|
||||||
const removingTraceKeys = reactive<Set<string>>(new Set())
|
const removingTraceKeys = reactive<Set<string>>(new Set())
|
||||||
@@ -148,6 +161,34 @@ function getPreviewLocalTraces(trace: Labrinth.TechReview.Internal.GlobalIssueDe
|
|||||||
return trace.local_traces.slice(0, localTracePreviewLimit)
|
return trace.local_traces.slice(0, localTracePreviewLimit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLatestLocalTrace(trace: Labrinth.TechReview.Internal.GlobalIssueDetail) {
|
||||||
|
return trace.local_traces.at(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeTracePath(path: string): string {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(path)
|
||||||
|
} catch {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSeverityBadgeColor(
|
||||||
|
severity: Labrinth.TechReview.Internal.DelphiSeverity | undefined,
|
||||||
|
): string {
|
||||||
|
switch (severity) {
|
||||||
|
case 'severe':
|
||||||
|
return 'border-red/60 bg-highlight-red text-red'
|
||||||
|
case 'high':
|
||||||
|
return 'border-orange/60 bg-highlight-orange text-orange'
|
||||||
|
case 'medium':
|
||||||
|
return 'border-green/60 bg-highlight-green text-green'
|
||||||
|
case 'low':
|
||||||
|
default:
|
||||||
|
return 'border-blue/60 bg-highlight-blue text-blue'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getVisibleLocalTraceTotal(trace: Labrinth.TechReview.Internal.GlobalIssueDetail) {
|
function getVisibleLocalTraceTotal(trace: Labrinth.TechReview.Internal.GlobalIssueDetail) {
|
||||||
return Math.max(trace.local_trace_count, trace.local_traces.length)
|
return Math.max(trace.local_trace_count, trace.local_traces.length)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
BanIcon,
|
BanIcon,
|
||||||
BugIcon,
|
BugIcon,
|
||||||
CheckCheckIcon,
|
CheckCheckIcon,
|
||||||
CheckCircleIcon,
|
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
@@ -20,7 +19,6 @@ import {
|
|||||||
ShieldCheckIcon,
|
ShieldCheckIcon,
|
||||||
SpinnerIcon,
|
SpinnerIcon,
|
||||||
TimerIcon,
|
TimerIcon,
|
||||||
TriangleAlertIcon,
|
|
||||||
XIcon,
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { type TechReviewContext, techReviewQuickReplies } from '@modrinth/moderation'
|
import { type TechReviewContext, techReviewQuickReplies } from '@modrinth/moderation'
|
||||||
@@ -36,6 +34,7 @@ import {
|
|||||||
NavTabs,
|
NavTabs,
|
||||||
OverflowMenu,
|
OverflowMenu,
|
||||||
type OverflowMenuOption,
|
type OverflowMenuOption,
|
||||||
|
Toggle,
|
||||||
useFormatBytes,
|
useFormatBytes,
|
||||||
useFormatDateTime,
|
useFormatDateTime,
|
||||||
useVIntl,
|
useVIntl,
|
||||||
@@ -247,6 +246,42 @@ function getAllDetails(): Labrinth.TechReview.Internal.ReportIssueDetail[] {
|
|||||||
return props.item.reports.flatMap((report) => report.issues.flatMap((issue) => issue.details))
|
return props.item.reports.flatMap((report) => report.issues.flatMap((issue) => issue.details))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hideGloballyPassed = ref(true)
|
||||||
|
|
||||||
|
function isDetailGloballyPassed(detail: Labrinth.TechReview.Internal.ReportIssueDetail): boolean {
|
||||||
|
if (detailDecisionScopes.get(detail.id) === 'global') {
|
||||||
|
return detailDecisions.get(detail.id) === 'safe'
|
||||||
|
}
|
||||||
|
|
||||||
|
return detail.global_status === 'safe'
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDetailGloballyResolved(detail: Labrinth.TechReview.Internal.ReportIssueDetail): boolean {
|
||||||
|
if (detailDecisionScopes.get(detail.id) === 'global') {
|
||||||
|
return detailDecisions.get(detail.id) !== 'pending'
|
||||||
|
}
|
||||||
|
|
||||||
|
return detail.global_status === 'safe' || detail.global_status === 'unsafe'
|
||||||
|
}
|
||||||
|
|
||||||
|
const globallyPassedSelectedFileCount = computed(() => {
|
||||||
|
if (!selectedFile.value) return 0
|
||||||
|
|
||||||
|
return selectedFile.value.issues.reduce(
|
||||||
|
(count, issue) => count + issue.details.filter(isDetailGloballyPassed).length,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const globallyResolvedSelectedFileCount = computed(() => {
|
||||||
|
if (!selectedFile.value) return 0
|
||||||
|
|
||||||
|
return selectedFile.value.issues.reduce(
|
||||||
|
(count, issue) => count + issue.details.filter(isDetailGloballyResolved).length,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
function applyDecisionToRelatedDetails(
|
function applyDecisionToRelatedDetails(
|
||||||
detailIds: string[],
|
detailIds: string[],
|
||||||
decision: DetailDecision,
|
decision: DetailDecision,
|
||||||
@@ -397,10 +432,7 @@ watch(
|
|||||||
allFiles.value = [...reports].sort((a, b) => {
|
allFiles.value = [...reports].sort((a, b) => {
|
||||||
const aComplete = getFileMarkedCount(a) === getFileDetailCount(a)
|
const aComplete = getFileMarkedCount(a) === getFileDetailCount(a)
|
||||||
const bComplete = getFileMarkedCount(b) === getFileDetailCount(b)
|
const bComplete = getFileMarkedCount(b) === getFileDetailCount(b)
|
||||||
if (aComplete !== bComplete) return aComplete ? 1 : -1
|
return aComplete === bComplete ? 0 : aComplete ? 1 : -1
|
||||||
const aSeverity = getFileHighestSeverity(a)
|
|
||||||
const bSeverity = getFileHighestSeverity(b)
|
|
||||||
return (severityOrder[bSeverity] ?? 0) - (severityOrder[aSeverity] ?? 0)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
@@ -615,6 +647,18 @@ const remainingUnmarkedCount = computed(() => {
|
|||||||
return getFileDetailCount(selectedFile.value) - getFileMarkedCount(selectedFile.value)
|
return getFileDetailCount(selectedFile.value) - getFileMarkedCount(selectedFile.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function getSelectedFileFlags(): ClassGroup['flags'] {
|
||||||
|
if (!selectedFile.value) return []
|
||||||
|
|
||||||
|
return selectedFile.value.issues.flatMap((issue) =>
|
||||||
|
issue.details.map((detail) => ({
|
||||||
|
issueId: issue.id,
|
||||||
|
issueType: issue.issue_type,
|
||||||
|
detail,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function getJarFlags(jarGroup: JarGroup): ClassGroup['flags'] {
|
function getJarFlags(jarGroup: JarGroup): ClassGroup['flags'] {
|
||||||
return jarGroup.classes.flatMap((classItem) => classItem.flags)
|
return jarGroup.classes.flatMap((classItem) => classItem.flags)
|
||||||
}
|
}
|
||||||
@@ -629,6 +673,74 @@ function getJarRemainingUnmarkedCount(jarGroup: JarGroup): number {
|
|||||||
|
|
||||||
const isBatchUpdating = ref(false)
|
const isBatchUpdating = ref(false)
|
||||||
|
|
||||||
|
function getRemainingGlobalDetailCount(flags: ClassGroup['flags']): number {
|
||||||
|
return new Set(
|
||||||
|
flags
|
||||||
|
.filter(
|
||||||
|
(flag) =>
|
||||||
|
getDetailDecision(flag.detail.id, flag.detail.status) === 'pending' &&
|
||||||
|
canUpdateGlobalDetail(flag.detail),
|
||||||
|
)
|
||||||
|
.map((flag) => flag.detail.key),
|
||||||
|
).size
|
||||||
|
}
|
||||||
|
|
||||||
|
async function batchMarkRemainingGlobally(flags: ClassGroup['flags'], verdict: 'safe' | 'unsafe') {
|
||||||
|
if (isBatchUpdating.value) return
|
||||||
|
|
||||||
|
const detailsByKey = new Map(
|
||||||
|
flags
|
||||||
|
.filter(
|
||||||
|
(flag) =>
|
||||||
|
getDetailDecision(flag.detail.id, flag.detail.status) === 'pending' &&
|
||||||
|
canUpdateGlobalDetail(flag.detail),
|
||||||
|
)
|
||||||
|
.map((flag) => [flag.detail.key, flag.detail]),
|
||||||
|
)
|
||||||
|
const details = [...detailsByKey.values()]
|
||||||
|
|
||||||
|
if (details.length === 0) return
|
||||||
|
|
||||||
|
isBatchUpdating.value = true
|
||||||
|
try {
|
||||||
|
await client.labrinth.tech_review_internal.updateGlobalIssueDetails(
|
||||||
|
details.map((detail) => ({ detail_key: detail.key, verdict })),
|
||||||
|
)
|
||||||
|
|
||||||
|
applyDecisionToRelatedDetails(
|
||||||
|
details.map((detail) => detail.id),
|
||||||
|
verdictToDecision(verdict),
|
||||||
|
'global',
|
||||||
|
)
|
||||||
|
|
||||||
|
addNotification({
|
||||||
|
type: 'success',
|
||||||
|
title: `Globally marked ${details.length} trace keys as ${verdict}`,
|
||||||
|
text: `All remaining eligible traces have been globally marked as ${
|
||||||
|
verdict === 'safe' ? 'false positives' : 'malicious'
|
||||||
|
}.`,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (
|
||||||
|
selectedFile.value &&
|
||||||
|
getFileMarkedCount(selectedFile.value) === getFileDetailCount(selectedFile.value)
|
||||||
|
) {
|
||||||
|
backToFileList()
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('refetch')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to batch update global traces:', error)
|
||||||
|
addNotification({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Global batch update failed',
|
||||||
|
text: 'An error occurred while globally updating traces.',
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
isBatchUpdating.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function batchMarkRemaining(verdict: 'safe' | 'unsafe') {
|
async function batchMarkRemaining(verdict: 'safe' | 'unsafe') {
|
||||||
if (!selectedFile.value || isBatchUpdating.value) return
|
if (!selectedFile.value || isBatchUpdating.value) return
|
||||||
|
|
||||||
@@ -913,7 +1025,7 @@ interface JarGroup {
|
|||||||
function splitJarSegments(jar: string | null, currentFileName: string | null): string[] {
|
function splitJarSegments(jar: string | null, currentFileName: string | null): string[] {
|
||||||
if (!jar) return []
|
if (!jar) return []
|
||||||
const segments = jar
|
const segments = jar
|
||||||
.split('#')
|
.split(/[/#]/)
|
||||||
.map((s) => decodeURIComponent(s.trim()))
|
.map((s) => decodeURIComponent(s.trim()))
|
||||||
.filter((s) => s.length > 0)
|
.filter((s) => s.length > 0)
|
||||||
// Skip the first segment if it matches the current file tab (it's already shown in the file list)
|
// Skip the first segment if it matches the current file tab (it's already shown in the file list)
|
||||||
@@ -934,6 +1046,10 @@ const groupedByClass = computed<ClassGroup[]>(() => {
|
|||||||
|
|
||||||
for (const issue of selectedFile.value.issues) {
|
for (const issue of selectedFile.value.issues) {
|
||||||
for (const detail of issue.details) {
|
for (const detail of issue.details) {
|
||||||
|
if (hideGloballyPassed.value && isDetailGloballyPassed(detail)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
const classKey = `${detail.jar ?? ''}::${detail.file_path}`
|
const classKey = `${detail.jar ?? ''}::${detail.file_path}`
|
||||||
if (!classMap.has(classKey)) {
|
if (!classMap.has(classKey)) {
|
||||||
classMap.set(classKey, {
|
classMap.set(classKey, {
|
||||||
@@ -959,20 +1075,11 @@ const groupedByClass = computed<ClassGroup[]>(() => {
|
|||||||
classGroup.flags.sort((a, b) => {
|
classGroup.flags.sort((a, b) => {
|
||||||
const aPreReviewed = isPreReviewed(a.detail.id, a.detail.status)
|
const aPreReviewed = isPreReviewed(a.detail.id, a.detail.status)
|
||||||
const bPreReviewed = isPreReviewed(b.detail.id, b.detail.status)
|
const bPreReviewed = isPreReviewed(b.detail.id, b.detail.status)
|
||||||
|
return aPreReviewed === bPreReviewed ? 0 : aPreReviewed ? 1 : -1
|
||||||
if (aPreReviewed !== bPreReviewed) {
|
|
||||||
return aPreReviewed ? 1 : -1
|
|
||||||
}
|
|
||||||
|
|
||||||
return (severityOrder[b.detail.severity] ?? 0) - (severityOrder[a.detail.severity] ?? 0)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(classMap.values()).sort((a, b) => {
|
return Array.from(classMap.values())
|
||||||
const aSeverity = getHighestSeverityInClass(a.flags)
|
|
||||||
const bSeverity = getHighestSeverityInClass(b.flags)
|
|
||||||
return (severityOrder[bSeverity] ?? 0) - (severityOrder[aSeverity] ?? 0)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const groupedByJar = computed<JarGroup[]>(() => {
|
const groupedByJar = computed<JarGroup[]>(() => {
|
||||||
@@ -994,11 +1101,7 @@ const groupedByJar = computed<JarGroup[]>(() => {
|
|||||||
return Array.from(jarMap.values()).sort((a, b) => {
|
return Array.from(jarMap.values()).sort((a, b) => {
|
||||||
const aRoot = isRootJarGroup(a)
|
const aRoot = isRootJarGroup(a)
|
||||||
const bRoot = isRootJarGroup(b)
|
const bRoot = isRootJarGroup(b)
|
||||||
if (aRoot !== bRoot) return aRoot ? -1 : 1
|
return aRoot === bRoot ? 0 : aRoot ? -1 : 1
|
||||||
|
|
||||||
const aSeverity = getHighestSeverityInClass(a.classes.flatMap((classItem) => classItem.flags))
|
|
||||||
const bSeverity = getHighestSeverityInClass(b.classes.flatMap((classItem) => classItem.flags))
|
|
||||||
return (severityOrder[bSeverity] ?? 0) - (severityOrder[aSeverity] ?? 0)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1589,21 +1692,73 @@ function copyId() {
|
|||||||
|
|
||||||
<template v-else-if="currentTab === 'File' && selectedFile">
|
<template v-else-if="currentTab === 'File' && selectedFile">
|
||||||
<div
|
<div
|
||||||
v-if="remainingUnmarkedCount > 0"
|
v-if="getFileDetailCount(selectedFile) > 0"
|
||||||
class="flex gap-2 border-x border-b border-t-0 border-solid border-surface-3 bg-surface-2 p-4"
|
class="flex flex-wrap items-center justify-between gap-3 border-x border-b border-t-0 border-solid border-surface-3 bg-surface-2 p-4"
|
||||||
>
|
>
|
||||||
<ButtonStyled color="brand" :disabled="isBatchUpdating">
|
<div
|
||||||
<button @click="batchMarkRemaining('safe')">
|
v-if="remainingUnmarkedCount > 0"
|
||||||
<CheckCircleIcon class="size-5" />
|
class="detail-verdict-buttons"
|
||||||
Remaining safe ({{ remainingUnmarkedCount }})
|
role="group"
|
||||||
|
aria-label="Remaining issue actions"
|
||||||
|
>
|
||||||
|
<span class="remaining-verdict-label"
|
||||||
|
>Remaining issues ({{ remainingUnmarkedCount }})</span
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-tooltip="'Remaining globally safe'"
|
||||||
|
class="detail-verdict-button detail-verdict-button--safe"
|
||||||
|
aria-label="Remaining globally safe"
|
||||||
|
:disabled="
|
||||||
|
isBatchUpdating || getRemainingGlobalDetailCount(getSelectedFileFlags()) === 0
|
||||||
|
"
|
||||||
|
@click="batchMarkRemainingGlobally(getSelectedFileFlags(), 'safe')"
|
||||||
|
>
|
||||||
|
<CheckCheckIcon aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
<button
|
||||||
<ButtonStyled color="red" :disabled="isBatchUpdating">
|
v-tooltip="'Remaining safe'"
|
||||||
<button @click="batchMarkRemaining('unsafe')">
|
class="detail-verdict-button detail-verdict-button--safe"
|
||||||
<TriangleAlertIcon class="size-5" />
|
aria-label="Remaining safe"
|
||||||
Remaining malware ({{ remainingUnmarkedCount }})
|
:disabled="isBatchUpdating"
|
||||||
|
@click="batchMarkRemaining('safe')"
|
||||||
|
>
|
||||||
|
<CheckIcon aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
<button
|
||||||
|
v-tooltip="'Remaining malware'"
|
||||||
|
class="detail-verdict-button detail-verdict-button--unsafe"
|
||||||
|
aria-label="Remaining malware"
|
||||||
|
:disabled="isBatchUpdating"
|
||||||
|
@click="batchMarkRemaining('unsafe')"
|
||||||
|
>
|
||||||
|
<BanIcon aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-tooltip="'Remaining globally unsafe'"
|
||||||
|
class="detail-verdict-button detail-verdict-button--unsafe"
|
||||||
|
aria-label="Remaining globally unsafe"
|
||||||
|
:disabled="
|
||||||
|
isBatchUpdating || getRemainingGlobalDetailCount(getSelectedFileFlags()) === 0
|
||||||
|
"
|
||||||
|
@click="batchMarkRemainingGlobally(getSelectedFileFlags(), 'unsafe')"
|
||||||
|
>
|
||||||
|
<ShieldAlertIcon aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<label class="ml-auto flex cursor-pointer items-center gap-3 text-sm">
|
||||||
|
<span class="text-right text-secondary">
|
||||||
|
Hide globally passed
|
||||||
|
<span class="text-tertiary block text-xs">
|
||||||
|
{{ globallyResolvedSelectedFileCount }}/{{ getFileDetailCount(selectedFile) }}
|
||||||
|
traces globally resolved
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<Toggle
|
||||||
|
v-model="hideGloballyPassed"
|
||||||
|
:disabled="globallyPassedSelectedFileCount === 0"
|
||||||
|
small
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="jarGroup in groupedByJar"
|
v-for="jarGroup in groupedByJar"
|
||||||
@@ -1637,25 +1792,55 @@ function copyId() {
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="getJarRemainingUnmarkedCount(jarGroup) > 0" class="flex gap-2">
|
<div
|
||||||
<ButtonStyled color="brand" size="small">
|
v-if="getJarRemainingUnmarkedCount(jarGroup) > 0"
|
||||||
<button
|
class="detail-verdict-buttons"
|
||||||
:disabled="isBatchUpdating"
|
role="group"
|
||||||
@click="batchMarkRemainingInJar(jarGroup, 'safe')"
|
aria-label="Remaining JAR issue actions"
|
||||||
>
|
>
|
||||||
<CheckCircleIcon class="size-4" />
|
<span class="remaining-verdict-label">
|
||||||
Remaining safe ({{ getJarRemainingUnmarkedCount(jarGroup) }})
|
Remaining issues ({{ getJarRemainingUnmarkedCount(jarGroup) }})
|
||||||
</button>
|
</span>
|
||||||
</ButtonStyled>
|
<button
|
||||||
<ButtonStyled color="red" size="small">
|
v-tooltip="'Remaining globally safe'"
|
||||||
<button
|
class="detail-verdict-button detail-verdict-button--safe"
|
||||||
:disabled="isBatchUpdating"
|
aria-label="Remaining globally safe"
|
||||||
@click="batchMarkRemainingInJar(jarGroup, 'unsafe')"
|
:disabled="
|
||||||
>
|
isBatchUpdating || getRemainingGlobalDetailCount(getJarFlags(jarGroup)) === 0
|
||||||
<TriangleAlertIcon class="size-4" />
|
"
|
||||||
Remaining malware ({{ getJarRemainingUnmarkedCount(jarGroup) }})
|
@click="batchMarkRemainingGlobally(getJarFlags(jarGroup), 'safe')"
|
||||||
</button>
|
>
|
||||||
</ButtonStyled>
|
<CheckCheckIcon aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-tooltip="'Remaining safe'"
|
||||||
|
class="detail-verdict-button detail-verdict-button--safe"
|
||||||
|
aria-label="Remaining safe"
|
||||||
|
:disabled="isBatchUpdating"
|
||||||
|
@click="batchMarkRemainingInJar(jarGroup, 'safe')"
|
||||||
|
>
|
||||||
|
<CheckIcon aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-tooltip="'Remaining malware'"
|
||||||
|
class="detail-verdict-button detail-verdict-button--unsafe"
|
||||||
|
aria-label="Remaining malware"
|
||||||
|
:disabled="isBatchUpdating"
|
||||||
|
@click="batchMarkRemainingInJar(jarGroup, 'unsafe')"
|
||||||
|
>
|
||||||
|
<BanIcon aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-tooltip="'Remaining globally unsafe'"
|
||||||
|
class="detail-verdict-button detail-verdict-button--unsafe"
|
||||||
|
aria-label="Remaining globally unsafe"
|
||||||
|
:disabled="
|
||||||
|
isBatchUpdating || getRemainingGlobalDetailCount(getJarFlags(jarGroup)) === 0
|
||||||
|
"
|
||||||
|
@click="batchMarkRemainingGlobally(getJarFlags(jarGroup), 'unsafe')"
|
||||||
|
>
|
||||||
|
<ShieldAlertIcon aria-hidden="true" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1968,6 +2153,14 @@ pre {
|
|||||||
background: var(--surface-3);
|
background: var(--surface-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.remaining-verdict-label {
|
||||||
|
padding-inline: 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
.detail-verdict-button {
|
.detail-verdict-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 2rem;
|
width: 2rem;
|
||||||
|
|||||||
@@ -20,20 +20,34 @@
|
|||||||
heading="Failed to load global detail trace"
|
heading="Failed to load global detail trace"
|
||||||
/>
|
/>
|
||||||
<article v-else-if="trace" class="universal-card flex flex-col gap-3">
|
<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="grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<div class="flex min-w-0 items-center gap-2">
|
<div class="flex min-w-0 flex-wrap items-center gap-2">
|
||||||
<HashIcon class="shrink-0 text-secondary" aria-hidden="true" />
|
<HashIcon class="shrink-0 text-secondary" aria-hidden="true" />
|
||||||
<h2 class="m-0 min-w-0 text-lg font-semibold text-contrast">
|
<h2 class="m-0 min-w-0 text-lg font-semibold text-contrast">
|
||||||
Trace
|
Trace
|
||||||
<span class="break-all font-mono text-base">{{ trace.detail_key }}</span>
|
<span class="break-all font-mono text-base">{{ trace.detail_key }}</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
<span
|
||||||
|
v-if="latestLocalTrace"
|
||||||
|
class="rounded-full border border-solid px-2.5 py-1 text-sm font-medium capitalize"
|
||||||
|
:class="getSeverityBadgeColor(latestLocalTrace.severity)"
|
||||||
|
>
|
||||||
|
{{ latestLocalTrace.severity }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="latestLocalTrace" class="mt-1 flex flex-wrap gap-x-3 text-sm">
|
||||||
|
<p class="m-0 break-all text-secondary">
|
||||||
|
<span class="font-semibold text-contrast">Issue</span>
|
||||||
|
{{ latestLocalTrace.issue_type }}
|
||||||
|
</p>
|
||||||
|
<p class="m-0 break-all text-secondary">
|
||||||
|
<span class="font-semibold text-contrast">Path</span>
|
||||||
|
{{ decodeTracePath(latestLocalTrace.file_path) }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="m-0 mt-1 text-sm text-secondary">
|
|
||||||
{{ pageStart }}-{{ pageEnd }} of {{ trace.local_trace_count }} local traces
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex shrink-0 flex-wrap items-center gap-2">
|
<div class="flex shrink-0 flex-nowrap items-center gap-2">
|
||||||
<Badge :type="trace.verdict" />
|
<Badge :type="trace.verdict" />
|
||||||
<ButtonStyled color="red">
|
<ButtonStyled color="red">
|
||||||
<button :disabled="isRemoving" @click="removeGlobalTrace">
|
<button :disabled="isRemoving" @click="removeGlobalTrace">
|
||||||
@@ -45,11 +59,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="trace.local_trace_count > localTracePageSize"
|
class="flex flex-wrap items-center justify-between gap-3 border-0 border-t border-solid border-divider pt-3"
|
||||||
class="flex flex-wrap items-center justify-between gap-3"
|
|
||||||
>
|
>
|
||||||
<Pagination :page="currentPage" :count="pageCount" @switch-page="switchPage" />
|
<p class="m-0 text-sm text-secondary">
|
||||||
<p v-if="isLoading" class="m-0 text-sm text-secondary">Loading page...</p>
|
{{ pageStart }}-{{ pageEnd }} of {{ trace.local_trace_count }} local traces
|
||||||
|
<span v-if="isLoading"> · Loading page...</span>
|
||||||
|
</p>
|
||||||
|
<Pagination
|
||||||
|
v-if="trace.local_trace_count > localTracePageSize"
|
||||||
|
:page="currentPage"
|
||||||
|
:count="pageCount"
|
||||||
|
@switch-page="switchPage"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="trace.local_traces.length > 0" class="flex flex-col gap-2">
|
<div v-if="trace.local_traces.length > 0" class="flex flex-col gap-2">
|
||||||
@@ -114,6 +135,29 @@ const pageStart = computed(() =>
|
|||||||
const pageEnd = computed(() =>
|
const pageEnd = computed(() =>
|
||||||
Math.min(currentPage.value * localTracePageSize, trace.value?.local_trace_count ?? 0),
|
Math.min(currentPage.value * localTracePageSize, trace.value?.local_trace_count ?? 0),
|
||||||
)
|
)
|
||||||
|
const latestLocalTrace = computed(() => trace.value?.local_traces.at(-1))
|
||||||
|
|
||||||
|
function decodeTracePath(path: string): string {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(path)
|
||||||
|
} catch {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSeverityBadgeColor(severity: Labrinth.TechReview.Internal.DelphiSeverity): string {
|
||||||
|
switch (severity) {
|
||||||
|
case 'severe':
|
||||||
|
return 'border-red/60 bg-highlight-red text-red'
|
||||||
|
case 'high':
|
||||||
|
return 'border-orange/60 bg-highlight-orange text-orange'
|
||||||
|
case 'medium':
|
||||||
|
return 'border-green/60 bg-highlight-green text-green'
|
||||||
|
case 'low':
|
||||||
|
default:
|
||||||
|
return 'border-blue/60 bg-highlight-blue text-blue'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchTracePage(afterDetailId: string | null) {
|
async function fetchTracePage(afterDetailId: string | null) {
|
||||||
return await client.labrinth.tech_review_internal.getGlobalIssueDetail({
|
return await client.labrinth.tech_review_internal.getGlobalIssueDetail({
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "SELECT id FROM versions WHERE mod_id = ANY($1::bigint[])",
|
"query": "SELECT id FROM versions WHERE mod_id = ANY($1::bigint[]) ORDER BY id",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -18,5 +18,5 @@
|
|||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "50d087248322373b2d5fc40e1c7b8a855b55d907d7fba0af0738c37366cb93e0"
|
"hash": "147e143aaaff82c71b89daddcc8805a0874d1c5ad8469dd83133ac771b86ea4e"
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"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.local_status AS \"local_status?: DelphiStatus\",\n didws.global_status AS \"global_status?: DelphiStatus\",\n didws.status AS \"status!: DelphiStatus\"\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.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.local_status AS \"local_status?: DelphiStatus\",\n didws.global_status AS \"global_status?: DelphiStatus\",\n didws.status AS \"status!: DelphiStatus\"\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.issue_id = ANY($1::bigint[])\n ORDER BY didws.issue_id, didws.id\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -117,5 +117,5 @@
|
|||||||
true
|
true
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "9070b1b6a5b1e93eb1fd1838c522fa6084ad6a5c86fafbfe8448ea7cc86f38ba"
|
"hash": "63a9a89443566baa880ddbe05f741e2a83b50d89f5954ab8d951a090e702de73"
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n SELECT\n id AS \"file_id: DBFileId\",\n version_id AS \"version_id: DBVersionId\",\n url,\n filename,\n size\n FROM files\n WHERE version_id = ANY($1::bigint[])\n ",
|
"query": "\n SELECT\n id AS \"file_id: DBFileId\",\n version_id AS \"version_id: DBVersionId\",\n url,\n filename,\n size\n FROM files\n WHERE version_id = ANY($1::bigint[])\n ORDER BY version_id, id\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -42,5 +42,5 @@
|
|||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "8e38571ffae8bf246973d328c890a45b87c0f71d53cf46f390c4b286677b2b31"
|
"hash": "72012eb0655d9130a7c7894f7682015c170b2fb325aaef554f08aac34457a3fa"
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n SELECT\n id AS \"report_id!: DelphiReportId\",\n file_id AS \"file_id!: DBFileId\",\n created,\n severity AS \"severity!: DelphiSeverity\"\n FROM delphi_reports\n WHERE file_id = ANY($1::bigint[])\n ",
|
"query": "\n SELECT\n id AS \"report_id!: DelphiReportId\",\n file_id AS \"file_id!: DBFileId\",\n created,\n severity AS \"severity!: DelphiSeverity\"\n FROM delphi_reports\n WHERE file_id = ANY($1::bigint[])\n ORDER BY file_id, created, id\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -48,5 +48,5 @@
|
|||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "10e2a3b31ba94b93ed2d6c9753a5aabf13190a0b336089e6521022069813cf17"
|
"hash": "8d37c8c2633b7d601c7ba74d63608ff40af3b6b56f43540ec3798ef30e5c0b9d"
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n SELECT\n id AS \"id: DelphiReportIssueId\",\n report_id AS \"report_id: DelphiReportId\",\n issue_type\n FROM delphi_report_issues\n WHERE report_id = ANY($1::bigint[])\n ",
|
"query": "\n SELECT\n id AS \"id: DelphiReportIssueId\",\n report_id AS \"report_id: DelphiReportId\",\n issue_type\n FROM delphi_report_issues\n WHERE report_id = ANY($1::bigint[])\n ORDER BY report_id, id\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -30,5 +30,5 @@
|
|||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "632b79f51dd39a75ebff838ca4beab463f84aa32246288cc251881ec41d207f5"
|
"hash": "c69a7a5296941a52f15c3c073e65e8eff6516004d0636357f146047789f8f8c1"
|
||||||
}
|
}
|
||||||
@@ -443,7 +443,7 @@ async fn fetch_project_reports(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let version_id_rows = sqlx::query!(
|
let version_id_rows = sqlx::query!(
|
||||||
"SELECT id FROM versions WHERE mod_id = ANY($1::bigint[])",
|
"SELECT id FROM versions WHERE mod_id = ANY($1::bigint[]) ORDER BY id",
|
||||||
&project_ids.iter().map(|id| id.0).collect::<Vec<_>>()
|
&project_ids.iter().map(|id| id.0).collect::<Vec<_>>()
|
||||||
)
|
)
|
||||||
.fetch_all(pool)
|
.fetch_all(pool)
|
||||||
@@ -469,6 +469,7 @@ async fn fetch_project_reports(
|
|||||||
size
|
size
|
||||||
FROM files
|
FROM files
|
||||||
WHERE version_id = ANY($1::bigint[])
|
WHERE version_id = ANY($1::bigint[])
|
||||||
|
ORDER BY version_id, id
|
||||||
"#,
|
"#,
|
||||||
&version_ids.iter().map(|id| id.0).collect::<Vec<_>>()
|
&version_ids.iter().map(|id| id.0).collect::<Vec<_>>()
|
||||||
)
|
)
|
||||||
@@ -485,6 +486,7 @@ async fn fetch_project_reports(
|
|||||||
severity AS "severity!: DelphiSeverity"
|
severity AS "severity!: DelphiSeverity"
|
||||||
FROM delphi_reports
|
FROM delphi_reports
|
||||||
WHERE file_id = ANY($1::bigint[])
|
WHERE file_id = ANY($1::bigint[])
|
||||||
|
ORDER BY file_id, created, id
|
||||||
"#,
|
"#,
|
||||||
&file_rows.iter().map(|f| f.file_id.0).collect::<Vec<_>>()
|
&file_rows.iter().map(|f| f.file_id.0).collect::<Vec<_>>()
|
||||||
)
|
)
|
||||||
@@ -500,6 +502,7 @@ async fn fetch_project_reports(
|
|||||||
issue_type
|
issue_type
|
||||||
FROM delphi_report_issues
|
FROM delphi_report_issues
|
||||||
WHERE report_id = ANY($1::bigint[])
|
WHERE report_id = ANY($1::bigint[])
|
||||||
|
ORDER BY report_id, id
|
||||||
"#,
|
"#,
|
||||||
&report_rows
|
&report_rows
|
||||||
.iter()
|
.iter()
|
||||||
@@ -528,6 +531,7 @@ async fn fetch_project_reports(
|
|||||||
didws.status AS "status!: DelphiStatus"
|
didws.status AS "status!: DelphiStatus"
|
||||||
FROM delphi_issue_details_with_statuses didws
|
FROM delphi_issue_details_with_statuses didws
|
||||||
WHERE didws.issue_id = ANY($1::bigint[])
|
WHERE didws.issue_id = ANY($1::bigint[])
|
||||||
|
ORDER BY didws.issue_id, didws.id
|
||||||
"#,
|
"#,
|
||||||
&issue_ids.iter().map(|i| i.0).collect::<Vec<_>>()
|
&issue_ids.iter().map(|i| i.0).collect::<Vec<_>>()
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user