mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 12:05:53 +00:00
wip: delphi trace transform rules
This commit is contained in:
@@ -15,7 +15,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FolderIcon, GlobeIcon, HashIcon, ReportIcon, ShieldCheckIcon } from '@modrinth/assets'
|
||||
import {
|
||||
FolderIcon,
|
||||
GlobeIcon,
|
||||
HashIcon,
|
||||
ReportIcon,
|
||||
SettingsIcon,
|
||||
ShieldCheckIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Chips, defineMessages, NavTabs, useVIntl } from '@modrinth/ui'
|
||||
|
||||
definePageMeta({
|
||||
@@ -51,6 +58,10 @@ const messages = defineMessages({
|
||||
id: 'moderation.page.global-detail-traces',
|
||||
defaultMessage: 'Global traces',
|
||||
},
|
||||
delphiRulesTitle: {
|
||||
id: 'moderation.page.delphi-rules',
|
||||
defaultMessage: 'Delphi rules',
|
||||
},
|
||||
})
|
||||
|
||||
const moderationLinks = [
|
||||
@@ -71,6 +82,11 @@ const moderationLinks = [
|
||||
href: '/moderation/global-traces',
|
||||
icon: HashIcon,
|
||||
},
|
||||
{
|
||||
label: formatMessage(messages.delphiRulesTitle),
|
||||
href: '/moderation/technical-review/rules',
|
||||
icon: SettingsIcon,
|
||||
},
|
||||
]
|
||||
|
||||
const mobileNavOptions = [
|
||||
@@ -79,12 +95,15 @@ const mobileNavOptions = [
|
||||
formatMessage(messages.reportsTitle),
|
||||
formatMessage(messages.externalFilesTitle),
|
||||
formatMessage(messages.globalDetailTracesTitle),
|
||||
formatMessage(messages.delphiRulesTitle),
|
||||
]
|
||||
|
||||
const selectedChip = computed({
|
||||
get() {
|
||||
const path = route.path
|
||||
if (path.startsWith('/moderation/technical-review')) {
|
||||
if (path.startsWith('/moderation/technical-review/rules')) {
|
||||
return formatMessage(messages.delphiRulesTitle)
|
||||
} else if (path.startsWith('/moderation/technical-review')) {
|
||||
return formatMessage(messages.technicalReviewTitle)
|
||||
} else if (path.startsWith('/moderation/reports')) {
|
||||
return formatMessage(messages.reportsTitle)
|
||||
@@ -110,6 +129,8 @@ function navigateToPage(selectedOption: string) {
|
||||
router.push('/moderation/external-projects')
|
||||
} else if (selectedOption === formatMessage(messages.globalDetailTracesTitle)) {
|
||||
router.push('/moderation/global-traces')
|
||||
} else if (selectedOption === formatMessage(messages.delphiRulesTitle)) {
|
||||
router.push('/moderation/technical-review/rules')
|
||||
} else {
|
||||
router.push('/moderation')
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@ import {
|
||||
ListFilterIcon,
|
||||
LoaderCircleIcon,
|
||||
SearchIcon,
|
||||
SettingsIcon,
|
||||
SortAscIcon,
|
||||
SortDescIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Combobox,
|
||||
type ComboboxOption,
|
||||
commonMessages,
|
||||
@@ -607,6 +609,13 @@ watch(totalPages, (pages) => {
|
||||
<div
|
||||
class="flex flex-col items-stretch justify-end gap-2 sm:flex-row sm:items-center lg:flex-shrink-0"
|
||||
>
|
||||
<ButtonStyled>
|
||||
<NuxtLink to="/moderation/technical-review/rules">
|
||||
<SettingsIcon class="size-5" />
|
||||
Rules
|
||||
</NuxtLink>
|
||||
</ButtonStyled>
|
||||
|
||||
<Combobox
|
||||
v-model="currentResponseFilter"
|
||||
class="!w-full flex-grow sm:!w-[120px] sm:flex-grow-0"
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
<template>
|
||||
<NewModal ref="ruleModal" :header="modalTitle">
|
||||
<form class="flex w-[36rem] max-w-full flex-col gap-3" @submit.prevent="saveRule">
|
||||
<label class="font-semibold text-contrast" for="rule-name">Name</label>
|
||||
<StyledInput
|
||||
id="rule-name"
|
||||
v-model="form.name"
|
||||
type="text"
|
||||
maxlength="128"
|
||||
placeholder="Known-safe obfuscated bootstrap"
|
||||
/>
|
||||
|
||||
<label class="font-semibold text-contrast" for="rule-priority">Priority</label>
|
||||
<StyledInput id="rule-priority" v-model="form.priority" type="number" placeholder="0" />
|
||||
<p class="m-0 text-sm text-secondary">Higher-priority rules are evaluated first.</p>
|
||||
|
||||
<label class="font-semibold text-contrast" for="rule-expression">CEL expression</label>
|
||||
<StyledInput
|
||||
id="rule-expression"
|
||||
v-model="form.expression"
|
||||
type="text"
|
||||
multiline
|
||||
resize="vertical"
|
||||
class="font-mono"
|
||||
input-class="min-h-64 font-mono"
|
||||
/>
|
||||
<p class="m-0 text-sm text-secondary">
|
||||
Return <code>null</code> when the rule does not match, or a map containing
|
||||
<code>severity</code> and/or <code>hidden</code> when it does.
|
||||
</p>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<ButtonStyled>
|
||||
<button type="button" @click="closeRuleModal">Cancel</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button type="submit" :disabled="isSaving">
|
||||
{{ isSaving ? 'Saving...' : 'Save rule' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</form>
|
||||
</NewModal>
|
||||
|
||||
<ConfirmModal
|
||||
ref="deleteModal"
|
||||
:title="`Delete ${ruleToDelete?.name ?? 'rule'}?`"
|
||||
description="This permanently deletes the rule, its revisions, and its materialized effects."
|
||||
:markdown="false"
|
||||
proceed-label="Delete rule"
|
||||
@proceed="deleteRule"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<ButtonStyled circular type="transparent">
|
||||
<NuxtLink to="/moderation/technical-review" aria-label="Back to tech review queue">
|
||||
<ArrowLeftIcon />
|
||||
</NuxtLink>
|
||||
</ButtonStyled>
|
||||
<div>
|
||||
<h1 class="m-0 text-2xl font-bold text-contrast">Delphi rules</h1>
|
||||
<p class="m-0 text-secondary">Transform or hide Delphi issue traces.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ButtonStyled color="brand">
|
||||
<button type="button" @click="openCreateModal">
|
||||
<PlusIcon />
|
||||
Create rule
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<details class="universal-card text-sm">
|
||||
<summary class="cursor-pointer font-semibold text-contrast">CEL contract and input</summary>
|
||||
<div class="mt-3 flex flex-col gap-2 text-secondary">
|
||||
<p class="m-0">
|
||||
Effects are maps such as
|
||||
<code>{ "severity": "low", "hidden": false }</code>. Severity can be <code>low</code>,
|
||||
<code>medium</code>, <code>high</code>, or <code>severe</code>.
|
||||
</p>
|
||||
<p class="m-0">
|
||||
The <code>input</code> object contains <code>schema_version</code>,
|
||||
<code>trace</code> (<code>key</code>, <code>issue_type</code>, <code>severity</code>,
|
||||
<code>jar</code>, <code>file_path</code>, <code>data</code>),
|
||||
<code>scan.delphi_version</code>, <code>artifact</code> (<code>size</code>,
|
||||
<code>hashes</code>), and stable IDs under <code>scope</code> (<code>project_id</code>,
|
||||
<code>version_id</code>, <code>file_id</code>).
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div v-if="isLoading" class="universal-card flex h-32 items-center justify-center">
|
||||
<LoaderCircleIcon class="size-8 animate-spin text-secondary" />
|
||||
</div>
|
||||
<div v-else-if="loadFailed" class="universal-card flex flex-col items-center gap-3 py-8">
|
||||
<p class="m-0 text-secondary">Failed to load Delphi rules.</p>
|
||||
<ButtonStyled>
|
||||
<button type="button" @click="loadRules">Try again</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<EmptyState
|
||||
v-else-if="rules.length === 0"
|
||||
type="no-search-result"
|
||||
heading="No Delphi rules"
|
||||
description="Create a rule to transform matching issue traces."
|
||||
/>
|
||||
<div v-else class="flex flex-col gap-3">
|
||||
<article v-for="rule in rules" :key="rule.id" class="universal-card flex flex-col gap-3">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-bold text-contrast">{{ rule.name }}</h2>
|
||||
<p class="m-0 text-sm text-secondary">
|
||||
Priority {{ rule.priority }} · revision {{ rule.revision_id }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled>
|
||||
<button type="button" @click="openEditModal(rule)">
|
||||
<EditIcon />
|
||||
Edit
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="red">
|
||||
<button type="button" @click="openDeleteModal(rule)">
|
||||
<TrashIcon />
|
||||
Delete
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<pre
|
||||
class="m-0 overflow-x-auto rounded-lg bg-bg-raised p-3 text-sm"
|
||||
><code>{{ rule.expression }}</code></pre>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { ArrowLeftIcon, EditIcon, LoaderCircleIcon, PlusIcon, TrashIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
ConfirmModal,
|
||||
EmptyState,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
NewModal,
|
||||
StyledInput,
|
||||
} from '@modrinth/ui'
|
||||
|
||||
const DEFAULT_EXPRESSION = `input.trace.issue_type == "OBFUSCATED_NAMES"
|
||||
? {"severity": "low", "hidden": false}
|
||||
: null`
|
||||
|
||||
useHead({ title: 'Delphi rules - Modrinth' })
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const ruleModal = useTemplateRef<InstanceType<typeof NewModal>>('ruleModal')
|
||||
const deleteModal = useTemplateRef<InstanceType<typeof ConfirmModal>>('deleteModal')
|
||||
|
||||
const rules = ref<Labrinth.TechReview.Internal.DelphiRule[]>([])
|
||||
const isLoading = ref(true)
|
||||
const isSaving = ref(false)
|
||||
const loadFailed = ref(false)
|
||||
const editingRuleId = ref<number | null>(null)
|
||||
const ruleToDelete = ref<Labrinth.TechReview.Internal.DelphiRule | null>(null)
|
||||
const form = reactive({
|
||||
name: '',
|
||||
priority: '0',
|
||||
expression: DEFAULT_EXPRESSION,
|
||||
})
|
||||
|
||||
const modalTitle = computed(() => (editingRuleId.value === null ? 'Create rule' : 'Edit rule'))
|
||||
|
||||
async function loadRules() {
|
||||
isLoading.value = true
|
||||
loadFailed.value = false
|
||||
try {
|
||||
rules.value = await client.labrinth.tech_review_internal.getRules()
|
||||
} catch (error) {
|
||||
console.error('Failed to load Delphi rules', error)
|
||||
loadFailed.value = true
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateModal() {
|
||||
editingRuleId.value = null
|
||||
form.name = ''
|
||||
form.priority = '0'
|
||||
form.expression = DEFAULT_EXPRESSION
|
||||
ruleModal.value?.show()
|
||||
}
|
||||
|
||||
function openEditModal(rule: Labrinth.TechReview.Internal.DelphiRule) {
|
||||
editingRuleId.value = rule.id
|
||||
form.name = rule.name
|
||||
form.priority = String(rule.priority)
|
||||
form.expression = rule.expression
|
||||
ruleModal.value?.show()
|
||||
}
|
||||
|
||||
function closeRuleModal() {
|
||||
ruleModal.value?.hide()
|
||||
}
|
||||
|
||||
async function saveRule() {
|
||||
if (isSaving.value) return
|
||||
|
||||
const priority = Number(form.priority)
|
||||
if (!form.name.trim() || !form.expression.trim() || !Number.isInteger(priority)) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Invalid rule',
|
||||
text: 'Enter a name, a CEL expression, and an integer priority.',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
isSaving.value = true
|
||||
const payload = {
|
||||
name: form.name,
|
||||
priority,
|
||||
expression: form.expression,
|
||||
}
|
||||
|
||||
try {
|
||||
if (editingRuleId.value === null) {
|
||||
await client.labrinth.tech_review_internal.createRule(payload)
|
||||
} else {
|
||||
await client.labrinth.tech_review_internal.updateRule(editingRuleId.value, payload)
|
||||
}
|
||||
closeRuleModal()
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Rule saved',
|
||||
text: 'The Delphi rule revision was saved.',
|
||||
})
|
||||
await loadRules()
|
||||
} catch (error) {
|
||||
console.error('Failed to save Delphi rule', error)
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Failed to save rule',
|
||||
text: 'Check the CEL expression and try again.',
|
||||
})
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openDeleteModal(rule: Labrinth.TechReview.Internal.DelphiRule) {
|
||||
ruleToDelete.value = rule
|
||||
deleteModal.value?.show()
|
||||
}
|
||||
|
||||
async function deleteRule() {
|
||||
const rule = ruleToDelete.value
|
||||
if (!rule) return
|
||||
|
||||
try {
|
||||
await client.labrinth.tech_review_internal.deleteRule(rule.id)
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Rule deleted',
|
||||
text: `${rule.name} was deleted.`,
|
||||
})
|
||||
await loadRules()
|
||||
} catch (error) {
|
||||
console.error('Failed to delete Delphi rule', error)
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Failed to delete rule',
|
||||
text: 'The Delphi rule could not be deleted.',
|
||||
})
|
||||
} finally {
|
||||
ruleToDelete.value = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadRules)
|
||||
</script>
|
||||
Reference in New Issue
Block a user