main
vue 269 lines 7.5 KB
Raw
1 <template>
2 <div>
3 <n-spin :show="loadingList">
4 <div v-if="portainerStack" class="flex flex-col gap-2">
5 <CardEntity embedded>
6 <template #headerMain>
7 <div class="flex items-center gap-2">
8 <span>#{{ portainerStack.Id }}</span>
9 <span>- {{ portainerStackType }}</span>
10 </div>
11 </template>
12 <template #headerExtra>
13 <div class="flex items-center gap-2 font-sans">
14 <Icon
15 v-if="portainerStack.Status === PortainerStackStatus.Offline"
16 :name="OfflineIcon"
17 :size="16"
18 class="text-warning"
19 />
20 <Icon v-else :name="OnlineIcon" :size="16" class="text-success" />
21
22 {{ portainerStack.Status === PortainerStackStatus.Online ? "Online" : "Offline" }}
23 </div>
24 </template>
25 <template #default>
26 {{ portainerStack.Name }}
27 </template>
28 <template #footerMain>
29 <div class="flex flex-wrap items-center gap-3">
30 <Badge type="splitted" color="primary">
31 <template #iconLeft>
32 <Icon :name="UserIcon" :size="14" />
33 </template>
34 <template #value>{{ portainerStack.CreatedBy }}</template>
35 </Badge>
36 <Badge type="splitted" color="primary">
37 <template #iconLeft>
38 <Icon :name="DateIcon" :size="14" />
39 </template>
40 <template #value>
41 {{ formatDate(portainerStack.CreationDate, dFormats.datetimesec) }}
42 </template>
43 </Badge>
44 </div>
45 </template>
46 <template #footerExtra>
47 <div class="flex flex-wrap items-center justify-end gap-3">
48 <n-button secondary size="small" @click.stop="showResourceControl = true">
49 <template #icon>
50 <Icon :name="ViewIcon" />
51 </template>
52 ResourceControl
53 </n-button>
54 <n-button
55 v-if="portainerStack.Status === PortainerStackStatus.Online"
56 :loading="loadingAction"
57 type="warning"
58 secondary
59 size="small"
60 @click.stop="stop()"
61 >
62 <template #icon>
63 <Icon :name="StopIcon" />
64 </template>
65 Stop Worker
66 </n-button>
67 <n-button
68 v-if="portainerStack.Status === PortainerStackStatus.Offline"
69 :loading="loadingAction"
70 type="success"
71 secondary
72 size="small"
73 @click.stop="start()"
74 >
75 <template #icon>
76 <Icon :name="StartIcon" />
77 </template>
78 Start Worker
79 </n-button>
80 </div>
81 </template>
82 </CardEntity>
83
84 <div class="grid-auto-fit-250 grid gap-2">
85 <CardKV v-for="(value, key) of properties" :key>
86 <template #key>
87 {{ key }}
88 </template>
89 <template #value>
90 {{ value ?? "-" }}
91 </template>
92 </CardKV>
93 </div>
94 </div>
95
96 <div v-else class="min-h-96">
97 <n-empty v-if="!loadingList" description="No Portainer Stack found" class="h-48 justify-center" />
98 </div>
99
100 <n-modal
101 v-model:show="showResourceControl"
102 preset="card"
103 content-class="@container"
104 :style="{ maxWidth: 'min(800px, 90vw)', overflow: 'hidden' }"
105 :bordered="false"
106 title="Resource Control"
107 >
108 <div class="flex flex-col gap-4 @2xl:flex-row!">
109 <SimpleJsonViewer
110 v-if="portainerStack"
111 class="vuesjv-override grow"
112 :model-value="portainerStack?.ResourceControl"
113 :initial-expanded-depth="2"
114 />
115
116 <div class="min-w-40">
117 <CardKV>
118 <template #key>Type legend</template>
119 <template #value>
120 <ul class="list-none p-0">
121 <li>1 = Container</li>
122 <li>2 = Service</li>
123 <li>3 = Volume</li>
124 <li>4 = Network</li>
125 <li>5 = Secret</li>
126 <li>6 = Stack</li>
127 <li>7 = Config</li>
128 </ul>
129 </template>
130 </CardKV>
131 </div>
132 </div>
133 </n-modal>
134 </n-spin>
135 </div>
136 </template>
137
138 <script setup lang="ts">
139 import type { SafeAny } from "@/types/common.d"
140 import type { PortainerStack } from "@/types/portainer.d"
141 import _castArray from "lodash/castArray"
142 import _pick from "lodash/pick"
143 import { NButton, NEmpty, NModal, NSpin, useMessage } from "naive-ui"
144 import { computed, onBeforeMount, ref } from "vue"
145 import { SimpleJsonViewer } from "vue-sjv"
146 import Api from "@/api"
147 import Badge from "@/components/common/Badge.vue"
148 import CardEntity from "@/components/common/cards/CardEntity.vue"
149 import CardKV from "@/components/common/cards/CardKV.vue"
150 import Icon from "@/components/common/Icon.vue"
151 import { useSettingsStore } from "@/stores/settings"
152 import { PortainerStackStatus } from "@/types/portainer.d"
153 import { formatDate } from "@/utils/format"
154 import "@/assets/scss/overrides/vuesjv-override.scss"
155
156 const { stackId } = defineProps<{ stackId: number }>()
157
158 const OfflineIcon = "carbon:error-filled"
159 const OnlineIcon = "carbon:checkmark-filled"
160 const UserIcon = "carbon:group-security"
161 const DateIcon = "carbon:time"
162 const ViewIcon = "iconoir:eye-solid"
163 const StopIcon = "carbon:stop"
164 const StartIcon = "carbon:play"
165
166 const message = useMessage()
167 const showResourceControl = ref(false)
168 const loadingList = ref(false)
169 const loadingAction = ref(false)
170 const dFormats = useSettingsStore().dateFormat
171 const portainerStack = ref<PortainerStack | null>(null)
172 const portainerStackType = computed(
173 () => ["", "Docker Swarm stack", "Standalone Docker stack", "Kubernetes stack"][portainerStack.value?.Type || 0]
174 )
175
176 const properties = computed(() => {
177 const props: Partial<{ [key in keyof PortainerStack]: SafeAny | null | Date }> = _pick(portainerStack.value || {}, [
178 "EndpointId",
179 "SwarmId",
180 "EntryPoint",
181 "Env",
182 "ProjectPath",
183 "UpdateDate",
184 "UpdatedBy",
185 "AdditionalFiles",
186 "AutoUpdate",
187 "Option",
188 "GitConfig",
189 "FromAppTemplate",
190 "Namespace",
191 "IsComposeFormat"
192 ])
193
194 props.Env = _castArray(props.Env).join(", ") || null
195 props.UpdateDate = props.UpdateDate
196 ? formatDate(props.UpdateDate as string | number | Date, dFormats.datetimesec)
197 : null
198 props.UpdatedBy = props.UpdatedBy || null
199 props.Namespace = props.Namespace || null
200
201 return props
202 })
203
204 function getPortainerStack() {
205 loadingList.value = true
206
207 Api.portainer
208 .getStackDetails(stackId)
209 .then(res => {
210 if (res.data.success) {
211 portainerStack.value = res.data.data
212 } else {
213 message.warning(res.data?.message || "An error occurred. Please try again later.")
214 }
215 })
216 .catch(err => {
217 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
218 })
219 .finally(() => {
220 loadingList.value = false
221 })
222 }
223
224 function stop() {
225 loadingAction.value = true
226
227 Api.portainer
228 .stopWazuhCustomerStack(stackId)
229 .then(res => {
230 if (res.data.success) {
231 portainerStack.value = res.data.data
232 message.success(`Wazuh Worker stopped${res.data?.message ? `: ${res.data.message}` : ""}`)
233 } else {
234 message.warning(res.data?.message || "An error occurred. Please try again later.")
235 }
236 })
237 .catch(err => {
238 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
239 })
240 .finally(() => {
241 loadingAction.value = false
242 })
243 }
244
245 function start() {
246 loadingAction.value = true
247
248 Api.portainer
249 .startWazuhCustomerStack(stackId)
250 .then(res => {
251 if (res.data.success) {
252 portainerStack.value = res.data.data
253 message.success(`Wazuh Worker started${res.data?.message ? `: ${res.data.message}` : ""}`)
254 } else {
255 message.warning(res.data?.message || "An error occurred. Please try again later.")
256 }
257 })
258 .catch(err => {
259 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
260 })
261 .finally(() => {
262 loadingAction.value = false
263 })
264 }
265
266 onBeforeMount(() => {
267 getPortainerStack()
268 })
269 </script>