Skip to content

Commit

Permalink
feat(ui): Response Panel > revamp the response history list (#175)
Browse files Browse the repository at this point in the history
  • Loading branch information
kobenguyent authored Jul 6, 2024
1 parent 3eab940 commit 0e5548b
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 51 deletions.
99 changes: 69 additions & 30 deletions packages/ui/src/components/ContextMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@
<div class="context-menu-container" :style="{ 'visibility': show ? 'visible': 'hidden' }">
<div class="context-menu-background" @click.stop="$emit('update:show', false)"></div>
<div class="context-menu" :style="contextMenuStyle">
<div v-for="option in options">
<div v-for="option in options" :key="option.value">
<template v-if="option.type === 'option'">
<button type="button" class="context-menu-item" :class="`${option.class ? option.class : ''}`" :disabled="option.disabled" @click.stop="$emit('click', option.value); $emit('update:show', false);">
<i :class="option.icon" v-if="option.icon"></i> {{ option.label }}
</button>
<slot name="option" :option="option">
<button
type="button"
class="context-menu-item"
:class="[{ 'selected': option.value === selectedOption }, option.class || '']"
:disabled="option.disabled"
@click.stop="handleClick(option)"
>
<i :class="option.icon" v-if="option.icon"></i><span v-html="isOptionSelected(option)"></span>

Check warning on line 15 in packages/ui/src/components/ContextMenu.vue

View workflow job for this annotation

GitHub Actions / test

'v-html' directive can lead to XSS attack
</button>
</slot>
</template>
<template v-if="option.type === 'separator'">
<div class="context-menu-separator"></div>
Expand Down Expand Up @@ -53,45 +61,53 @@ import { nextTick } from 'vue'
export default {
props: {
options: Array,
element: Element,
options: {
type: Array,
required: true,
default: () => []
},
element: {
type: [Element, null],
default: null
},
show: {
type: Boolean,
default: false
},
x: {
type: Number,
required: false
default: null
},
y: {
type: Number,
required: false
default: null
},
xOffset: {
type: Number,
required: false
default: 0
}
},
data() {
return {
contextMenuStyle: {}
contextMenuStyle: {},
selectedOption: null
}
},
computed: {
elementRect() {
if(this.element) {
return this.element.getBoundingClientRect()
}
return null
}
},
watch: {
show() {
if(this.show) {
show(newVal) {
if (newVal) {
nextTick(() => {
this.$store.state.openContextMenuElement = this.$el
this.setContextMenuStyle()
this.selectFirstOption()
})
} else {
this.$store.state.openContextMenuElement = null
Expand All @@ -104,43 +120,59 @@ export default {
const xDefined = this.x !== null && this.x !== undefined
const yDefined = this.y !== null && this.y !== undefined
if((!xDefined && !xDefined) && !this.element) {
if (!xDefined && !yDefined && !this.element) {
return {}
}
let x = xDefined ? this.x : this.elementRect.left + (this.xOffset ? this.xOffset : 0)
let x = xDefined ? this.x : this.elementRect.left + (this.xOffset || 0)
let y = yDefined ? this.y : this.elementRect.bottom
const contextMenuPosition = getContextMenuPostion(x, y, this.$el.querySelector('.context-menu'), yDefined ? 0 : this.elementRect.height)
x = contextMenuPosition.x
y = contextMenuPosition.y
this.contextMenuStyle = {
left: x + 'px',
top: y + 'px',
maxHeight: contextMenuPosition.maxHeight + 'px',
left: `${contextMenuPosition.x}px`,
top: `${contextMenuPosition.y}px`,
maxHeight: contextMenuPosition.maxHeight ? `${contextMenuPosition.maxHeight}px` : 'auto',
}
},
handleClick(option) {
this.selectedOption = option.value
this.$emit('click', option.value)
this.$emit('update:show', false)
},
selectFirstOption() {
if (this.options.length > 0 && !this.selectedOption) {
this.selectedOption = this.options[0].value
}
},
isOptionSelected(option) {
if (option.value?._id) {
if (option.value === this.selectedOption) {
return `<span class="selected-indicator">✔</span> ${option.label}`
}
return `<span class="selected-indicator">&nbsp;&nbsp;&nbsp;&nbsp;</span>${option.label}`
}
return option.label
}
}
}
</script>
<style scoped>
.context-menu-background {
position: fixed;
z-index: 1;
left: 0;
right: 0;
top: 0;
bottom: 0;
position: fixed;
z-index: 1;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
.context-menu {
position: fixed;
z-index: 1;
border: 1px solid var(--menu-border-color);
box-shadow: 0 0 1rem 0 var(--box-shadow-color);
border-radius: calc(1rem * 0.3);
border-radius: 0.3rem;
min-width: 15rem;
padding-top: 5px;
padding-bottom: 5px;
Expand All @@ -151,13 +183,14 @@ export default {
button.context-menu-item {
padding: 0.5rem;
outline: 0;
outline: none;
background: var(--background-color);
border: 0;
border: none;
display: block;
width: 100%;
text-align: left;
color: var(--text-color);
cursor: pointer;
}
button.context-menu-item:not(:active):focus {
Expand Down Expand Up @@ -185,4 +218,10 @@ button.context-menu-item > i {
margin-top: 5px;
margin-bottom: 5px;
}
.selected-indicator {
padding-right: 0.1rem;
font-size: 0.5rem;
color: var(--button-text-color);
}
</style>
13 changes: 0 additions & 13 deletions packages/ui/src/components/RequestPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -887,17 +887,4 @@ export default {
align-items: center;
margin-top: 0.5rem;
}
.custom-dropdown {
cursor: pointer;
padding-left: 0.8rem;
display: flex;
align-items: center;
user-select: none;
height: 100%;
}
.custom-dropdown i {
padding-left: 4px;
}
</style>
50 changes: 42 additions & 8 deletions packages/ui/src/components/ResponsePanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,7 @@
<div class="response-panel-address-bar">
<div class="response-panel-address-bar-tag-container">
<div
class="tag" :class="{
'green': response.status >= 200 && response.status <= 299,
'yellow': response.status >= 400 && response.status <= 499,
'red': response.status >= 500 || response.statusText === 'Error'
}"
class="tag" :class="responseStatusColorMapping(response)"
>
<span class="bold">{{ response.status }}</span>
{{ response.statusText === '' ? getStatusText(response.status) : response.statusText }}
Expand All @@ -25,9 +21,19 @@
<div class="tag ml-0_6rem" v-if="responseSize">{{ humanFriendlySize(responseSize) }}</div>
</div>
<div class="response-panel-address-bar-select-container">
<select v-model="response" v-if="responses.length > 0" @contextmenu.prevent="handleResponseHistoryContextMenu">
<option v-for="response in responses" :value="response">{{ dateFormat(response.createdAt, true) }} | {{ response.name ?? response.url }}</option>
</select>
<div v-if="response.createdAt" class="custom-dropdown" @click="handleResponseHistoryMenu" @contextmenu.prevent="handleResponseHistoryContextMenu">
{{ timeAgo(response.createdAt) }} | {{ dateFormat(response.createdAt, true) }} | {{ response.name ?? response.url }}
<i class="fa fa-caret-down space-right"></i>
</div>

<ContextMenu
:options="getHistoryResponses()"
:show="showContextMenu"
:x="menuX"
:y="menuY"
@update:show="showContextMenu = $event"
@click="handleMenuClick"
/>
</div>
</div>
<div class="response-panel-tabs">
Expand Down Expand Up @@ -168,6 +174,8 @@ import {
getAlertConfirmPromptContainer,
getStatusText,
bufferToString,
timeAgo,
responseStatusColorMapping,
} from '@/helpers'
import { emitter } from '@/event-bus'
import {JSONPath} from 'jsonpath-plus'
Expand Down Expand Up @@ -198,6 +206,9 @@ export default {
responseFilter: '',
scrollableAreaEventListenerAttached: false,
scrollableAreaScrollTop: null,
showContextMenu: false,
menuX: null,
menuY: null,
}
},
computed: {
Expand Down Expand Up @@ -411,6 +422,7 @@ export default {
}
},
methods: {
timeAgo,
filterResponse(buffer, jsonPath) {
try {
const responseData = JSON.parse(this.bufferToJSONString(buffer))
Expand Down Expand Up @@ -570,6 +582,27 @@ export default {
scrollableAreaOnScroll(event) {
this.scrollableAreaScrollTop = event.target.scrollTop
},
handleResponseHistoryMenu(event) {
this.menuX = event.clientX
this.menuY = event.clientY
this.showContextMenu = true
},
handleMenuClick(value) {
this.response = value
},
responseStatusColorMapping,
getHistoryResponses() {
return this.responses.map(item => {
const color = responseStatusColorMapping(item)
const label = `${dateFormat(item.createdAt, true)} | <span class="tag ${color}">${item.status} ${this.getStatusText(item.status)}</span><span class="request-method--${item.request.method}"> ${item.request.method} </span> ${item.name ?? item.url}`
return {
type: 'option',
label,
value: item
}
})
}
},
activated() {
if(this.response && this.scrollableAreaEventListenerAttached && this.scrollableAreaScrollTop !== null) {
Expand Down Expand Up @@ -665,6 +698,7 @@ export default {
.response-panel-address-bar .response-panel-address-bar-select-container {
height: 100%;
margin-left: 1rem;
margin-right: 1rem;
}
.response-panel-address-bar .response-panel-address-bar-select-container select {
Expand Down
46 changes: 46 additions & 0 deletions packages/ui/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1634,3 +1634,49 @@ export function bufferToString(buffer: BufferSource) {
const textDecoder = new TextDecoder('utf-8')
return textDecoder.decode(buffer)
}

export function timeAgo(timestamp: number) {
const now: any = new Date()
const date: any = new Date(timestamp)
const secondsPast = Math.floor((now - date) / 1000)

if (secondsPast < 60) {
return secondsPast === 1 || secondsPast < 1 ? 'Just Now' : `${secondsPast} seconds ago`
}
if (secondsPast < 3600) {
const minutesPast = Math.floor(secondsPast / 60)
return minutesPast === 1 ? 'one minute ago' : `${minutesPast} minutes ago`
}
if (secondsPast < 86400) {
const hoursPast = Math.floor(secondsPast / 3600)
return hoursPast === 1 ? 'one hour ago' : `${hoursPast} hours ago`
}
if (secondsPast < 2592000) { // Less than 30 days
const daysPast = Math.floor(secondsPast / 86400)
return daysPast === 1 ? 'one day ago' : `${daysPast} days ago`
}
if (secondsPast < 31536000) { // Less than 365 days
const monthsPast = Math.floor(secondsPast / 2592000)
return monthsPast === 1 ? 'one month ago' : `${monthsPast} months ago`
}
const yearsPast = Math.floor(secondsPast / 31536000)
return yearsPast === 1 ? 'one year ago' : `${yearsPast} years ago`
}

export function responseStatusColorMapping(response: Response) {
let color

if(response.status >= 200 && response.status <= 299) {
color = 'green'
}

if(response.status >= 400 && response.status <= 499) {
color = 'yellow'
}

if(response.status >= 500 || response.statusText === 'Error') {
color = 'red'
}

return color
}
28 changes: 28 additions & 0 deletions packages/ui/src/styles/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -657,3 +657,31 @@ select {
.sidebar-item {
cursor: pointer;
}

.tag.green {
background: #75ba24;
color: white;
}

.tag.yellow {
background: #ec8702;
color: white;
}

.tag.red {
background: #e15251;
color: white;
}

.custom-dropdown {
cursor: pointer;
padding-left: 0.8rem;
display: flex;
align-items: center;
user-select: none;
height: 100%;
}

.custom-dropdown i {
padding-left: 4px;
}

0 comments on commit 0e5548b

Please sign in to comment.