| 1 | <template> |
| 2 | <n-spin :show="loading" class="active-response-wizard"> |
| 3 | <div class="flex min-h-120 flex-col"> |
| 4 | <div class="flex grow flex-col"> |
| 5 | <n-scrollbar x-scrollable trigger="none"> |
| 6 | <div class="p-6 pt-3"> |
| 7 | <n-steps :current size="small" :status="currentStatus"> |
| 8 | <n-step title="Operative System" /> |
| 9 | <n-step title="Active Response" /> |
| 10 | <n-step title="Submission" /> |
| 11 | </n-steps> |
| 12 | </div> |
| 13 | </n-scrollbar> |
| 14 | |
| 15 | <div class="mt-4 flex grow flex-col"> |
| 16 | <Transition :name="`slide-form-${slideFormDirection}`"> |
| 17 | <div v-if="current === 1" class="flex flex-col gap-2 px-6"> |
| 18 | <CardEntity embedded hoverable clickable @click.stop="setOs('linux')"> |
| 19 | <div class="flex items-center gap-3"> |
| 20 | <Icon :size="18" :name="iconFromOs('linux')" /> |
| 21 | <span>LINUX</span> |
| 22 | </div> |
| 23 | </CardEntity> |
| 24 | <CardEntity embedded hoverable clickable @click.stop="setOs('windows')"> |
| 25 | <div class="flex items-center gap-3"> |
| 26 | <Icon :size="18" :name="iconFromOs('windows')" /> |
| 27 | <span>WINDOWS</span> |
| 28 | </div> |
| 29 | </CardEntity> |
| 30 | <CardEntity embedded hoverable clickable @click.stop="setOs('macos')"> |
| 31 | <div class="flex items-center gap-3"> |
| 32 | <Icon :size="18" :name="iconFromOs('macos')" /> |
| 33 | <span>MACOS</span> |
| 34 | </div> |
| 35 | </CardEntity> |
| 36 | </div> |
| 37 | |
| 38 | <div v-else-if="current === 2" class="px-6"> |
| 39 | <n-spin :show="loadingActiveResponse"> |
| 40 | <div class="flex flex-col gap-2"> |
| 41 | <template v-if="activeResponseFiltered.length"> |
| 42 | <ActiveResponseItem |
| 43 | v-for="activeResponse of activeResponseFiltered" |
| 44 | :key="activeResponse.name" |
| 45 | :active-response |
| 46 | embedded |
| 47 | clickable |
| 48 | hide-actions |
| 49 | @click.stop="setActiveResponse(activeResponse)" |
| 50 | /> |
| 51 | </template> |
| 52 | <template v-else> |
| 53 | <n-empty |
| 54 | v-if="!loadingActiveResponse" |
| 55 | description="No items found" |
| 56 | class="h-48 justify-center" |
| 57 | /> |
| 58 | </template> |
| 59 | </div> |
| 60 | </n-spin> |
| 61 | </div> |
| 62 | <div v-else-if="current === 3" class="flex min-h-[401px] grow flex-col px-6 pb-7"> |
| 63 | <ActiveResponseInvokeForm |
| 64 | v-if="selectedActiveResponse" |
| 65 | :active-response="selectedActiveResponse" |
| 66 | @mounted="activeResponseInvokeFormCTX = $event" |
| 67 | @submitted="reset()" |
| 68 | @start-loading="loadingActiveResponseInvoke = true" |
| 69 | @stop-loading="loadingActiveResponseInvoke = false" |
| 70 | > |
| 71 | <template #additionalActions> |
| 72 | <n-button :disabled="loadingActiveResponseInvoke" @click.stop="prev()"> |
| 73 | <template #icon> |
| 74 | <Icon :name="ArrowLeftIcon" /> |
| 75 | </template> |
| 76 | Prev |
| 77 | </n-button> |
| 78 | </template> |
| 79 | </ActiveResponseInvokeForm> |
| 80 | </div> |
| 81 | </Transition> |
| 82 | </div> |
| 83 | </div> |
| 84 | |
| 85 | <div v-if="current !== 3" class="flex justify-between gap-4 p-6 pt-3"> |
| 86 | <div class="flex gap-4"> |
| 87 | <n-button v-if="isPrevStepEnabled" @click.stop="prev()"> |
| 88 | <template #icon> |
| 89 | <Icon :name="ArrowLeftIcon" /> |
| 90 | </template> |
| 91 | Prev |
| 92 | </n-button> |
| 93 | </div> |
| 94 | <div class="flex gap-4"> |
| 95 | <slot name="additionalActions"></slot> |
| 96 | </div> |
| 97 | </div> |
| 98 | </div> |
| 99 | </n-spin> |
| 100 | </template> |
| 101 | |
| 102 | <script setup lang="ts"> |
| 103 | import type { StepsProps } from "naive-ui" |
| 104 | import type { SupportedActiveResponse } from "@/types/activeResponse.d" |
| 105 | import type { OsTypesLower } from "@/types/common.d" |
| 106 | import { NButton, NEmpty, NScrollbar, NSpin, NStep, NSteps, useMessage } from "naive-ui" |
| 107 | import { computed, onBeforeMount, onMounted, ref, watch } from "vue" |
| 108 | import Api from "@/api" |
| 109 | import CardEntity from "@/components/common/cards/CardEntity.vue" |
| 110 | import Icon from "@/components/common/Icon.vue" |
| 111 | import { iconFromOs } from "@/utils" |
| 112 | import ActiveResponseInvokeForm from "./ActiveResponseInvokeForm.vue" |
| 113 | import ActiveResponseItem from "./ActiveResponseItem.vue" |
| 114 | |
| 115 | const emit = defineEmits<{ |
| 116 | (e: "update:loading", value: boolean): void |
| 117 | ( |
| 118 | e: "mounted", |
| 119 | value: { |
| 120 | reset: () => void |
| 121 | } |
| 122 | ): void |
| 123 | }>() |
| 124 | |
| 125 | const ArrowLeftIcon = "carbon:arrow-left" |
| 126 | |
| 127 | const loadingActiveResponse = ref(false) |
| 128 | const loadingActiveResponseInvoke = ref(false) |
| 129 | const loading = computed(() => loadingActiveResponseInvoke.value) |
| 130 | const slideFormDirection = ref<"right" | "left">("right") |
| 131 | const activeResponseList = ref<SupportedActiveResponse[]>([]) |
| 132 | const message = useMessage() |
| 133 | const current = ref<number>(1) |
| 134 | const currentStatus = ref<StepsProps["status"]>("process") |
| 135 | const selectedOS = ref<OsTypesLower | null>(null) |
| 136 | const selectedActiveResponse = ref<SupportedActiveResponse | null>(null) |
| 137 | const activeResponseInvokeFormCTX = ref<{ reset: () => void } | null>(null) |
| 138 | |
| 139 | watch(loading, val => { |
| 140 | emit("update:loading", val) |
| 141 | }) |
| 142 | |
| 143 | const activeResponseFiltered = computed(() => { |
| 144 | if (selectedOS.value === null) { |
| 145 | return activeResponseList.value |
| 146 | } |
| 147 | return activeResponseList.value.filter(o => o.name.toLowerCase().indexOf(selectedOS.value || "") === 0) |
| 148 | }) |
| 149 | const isPrevStepEnabled = computed(() => current.value > 1) |
| 150 | |
| 151 | function next() { |
| 152 | currentStatus.value = "process" |
| 153 | slideFormDirection.value = "right" |
| 154 | current.value++ |
| 155 | } |
| 156 | |
| 157 | function prev() { |
| 158 | currentStatus.value = "process" |
| 159 | slideFormDirection.value = "left" |
| 160 | current.value-- |
| 161 | activeResponseInvokeFormCTX.value?.reset() |
| 162 | } |
| 163 | |
| 164 | function reset() { |
| 165 | if (!loadingActiveResponseInvoke.value) { |
| 166 | currentStatus.value = "process" |
| 167 | slideFormDirection.value = "right" |
| 168 | current.value = 1 |
| 169 | selectedOS.value = null |
| 170 | selectedActiveResponse.value = null |
| 171 | activeResponseInvokeFormCTX.value?.reset() |
| 172 | } |
| 173 | } |
| 174 | |
| 175 | function getActiveResponseList() { |
| 176 | loadingActiveResponse.value = true |
| 177 | |
| 178 | Api.activeResponse |
| 179 | .getSupported() |
| 180 | .then(res => { |
| 181 | if (res.data.success) { |
| 182 | activeResponseList.value = res.data?.supported_active_responses || [] |
| 183 | } else { |
| 184 | message.warning(res.data?.message || "An error occurred. Please try again later.") |
| 185 | } |
| 186 | }) |
| 187 | .catch(err => { |
| 188 | message.error(err.response?.data?.message || "An error occurred. Please try again later.") |
| 189 | }) |
| 190 | .finally(() => { |
| 191 | loadingActiveResponse.value = false |
| 192 | }) |
| 193 | } |
| 194 | |
| 195 | function setOs(os: OsTypesLower) { |
| 196 | selectedOS.value = os |
| 197 | next() |
| 198 | } |
| 199 | |
| 200 | function setActiveResponse(activeResponse: SupportedActiveResponse) { |
| 201 | selectedActiveResponse.value = activeResponse |
| 202 | next() |
| 203 | } |
| 204 | |
| 205 | onBeforeMount(() => { |
| 206 | getActiveResponseList() |
| 207 | }) |
| 208 | |
| 209 | onMounted(() => { |
| 210 | emit("mounted", { |
| 211 | reset |
| 212 | }) |
| 213 | }) |
| 214 | </script> |
| 215 | |
| 216 | <style lang="scss" scoped> |
| 217 | .active-response-wizard { |
| 218 | .slide-form-right-enter-active, |
| 219 | .slide-form-right-leave-active, |
| 220 | .slide-form-left-enter-active, |
| 221 | .slide-form-left-leave-active { |
| 222 | transition: all 0.2s ease-out; |
| 223 | position: absolute; |
| 224 | width: 100%; |
| 225 | } |
| 226 | |
| 227 | .slide-form-left-enter-from { |
| 228 | transform: translateX(-100%); |
| 229 | } |
| 230 | |
| 231 | .slide-form-left-leave-to { |
| 232 | transform: translateX(100%); |
| 233 | } |
| 234 | |
| 235 | .slide-form-right-enter-from { |
| 236 | transform: translateX(100%); |
| 237 | } |
| 238 | |
| 239 | .slide-form-right-leave-to { |
| 240 | transform: translateX(-100%); |
| 241 | } |
| 242 | } |
| 243 | </style> |