1import { ref, computed, watch, onMounted } from "vue" 2import { QBtn, QIcon, QInput, QItem, QTooltip, QMenu, QVirtualScroll } from "quasar" 3import { useWindowSize } from "@vueuse/core" 4import LaptopInfo from "appDeviceLogs/football/logViewer/overview/LaptopInfo" 5import iosDeviceCard from "appDeviceLogs/football/logViewer/deviceInfo/iosDeviceCard" 6import Jdenticon, { useJdenticon, refreshJdenticon } from "appDeviceLogs/composables/jdenticon" 7import dateFormat from "dateformat" 8import _ from "lodash" 9import socketDisconnectReason from "appDeviceLogs/utils/socketDisconnectReasons" 10 11export default { 12 components: { 13 QBtn, 14 QIcon, 15 QInput, 16 QItem, 17 QTooltip, 18 QMenu, 19 QVirtualScroll, 20 LaptopInfo, 21 iosDeviceCard, 22 Jdenticon, 23 }, 24 props: { 25 selectedLog: Object, 26 isLeftPanelDocked: Boolean, 27 }, 28 setup(props) { 29 useJdenticon() 30 const { width } = useWindowSize() 31 const rootElem = ref(null) 32 const cardWidth = ref({ maxWidth: "0px" }) 33 function updateCardWidth() { 34 const widthVal = `${document.body.clientWidth}px` 35 const { paddingLeft, paddingRight } = getComputedStyle(rootElem.value.parentElement) 36 const leftPanel = document.getElementById("leftPanelContainer") 37 const panelWidth = props.isLeftPanelDocked ? `${leftPanel.clientWidth}px` : "0px" 38 cardWidth.value.maxWidth = `calc(${widthVal} - ${panelWidth} - ${paddingLeft} - ${paddingRight}) !important` 39 } 40 watch(width, updateCardWidth) 41 watch(() => props.isLeftPanelDocked, updateCardWidth) 42 onMounted(() => { 43 updateCardWidth() 44 }) 45 const searchTerm = ref("") 46 const sections = computed(() => { 47 let result = [] 48 props.selectedLog.events.forEach((event) => { 49 const eventCopy = _.cloneDeep(event) 50 // Set additional props on event 51 if (eventCopy.level == "error") { 52 const error = (() => { 53 if (typeof eventCopy.eventData == "string") return { message: eventCopy.eventData } 54 if (typeof eventCopy.eventData != "object") 55 return { message: JSON.stringify(eventCopy.eventData) } 56 if (eventCopy.eventData.stack) { 57 eventCopy.eventData.stack = eventCopy.eventData.stack.split("\n").map((line) => { 58 return line.trim().replaceAll("<anonymous>", "anonymous") 59 }) 60 } 61 return eventCopy.eventData 62 })() 63 eventCopy.label = `${eventCopy.eventID} ${error.message || ""}` 64 eventCopy.error = error 65 } else if (eventCopy.eventID == "Disconnected from Server") { 66 const disconnectReason = socketDisconnectReason(event.eventData) 67 eventCopy.label = `${eventCopy.eventID}: ${disconnectReason}` 68 } else { 69 eventCopy.label = `${eventCopy.eventID} ${JSON.stringify(eventCopy.eventData) || ""}` 70 } 71 if ( 72 searchTerm.value && 73 !eventCopy.label.toLowerCase().includes(searchTerm.value.toLowerCase()) 74 ) { 75 return 76 } 77 // Create new section if needed 78 if (result.length == 0 || _.last(result).name != eventCopy.section) { 79 result.push({ 80 name: eventCopy.section, 81 start: eventCopy.timestamp, 82 end: eventCopy.timestamp, 83 events: [eventCopy], 84 }) 85 } 86 // Append event to existing section 87 else { 88 _.last(result).end = event.timestamp 89 _.last(result).events.push(eventCopy) 90 } 91 }) 92 return result 93 }) 94 95 const devices = computed(() => { 96 let map = new Map() 97 props.selectedLog.devices.forEach((device) => { 98 map.set(device.id, device) 99 }) 100 return map 101 }) 102 103 function timeSpread(index, events) { 104 if (index == events.length - 1) return `` 105 const spread = events[index + 1].timestamp - events[index].timestamp 106 const format = (() => { 107 if (spread < 60 * 1000) return `s.l "s"` 108 if (spread < 60 * 60 * 1000) return `M "m" s "s"` 109 return `h "h" M "m" s "s"` 110 })() 111 const d = new Date() 112 d.setHours(0, 0, 0, 0) 113 return dateFormat(d.getTime() + spread, format) 114 } 115 116 function deviceForEvent(event) { 117 return devices.value.get(event.deviceID) || { id: event.deviceID, name: "unknown device" } 118 } 119 120 return { 121 rootElem, 122 cardWidth, 123 searchTerm, 124 devices, 125 dateFormat, 126 sections, 127 deviceForEvent, 128 timeSpread, 129 refreshJdenticon, 130 } 131 }, 132 template: /*html*/ ` 133 <div ref="rootElem" class="w-full"> 134 135 <!-- Search bar --> 136 <div class="sticky z-50 top-[3.25rem] mb-4"> 137 <div class="abs-c w-screen lg:w-[calc(100%+2rem)] h-full bg-gray-50"> 138 </div> 139 <div class="relative row items-center px-4 py-2"> 140 <q-input v-model="searchTerm" label="Search Events" bg-color="white" filled dense class="w-full">
141 <template #prepend> 142 <q-icon name="mdi-magnify" /> 143 </template> 144 <template #append> 145 <q-icon 146 name="mdi-close-circle" 147 size="1rem" 148 class="text-gray-500 hover:text-gray-600 cursor-pointer" 149 :class="{ 150 'invisible': searchTerm == '' 151 }" 152 @click="searchTerm = ''"/> 153 </template> 154 </q-input> 155 156 </div> 157 </div> 158 159 <!-- Session internal sections --> 160 <div v-for="(section, index) in sections" class="pl-2"> 161 162 <!-- Section label --> 163 <span class="w-full row gap-2 text-2xs text-gray-600"> 164 <span>{{section.name}}</span> 165 <span> | </span> 166 <span>{{dateFormat(section.start, "hh:MMtt ss")}}s</span> 167 <span>–</span> 168 <span>{{dateFormat(section.end, "hh:MMtt ss")}}s</span> 169 </span> 170 171 <!-- Events list table --> 172 <div class="card p-4 text-sm w-full mt-2" :style="cardWidth"> 173 <q-virtual-scroll 174 type="table" 175 :items="Object.freeze(section.events)" 176 :virtual-scroll-item-size="48" 177 v-slot="{ item: event, index }" 178 separator="none" 179 class="w-full text-left text-xs text-gray-800 max-h-[30rem] shadow-none" 180 @virtual-scroll="refreshJdenticon" 181 > 182 <tr :key="index" class="h-[48px] relative"> 183 <!-- Time spread --> 184 <td class="w-16 p-2 py-3"> 185 <div class="relative h-full w-full"> 186 <span class="absolute top-full left-0 text-2xs text-gray-500 whitespace-nowrap"> 187 {{timeSpread(index, section.events)}} 188 </span> 189 </div> 190 </td> 191 <!-- Timestamp --> 192 <td class="w-40 p-2 py-3"> 193 {{ dateFormat(event.timestamp, "hh:MMtt ss") }}s 194 </td> 195 <!-- Device Identicon --> 196 <td class="w-8 p-2 py-3"> 197 <Jdenticon :jdid="deviceForEvent(event).id"/> 198 </td> 199 <!-- Device --> 200 <td class="w-36 p-2 py-3"> 201 <span>{{ deviceForEvent(event).name }}</span> 202 <q-tooltip 203 :delay="500" 204 transition-show="jump-down" 205 transition-hide="jump-up" 206 class="bg-transparent p-0 overflow-visible text-gray-800" 207 > 208 209 <laptop-info v-if="deviceForEvent(event).type == 'Laptop'" :selected-log="selectedLog"/> 210 <ios-device-card 211 v-else 212 :selected-log="selectedLog" 213 :device-id="deviceForEvent(event).id" 214 :device-name="deviceForEvent(event).name" 215 class="text-gray-800" 216 /> 217 </q-tooltip> 218 </td> 219 <!-- Icon --> 220 <td class="w-10 p-2 py-3"> 221 <div class="relative w-full h-full"> 222 <q-icon v-if="event.level == 'warning'" name="mdi-alert-rhombus" size="1.5em" class="abs-c text-amber-600"/> 223 <q-icon v-if="event.level == 'error'" name="mdi-alert" size="1.5em" class="abs-c text-red-700"/> 224 </div> 225 </td> 226 <!-- Event name --> 227 <td class="flex-grow p-2 py-3"> 228 <span v-if="event.error === undefined"> 229 {{ event.label }} 230 </span> 231 <div v-else class="row items-center justify-between"> 232 <span>{{ event.label }}</span> 233 <q-btn v-if="event.error.stack != undefined" icon="mdi-information-outline" size="xs" flat rounded class="h-full sticky right-0 z-50 bg-white"> 234 <q-menu transition-show="jump-down" transition-hide="jump-up" class="column items-start shadow-lg max-h-40 p-4 bg-red-50 text-red-700 border-l-8 border-red-700"> 235 <span v-for="line of event.error.stack" class="whitespace-nowrap">{{ line }}</span> 236 </q-menu> 237 </q-btn> 238 </div> 239 </td> 240 </tr> 241 242 </q-virtual-scroll> 243 </div> 244 245 <!-- Line --> 246 <div v-if="index < sections.length-1" class="w-[2px] h-12 bg-gray-300 m-8 mb-4"></div> 247 248 </div> 249 </div> 250 `, 251};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.