feat: add dependent tasks view for environments (#126)

Add a dialog in the environment variables list to view which tasks depend on a specific variable/secret.
This commit is contained in:
duorameng
2026-06-12 09:16:24 +08:00
parent b1509a3afb
commit 626d20c9b8
2 changed files with 149 additions and 18 deletions
+34 -18
View File
@@ -3,7 +3,7 @@ import { ref, watch, onMounted } from 'vue'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import Pagination from '@/components/Pagination.vue'
import { Plus, Pencil, Trash2, Eye, EyeOff, Search, AlertTriangle, Terminal, Zap, ZapOff, Shield, Tag } from 'lucide-vue-next'
import { Plus, Pencil, Trash2, Eye, EyeOff, Search, AlertTriangle, Terminal, Zap, ZapOff, Shield, Tag, Link } from 'lucide-vue-next'
import TextOverflow from '@/components/TextOverflow.vue'
import TagInput from '@/components/TagInput.vue'
import { api, type EnvVar } from '@/api'
@@ -16,6 +16,7 @@ import { Badge } from '@/components/ui/badge'
import EditEnvDialog from './components/EditEnvDialog.vue'
import DeleteEnvDialog from './components/DeleteEnvDialog.vue'
import DependentTasksDialog from './components/DependentTasksDialog.vue'
function formatDate(dateStr?: string) {
if (!dateStr) return '-'
@@ -41,6 +42,7 @@ let searchTimer: ReturnType<typeof setTimeout> | null = null
const editDialogRef = ref<InstanceType<typeof EditEnvDialog> | null>(null)
const deleteDialogRef = ref<InstanceType<typeof DeleteEnvDialog> | null>(null)
const dependentTasksDialogRef = ref<InstanceType<typeof DependentTasksDialog> | null>(null)
async function checkSecretStatus() {
try {
@@ -100,6 +102,10 @@ function openEdit(env: EnvVar) {
editDialogRef.value?.openEdit(env)
}
function openDependentTasks(env: EnvVar) {
dependentTasksDialogRef.value?.open(env)
}
function confirmDelete(id: string) {
deleteDialogRef.value?.confirmDelete(id, activeTab.value)
}
@@ -239,16 +245,19 @@ onMounted(() => {
</span>
</div>
<div class="w-24 shrink-0 flex items-center justify-center gap-1">
<Button variant="ghost" size="icon" class="h-7 w-7" @click="toggleShow(env.id)" :title="showValues[env.id] ? '隐藏' : '显示'">
<Eye v-if="!showValues[env.id]" class="h-3.5 w-3.5" />
<EyeOff v-else class="h-3.5 w-3.5" />
<div class="w-24 shrink-0 flex justify-center">
<Button variant="ghost" size="icon" class="h-6 w-6" @click="toggleShow(env.id)" :title="showValues[env.id] ? '隐藏' : '显示'">
<Eye v-if="!showValues[env.id]" class="h-3 w-3" />
<EyeOff v-else class="h-3 w-3" />
</Button>
<Button variant="ghost" size="icon" class="h-7 w-7" @click="openEdit(env)" title="编辑">
<Pencil class="h-3.5 w-3.5" />
<Button variant="ghost" size="icon" class="h-6 w-6" @click="openDependentTasks(env)" title="依赖任务">
<Link class="h-3 w-3" />
</Button>
<Button variant="ghost" size="icon" class="h-7 w-7 text-destructive" @click="confirmDelete(env.id)" title="删除">
<Trash2 class="h-3.5 w-3.5" />
<Button variant="ghost" size="icon" class="h-6 w-6" @click="openEdit(env)" title="编辑">
<Pencil class="h-3 w-3" />
</Button>
<Button variant="ghost" size="icon" class="h-6 w-6 text-destructive" @click="confirmDelete(env.id)" title="删除">
<Trash2 class="h-3 w-3" />
</Button>
</div>
</div>
@@ -296,16 +305,19 @@ onMounted(() => {
</span>
</div>
<div class="w-24 shrink-0 flex items-center justify-center gap-1">
<Button variant="ghost" size="icon" class="h-7 w-7" @click="toggleShow(env.id)">
<Eye v-if="!showValues[env.id]" class="h-3.5 w-3.5" />
<EyeOff v-else class="h-3.5 w-3.5" />
<div class="w-24 shrink-0 flex justify-center">
<Button variant="ghost" size="icon" class="h-6 w-6" @click="toggleShow(env.id)">
<Eye v-if="!showValues[env.id]" class="h-3 w-3" />
<EyeOff v-else class="h-3 w-3" />
</Button>
<Button variant="ghost" size="icon" class="h-7 w-7" @click="openEdit(env)">
<Pencil class="h-3.5 w-3.5" />
<Button variant="ghost" size="icon" class="h-6 w-6" @click="openDependentTasks(env)" title="依赖任务">
<Link class="h-3 w-3" />
</Button>
<Button variant="ghost" size="icon" class="h-7 w-7 text-destructive" @click="confirmDelete(env.id)">
<Trash2 class="h-3.5 w-3.5" />
<Button variant="ghost" size="icon" class="h-6 w-6" @click="openEdit(env)">
<Pencil class="h-3 w-3" />
</Button>
<Button variant="ghost" size="icon" class="h-6 w-6 text-destructive" @click="confirmDelete(env.id)">
<Trash2 class="h-3 w-3" />
</Button>
</div>
</div>
@@ -353,12 +365,15 @@ onMounted(() => {
</div>
</div>
<div class="grid grid-cols-3 items-center pt-2 mt-2 border-t border-border/40 -mx-1">
<div class="grid grid-cols-4 items-center pt-2 mt-2 border-t border-border/40 -mx-1">
<Button variant="ghost" class="h-9 px-0 text-xs gap-1.5 hover:bg-primary/5 rounded-none" @click="toggleShow(env.id)">
<Eye v-if="!showValues[env.id]" class="h-3.5 w-3.5" />
<EyeOff v-else class="h-3.5 w-3.5" />
{{ showValues[env.id] ? '隐藏' : '显示' }}
</Button>
<Button variant="ghost" class="h-9 px-0 text-xs gap-1.5 hover:bg-primary/5 rounded-none border-l border-border/10" @click="openDependentTasks(env)">
<Link class="h-3.5 w-3.5" />任务
</Button>
<Button variant="ghost" class="h-9 px-0 text-xs gap-1.5 hover:bg-primary/5 rounded-none border-l border-border/10" @click="openEdit(env)">
<Pencil class="h-3.5 w-3.5" />编辑
</Button>
@@ -377,5 +392,6 @@ onMounted(() => {
<EditEnvDialog ref="editDialogRef" @saved="loadEnvVars" />
<DeleteEnvDialog ref="deleteDialogRef" @deleted="loadEnvVars" />
<DependentTasksDialog ref="dependentTasksDialogRef" />
</Tabs>
</template>
@@ -0,0 +1,115 @@
<script setup lang="ts">
import { ref } from 'vue'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { api, type Task, type EnvVar } from '@/api'
import { toast } from 'vue-sonner'
import { AlertCircle, Terminal, RefreshCw, CalendarClock, Link } from 'lucide-vue-next'
import { ENV_TYPE } from '@/constants'
const isOpen = ref(false)
const isLoading = ref(false)
const envVar = ref<EnvVar | null>(null)
const tasks = ref<Task[]>([])
async function open(env: EnvVar) {
envVar.value = env
isOpen.value = true
await loadTasks()
}
async function loadTasks() {
if (!envVar.value?.id) return
isLoading.value = true
try {
const res = await api.env.tasks(envVar.value.id)
tasks.value = res || []
} catch {
toast.error('加载关联任务失败')
} finally {
isLoading.value = false
}
}
function getTriggerIcon(type: string) {
if (type === 'cron') return CalendarClock
if (type === 'startup') return RefreshCw
return Terminal
}
function getTriggerText(type: string) {
if (type === 'cron') return '定时执行'
if (type === 'startup') return '启动执行'
return '终端任务'
}
defineExpose({
open
})
</script>
<template>
<Dialog v-model:open="isOpen">
<DialogContent class="w-[calc(100vw-2rem)] max-w-md min-w-0 flex flex-col max-h-[85vh]">
<DialogHeader class="shrink-0 text-left">
<DialogTitle class="flex items-center gap-2">
<Link class="h-4 w-4" />
<span>依赖该{{ envVar?.type === ENV_TYPE.SECRET ? '机密' : '变量' }}的任务</span>
</DialogTitle>
<DialogDescription class="truncate" :title="envVar?.name">
{{ envVar?.name }}
</DialogDescription>
</DialogHeader>
<div class="flex-1 overflow-y-auto min-h-[200px] -mx-1 px-1">
<div v-if="isLoading" class="flex items-center justify-center h-full text-muted-foreground text-sm py-12">
加载中...
</div>
<div v-else-if="tasks.length === 0" class="flex flex-col items-center justify-center h-full text-muted-foreground text-sm py-12 gap-3">
<div class="h-10 w-10 rounded-full bg-muted flex items-center justify-center">
<AlertCircle class="h-5 w-5 opacity-50" />
</div>
<span>当前没有任务依赖此{{ envVar?.type === ENV_TYPE.SECRET ? '机密' : '变量' }}</span>
</div>
<div v-else class="space-y-2">
<div v-for="task in tasks" :key="task.id" class="flex flex-col p-3 rounded-lg border bg-card/50 hover:bg-muted/50 transition-colors">
<div class="flex items-start justify-between gap-3">
<div class="flex flex-col min-w-0">
<span class="font-medium text-sm truncate" :title="task.name">{{ task.name }}</span>
<div class="flex items-center gap-1.5 mt-1">
<Badge variant="secondary" class="text-[9px] h-4 px-1 rounded uppercase font-medium">
<component :is="getTriggerIcon(task.trigger_type)" class="h-2.5 w-2.5 mr-1 inline-block" />
{{ getTriggerText(task.trigger_type) }}
</Badge>
<span v-if="task.schedule" class="text-[10px] text-muted-foreground bg-muted px-1 rounded truncate">
{{ task.schedule }}
</span>
</div>
</div>
<div class="shrink-0 pt-0.5">
<div v-if="task.enabled" class="px-1.5 py-0.5 rounded text-[10px] bg-green-500/10 text-green-600 dark:text-green-400 font-medium whitespace-nowrap">
已启用
</div>
<div v-else class="px-1.5 py-0.5 rounded text-[10px] bg-muted text-muted-foreground font-medium whitespace-nowrap">
已禁用
</div>
</div>
</div>
<div v-if="task.remark" class="mt-2 text-xs text-muted-foreground line-clamp-2">
{{ task.remark }}
</div>
</div>
</div>
</div>
<DialogFooter class="shrink-0 mt-4 sm:mt-4">
<Button variant="outline" @click="isOpen = false">关闭</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>