feat(common): add copy functionality to console output entries (#5743)
Co-authored-by: James George <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
parent
84c3e8642c
commit
6c2128fbed
2 changed files with 100 additions and 11 deletions
|
|
@ -1,35 +1,47 @@
|
|||
<template>
|
||||
<div
|
||||
class="flex items-start px-4 py-2 text-tiny text-secondaryDark rounded-md"
|
||||
:class="color"
|
||||
>
|
||||
<div class="console-entry-wrapper group" :class="[color, hoverClass]">
|
||||
<component :is="icon" class="mr-2 shrink-0 svg-icons" />
|
||||
|
||||
<div class="flex flex-col space-y-2 overflow-x-auto text-xs flex-1">
|
||||
<div class="flex flex-col space-y-2 text-xs flex-1 min-w-0">
|
||||
<div class="text-secondary">{{ formattedTimestamp }}</div>
|
||||
|
||||
<div class="flex flex-col space-y-1">
|
||||
<ConsoleValue
|
||||
v-for="(arg, idx) in entry.args"
|
||||
v-for="(arg, idx) in sanitizedArgs"
|
||||
:key="idx"
|
||||
:value="arg"
|
||||
class="overflow-auto"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.copy')"
|
||||
:aria-label="t('action.copy')"
|
||||
class="ml-2 shrink-0 opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity p-1 hover:bg-dividerLight rounded"
|
||||
@click="copyEntry"
|
||||
>
|
||||
<component :is="copyIcon" class="w-4 h-4 svg-icons" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue"
|
||||
import { refAutoReset } from "@vueuse/core"
|
||||
|
||||
import IconAlertCircle from "~icons/lucide/alert-circle"
|
||||
import IconAlertTriangle from "~icons/lucide/alert-triangle"
|
||||
import IconBug from "~icons/lucide/bug"
|
||||
import IconInfo from "~icons/lucide/info"
|
||||
import IconTerminal from "~icons/lucide/terminal"
|
||||
import IconCopy from "~icons/lucide/copy"
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
|
||||
import { ConsoleEntry, ConsoleLogLevel } from "./Panel.vue"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { copyToClipboard } from "@helpers/utils/clipboard"
|
||||
import { useToast } from "@composables/toast"
|
||||
|
||||
type LogLevelsWithBgColor = "info" | "warn" | "error"
|
||||
|
||||
|
|
@ -37,6 +49,10 @@ const props = defineProps<{
|
|||
entry: ConsoleEntry
|
||||
}>()
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
const copyIcon = refAutoReset(IconCopy, 1000)
|
||||
|
||||
const bgColors: Pick<Record<ConsoleLogLevel, string>, LogLevelsWithBgColor> = {
|
||||
info: "bg-bannerInfo",
|
||||
warn: "bg-bannerWarning",
|
||||
|
|
@ -54,8 +70,55 @@ const icons: Record<ConsoleLogLevel, unknown> = {
|
|||
const color = computed(() => bgColors[props.entry.type as LogLevelsWithBgColor])
|
||||
const icon = computed(() => icons[props.entry.type])
|
||||
|
||||
// Only apply hover background for entries without semantic backgrounds (log, debug)
|
||||
const hoverClass = computed(() =>
|
||||
props.entry.type in bgColors ? "" : "hover:bg-primaryLight"
|
||||
)
|
||||
|
||||
const formattedTimestamp = computed(() => {
|
||||
const dateEntry = new Date(props.entry.timestamp)
|
||||
return dateEntry.toLocaleTimeString()
|
||||
})
|
||||
|
||||
const sanitizeArg = (arg: unknown): unknown =>
|
||||
typeof arg === "bigint" ? `${arg}n` : arg
|
||||
|
||||
const sanitizedArgs = computed(() => props.entry.args.map(sanitizeArg))
|
||||
|
||||
// Serialize value for clipboard copy
|
||||
const serializeConsoleValue = (value: unknown): string => {
|
||||
if (typeof value === "string") return value
|
||||
|
||||
if (typeof value === "number" || typeof value === "boolean")
|
||||
return String(value)
|
||||
|
||||
if (value === null) return "null"
|
||||
|
||||
if (value === undefined) return "undefined"
|
||||
|
||||
try {
|
||||
const serialized = JSON.stringify(
|
||||
value,
|
||||
(_key, val) => (typeof val === "bigint" ? `${val}n` : val),
|
||||
2
|
||||
)
|
||||
return serialized !== undefined ? serialized : "[Unserializable]"
|
||||
} catch {
|
||||
return "[Unserializable]"
|
||||
}
|
||||
}
|
||||
|
||||
const copyEntry = () => {
|
||||
const content = sanitizedArgs.value.map(serializeConsoleValue).join(" ")
|
||||
|
||||
copyToClipboard(content)
|
||||
copyIcon.value = IconCheck
|
||||
toast.success(t("state.copied_to_clipboard"))
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.console-entry-wrapper {
|
||||
@apply flex items-start px-4 py-2 text-tiny text-secondaryDark rounded-md transition-colors;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="whitespace-pre-wrap font-mono text-[12px]">
|
||||
<div class="whitespace-pre-wrap font-mono text-[12px] select-text">
|
||||
<VueJsonPretty
|
||||
v-if="isObjectOrArray"
|
||||
:data="parsedValue"
|
||||
|
|
@ -7,16 +7,16 @@
|
|||
:show-line="false"
|
||||
:show-line-numbers="true"
|
||||
:deep="2"
|
||||
class="p-4 bg-primary text-secondaryDark border !border-dividerLight !text-[12px] rounded !font-mono"
|
||||
class="p-4 bg-primary text-secondaryDark border !border-dividerLight !text-[12px] rounded !font-mono select-text"
|
||||
/>
|
||||
|
||||
<pre
|
||||
v-else-if="parsedJSON"
|
||||
class="overflow-auto max-h-96 p-4 bg-primary text-secondaryDark border !border-dividerLight rounded"
|
||||
class="overflow-auto max-h-96 p-4 bg-primary text-secondaryDark border !border-dividerLight rounded select-text"
|
||||
>{{ formattedJSONString }}
|
||||
</pre>
|
||||
|
||||
<pre v-else class="truncate"
|
||||
<pre v-else class="truncate select-text"
|
||||
>{{ formattedPrimitive }}
|
||||
</pre>
|
||||
</div>
|
||||
|
|
@ -112,3 +112,29 @@ const treeViewTheme = computed(() => (isDarkTheme.value ? "dark" : "light"))
|
|||
color: var(--secondary-dark-color) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
/* Force scrollbars to always be visible (override macOS auto-hide) */
|
||||
pre {
|
||||
scrollbar-width: thin; /* Firefox */
|
||||
scrollbar-color: var(--divider-color) transparent; /* Firefox: thumb track */
|
||||
}
|
||||
|
||||
pre::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
pre::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
pre::-webkit-scrollbar-thumb {
|
||||
background: var(--divider-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
pre::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--secondary-light-color);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in a new issue