Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"textarea-caret": "^3.1.0",
"vue": "^3.5.13",
"vue-echarts": "^8.0.1",
"vue-json-pretty": "^2.6.0",
"vue-router": "^4.6.4"
},
"devDependencies": {
Expand Down
10 changes: 6 additions & 4 deletions src/components/EmailSignalCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { api } from '@/lib/api'
import ActionBadge from '@/components/ActionBadge.vue'
import CopyMenuItem from '@/components/CopyMenuItem.vue'
import OverflowMenu from '@/components/ui/OverflowMenu.vue'
import JsonView from '@/components/ui/JsonView.vue'

const props = withDefaults(defineProps<{ signal: Signal; defaultExpanded?: boolean }>(), { defaultExpanded: true })
const emit = defineEmits<{ reply: []; reprocessed: [] }>()
Expand Down Expand Up @@ -126,7 +127,8 @@ const subjectLine = computed(() => {
return props.signal.data.subject
})

const signalObjectJson = ref('')
const signalObjectData = ref<Signal | null>(null)
const signalObjectJson = computed(() => (signalObjectData.value ? JSON.stringify(signalObjectData.value, null, 2) : ''))
const showSignalObjectModal = ref(false)
const showOriginalModal = ref(false)
const showMatchedRulesModal = ref(false)
Expand All @@ -148,14 +150,14 @@ function viewSignalObject() {
showSignalObjectModal.value = true
signalObjectError.value = null

if (signalObjectJson.value) return
if (signalObjectData.value) return

const threadId = props.signal.threadId ?? (props.signal.status === 'block_hidden' || props.signal.status === 'block_reject' ? 'BLOCKED' : 'QUARANTINED')
signalObjectLoading.value = true
void api.getSignal(accountStore.accountId, threadId, props.signal.signalId).then((result) => {
signalObjectLoading.value = false
if (result.isOk()) {
signalObjectJson.value = JSON.stringify(result.value, null, 2)
signalObjectData.value = result.value
} else {
signalObjectError.value = result.error.message
}
Expand Down Expand Up @@ -535,7 +537,7 @@ const iframeStyle = {
<div v-else-if="signalObjectError" class="p-4">
<span class="text-sm text-ctp-red">{{ signalObjectError }}</span>
</div>
<pre v-else class="overflow-auto rounded-lg bg-ctp-base p-3 font-mono text-xs text-ctp-text break-all whitespace-pre-wrap">{{ signalObjectJson }}</pre>
<JsonView v-else :data="signalObjectData" />
</div>
</div>
</Teleport>
Expand Down
105 changes: 105 additions & 0 deletions src/components/ui/JsonView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<script setup lang="ts">
import { h, reactive } from 'vue'
import VueJsonPretty from 'vue-json-pretty'
import type { NodeDataType } from 'vue-json-pretty/types/components/TreeNode'
import 'vue-json-pretty/lib/styles.css'

const props = withDefaults(
defineProps<{
data: unknown
/** Attribute names whose value starts collapsed (objects/arrays collapse; long strings truncate). */
collapsedKeys?: string[]
/** String values longer than this collapse behind a toggle when their key is in collapsedKeys. */
collapseStringsOver?: number
}>(),
{ collapsedKeys: () => ['body'], collapseStringsOver: 80 },
)

const expandedPaths = reactive(new Set<string>())

function isCollapsedKey(key: string | number | undefined): boolean {
return typeof key === 'string' && props.collapsedKeys.includes(key)
}

function pathCollapsible(node: NodeDataType): boolean {
return isCollapsedKey(node.key)
}

function renderNodeValue(opt: { node: NodeDataType; defaultValue: unknown }): unknown {
const { node, defaultValue } = opt
if (
node.type !== 'content' ||
typeof node.content !== 'string' ||
node.content.length <= props.collapseStringsOver ||
!isCollapsedKey(node.key)
) {
return defaultValue
}

if (expandedPaths.has(node.path)) {
return defaultValue
}

return h(
'span',
{
class: 'json-view-truncated',
onClick: (e: MouseEvent) => {
e.stopPropagation()
expandedPaths.add(node.path)
},
},
[`"${node.content.slice(0, props.collapseStringsOver)}…" `, h('em', `(${node.content.length} chars — click to expand)`)],
)
}
</script>

<template>
<div class="json-view rounded-lg border border-ctp-surface1 bg-ctp-mantle p-4 text-xs">
<VueJsonPretty
:data="data as never"
theme="dark"
:show-icon="true"
:show-length="true"
:path-collapsible="pathCollapsible"
:render-node-value="renderNodeValue"
/>
</div>
</template>

<style scoped>
.json-view :deep(.vjs-tree) {
font-family: inherit;
font-size: 0.75rem;
color: var(--ctp-text);
background: transparent;
}
.json-view :deep(.vjs-tree-node:hover),
.json-view :deep(.vjs-tree-node.is-highlight) {
background-color: var(--ctp-surface0);
}
.json-view :deep(.vjs-key) {
color: var(--ctp-subtext1);
}
.json-view :deep(.vjs-value-string) {
color: var(--ctp-green);
}
.json-view :deep(.vjs-value-number),
.json-view :deep(.vjs-value-boolean) {
color: var(--ctp-blue);
}
.json-view :deep(.vjs-value-null),
.json-view :deep(.vjs-value-undefined) {
color: var(--ctp-mauve);
}
.json-view :deep(.vjs-comment) {
color: var(--ctp-overlay0);
}
.json-view :deep(.json-view-truncated) {
cursor: pointer;
color: var(--ctp-peach);
}
.json-view :deep(.json-view-truncated:hover) {
text-decoration: underline;
}
</style>
5 changes: 3 additions & 2 deletions src/views/AdminView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useAccountStore } from '@/stores/account'
import { useLogStore } from '@/stores/logs'
import { getUndoExpiresAt } from '@/composables/usePendingSend'
import AsyncButton from '@/components/ui/AsyncButton.vue'
import JsonView from '@/components/ui/JsonView.vue'
import BuildInfo from '@/components/BuildInfo.vue'
import type { Thread, Signal, HealthCheckValidation, HealthCheckStatus } from '@/types/server'

Expand Down Expand Up @@ -319,7 +320,7 @@ async function lookup() {
<!-- Thread JSON -->
<section v-if="displayedThread">
<h3 class="mb-2 text-sm font-medium text-ctp-subtext1">Thread</h3>
<pre class="overflow-x-auto rounded-lg border border-ctp-surface1 bg-ctp-mantle p-4 text-xs text-ctp-text">{{ JSON.stringify(displayedThread, null, 2) }}</pre>
<JsonView :data="displayedThread" />
</section>
<section v-else-if="displayedSignal && !displayedSignal.threadId">
<h3 class="mb-2 text-sm font-medium text-ctp-subtext1">Thread</h3>
Expand Down Expand Up @@ -364,7 +365,7 @@ async function lookup() {
</AsyncButton>
</div>

<pre class="overflow-x-auto rounded-lg border border-ctp-surface1 bg-ctp-mantle p-4 text-xs text-ctp-text">{{ JSON.stringify(displayedSignal, null, 2) }}</pre>
<JsonView :data="displayedSignal" />
</section>

<!-- Raw email -->
Expand Down
Loading