| 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> |