kin: auto-commit after pipeline
This commit is contained in:
parent
248934d5d7
commit
939a30a3de
6 changed files with 796 additions and 3 deletions
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, computed, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { api, ApiError, type ProjectDetail, type AuditResult, type Phase, type Task, type ProjectEnvironment } from '../api'
|
||||
import { api, ApiError, type ProjectDetail, type AuditResult, type Phase, type Task, type ProjectEnvironment, type DeployResult, type ProjectLink } from '../api'
|
||||
import Badge from '../components/Badge.vue'
|
||||
import Modal from '../components/Modal.vue'
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ const router = useRouter()
|
|||
const project = ref<ProjectDetail | null>(null)
|
||||
const loading = ref(true)
|
||||
const error = ref('')
|
||||
const activeTab = ref<'tasks' | 'phases' | 'decisions' | 'modules' | 'kanban' | 'environments'>('tasks')
|
||||
const activeTab = ref<'tasks' | 'phases' | 'decisions' | 'modules' | 'kanban' | 'environments' | 'links'>('tasks')
|
||||
|
||||
// Phases
|
||||
const phases = ref<Phase[]>([])
|
||||
|
|
@ -369,6 +369,81 @@ async function deleteEnv(envId: number) {
|
|||
}
|
||||
}
|
||||
|
||||
// Deploy
|
||||
const deploying = ref(false)
|
||||
const deployResult = ref<DeployResult | null>(null)
|
||||
|
||||
const hasDeployConfig = computed(() => {
|
||||
if (!project.value) return false
|
||||
return !!(project.value.deploy_host && project.value.deploy_path && project.value.deploy_runtime) || !!project.value.deploy_command
|
||||
})
|
||||
|
||||
async function runDeploy() {
|
||||
deploying.value = true
|
||||
deployResult.value = null
|
||||
try {
|
||||
deployResult.value = await api.deployProject(props.id)
|
||||
} catch (e: any) {
|
||||
error.value = e.message
|
||||
} finally {
|
||||
deploying.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Project Links
|
||||
const links = ref<ProjectLink[]>([])
|
||||
const linksLoading = ref(false)
|
||||
const linksError = ref('')
|
||||
const showAddLink = ref(false)
|
||||
const linkForm = ref({ to_project: '', link_type: 'depends_on', description: '' })
|
||||
const linkFormError = ref('')
|
||||
const linkSaving = ref(false)
|
||||
|
||||
async function loadLinks() {
|
||||
linksLoading.value = true
|
||||
linksError.value = ''
|
||||
try {
|
||||
links.value = await api.projectLinks(props.id)
|
||||
} catch (e: any) {
|
||||
linksError.value = e.message
|
||||
} finally {
|
||||
linksLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function addLink() {
|
||||
linkFormError.value = ''
|
||||
if (!linkForm.value.to_project) { linkFormError.value = 'Выберите проект'; return }
|
||||
linkSaving.value = true
|
||||
try {
|
||||
await api.createProjectLink({
|
||||
from_project: props.id,
|
||||
to_project: linkForm.value.to_project,
|
||||
link_type: linkForm.value.link_type,
|
||||
description: linkForm.value.description || undefined,
|
||||
})
|
||||
showAddLink.value = false
|
||||
linkForm.value = { to_project: '', link_type: 'depends_on', description: '' }
|
||||
await loadLinks()
|
||||
} catch (e: any) {
|
||||
linkFormError.value = e.message
|
||||
} finally {
|
||||
linkSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteLink(id: number) {
|
||||
if (!confirm('Удалить связь?')) return
|
||||
try {
|
||||
await api.deleteProjectLink(id)
|
||||
await loadLinks()
|
||||
} catch (e: any) {
|
||||
linksError.value = e.message
|
||||
}
|
||||
}
|
||||
|
||||
const allProjects = ref<{ id: string; name: string }[]>([])
|
||||
|
||||
// Add task modal
|
||||
const TASK_CATEGORIES = ['SEC', 'UI', 'API', 'INFRA', 'BIZ', 'DB', 'ARCH', 'TEST', 'PERF', 'DOCS', 'FIX', 'OBS']
|
||||
const CATEGORY_COLORS: Record<string, string> = {
|
||||
|
|
@ -425,12 +500,19 @@ watch(() => props.id, () => {
|
|||
environments.value = []
|
||||
showScanBanner.value = false
|
||||
scanTaskId.value = null
|
||||
links.value = []
|
||||
deployResult.value = null
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await load()
|
||||
await loadPhases()
|
||||
await loadEnvironments()
|
||||
await loadLinks()
|
||||
try {
|
||||
const all = await api.projects()
|
||||
allProjects.value = all.map(p => ({ id: p.id, name: p.name }))
|
||||
} catch {}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,14 @@ const saveAutoTestStatus = ref<Record<string, string>>({})
|
|||
const syncResults = ref<Record<string, ObsidianSyncResult | null>>({})
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
// Deploy config
|
||||
const deployHosts = ref<Record<string, string>>({})
|
||||
const deployPaths = ref<Record<string, string>>({})
|
||||
const deployRuntimes = ref<Record<string, string>>({})
|
||||
const deployRestartCmds = ref<Record<string, string>>({})
|
||||
const savingDeployConfig = ref<Record<string, boolean>>({})
|
||||
const saveDeployConfigStatus = ref<Record<string, string>>({})
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
projects.value = await api.projects()
|
||||
|
|
@ -27,12 +35,34 @@ onMounted(async () => {
|
|||
deployCommands.value[p.id] = p.deploy_command ?? ''
|
||||
testCommands.value[p.id] = p.test_command ?? ''
|
||||
autoTestEnabled.value[p.id] = !!(p.auto_test_enabled)
|
||||
deployHosts.value[p.id] = p.deploy_host ?? ''
|
||||
deployPaths.value[p.id] = p.deploy_path ?? ''
|
||||
deployRuntimes.value[p.id] = p.deploy_runtime ?? ''
|
||||
deployRestartCmds.value[p.id] = p.deploy_restart_cmd ?? ''
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = String(e)
|
||||
}
|
||||
})
|
||||
|
||||
async function saveDeployConfig(projectId: string) {
|
||||
savingDeployConfig.value[projectId] = true
|
||||
saveDeployConfigStatus.value[projectId] = ''
|
||||
try {
|
||||
await api.patchProject(projectId, {
|
||||
deploy_host: deployHosts.value[projectId] || undefined,
|
||||
deploy_path: deployPaths.value[projectId] || undefined,
|
||||
deploy_runtime: deployRuntimes.value[projectId] || undefined,
|
||||
deploy_restart_cmd: deployRestartCmds.value[projectId] || undefined,
|
||||
})
|
||||
saveDeployConfigStatus.value[projectId] = 'Saved'
|
||||
} catch (e) {
|
||||
saveDeployConfigStatus.value[projectId] = `Error: ${e}`
|
||||
} finally {
|
||||
savingDeployConfig.value[projectId] = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveVaultPath(projectId: string) {
|
||||
saving.value[projectId] = true
|
||||
saveStatus.value[projectId] = ''
|
||||
|
|
@ -172,6 +202,63 @@ async function runSync(projectId: string) {
|
|||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Deploy Config -->
|
||||
<div class="mb-2 pt-2 border-t border-gray-800">
|
||||
<p class="text-xs font-semibold text-gray-400 mb-2">Deploy Config</p>
|
||||
<div class="mb-2">
|
||||
<label class="block text-xs text-gray-500 mb-1">Server host</label>
|
||||
<input
|
||||
v-model="deployHosts[project.id]"
|
||||
type="text"
|
||||
placeholder="server host (e.g. vdp-prod)"
|
||||
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 font-mono focus:outline-none focus:border-gray-500"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="block text-xs text-gray-500 mb-1">Project path on server</label>
|
||||
<input
|
||||
v-model="deployPaths[project.id]"
|
||||
type="text"
|
||||
placeholder="/srv/myproject"
|
||||
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 font-mono focus:outline-none focus:border-gray-500"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="block text-xs text-gray-500 mb-1">Runtime</label>
|
||||
<select
|
||||
v-model="deployRuntimes[project.id]"
|
||||
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-300 focus:outline-none focus:border-gray-500"
|
||||
>
|
||||
<option value="">— выберите runtime —</option>
|
||||
<option value="docker">docker</option>
|
||||
<option value="node">node</option>
|
||||
<option value="python">python</option>
|
||||
<option value="static">static</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="block text-xs text-gray-500 mb-1">Restart command (optional override)</label>
|
||||
<input
|
||||
v-model="deployRestartCmds[project.id]"
|
||||
type="text"
|
||||
placeholder="optional override command"
|
||||
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 font-mono focus:outline-none focus:border-gray-500"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 flex-wrap mb-3">
|
||||
<button
|
||||
@click="saveDeployConfig(project.id)"
|
||||
:disabled="savingDeployConfig[project.id]"
|
||||
class="px-3 py-1.5 text-sm bg-teal-900/50 text-teal-400 border border-teal-800 rounded hover:bg-teal-900 disabled:opacity-50"
|
||||
>
|
||||
{{ savingDeployConfig[project.id] ? 'Saving…' : 'Save Deploy Config' }}
|
||||
</button>
|
||||
<span v-if="saveDeployConfigStatus[project.id]" class="text-xs" :class="saveDeployConfigStatus[project.id].startsWith('Error') ? 'text-red-400' : 'text-green-400'">
|
||||
{{ saveDeployConfigStatus[project.id] }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<label class="flex items-center gap-2 cursor-pointer select-none">
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import Badge from '../components/Badge.vue'
|
|||
import Modal from '../components/Modal.vue'
|
||||
import AttachmentUploader from '../components/AttachmentUploader.vue'
|
||||
import AttachmentList from '../components/AttachmentList.vue'
|
||||
import LiveConsole from '../components/LiveConsole.vue'
|
||||
|
||||
const props = defineProps<{ id: string }>()
|
||||
const route = useRoute()
|
||||
|
|
@ -468,6 +469,14 @@ async function saveEdit() {
|
|||
No pipeline steps yet.
|
||||
</div>
|
||||
|
||||
<!-- Live Console -->
|
||||
<LiveConsole
|
||||
v-if="task.pipeline_id"
|
||||
:pipeline-id="task.pipeline_id"
|
||||
:pipeline-status="task.status"
|
||||
class="mb-6"
|
||||
/>
|
||||
|
||||
<!-- Selected step output -->
|
||||
<div v-if="selectedStep" class="mb-6">
|
||||
<h2 class="text-sm font-semibold text-gray-300 mb-2">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue