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:
@@ -3,7 +3,7 @@ import { ref, watch, onMounted } from 'vue'
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import Pagination from '@/components/Pagination.vue'
|
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 TextOverflow from '@/components/TextOverflow.vue'
|
||||||
import TagInput from '@/components/TagInput.vue'
|
import TagInput from '@/components/TagInput.vue'
|
||||||
import { api, type EnvVar } from '@/api'
|
import { api, type EnvVar } from '@/api'
|
||||||
@@ -16,6 +16,7 @@ import { Badge } from '@/components/ui/badge'
|
|||||||
|
|
||||||
import EditEnvDialog from './components/EditEnvDialog.vue'
|
import EditEnvDialog from './components/EditEnvDialog.vue'
|
||||||
import DeleteEnvDialog from './components/DeleteEnvDialog.vue'
|
import DeleteEnvDialog from './components/DeleteEnvDialog.vue'
|
||||||
|
import DependentTasksDialog from './components/DependentTasksDialog.vue'
|
||||||
|
|
||||||
function formatDate(dateStr?: string) {
|
function formatDate(dateStr?: string) {
|
||||||
if (!dateStr) return '-'
|
if (!dateStr) return '-'
|
||||||
@@ -41,6 +42,7 @@ let searchTimer: ReturnType<typeof setTimeout> | null = null
|
|||||||
|
|
||||||
const editDialogRef = ref<InstanceType<typeof EditEnvDialog> | null>(null)
|
const editDialogRef = ref<InstanceType<typeof EditEnvDialog> | null>(null)
|
||||||
const deleteDialogRef = ref<InstanceType<typeof DeleteEnvDialog> | null>(null)
|
const deleteDialogRef = ref<InstanceType<typeof DeleteEnvDialog> | null>(null)
|
||||||
|
const dependentTasksDialogRef = ref<InstanceType<typeof DependentTasksDialog> | null>(null)
|
||||||
|
|
||||||
async function checkSecretStatus() {
|
async function checkSecretStatus() {
|
||||||
try {
|
try {
|
||||||
@@ -100,6 +102,10 @@ function openEdit(env: EnvVar) {
|
|||||||
editDialogRef.value?.openEdit(env)
|
editDialogRef.value?.openEdit(env)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openDependentTasks(env: EnvVar) {
|
||||||
|
dependentTasksDialogRef.value?.open(env)
|
||||||
|
}
|
||||||
|
|
||||||
function confirmDelete(id: string) {
|
function confirmDelete(id: string) {
|
||||||
deleteDialogRef.value?.confirmDelete(id, activeTab.value)
|
deleteDialogRef.value?.confirmDelete(id, activeTab.value)
|
||||||
}
|
}
|
||||||
@@ -239,16 +245,19 @@ onMounted(() => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-24 shrink-0 flex items-center justify-center gap-1">
|
<div class="w-24 shrink-0 flex justify-center">
|
||||||
<Button variant="ghost" size="icon" class="h-7 w-7" @click="toggleShow(env.id)" :title="showValues[env.id] ? '隐藏' : '显示'">
|
<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.5 w-3.5" />
|
<Eye v-if="!showValues[env.id]" class="h-3 w-3" />
|
||||||
<EyeOff v-else class="h-3.5 w-3.5" />
|
<EyeOff v-else class="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" class="h-7 w-7" @click="openEdit(env)" title="编辑">
|
<Button variant="ghost" size="icon" class="h-6 w-6" @click="openDependentTasks(env)" title="依赖任务">
|
||||||
<Pencil class="h-3.5 w-3.5" />
|
<Link class="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" class="h-7 w-7 text-destructive" @click="confirmDelete(env.id)" title="删除">
|
<Button variant="ghost" size="icon" class="h-6 w-6" @click="openEdit(env)" title="编辑">
|
||||||
<Trash2 class="h-3.5 w-3.5" />
|
<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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -296,16 +305,19 @@ onMounted(() => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-24 shrink-0 flex items-center justify-center gap-1">
|
<div class="w-24 shrink-0 flex justify-center">
|
||||||
<Button variant="ghost" size="icon" class="h-7 w-7" @click="toggleShow(env.id)">
|
<Button variant="ghost" size="icon" class="h-6 w-6" @click="toggleShow(env.id)">
|
||||||
<Eye v-if="!showValues[env.id]" class="h-3.5 w-3.5" />
|
<Eye v-if="!showValues[env.id]" class="h-3 w-3" />
|
||||||
<EyeOff v-else class="h-3.5 w-3.5" />
|
<EyeOff v-else class="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" class="h-7 w-7" @click="openEdit(env)">
|
<Button variant="ghost" size="icon" class="h-6 w-6" @click="openDependentTasks(env)" title="依赖任务">
|
||||||
<Pencil class="h-3.5 w-3.5" />
|
<Link class="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" class="h-7 w-7 text-destructive" @click="confirmDelete(env.id)">
|
<Button variant="ghost" size="icon" class="h-6 w-6" @click="openEdit(env)">
|
||||||
<Trash2 class="h-3.5 w-3.5" />
|
<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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -353,12 +365,15 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</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)">
|
<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" />
|
<Eye v-if="!showValues[env.id]" class="h-3.5 w-3.5" />
|
||||||
<EyeOff v-else class="h-3.5 w-3.5" />
|
<EyeOff v-else class="h-3.5 w-3.5" />
|
||||||
{{ showValues[env.id] ? '隐藏' : '显示' }}
|
{{ showValues[env.id] ? '隐藏' : '显示' }}
|
||||||
</Button>
|
</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)">
|
<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" />编辑
|
<Pencil class="h-3.5 w-3.5" />编辑
|
||||||
</Button>
|
</Button>
|
||||||
@@ -377,5 +392,6 @@ onMounted(() => {
|
|||||||
|
|
||||||
<EditEnvDialog ref="editDialogRef" @saved="loadEnvVars" />
|
<EditEnvDialog ref="editDialogRef" @saved="loadEnvVars" />
|
||||||
<DeleteEnvDialog ref="deleteDialogRef" @deleted="loadEnvVars" />
|
<DeleteEnvDialog ref="deleteDialogRef" @deleted="loadEnvVars" />
|
||||||
|
<DependentTasksDialog ref="dependentTasksDialogRef" />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</template>
|
</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>
|
||||||
Reference in New Issue
Block a user