Compare commits
2 Commits
hotfix/fet
...
feat/realt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d13b381097 | ||
|
|
663da34e08 |
@@ -7,6 +7,12 @@
|
|||||||
{{ title }}
|
{{ title }}
|
||||||
</label>
|
</label>
|
||||||
<div>
|
<div>
|
||||||
|
<ButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('action.search')"
|
||||||
|
svg="search"
|
||||||
|
@click.native="toggleSearch = !toggleSearch"
|
||||||
|
/>
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="t('action.delete')"
|
:title="t('action.delete')"
|
||||||
@@ -37,6 +43,26 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="toggleSearch"
|
||||||
|
class="w-full p-2 sticky top-0 z-10 text-center border-b border-dividerLight"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="bg-primaryLight border-divider text-secondaryDark rounded inline-flex"
|
||||||
|
>
|
||||||
|
<ButtonSecondary svg="search" class="item-center" />
|
||||||
|
|
||||||
|
<input
|
||||||
|
id=""
|
||||||
|
v-model="pattern"
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter search pattern"
|
||||||
|
class="rounded w-64 bg-primaryLight text-secondaryDark text-center"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="log.length !== 0"
|
v-if="log.length !== 0"
|
||||||
ref="logs"
|
ref="logs"
|
||||||
@@ -46,9 +72,10 @@
|
|||||||
class="flex flex-col h-auto h-full border-r divide-y divide-dividerLight border-dividerLight"
|
class="flex flex-col h-auto h-full border-r divide-y divide-dividerLight border-dividerLight"
|
||||||
>
|
>
|
||||||
<RealtimeLogEntry
|
<RealtimeLogEntry
|
||||||
v-for="(entry, index) in log"
|
v-for="(entry, index) in logEntries"
|
||||||
:key="`entry-${index}`"
|
:key="`entry-${index}`"
|
||||||
:entry="entry"
|
:entry="entry"
|
||||||
|
:highlight-regex="pattern === '' ? undefined : patternRegex"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -56,8 +83,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, PropType, computed, watch } from "@nuxtjs/composition-api"
|
import { ref, computed, watch } from "@nuxtjs/composition-api"
|
||||||
import { useThrottleFn, useScroll } from "@vueuse/core"
|
import { useThrottleFn, useScroll } from "@vueuse/core"
|
||||||
|
import { regexEscape } from "~/helpers/functional/regex"
|
||||||
import { useI18n } from "~/helpers/utils/composables"
|
import { useI18n } from "~/helpers/utils/composables"
|
||||||
|
|
||||||
export type LogEntryData = {
|
export type LogEntryData = {
|
||||||
@@ -68,13 +96,7 @@ export type LogEntryData = {
|
|||||||
event: "connecting" | "connected" | "disconnected" | "error"
|
event: "connecting" | "connected" | "disconnected" | "error"
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps<{ log: LogEntryData[]; title: string }>()
|
||||||
log: { type: Array as PropType<LogEntryData[]>, default: () => [] },
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "delete"): void
|
(e: "delete"): void
|
||||||
@@ -121,6 +143,19 @@ const toggleAutoscroll = () => {
|
|||||||
autoScrollEnabled.value = !autoScrollEnabled.value
|
autoScrollEnabled.value = !autoScrollEnabled.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pattern = ref("")
|
||||||
|
const toggleSearch = ref(false)
|
||||||
|
|
||||||
|
const patternRegex = computed(
|
||||||
|
() => new RegExp(regexEscape(pattern.value), "gi")
|
||||||
|
)
|
||||||
|
|
||||||
|
const logEntries = computed(() => {
|
||||||
|
if (patternRegex.value) {
|
||||||
|
return props.log.filter((entry) => entry.payload.match(patternRegex.value))
|
||||||
|
} else return props.log
|
||||||
|
})
|
||||||
|
|
||||||
const toggleAutoscrollColor = computed(() =>
|
const toggleAutoscrollColor = computed(() =>
|
||||||
autoScrollEnabled.value ? "text-green-500" : "text-red-500"
|
autoScrollEnabled.value ? "text-green-500" : "text-red-500"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -31,7 +31,13 @@
|
|||||||
<span v-if="entry.prefix !== undefined" class="!inline">{{
|
<span v-if="entry.prefix !== undefined" class="!inline">{{
|
||||||
entry.prefix
|
entry.prefix
|
||||||
}}</span>
|
}}</span>
|
||||||
{{ entry.payload }}
|
<span
|
||||||
|
v-for="(section, index) in highlightingSections"
|
||||||
|
:key="index"
|
||||||
|
class="!inline"
|
||||||
|
:class="section.mode === 'highlight' ? 'highlight' : ''"
|
||||||
|
>{{ section.text }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -208,6 +214,7 @@ import { LogEntryData } from "./Log.vue"
|
|||||||
import { useI18n } from "~/helpers/utils/composables"
|
import { useI18n } from "~/helpers/utils/composables"
|
||||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||||
import { isJSON } from "~/helpers/functional/json"
|
import { isJSON } from "~/helpers/functional/json"
|
||||||
|
import { regexFindAllMatches } from "~/helpers/functional/regex"
|
||||||
import useCopyResponse from "~/helpers/lenses/composables/useCopyResponse"
|
import useCopyResponse from "~/helpers/lenses/composables/useCopyResponse"
|
||||||
import useDownloadResponse from "~/helpers/lenses/composables/useDownloadResponse"
|
import useDownloadResponse from "~/helpers/lenses/composables/useDownloadResponse"
|
||||||
import { useCodemirror } from "~/helpers/editor/codemirror"
|
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||||
@@ -220,12 +227,55 @@ import {
|
|||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
const props = defineProps<{ entry: LogEntryData }>()
|
const props = defineProps<{
|
||||||
|
entry: LogEntryData
|
||||||
|
highlightRegex?: RegExp
|
||||||
|
}>()
|
||||||
const outlineOptions = ref<any | null>(null)
|
const outlineOptions = ref<any | null>(null)
|
||||||
const editor = ref<any | null>(null)
|
const editor = ref<any | null>(null)
|
||||||
const linewrapEnabled = ref(true)
|
const linewrapEnabled = ref(true)
|
||||||
const logPayload = computed(() => props.entry.payload)
|
const logPayload = computed(() => props.entry.payload)
|
||||||
|
|
||||||
|
type HighlightSection = {
|
||||||
|
mode: "normal" | "highlight"
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const highlightingSections = computed<HighlightSection[]>(() => {
|
||||||
|
if (!props.highlightRegex)
|
||||||
|
return [{ mode: "normal", text: props.entry.payload }]
|
||||||
|
|
||||||
|
const line = props.entry.payload.split("\n")[0]
|
||||||
|
|
||||||
|
const ranges = pipe(line, regexFindAllMatches(props.highlightRegex))
|
||||||
|
|
||||||
|
const result: HighlightSection[] = []
|
||||||
|
let point = 0
|
||||||
|
|
||||||
|
ranges.forEach(({ startIndex, endIndex }) => {
|
||||||
|
if (point < startIndex)
|
||||||
|
result.push({
|
||||||
|
mode: "normal",
|
||||||
|
text: line.slice(point, startIndex),
|
||||||
|
})
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
mode: "highlight",
|
||||||
|
text: line.slice(startIndex, endIndex + 1),
|
||||||
|
})
|
||||||
|
|
||||||
|
point = endIndex + 1
|
||||||
|
})
|
||||||
|
|
||||||
|
if (point < line.length)
|
||||||
|
result.push({
|
||||||
|
mode: "normal",
|
||||||
|
text: line.slice(point, line.length),
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
const selectedTab = ref<"json" | "raw">(
|
const selectedTab = ref<"json" | "raw">(
|
||||||
isJSON(props.entry.payload) ? "json" : "raw"
|
isJSON(props.entry.payload) ? "json" : "raw"
|
||||||
)
|
)
|
||||||
@@ -385,4 +435,8 @@ const iconName = computed(() => ICONS[props.entry.source].iconName)
|
|||||||
.ts-font {
|
.ts-font {
|
||||||
font-size: 0.6rem;
|
font-size: 0.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.highlight {
|
||||||
|
color: yellow;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
33
packages/hoppscotch-app/helpers/functional/regex.ts
Normal file
33
packages/hoppscotch-app/helpers/functional/regex.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Escapes special regex characters in a string.
|
||||||
|
* @param text The string to transform
|
||||||
|
* @returns Escaped string
|
||||||
|
*/
|
||||||
|
export const regexEscape = (text: string) =>
|
||||||
|
text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&")
|
||||||
|
|
||||||
|
export type RegexMatch = {
|
||||||
|
matchString: string
|
||||||
|
startIndex: number
|
||||||
|
endIndex: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all the regex match ranges for a given input
|
||||||
|
* @param regex The Regular Expression to find from
|
||||||
|
* @param input The input string to get match ranges from
|
||||||
|
* @returns An array of `RegexMatch` objects giving info about the matches
|
||||||
|
*/
|
||||||
|
export const regexFindAllMatches = (regex: RegExp) => (input: string) => {
|
||||||
|
const matches: RegexMatch[] = []
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-cond-assign, prettier/prettier
|
||||||
|
for (let match; match = regex.exec(input); match !== null)
|
||||||
|
matches.push({
|
||||||
|
matchString: match[0],
|
||||||
|
startIndex: match.index,
|
||||||
|
endIndex: match.index + match[0].length - 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
return matches
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user