kin: KIN-108-frontend_dev
This commit is contained in:
parent
8b409fd7db
commit
353416ead1
16 changed files with 799 additions and 212 deletions
|
|
@ -1,7 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { api, type Attachment } from '../api'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{ attachments: Attachment[]; taskId: string }>()
|
||||
const emit = defineEmits<{ deleted: [] }>()
|
||||
|
||||
|
|
@ -48,7 +51,7 @@ function formatSize(bytes: number): string {
|
|||
@click="remove(att.id)"
|
||||
:disabled="deletingId === att.id"
|
||||
class="absolute top-1 right-1 w-5 h-5 rounded-full bg-red-900/80 text-red-400 text-xs leading-none opacity-0 group-hover:opacity-100 transition-opacity disabled:opacity-50 flex items-center justify-center"
|
||||
title="Удалить"
|
||||
:title="t('attachments.delete_title')"
|
||||
>✕</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { api } from '../api'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{ taskId: string }>()
|
||||
const emit = defineEmits<{ uploaded: [] }>()
|
||||
|
||||
|
|
@ -12,7 +15,7 @@ const fileInput = ref<HTMLInputElement | null>(null)
|
|||
|
||||
async function upload(file: File) {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
error.value = 'Поддерживаются только изображения'
|
||||
error.value = t('attachments.images_only')
|
||||
return
|
||||
}
|
||||
uploading.value = true
|
||||
|
|
@ -52,10 +55,10 @@ function onDrop(event: DragEvent) {
|
|||
<input ref="fileInput" type="file" accept="image/*" class="hidden" @change="onFileChange" />
|
||||
<div v-if="uploading" class="flex items-center justify-center gap-2 text-xs text-blue-400">
|
||||
<span class="inline-block w-3 h-3 border-2 border-blue-400 border-t-transparent rounded-full animate-spin"></span>
|
||||
Загрузка...
|
||||
{{ t('attachments.uploading') }}
|
||||
</div>
|
||||
<div v-else class="text-xs text-gray-500">
|
||||
Перетащите изображение или <span class="text-blue-400">нажмите для выбора</span>
|
||||
{{ t('attachments.drop_hint') }} <span class="text-blue-400">{{ t('attachments.click_to_select') }}</span>
|
||||
</div>
|
||||
<p v-if="error" class="text-red-400 text-xs mt-1">{{ error }}</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { api, type EscalationNotification } from '../api'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
const STORAGE_KEY = 'kin_dismissed_escalations'
|
||||
const WATCHDOG_TOAST_KEY = 'kin_dismissed_watchdog_toasts'
|
||||
|
||||
|
|
@ -106,7 +109,7 @@ function dismissAll() {
|
|||
|
||||
function formatTime(iso: string): string {
|
||||
try {
|
||||
return new Date(iso).toLocaleString('ru-RU', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })
|
||||
return new Date(iso).toLocaleString(locale.value === 'ru' ? 'ru-RU' : 'en-US', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })
|
||||
} catch {
|
||||
return iso
|
||||
}
|
||||
|
|
@ -136,7 +139,7 @@ onUnmounted(() => {
|
|||
>
|
||||
<span class="shrink-0 text-sm">⚠</span>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-xs leading-snug">Watchdog: задача <span class="font-mono font-semibold">{{ toast.task_id }}</span> заблокирована — {{ toast.reason }}</p>
|
||||
<p class="text-xs leading-snug">{{ t('escalation.watchdog_blocked', { task_id: toast.task_id, reason: toast.reason }) }}</p>
|
||||
</div>
|
||||
<button
|
||||
@click="dismissWatchdogToast(toast.task_id)"
|
||||
|
|
@ -153,7 +156,7 @@ onUnmounted(() => {
|
|||
class="relative flex items-center gap-1.5 px-2.5 py-1 text-xs bg-red-900/50 text-red-400 border border-red-800 rounded hover:bg-red-900 transition-colors"
|
||||
>
|
||||
<span class="inline-block w-1.5 h-1.5 bg-red-500 rounded-full animate-pulse"></span>
|
||||
Эскалации
|
||||
{{ t('escalation.escalations') }}
|
||||
<span class="ml-0.5 font-bold">{{ visible.length }}</span>
|
||||
</button>
|
||||
|
||||
|
|
@ -163,12 +166,12 @@ onUnmounted(() => {
|
|||
class="absolute right-0 top-full mt-2 w-96 bg-gray-900 border border-red-900/60 rounded-lg shadow-2xl z-50"
|
||||
>
|
||||
<div class="flex items-center justify-between px-4 py-2.5 border-b border-gray-800">
|
||||
<span class="text-xs font-semibold text-red-400">Эскалации — требуется решение</span>
|
||||
<span class="text-xs font-semibold text-red-400">{{ t('escalation.escalations_panel_title') }}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
@click="dismissAll"
|
||||
class="text-xs text-gray-500 hover:text-gray-300"
|
||||
>Принять все</button>
|
||||
>{{ t('escalation.dismiss_all') }}</button>
|
||||
<button @click="showPanel = false" class="text-gray-500 hover:text-gray-300 text-lg leading-none">×</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -193,7 +196,7 @@ onUnmounted(() => {
|
|||
<button
|
||||
@click="dismiss(n.task_id)"
|
||||
class="shrink-0 px-2 py-1 text-xs bg-gray-800 text-gray-400 border border-gray-700 rounded hover:bg-gray-700 hover:text-gray-200"
|
||||
>Принято</button>
|
||||
>{{ t('escalation.dismiss') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, watch, onUnmounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { api, type PipelineLog } from '../api'
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
@ -7,6 +8,8 @@ const props = defineProps<{
|
|||
pipelineStatus: string
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const visible = ref(false)
|
||||
const logs = ref<PipelineLog[]>([])
|
||||
const error = ref('')
|
||||
|
|
@ -102,12 +105,12 @@ onUnmounted(() => {
|
|||
@click="toggle"
|
||||
class="text-xs text-gray-500 hover:text-gray-300 border border-gray-800 rounded px-3 py-1.5 bg-gray-900/50 hover:bg-gray-900 transition-colors"
|
||||
>
|
||||
{{ visible ? '▲ Скрыть лог' : '▼ Показать лог' }}
|
||||
{{ visible ? t('liveConsole.hide_log') : t('liveConsole.show_log') }}
|
||||
</button>
|
||||
|
||||
<div v-show="visible" class="mt-2 bg-gray-950 border border-gray-800 rounded-lg p-4 font-mono text-xs max-h-[400px] overflow-y-auto" ref="consoleEl" @scroll="onScroll">
|
||||
<div v-if="!logs.length && !error" class="text-gray-600">Нет записей...</div>
|
||||
<div v-if="error" class="text-red-400">Ошибка: {{ error }}</div>
|
||||
<div v-if="!logs.length && !error" class="text-gray-600">{{ t('liveConsole.no_records') }}</div>
|
||||
<div v-if="error" class="text-red-400">{{ t('liveConsole.error_prefix') }} {{ error }}</div>
|
||||
<div v-for="log in logs" :key="log.id" class="mb-1">
|
||||
<span class="text-gray-600">{{ log.ts }}</span>
|
||||
<span :class="[levelClass(log.level), 'ml-2 font-semibold']">[{{ log.level }}]</span>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue