mirror of
https://github.com/OpenBankProject/API-Explorer-II.git
synced 2026-02-06 10:47:04 +00:00
Merge pull request #148 from simonredfern/develop
Json schema resource docs
This commit is contained in:
commit
171aabaecb
3
.gitignore
vendored
3
.gitignore
vendored
@ -62,3 +62,6 @@ test-results/
|
||||
playwright-report/
|
||||
playwright-coverage/
|
||||
shared-constants.js
|
||||
|
||||
# Documentation
|
||||
untracked_docs/
|
||||
|
||||
2
components.d.ts
vendored
2
components.d.ts
vendored
@ -44,7 +44,9 @@ declare module 'vue' {
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
GlossarySearchNav: typeof import('./src/components/GlossarySearchNav.vue')['default']
|
||||
HeaderNav: typeof import('./src/components/HeaderNav.vue')['default']
|
||||
JsonSchemaViewer: typeof import('./src/components/JsonSchemaViewer.vue')['default']
|
||||
Menu: typeof import('./src/components/Menu.vue')['default']
|
||||
MessageDocsJsonSchemaSearchNav: typeof import('./src/components/MessageDocsJsonSchemaSearchNav.vue')['default']
|
||||
MessageDocsSearchNav: typeof import('./src/components/MessageDocsSearchNav.vue')['default']
|
||||
Preview: typeof import('./src/components/Preview.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
|
||||
@ -36,7 +36,7 @@ import {
|
||||
HEADER_LINKS_HOVER_COLOR as headerLinksHoverColorSetting,
|
||||
HEADER_LINKS_BACKGROUND_COLOR as headerLinksBackgroundColorSetting
|
||||
} from '../obp/style-setting'
|
||||
import { obpApiActiveVersionsKey, obpGroupedMessageDocsKey, obpMyCollectionsEndpointKey } from '@/obp/keys'
|
||||
import { obpApiActiveVersionsKey, obpGroupedMessageDocsKey, obpGroupedMessageDocsJsonSchemaKey, obpMyCollectionsEndpointKey } from '@/obp/keys'
|
||||
import SvelteDropdown from './SvelteDropdown.vue'
|
||||
|
||||
const route = useRoute()
|
||||
@ -51,6 +51,14 @@ const loginUsername = ref('')
|
||||
const logoffurl = ref('')
|
||||
const obpApiVersions = ref(inject(obpApiActiveVersionsKey) || [])
|
||||
const obpMessageDocs = ref(Object.keys(inject(obpGroupedMessageDocsKey) || {}))
|
||||
const obpMessageDocsJsonSchema = ref(Object.keys(inject(obpGroupedMessageDocsJsonSchemaKey) || {}))
|
||||
|
||||
// Combine message docs with JSON Schema items (with "J Schema" postfix)
|
||||
const combinedMessageDocs = computed(() => {
|
||||
const regularDocs = obpMessageDocs.value || []
|
||||
const jsonSchemaDocs = (obpMessageDocsJsonSchema.value || []).map(connector => `${connector} J Schema`)
|
||||
return [...regularDocs, ...jsonSchemaDocs]
|
||||
})
|
||||
|
||||
// Debug menu items
|
||||
const debugMenuItems = ref(['/debug/providers-status', '/debug/oidc'])
|
||||
@ -189,8 +197,8 @@ const setActive = (target: HTMLElement | null) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleMore = (command: string) => {
|
||||
console.log('handleMore called with command:', command)
|
||||
const handleMore = (command: string, source?: string) => {
|
||||
console.log('handleMore called with command:', command, 'source:', source)
|
||||
|
||||
// Ignore divider
|
||||
if (command === '---') {
|
||||
@ -201,11 +209,22 @@ const handleMore = (command: string) => {
|
||||
if (element !== null) {
|
||||
element.textContent = command;
|
||||
}
|
||||
if (command === '/message-docs') {
|
||||
|
||||
// Check if command ends with " J Schema" - if so, it's a JSON Schema message doc
|
||||
if (command.endsWith(' J Schema')) {
|
||||
const connector = command.replace(' J Schema', '')
|
||||
console.log('Navigating to message docs JSON schema:', connector)
|
||||
router.push({ name: 'message-docs-json-schema', params: { id: connector } })
|
||||
} else if (command === '/message-docs') {
|
||||
// Navigate to message docs list
|
||||
console.log('Navigating to message docs list')
|
||||
router.push({ name: 'message-docs-list' })
|
||||
} else if (command === '/message-docs-json-schema') {
|
||||
// Navigate to message docs JSON schema list
|
||||
console.log('Navigating to message docs JSON schema list')
|
||||
router.push({ name: 'message-docs-json-schema-list' })
|
||||
} else if (command.includes('_')) {
|
||||
// Regular message docs (connector names contain underscores)
|
||||
console.log('Navigating to message docs:', command)
|
||||
router.push({ name: 'message-docs', params: { id: command } })
|
||||
} else if (command.startsWith('/debug/')) {
|
||||
@ -292,7 +311,7 @@ const getCurrentPath = () => {
|
||||
class="menu-right"
|
||||
id="header-nav-message-docs"
|
||||
label="Message Docs"
|
||||
:items="obpMessageDocs"
|
||||
:items="combinedMessageDocs"
|
||||
:hover-color="headerLinksHoverColor"
|
||||
:background-color="headerLinksBackgroundColor"
|
||||
@select="handleMore"
|
||||
|
||||
328
src/components/JsonSchemaViewer.vue
Normal file
328
src/components/JsonSchemaViewer.vue
Normal file
@ -0,0 +1,328 @@
|
||||
<!--
|
||||
- Open Bank Project - API Explorer II
|
||||
- Copyright (C) 2023-2024, TESOBE GmbH
|
||||
-
|
||||
- This program is free software: you can redistribute it and/or modify
|
||||
- it under the terms of the GNU Affero General Public License as published by
|
||||
- the Free Software Foundation, either version 3 of the License, or
|
||||
- (at your option) any later version.
|
||||
-
|
||||
- This program is distributed in the hope that it will be useful,
|
||||
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
- GNU Affero General Public License for more details.
|
||||
-
|
||||
- You should have received a copy of the GNU Affero General Public License
|
||||
- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-
|
||||
- Email: contact@tesobe.com
|
||||
- TESOBE GmbH
|
||||
- Osloerstrasse 16/17
|
||||
- Berlin 13359, Germany
|
||||
-
|
||||
- This product includes software developed at
|
||||
- TESOBE (http://www.tesobe.com/)
|
||||
-
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { DocumentCopy, Check } from '@element-plus/icons-vue'
|
||||
|
||||
interface Props {
|
||||
schema: any
|
||||
copyable?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
copyable: false
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
refClick: [refId: string]
|
||||
}>()
|
||||
|
||||
const copied = ref(false)
|
||||
|
||||
const copyToClipboard = async () => {
|
||||
try {
|
||||
const text = JSON.stringify(props.schema, null, 2)
|
||||
await navigator.clipboard.writeText(text)
|
||||
copied.value = true
|
||||
setTimeout(() => {
|
||||
copied.value = false
|
||||
}, 2000)
|
||||
} catch (err) {
|
||||
console.error('Failed to copy: ', err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefClick = (event: Event, refId: string) => {
|
||||
event.preventDefault()
|
||||
emit('refClick', refId)
|
||||
}
|
||||
|
||||
// Function to render JSON with clickable $ref links
|
||||
const renderJsonWithRefs = (obj: any, indent: number = 0): any[] => {
|
||||
const result: any[] = []
|
||||
const indentStr = ' '.repeat(indent)
|
||||
|
||||
if (obj === null || obj === undefined) {
|
||||
result.push({ type: 'value', text: String(obj) })
|
||||
return result
|
||||
}
|
||||
|
||||
if (typeof obj !== 'object') {
|
||||
const valueClass = typeof obj === 'string' ? 'json-string' :
|
||||
typeof obj === 'number' ? 'json-number' :
|
||||
typeof obj === 'boolean' ? 'json-boolean' : 'json-value'
|
||||
const displayValue = typeof obj === 'string' ? `"${obj}"` : String(obj)
|
||||
result.push({ type: 'value', text: displayValue, class: valueClass })
|
||||
return result
|
||||
}
|
||||
|
||||
const isArray = Array.isArray(obj)
|
||||
const openBracket = isArray ? '[' : '{'
|
||||
const closeBracket = isArray ? ']' : '}'
|
||||
|
||||
result.push({ type: 'bracket', text: openBracket })
|
||||
|
||||
const entries = isArray ? obj.map((val, idx) => [idx, val]) : Object.entries(obj)
|
||||
const totalEntries = entries.length
|
||||
|
||||
entries.forEach(([key, value], index) => {
|
||||
const isLast = index === totalEntries - 1
|
||||
const newIndent = indent + 1
|
||||
const newIndentStr = ' '.repeat(newIndent)
|
||||
|
||||
result.push({ type: 'newline', text: '\n' })
|
||||
result.push({ type: 'indent', text: newIndentStr })
|
||||
|
||||
// Add key for objects
|
||||
if (!isArray) {
|
||||
result.push({ type: 'key', text: `"${key}"`, class: 'json-key' })
|
||||
result.push({ type: 'separator', text: ': ' })
|
||||
}
|
||||
|
||||
// Check if this is a $ref
|
||||
if (key === '$ref' && typeof value === 'string') {
|
||||
// Extract definition name from $ref
|
||||
const refMatch = value.match(/#\/definitions\/(.+)$/)
|
||||
if (refMatch) {
|
||||
const defName = refMatch[1]
|
||||
result.push({ type: 'ref', text: `"${value}"`, href: `#def-${defName}`, defName })
|
||||
} else {
|
||||
result.push({ type: 'value', text: `"${value}"`, class: 'json-string' })
|
||||
}
|
||||
} else if (value && typeof value === 'object') {
|
||||
// Recursively render nested objects/arrays
|
||||
const nested = renderJsonWithRefs(value, newIndent)
|
||||
result.push(...nested)
|
||||
} else {
|
||||
// Render primitive values
|
||||
const valueClass = typeof value === 'string' ? 'json-string' :
|
||||
typeof value === 'number' ? 'json-number' :
|
||||
typeof value === 'boolean' ? 'json-boolean' :
|
||||
value === null ? 'json-null' : 'json-value'
|
||||
const displayValue = typeof value === 'string' ? `"${value}"` : String(value)
|
||||
result.push({ type: 'value', text: displayValue, class: valueClass })
|
||||
}
|
||||
|
||||
// Add comma if not last
|
||||
if (!isLast) {
|
||||
result.push({ type: 'comma', text: ',' })
|
||||
}
|
||||
})
|
||||
|
||||
if (totalEntries > 0) {
|
||||
result.push({ type: 'newline', text: '\n' })
|
||||
result.push({ type: 'indent', text: indentStr })
|
||||
}
|
||||
result.push({ type: 'bracket', text: closeBracket })
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const jsonElements = computed(() => {
|
||||
return renderJsonWithRefs(props.schema)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="json-schema-viewer">
|
||||
<div v-if="copyable" class="schema-header">
|
||||
<button
|
||||
@click="copyToClipboard"
|
||||
class="copy-button"
|
||||
:class="{ 'copied': copied }"
|
||||
>
|
||||
<span v-if="!copied">
|
||||
<el-icon :size="10">
|
||||
<DocumentCopy />
|
||||
</el-icon>
|
||||
Copy
|
||||
</span>
|
||||
<span v-else>
|
||||
<el-icon :size="10">
|
||||
<Check />
|
||||
</el-icon>
|
||||
Copied!
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="schema-container">
|
||||
<pre class="schema-pre"><code class="schema-code"><template v-for="(element, index) in jsonElements" :key="index"><span
|
||||
v-if="element.type === 'ref'"
|
||||
class="json-ref"
|
||||
:title="`Jump to definition: ${element.defName}`"
|
||||
><a :href="element.href" class="ref-link" @click="handleRefClick($event, element.href)">{{ element.text }}</a></span><span
|
||||
v-else-if="element.type === 'key' || element.type === 'value'"
|
||||
:class="element.class"
|
||||
>{{ element.text }}</span><span v-else>{{ element.text }}</span></template></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.json-schema-viewer {
|
||||
margin: 1rem 0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #1e1e1e;
|
||||
border: 1px solid #333;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.schema-header {
|
||||
background: #2d2d2d;
|
||||
padding: 0.5rem 1rem;
|
||||
border-bottom: 1px solid #333;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
background: #444;
|
||||
border: 1px solid #666;
|
||||
color: #ddd;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.copy-button:hover {
|
||||
background: #555;
|
||||
border-color: #777;
|
||||
}
|
||||
|
||||
.copy-button.copied {
|
||||
background: #4caf50;
|
||||
border-color: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.schema-container {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.schema-pre {
|
||||
margin: 0;
|
||||
padding: 1.5rem;
|
||||
background: #1e1e1e;
|
||||
color: #ddd;
|
||||
font-family: 'Fira Code', 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.schema-code {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
/* JSON Syntax Highlighting */
|
||||
.json-key {
|
||||
color: #e06c75;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.json-string {
|
||||
color: #98c379;
|
||||
}
|
||||
|
||||
.json-number {
|
||||
color: #d19a66;
|
||||
}
|
||||
|
||||
.json-boolean {
|
||||
color: #56b6c2;
|
||||
}
|
||||
|
||||
.json-null {
|
||||
color: #c678dd;
|
||||
}
|
||||
|
||||
.json-ref {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ref-link {
|
||||
color: #61afef;
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dotted;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.ref-link:hover {
|
||||
color: #84c5ff;
|
||||
text-decoration-style: solid;
|
||||
background-color: rgba(97, 175, 239, 0.1);
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
.schema-container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.schema-container::-webkit-scrollbar-track {
|
||||
background: #2d2d2d;
|
||||
}
|
||||
|
||||
.schema-container::-webkit-scrollbar-thumb {
|
||||
background: #555;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.schema-container::-webkit-scrollbar-thumb:hover {
|
||||
background: #777;
|
||||
}
|
||||
|
||||
.schema-pre::-webkit-scrollbar {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.schema-pre::-webkit-scrollbar-track {
|
||||
background: #2d2d2d;
|
||||
}
|
||||
|
||||
.schema-pre::-webkit-scrollbar-thumb {
|
||||
background: #555;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.schema-pre::-webkit-scrollbar-thumb:hover {
|
||||
background: #777;
|
||||
}
|
||||
</style>
|
||||
183
src/components/MessageDocsJsonSchemaSearchNav.vue
Normal file
183
src/components/MessageDocsJsonSchemaSearchNav.vue
Normal file
@ -0,0 +1,183 @@
|
||||
<!--
|
||||
- Open Bank Project - API Explorer II
|
||||
- Copyright (C) 2023-2024, TESOBE GmbH
|
||||
-
|
||||
- This program is free software: you can redistribute it and/or modify
|
||||
- it under the terms of the GNU Affero General Public License as published by
|
||||
- the Free Software Foundation, either version 3 of the License, or
|
||||
- (at your option) any later version.
|
||||
-
|
||||
- This program is distributed in the hope that it will be useful,
|
||||
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
- GNU Affero General Public License for more details.
|
||||
-
|
||||
- You should have received a copy of the GNU Affero General Public License
|
||||
- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-
|
||||
- Email: contact@tesobe.com
|
||||
- TESOBE GmbH
|
||||
- Osloerstrasse 16/17
|
||||
- Berlin 13359, Germany
|
||||
-
|
||||
- This product includes software developed at
|
||||
- TESOBE (http://www.tesobe.com/)
|
||||
-
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, onBeforeMount, inject, watch } from 'vue'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { SEARCH_LINKS_COLOR as searchLinksColorSetting } from '../obp/style-setting'
|
||||
import { connectors } from '../obp/message-docs'
|
||||
import { obpGroupedMessageDocsJsonSchemaKey } from '@/obp/keys'
|
||||
|
||||
let connector = connectors[0]
|
||||
const route = useRoute()
|
||||
const groupedMessageDocsJsonSchema = ref(inject(obpGroupedMessageDocsJsonSchemaKey) || {})
|
||||
const docs = ref({})
|
||||
const groups = ref({})
|
||||
const sortedKeys = ref([])
|
||||
const activeKeys = ref([])
|
||||
const messageDocKeys = ref([])
|
||||
const searchLinksColor = ref(searchLinksColorSetting)
|
||||
const form = reactive({
|
||||
search: ''
|
||||
})
|
||||
|
||||
onBeforeMount(() => {
|
||||
setDocs()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => route.params.id,
|
||||
async (id) => {
|
||||
setDocs()
|
||||
}
|
||||
)
|
||||
|
||||
const isKeyFound = (keys, item) => keys.every((k) => item.toLowerCase().includes(k))
|
||||
|
||||
const filterKeys = (keys, key) => {
|
||||
const splitKey = key.split(' ').map((k) => k.toLowerCase())
|
||||
return keys.filter((title) => {
|
||||
const isGroupFound = isKeyFound(splitKey, title)
|
||||
const items = docs.value[title].filter((item) => isGroupFound || isKeyFound(splitKey, item))
|
||||
groups.value[title] = items
|
||||
return isGroupFound || items.length > 0
|
||||
})
|
||||
}
|
||||
|
||||
const searchEvent = (value) => {
|
||||
if (value) {
|
||||
messageDocKeys.value = filterKeys(activeKeys.value, value)
|
||||
} else {
|
||||
groups.value = JSON.parse(JSON.stringify(docs.value))
|
||||
messageDocKeys.value = Object.keys(groups.value)
|
||||
}
|
||||
}
|
||||
|
||||
const setDocs = () => {
|
||||
const paramConnector = route.params.id
|
||||
if (connectors.includes(paramConnector)) {
|
||||
connector = paramConnector
|
||||
}
|
||||
const messageDocsJsonSchemaData = groupedMessageDocsJsonSchema.value[connector]
|
||||
const messageDocsJsonSchema = messageDocsJsonSchemaData?.grouped || messageDocsJsonSchemaData || {}
|
||||
|
||||
docs.value = Object.keys(messageDocsJsonSchema).reduce((doc, key) => {
|
||||
doc[key] = messageDocsJsonSchema[key].map((group) => group.method_name)
|
||||
return doc
|
||||
}, {})
|
||||
groups.value = JSON.parse(JSON.stringify(docs.value))
|
||||
messageDocKeys.value = Object.keys(groups.value)
|
||||
activeKeys.value = Object.keys(groups.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-input
|
||||
v-model="form.search"
|
||||
placeholder="Search"
|
||||
:prefix-icon="Search"
|
||||
@input="searchEvent"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-collapse v-model="activeKeys">
|
||||
<el-collapse-item v-for="key in messageDocKeys" :title="key" :key="key" :name="key">
|
||||
<div class="el-tabs--right">
|
||||
<div v-for="(value, key) of groups[key]" :key="value" class="message-docs-router-tab">
|
||||
<a class="message-docs-router-link" :id="`${value}-quick-nav`" v-bind:href="`#${value}`">
|
||||
{{ value }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.api-router-link {
|
||||
width: 100%;
|
||||
margin-left: 15px;
|
||||
font-family: 'Roboto';
|
||||
text-decoration: none;
|
||||
color: #39455f;
|
||||
display: inline-block;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.api-router-tab {
|
||||
border-left: 2px solid var(--el-menu-border-color);
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.api-router-tab:hover,
|
||||
.active-api-router-tab {
|
||||
border-left: 2px solid v-bind(searchLinksColor);
|
||||
}
|
||||
|
||||
.api-router-tab:hover .api-router-link,
|
||||
.active-api-router-link {
|
||||
color: v-bind(searchLinksColor);
|
||||
}
|
||||
|
||||
.message-docs-router-link {
|
||||
margin-left: 15px;
|
||||
font-size: 13px;
|
||||
font-family: 'Roboto';
|
||||
text-decoration: none;
|
||||
color: #39455f;
|
||||
display: inline-block;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.message-docs-router-tab {
|
||||
border-left: 2px solid var(--el-menu-border-color);
|
||||
line-height: 30px;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.message-docs-router-tab:hover,
|
||||
.active-message-docs-router-tab {
|
||||
border-left: 2px solid v-bind(searchLinksColor);
|
||||
}
|
||||
|
||||
.message-docs-router-tab:hover .message-docs-router-link {
|
||||
color: v-bind(searchLinksColor);
|
||||
}
|
||||
</style>
|
||||
28
src/main.ts
28
src/main.ts
@ -38,7 +38,12 @@ import { createI18n } from 'vue-i18n'
|
||||
import { languages, defaultLocale } from './language'
|
||||
|
||||
import { cache as cacheResourceDocs, cacheDoc as cacheResourceDocsDoc } from './obp/resource-docs'
|
||||
import { cache as cacheMessageDocs, cacheDoc as cacheMessageDocsDoc } from './obp/message-docs'
|
||||
import {
|
||||
cache as cacheMessageDocs,
|
||||
cacheDoc as cacheMessageDocsDoc,
|
||||
cacheJsonSchema as cacheMessageDocsJsonSchema,
|
||||
cacheDocJsonSchema as cacheMessageDocsJsonSchemaDoc
|
||||
} from './obp/message-docs'
|
||||
import { OBP_API_VERSION, getMyAPICollections, getMyAPICollectionsEndpoint } from './obp'
|
||||
import { getOBPGlossary } from './obp/glossary'
|
||||
|
||||
@ -47,16 +52,18 @@ import './assets/main.css'
|
||||
import '@fontsource/roboto/300.css'
|
||||
import '@fontsource/roboto/400.css'
|
||||
import '@fontsource/roboto/700.css'
|
||||
|
||||
import { getCacheStorageInfo } from './obp/common-functions'
|
||||
import {
|
||||
obpApiActiveVersionsKey,
|
||||
obpApiHostKey,
|
||||
obpGlossaryKey,
|
||||
obpGroupedMessageDocsKey,
|
||||
obpGroupedMessageDocsJsonSchemaKey,
|
||||
obpGroupedResourceDocsKey,
|
||||
obpMyCollectionsEndpointKey,
|
||||
obpResourceDocsKey
|
||||
} from './obp/keys'
|
||||
import { getCacheStorageInfo } from './obp/common-functions'
|
||||
;(async () => {
|
||||
const app = createApp(App)
|
||||
const router = await appRouter()
|
||||
@ -272,6 +279,13 @@ async function setupData(app: App<Element>, worker: Worker) {
|
||||
const cacheStorageOfMessageDocs = await caches.open('obp-message-docs-cache') // Please note: The global 'caches' read-only property returns the 'CacheStorage' object associated with the current context.
|
||||
// 'match': Checks if a given Request is a key in any of the Cache objects that the CacheStorage object tracks, and returns a Promise that resolves to that match.
|
||||
const cachedResponseOfMessageDocs = await cacheStorageOfMessageDocs.match('/')
|
||||
// 'open': Returns a Promise that resolves to the Cache object matching the cacheName(obp-message-docs-json-schema-cache) (a new cache is created if it doesn't already exist.)
|
||||
const cacheStorageOfMessageDocsJsonSchema = await caches.open(
|
||||
'obp-message-docs-json-schema-cache'
|
||||
) // Please note: The global 'caches' read-only property returns the 'CacheStorage' object associated with the current context.
|
||||
// 'match': Checks if a given Request is a key in any of the Cache objects that the CacheStorage object tracks, and returns a Promise that resolves to that match.
|
||||
const cachedResponseOfMessageDocsJsonSchema =
|
||||
await cacheStorageOfMessageDocsJsonSchema.match('/')
|
||||
|
||||
// Listen to Web worker
|
||||
worker.onmessage = async (event) => {
|
||||
@ -286,6 +300,10 @@ async function setupData(app: App<Element>, worker: Worker) {
|
||||
await cacheMessageDocsDoc(cacheStorageOfMessageDocs)
|
||||
console.log('Message Docs cache was updated.')
|
||||
}
|
||||
if (event.data === 'update-message-docs-json-schema') {
|
||||
await cacheMessageDocsJsonSchemaDoc(cacheStorageOfMessageDocsJsonSchema)
|
||||
console.log('Message Docs JSON Schema cache was updated.')
|
||||
}
|
||||
}
|
||||
|
||||
const { resourceDocs, groupedDocs } = await cacheResourceDocs(
|
||||
@ -298,6 +316,11 @@ async function setupData(app: App<Element>, worker: Worker) {
|
||||
cachedResponseOfMessageDocs,
|
||||
worker
|
||||
)
|
||||
const messageDocsJsonSchema = await cacheMessageDocsJsonSchema(
|
||||
cacheStorageOfMessageDocsJsonSchema,
|
||||
cachedResponseOfMessageDocsJsonSchema,
|
||||
worker
|
||||
)
|
||||
|
||||
// Provide data to a component's descendants
|
||||
// App-level provides are available to all components rendered in the app
|
||||
@ -306,6 +329,7 @@ async function setupData(app: App<Element>, worker: Worker) {
|
||||
app.provide(obpApiActiveVersionsKey, Object.keys(resourceDocs).sort())
|
||||
app.provide(obpGroupedResourceDocsKey, groupedDocs)
|
||||
app.provide(obpGroupedMessageDocsKey, messageDocs)
|
||||
app.provide(obpGroupedMessageDocsJsonSchemaKey, messageDocsJsonSchema)
|
||||
app.provide(obpApiHostKey, import.meta.env.VITE_OBP_API_HOST)
|
||||
const glossary = await getOBPGlossary()
|
||||
app.provide(obpGlossaryKey, glossary)
|
||||
|
||||
@ -31,6 +31,9 @@ export const obpResourceDocsKey = Symbol('OBP-ResourceDocs') as InjectionKey<any
|
||||
export const obpApiActiveVersionsKey = Symbol('OBP-APIActiveVersions') as InjectionKey<any>
|
||||
export const obpGroupedResourceDocsKey = Symbol('OBP-GroupedResourceDocs') as InjectionKey<any>
|
||||
export const obpGroupedMessageDocsKey = Symbol('OBP-GroupedMessageDocs') as InjectionKey<any> // This cause an issue
|
||||
export const obpGroupedMessageDocsJsonSchemaKey = Symbol(
|
||||
'OBP-GroupedMessageDocsJsonSchema'
|
||||
) as InjectionKey<any>
|
||||
export const obpApiHostKey = Symbol('OBP-API-Host') as InjectionKey<any>
|
||||
export const obpGlossaryKey = Symbol('OBP-Glossary') as InjectionKey<any>
|
||||
export const obpMyCollectionsEndpointKey = Symbol('OBP-MyCollectionsEndpoint') as InjectionKey<any>
|
||||
export const obpMyCollectionsEndpointKey = Symbol('OBP-MyCollectionsEndpoint') as InjectionKey<any>
|
||||
|
||||
@ -43,7 +43,15 @@ export async function getOBPMessageDocs(item: string): Promise<any> {
|
||||
return await get(`obp/${OBP_API_VERSION}/message-docs/${item}`)
|
||||
}
|
||||
|
||||
export function getGroupedMessageDocs(docs: any): Promise<any> {
|
||||
// Get Message Docs JSON Schema
|
||||
export async function getOBPMessageDocsJsonSchema(item: string): Promise<any> {
|
||||
const logMessage = `Loading message docs JSON schema { connector: ${item} }`
|
||||
console.log(logMessage)
|
||||
updateLoadingInfoMessage(logMessage)
|
||||
return await get(`obp/v6.0.0/message-docs/${item}/json-schema`)
|
||||
}
|
||||
|
||||
export function getGroupedMessageDocs(docs: any): any {
|
||||
return docs.message_docs.reduce((values: any, doc: any) => {
|
||||
const tag = doc.adapter_implementation.group.replace('-', '').trim()
|
||||
;(values[tag] = values[tag] || []).push(doc)
|
||||
@ -51,6 +59,77 @@ export function getGroupedMessageDocs(docs: any): Promise<any> {
|
||||
}, {})
|
||||
}
|
||||
|
||||
export function getGroupedMessageDocsJsonSchema(docs: any): any {
|
||||
console.log('getGroupedMessageDocsJsonSchema - Raw docs:', docs)
|
||||
|
||||
// Access messages from the correct path: properties.messages.items
|
||||
const messages = docs.properties?.messages?.items
|
||||
const definitions = docs.definitions || {}
|
||||
|
||||
if (!messages || !Array.isArray(messages)) {
|
||||
console.log('No messages array found, falling back to definitions')
|
||||
// Fallback to old structure if messages array doesn't exist
|
||||
if (!definitions || typeof definitions !== 'object') {
|
||||
console.log('No definitions object found either')
|
||||
return { grouped: {}, definitions: {} }
|
||||
}
|
||||
|
||||
// Convert definitions object to array format and group by InBound/OutBound prefix
|
||||
const grouped: any = {}
|
||||
Object.keys(definitions).forEach((methodName: string) => {
|
||||
const schema = definitions[methodName]
|
||||
|
||||
// Determine category based on method name prefix
|
||||
let category = 'Uncategorized'
|
||||
if (methodName.startsWith('InBound')) {
|
||||
category = 'Inbound Methods'
|
||||
} else if (methodName.startsWith('OutBound')) {
|
||||
category = 'Outbound Methods'
|
||||
}
|
||||
|
||||
if (!grouped[category]) {
|
||||
grouped[category] = []
|
||||
}
|
||||
|
||||
grouped[category].push({
|
||||
method_name: methodName,
|
||||
category: category,
|
||||
outbound_schema: schema,
|
||||
inbound_schema: schema
|
||||
})
|
||||
})
|
||||
|
||||
console.log('Grouped definitions result:', grouped)
|
||||
return { grouped, definitions }
|
||||
}
|
||||
|
||||
// Group messages by adapter_implementation.group
|
||||
console.log('Processing messages array')
|
||||
const grouped: any = {}
|
||||
messages.forEach((message: any) => {
|
||||
const category =
|
||||
message.adapter_implementation?.group?.replace('-', '').trim() || 'Uncategorized'
|
||||
|
||||
if (!grouped[category]) {
|
||||
grouped[category] = []
|
||||
}
|
||||
|
||||
// Keep original schemas with $refs intact
|
||||
grouped[category].push({
|
||||
method_name: message.process,
|
||||
category: category,
|
||||
description: message.description,
|
||||
outbound_schema: message.outbound_schema,
|
||||
inbound_schema: message.inbound_schema,
|
||||
message_format: message.message_format
|
||||
})
|
||||
})
|
||||
|
||||
console.log('Grouped messages result:', grouped)
|
||||
console.log('Definitions:', definitions)
|
||||
return { grouped, definitions }
|
||||
}
|
||||
|
||||
export async function cacheDoc(cacheStorageOfMessageDocs: any): Promise<any> {
|
||||
const messageDocs = await connectors.reduce(async (agroup: any, connector: any) => {
|
||||
const logMessage = `Caching message docs { connector: ${connector} }`
|
||||
@ -71,11 +150,30 @@ async function getCacheDoc(cacheStorageOfMessageDocs: any): Promise<any> {
|
||||
return await cacheDoc(cacheStorageOfMessageDocs)
|
||||
}
|
||||
|
||||
export async function cache(
|
||||
cacheStorage: any,
|
||||
cachedResponse: any,
|
||||
worker: any
|
||||
): Promise<any> {
|
||||
export async function cacheDocJsonSchema(cacheStorageOfMessageDocsJsonSchema: any): Promise<any> {
|
||||
const messageDocsJsonSchema = await connectors.reduce(async (agroup: any, connector: any) => {
|
||||
const logMessage = `Caching message docs JSON schema { connector: ${connector} }`
|
||||
console.log(logMessage)
|
||||
updateLoadingInfoMessage(logMessage)
|
||||
const group = await agroup
|
||||
const docs = await getOBPMessageDocsJsonSchema(connector)
|
||||
if (!Object.keys(docs).includes('code')) {
|
||||
group[connector] = getGroupedMessageDocsJsonSchema(docs)
|
||||
}
|
||||
return group
|
||||
}, Promise.resolve({}))
|
||||
await cacheStorageOfMessageDocsJsonSchema.put(
|
||||
'/',
|
||||
new Response(JSON.stringify(messageDocsJsonSchema))
|
||||
)
|
||||
return messageDocsJsonSchema
|
||||
}
|
||||
|
||||
async function getCacheDocJsonSchema(cacheStorageOfMessageDocsJsonSchema: any): Promise<any> {
|
||||
return await cacheDocJsonSchema(cacheStorageOfMessageDocsJsonSchema)
|
||||
}
|
||||
|
||||
export async function cache(cacheStorage: any, cachedResponse: any, worker: any): Promise<any> {
|
||||
try {
|
||||
worker.postMessage('update-message-docs')
|
||||
return await cachedResponse.json()
|
||||
@ -87,3 +185,20 @@ export async function cache(
|
||||
return await getCacheDoc(cacheStorage)
|
||||
}
|
||||
}
|
||||
|
||||
export async function cacheJsonSchema(
|
||||
cacheStorage: any,
|
||||
cachedResponse: any,
|
||||
worker: any
|
||||
): Promise<any> {
|
||||
try {
|
||||
worker.postMessage('update-message-docs-json-schema')
|
||||
return await cachedResponse.json()
|
||||
} catch (error) {
|
||||
console.warn('No message docs JSON schema cache or malformed cache.')
|
||||
console.log('Caching message docs JSON schema...')
|
||||
const isServerActive = await isServerUp()
|
||||
if (!isServerActive) throw new Error('API Server is not responding.')
|
||||
return await getCacheDocJsonSchema(cacheStorage)
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,6 +30,8 @@ import GlossaryView from '../views/GlossaryView.vue'
|
||||
import HelpView from '../views/HelpView.vue'
|
||||
import MessageDocsView from '../views/MessageDocsView.vue'
|
||||
import MessageDocsListView from '../views/MessageDocsListView.vue'
|
||||
import MessageDocsJsonSchemaView from '../views/MessageDocsJsonSchemaView.vue'
|
||||
import MessageDocsJsonSchemaListView from '../views/MessageDocsJsonSchemaListView.vue'
|
||||
import BodyView from '../views/BodyView.vue'
|
||||
import Content from '../components/Content.vue'
|
||||
import Preview from '../components/Preview.vue'
|
||||
@ -86,6 +88,16 @@ export default async function router(): Promise<any> {
|
||||
name: 'message-docs',
|
||||
component: isServerActive ? MessageDocsView : InternalServerErrorView
|
||||
},
|
||||
{
|
||||
path: '/message-docs-json-schema',
|
||||
name: 'message-docs-json-schema-list',
|
||||
component: isServerActive ? MessageDocsJsonSchemaListView : InternalServerErrorView
|
||||
},
|
||||
{
|
||||
path: '/message-docs-json-schema/:id',
|
||||
name: 'message-docs-json-schema',
|
||||
component: isServerActive ? MessageDocsJsonSchemaView : InternalServerErrorView
|
||||
},
|
||||
{
|
||||
path: '/resource-docs',
|
||||
redirect: () => {
|
||||
|
||||
114
src/views/MessageDocsJsonSchemaListView.vue
Normal file
114
src/views/MessageDocsJsonSchemaListView.vue
Normal file
@ -0,0 +1,114 @@
|
||||
<!--
|
||||
- Open Bank Project - API Explorer II
|
||||
- Copyright (C) 2023-2024, TESOBE GmbH
|
||||
-
|
||||
- This program is free software: you can redistribute it and/or modify
|
||||
- it under the terms of the GNU Affero General Public License as published by
|
||||
- the Free Software Foundation, either version 3 of the License, or
|
||||
- (at your option) any later version.
|
||||
-
|
||||
- This program is distributed in the hope that it will be useful,
|
||||
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
- GNU Affero General Public License for more details.
|
||||
-
|
||||
- You should have received a copy of the GNU Affero General Public License
|
||||
- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-
|
||||
- Email: contact@tesobe.com
|
||||
- TESOBE GmbH
|
||||
- Osloerstrasse 16/17
|
||||
- Berlin 13359, Germany
|
||||
-
|
||||
- This product includes software developed at
|
||||
- TESOBE (http://www.tesobe.com/)
|
||||
-
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, inject, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { obpGroupedMessageDocsJsonSchemaKey } from '@/obp/keys'
|
||||
|
||||
const router = useRouter()
|
||||
const groupedMessageDocsJsonSchema = ref(inject(obpGroupedMessageDocsJsonSchemaKey) || {})
|
||||
|
||||
const connectorList = computed(() => {
|
||||
return Object.keys(groupedMessageDocsJsonSchema.value || {}).sort()
|
||||
})
|
||||
|
||||
function navigateToConnector(connectorId: string) {
|
||||
router.push(`/message-docs-json-schema/${connectorId}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-container class="message-docs-list-container">
|
||||
<el-main>
|
||||
<h1>Message Documentation - JSON Schema</h1>
|
||||
<p class="subtitle">Browse connector message documentation with JSON Schema definitions</p>
|
||||
|
||||
<div class="message-docs-list">
|
||||
<div v-if="connectorList.length === 0" class="empty-message">
|
||||
No JSON schema message documentation available
|
||||
</div>
|
||||
<div v-else>
|
||||
<a
|
||||
v-for="connector in connectorList"
|
||||
:key="connector"
|
||||
@click="navigateToConnector(connector)"
|
||||
class="message-doc-link"
|
||||
>
|
||||
{{ connector }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.message-docs-list-container {
|
||||
min-height: calc(100vh - 60px);
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 0.9rem;
|
||||
color: #909399;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.message-docs-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.message-doc-link {
|
||||
padding: 12px 16px;
|
||||
color: #409eff;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.message-doc-link:hover {
|
||||
background-color: #ecf5ff;
|
||||
color: #337ecc;
|
||||
}
|
||||
|
||||
.empty-message {
|
||||
color: #909399;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
439
src/views/MessageDocsJsonSchemaView.vue
Normal file
439
src/views/MessageDocsJsonSchemaView.vue
Normal file
@ -0,0 +1,439 @@
|
||||
<!--
|
||||
- Open Bank Project - API Explorer II
|
||||
- Copyright (C) 2023-2024, TESOBE GmbH
|
||||
-
|
||||
- This program is free software: you can redistribute it and/or modify
|
||||
- it under the terms of the GNU Affero General Public License as published by
|
||||
- the Free Software Foundation, either version 3 of the License, or
|
||||
- (at your option) any later version.
|
||||
-
|
||||
- This program is distributed in the hope that it will be useful,
|
||||
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
- GNU Affero General Public License for more details.
|
||||
-
|
||||
- You should have received a copy of the GNU Affero General Public License
|
||||
- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-
|
||||
- Email: contact@tesobe.com
|
||||
- TESOBE GmbH
|
||||
- Osloerstrasse 16/17
|
||||
- Berlin 13359, Germany
|
||||
-
|
||||
- This product includes software developed at
|
||||
- TESOBE (http://www.tesobe.com/)
|
||||
-
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onBeforeMount, inject, watch, onMounted, nextTick } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import SearchNav from '../components/MessageDocsJsonSchemaSearchNav.vue'
|
||||
import { connectors } from '../obp/message-docs'
|
||||
import { obpGroupedMessageDocsJsonSchemaKey } from '@/obp/keys'
|
||||
import JsonSchemaViewer from '../components/JsonSchemaViewer.vue'
|
||||
|
||||
let connector = connectors[0]
|
||||
const route = useRoute()
|
||||
const groupedMessageDocsJsonSchema = ref(inject(obpGroupedMessageDocsJsonSchemaKey) || {})
|
||||
const messageDocsJsonSchema = ref(null as any)
|
||||
const definitions = ref(null as any)
|
||||
const definitionsPanelScrollbar = ref(null as any)
|
||||
|
||||
onBeforeMount(() => {
|
||||
setDoc()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => route.params.id,
|
||||
async (id) => {
|
||||
setDoc()
|
||||
}
|
||||
)
|
||||
|
||||
const setDoc = () => {
|
||||
const paramConnector = route.params.id
|
||||
if (connectors.includes(paramConnector)) {
|
||||
connector = paramConnector
|
||||
}
|
||||
const data = groupedMessageDocsJsonSchema.value[connector]
|
||||
// Handle both old and new data structures
|
||||
if (data?.grouped) {
|
||||
messageDocsJsonSchema.value = data.grouped
|
||||
definitions.value = data.definitions
|
||||
} else {
|
||||
messageDocsJsonSchema.value = data
|
||||
definitions.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function hasSchema(value: any) {
|
||||
return value && (value.outbound_schema || value.inbound_schema)
|
||||
}
|
||||
|
||||
// Handle $ref link clicks from JsonSchemaViewer components
|
||||
function handleRefClick(href: string) {
|
||||
console.log('handleRefClick called with href:', href)
|
||||
if (!href) {
|
||||
console.log('No href provided')
|
||||
return
|
||||
}
|
||||
|
||||
// Extract the definition ID from the href (e.g., #def-BasicGeneralContext)
|
||||
const targetId = href.substring(1) // Remove the #
|
||||
console.log('Target ID:', targetId)
|
||||
const targetElement = document.getElementById(targetId)
|
||||
console.log('Target element:', targetElement)
|
||||
console.log('Definitions panel scrollbar:', definitionsPanelScrollbar.value)
|
||||
|
||||
if (targetElement && definitionsPanelScrollbar.value) {
|
||||
// Get the scrollbar's wrap element
|
||||
const scrollWrap = definitionsPanelScrollbar.value.$refs.wrap as HTMLElement
|
||||
console.log('Scroll wrap:', scrollWrap)
|
||||
if (scrollWrap) {
|
||||
// Calculate the position of the target element relative to the scrollable container
|
||||
const containerTop = scrollWrap.getBoundingClientRect().top
|
||||
const targetTop = targetElement.getBoundingClientRect().top
|
||||
const currentScroll = scrollWrap.scrollTop
|
||||
const offset = targetTop - containerTop + currentScroll - 20 // 20px padding from top
|
||||
console.log('Scrolling to offset:', offset)
|
||||
|
||||
// Smooth scroll to the target
|
||||
scrollWrap.scrollTo({
|
||||
top: offset,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
|
||||
// Add a highlight effect
|
||||
targetElement.classList.add('highlight-definition')
|
||||
setTimeout(() => {
|
||||
targetElement.classList.remove('highlight-definition')
|
||||
}, 2000)
|
||||
} else {
|
||||
console.log('No scroll wrap found')
|
||||
}
|
||||
} else {
|
||||
console.log('Target element or scrollbar not found')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-container class="message-docs-container">
|
||||
<el-aside class="search-nav" width="18%">
|
||||
<el-scrollbar>
|
||||
<SearchNav />
|
||||
</el-scrollbar>
|
||||
</el-aside>
|
||||
<el-main class="message-docs-content">
|
||||
<el-scrollbar>
|
||||
<el-backtop :right="100" :bottom="100" />
|
||||
<div class="message-docs-header">
|
||||
<h1>{{ connector }}</h1>
|
||||
<p class="connector-subtitle">Message Docs - JSON Schema</p>
|
||||
<p class="version-indicator">v1.2.4 - Debug Click Events</p>
|
||||
</div>
|
||||
<div v-for="(group, key) of messageDocsJsonSchema" :key="key">
|
||||
<div v-for="(value, key) of group" :key="value">
|
||||
<el-divider></el-divider>
|
||||
<a v-bind:href="`#${value.method_name}`" :id="value.method_name">
|
||||
<h2>{{ value.method_name }}</h2>
|
||||
</a>
|
||||
<p v-if="value.description">{{ value.description }}</p>
|
||||
|
||||
<section class="topics">
|
||||
<div>
|
||||
<strong>Category: </strong>
|
||||
<el-tag type="info" round>{{ value.category || 'Uncategorized' }}</el-tag>
|
||||
</div>
|
||||
<div v-if="value.message_format">
|
||||
<strong>Message Format: </strong>
|
||||
<el-tag type="success" round>{{ value.message_format }}</el-tag>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="value.outbound_schema">
|
||||
<h3>Outbound Schema</h3>
|
||||
<JsonSchemaViewer :schema="value.outbound_schema" copyable @refClick="handleRefClick" />
|
||||
</section>
|
||||
|
||||
<section v-if="value.inbound_schema">
|
||||
<h3>Inbound Schema</h3>
|
||||
<JsonSchemaViewer :schema="value.inbound_schema" copyable @refClick="handleRefClick" />
|
||||
</section>
|
||||
|
||||
<section v-if="!hasSchema(value)">
|
||||
<p class="no-schema-message">No schema information available for this method.</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
<el-aside class="definitions-panel" width="28%">
|
||||
<el-scrollbar ref="definitionsPanelScrollbar">
|
||||
<div class="definitions-panel-content">
|
||||
<div class="definitions-header">
|
||||
<h2>Schema Definitions</h2>
|
||||
<p class="definitions-subtitle">
|
||||
Reference schemas used in messages
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Debug info -->
|
||||
<div v-if="false">
|
||||
{{ console.log('Definitions:', definitions) }}
|
||||
{{ console.log('Definitions keys:', definitions ? Object.keys(definitions) : 'null') }}
|
||||
{{ console.log('Message docs:', messageDocsJsonSchema) }}
|
||||
</div>
|
||||
|
||||
<div v-if="definitions && Object.keys(definitions).length > 0">
|
||||
<div v-for="(defSchema, defName) in definitions" :key="defName" class="definition-item">
|
||||
<a v-bind:href="`#def-${defName}`" :id="`def-${defName}`">
|
||||
<h3>{{ defName }}</h3>
|
||||
</a>
|
||||
<JsonSchemaViewer :schema="defSchema" copyable @refClick="handleRefClick" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="no-definitions">
|
||||
<p>No schema definitions available</p>
|
||||
<p style="font-size: 0.8rem; margin-top: 10px;">
|
||||
Debug: definitions = {{ definitions ? 'exists' : 'null' }},
|
||||
keys = {{ definitions ? Object.keys(definitions).length : 0 }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-aside>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.message-docs-container {
|
||||
height: calc(100vh - 60px);
|
||||
}
|
||||
|
||||
/* Left Sidebar - Search Navigation */
|
||||
.search-nav {
|
||||
border-right: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.search-nav :deep(.el-scrollbar__wrap) {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.search-nav :deep(.el-scrollbar__view) {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* Main Content Area */
|
||||
.message-docs-content {
|
||||
color: #39455f;
|
||||
font-family: 'Roboto';
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.message-docs-content :deep(.el-scrollbar__wrap) {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.message-docs-content :deep(.el-scrollbar__view) {
|
||||
padding: 25px 30px;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Right Sidebar - Definitions Panel */
|
||||
.definitions-panel {
|
||||
border-left: 2px solid #e4e7ed;
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
.definitions-panel :deep(.el-scrollbar__wrap) {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.definitions-panel :deep(.el-scrollbar__view) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.definitions-panel-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.definitions-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: #f9fafb;
|
||||
padding: 10px 0 20px 0;
|
||||
margin-bottom: 10px;
|
||||
border-bottom: 2px solid #e4e7ed;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.definitions-header h2 {
|
||||
color: #303133;
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 1.3rem;
|
||||
font-family: 'Roboto';
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.definitions-subtitle {
|
||||
color: #909399;
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
font-family: 'Roboto';
|
||||
}
|
||||
|
||||
h2 {
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
section {
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
max-width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family: 'Roboto';
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #39455f;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
div {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.content :deep(strong) {
|
||||
font-family: 'Roboto';
|
||||
}
|
||||
|
||||
.content :deep(a):hover {
|
||||
background-color: #39455f;
|
||||
}
|
||||
|
||||
.message-docs-header {
|
||||
padding: 20px 0;
|
||||
border-bottom: 2px solid #e4e7ed;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.message-docs-header h1 {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0 0 0.5rem 0;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.connector-subtitle {
|
||||
font-size: 1rem;
|
||||
color: #909399;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.version-indicator {
|
||||
font-size: 0.75rem;
|
||||
color: #67c23a;
|
||||
margin: 0.25rem 0 0 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.topics {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.topics > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.no-schema-message {
|
||||
color: #909399;
|
||||
font-style: italic;
|
||||
padding: 15px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.definition-item {
|
||||
margin-bottom: 25px;
|
||||
padding: 15px;
|
||||
background-color: #ffffff;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e4e7ed;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.definition-item:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
.definition-item h3 {
|
||||
color: #409eff;
|
||||
margin-top: 0;
|
||||
margin-bottom: 12px;
|
||||
font-size: 1rem;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
font-weight: 600;
|
||||
font-family: 'Roboto';
|
||||
}
|
||||
|
||||
.definition-item a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.definition-item a:hover h3 {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.no-definitions {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #909399;
|
||||
font-style: italic;
|
||||
font-family: 'Roboto';
|
||||
}
|
||||
|
||||
/* Highlight animation for scrolled-to definitions */
|
||||
@keyframes highlight-pulse {
|
||||
0% {
|
||||
background-color: rgba(64, 158, 255, 0.15);
|
||||
}
|
||||
50% {
|
||||
background-color: rgba(64, 158, 255, 0.25);
|
||||
}
|
||||
100% {
|
||||
background-color: rgba(64, 158, 255, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.definition-item.highlight-definition {
|
||||
animation: highlight-pulse 0.6s ease-in-out 3;
|
||||
border-color: #409eff;
|
||||
box-shadow: 0 2px 12px rgba(64, 158, 255, 0.3);
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue
Block a user