All files / src/views DeploymentsListView.vue

100% Statements 117/117
100% Branches 0/0
100% Functions 0/0
100% Lines 117/117

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169  1x   1x                 1x 1x 1x 1x 1x 1x   1x 1x   1x 1x 1x 1x   1x 1x 1x 1x     1x 1x 1x 1x 1x 1x 1x 1x 1x 1x                         1x 1x 1x 1x 1x 1x 1x               1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x         1x 1x 1x 1x 1x 1x 1x 1x 1x   1x   1x 1x 1x 1x 1x   1x 1x 1x 1x 1x 1x 1x             1x 1x 1x 1x 1x 1x   1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x   1x 1x 1x   1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x    
<script setup lang="ts">
import { onMounted, computed } from 'vue'
 
import {
  BarChart3,
  Plus,
  Inbox,
  GitBranch,
  Box,
  Clock,
} from 'lucide-vue-next'
 
import BaseButton from '@/components/ui/BaseButton.vue'
import Card from '@/components/ui/Card.vue'
import PageHeader from '@/components/ui/PageHeader.vue'
import EntityListState from '@/components/ui/EntityListState.vue'
import { useDeploymentStore } from '@/stores/deployment.store'
import { useAppStore } from '@/stores/app.store'
 
const deploymentStore = useDeploymentStore()
const appStore = useAppStore()
 
onMounted(async () => {
  deploymentStore.fetchDeployments()
  appStore.fetchApps()
})
 
const getAppName = (appId: string) => {
  const app = appStore.apps.find(a => a.appId === appId)
  return app ? app.name : '-'
}
 
// Datum formatieren
const formatDate = (dateString: string) => {
  const date = new Date(dateString)
  return new Intl.DateTimeFormat('de-DE', {
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit'
  }).format(date)
}
 
/**
 * Sortierung: neueste Deployments zuerst.
 *
 * Server liefert die Liste in DB-Insert-Reihenfolge — visuell wirkt
 * das wie eine "älteste zuerst"-Sortierung, was beim Anlegen eines
 * neuen Deployments unnatürlich ist (User erwartet sein gerade
 * erstelltes Deployment ganz oben). Daher hier client-seitig nach
 * ``created_at`` absteigend sortieren. Ohne ``created_at`` (Edge-
 * Case bei noch nicht erstelltem ersten Task) fällt das Item ans
 * Ende — besser als ``NaN`` im Vergleich.
 */
const sortedDeployments = computed(() =>
  [...deploymentStore.deployments].sort((a, b) => {
    const ta = a.created_at ? new Date(a.created_at).getTime() : 0
    const tb = b.created_at ? new Date(b.created_at).getTime() : 0
    return tb - ta
  })
)
 
// Status-Pillen. Identische Palette wie vor dem Karten-Refactor —
// die Farb-Semantik (orange = destroy, amber = lifecycle-pending,
// slate = ruhe-paused) ist über die App gelernt, dazu gibt's auch
// die ``BarChart3``-Statistik-Page. Wir behalten die Map, ändern nur
// das Rendering: nicht mehr Tabellen-Zelle, sondern Pill innerhalb
// der Karte.
const getStatusColor = (status: string) => {
  const colors = {
    'success': 'bg-green-100 text-green-800 border-green-300',
    'failed': 'bg-red-100 text-red-800 border-red-300',
    'running': 'bg-blue-100 text-blue-800 border-blue-300',
    'pending': 'bg-yellow-100 text-yellow-800 border-yellow-300',
    'cancelled': 'bg-gray-100 text-gray-700 border-gray-300',
    'destroyed': 'bg-orange-100 text-orange-800 border-orange-300',
    'destroying': 'bg-orange-100 text-orange-700 border-orange-300',
    'pausing': 'bg-amber-100 text-amber-800 border-amber-300',
    'paused': 'bg-slate-100 text-slate-700 border-slate-300',
    'resuming': 'bg-emerald-100 text-emerald-800 border-emerald-300',
    'pause_failed': 'bg-amber-100 text-amber-900 border-amber-300',
    'resume_failed': 'bg-amber-100 text-amber-900 border-amber-300',
  }
  return colors[status as keyof typeof colors] || 'bg-gray-100 text-gray-800 border-gray-300'
}
</script>
 
 
<template>
  <div class="p-6">
    <PageHeader :title="$t('DeploymentsView.title')" :subtitle="$t('DeploymentsView.subtitle')">
      <template #actions>
        <RouterLink :to="{ name: 'apps' }">
          <BaseButton class="flex items-center gap-2">
            <Plus :size="16" />
            {{ $t('DeploymentsView.newDeployment') }}
          </BaseButton>
        </RouterLink>
      </template>
    </PageHeader>
 
    <EntityListState
      :is-loading="deploymentStore.isLoading && deploymentStore.deployments.length === 0"
      :is-empty="!deploymentStore.isLoading && deploymentStore.deployments.length === 0"
      :icon="Inbox"
      :empty-message="$t('DeploymentsView.deploymentsMissingMessage')"
    >
      <template #empty-action>
        <RouterLink :to="{ name: 'apps' }">
          <BaseButton class="flex items-center gap-2">
            <Plus :size="16" />
            {{ $t('DeploymentsView.newDeployment') }}
          </BaseButton>
        </RouterLink>
      </template>
 
      <!-- Karten-Grid. Eine Karte pro Deployment: Name (groß),
           App-Name (Untertitel), Status-Pill, Release-Tag, Erstell-
           datum. Klick führt zum Detail. Sortierung: neueste oben
           (siehe ``sortedDeployments`` im script). -->
      <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        <RouterLink
          v-for="deployment in sortedDeployments"
          :key="deployment.deploymentId"
          :to="{ name: 'deployments.detail', params: { id: deployment.deploymentId } }"
          class="block"
        >
          <Card class="flex flex-col h-full cursor-pointer hover:border-emerald-200 transition">
            <div class="flex items-start justify-between gap-3 mb-3">
              <div class="flex items-center gap-3 min-w-0">
                <div class="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center flex-shrink-0">
                  <BarChart3 :size="20" class="text-primary" />
                </div>
                <div class="min-w-0">
                  <h3 class="font-semibold text-gray-900 truncate" :title="deployment.name">
                    {{ deployment.name }}
                  </h3>
                  <p class="text-xs text-gray-500 truncate mt-0.5">
                    <Box :size="11" class="inline-block mr-1 align-text-bottom" />
                    {{ getAppName(deployment.appId) }}
                  </p>
                </div>
              </div>
              <span
                class="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-semibold border capitalize whitespace-nowrap"
                :class="getStatusColor(deployment.status)"
              >
                {{ deployment.status }}
              </span>
            </div>
 
            <div class="mt-auto pt-3 border-t border-gray-100 flex items-center justify-between text-xs text-gray-500">
              <span class="inline-flex items-center gap-1 px-2 py-0.5 rounded bg-indigo-50 text-indigo-700 border border-indigo-100 font-mono">
                <GitBranch :size="11" />
                {{ deployment.releaseTag }}
              </span>
              <span class="inline-flex items-center gap-1">
                <Clock :size="11" />
                {{ formatDate(deployment.created_at) }}
              </span>
            </div>
          </Card>
        </RouterLink>
      </div>
    </EntityListState>
  </div>
</template>