Skip to content

Commit a6af7c2

Browse files
committed
Display of collections
1 parent 28cce58 commit a6af7c2

4 files changed

Lines changed: 337 additions & 6 deletions

File tree

src/components/Content.vue

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ const summary = ref('')
5050
const tags = ref<string[]>([])
5151
const allTags = ref<string[]>([])
5252
const resourceDocs = inject(obpResourceDocsKey)
53-
const displayPrev = ref(true)
54-
const displayNext = ref(true)
53+
const displayPrev = ref(false)
54+
const displayNext = ref(false)
5555
const prev = ref({ id: 'prev' })
5656
const next = ref({ id: 'next' })
5757
const favoriteButtonStyle = ref('favorite favoriteButton')
@@ -319,18 +319,18 @@ onBeforeRouteUpdate(async (to) => {
319319
<el-divider class="divider" />
320320
<el-row>
321321
<el-col :span="12" class="pager-left">
322-
<el-icon v-show="displayPrev">
322+
<el-icon v-if="displayPrev">
323323
<ArrowLeftBold />
324324
</el-icon>
325-
<RouterLink v-show="displayPrev" class="pager-router-link"
325+
<RouterLink v-if="displayPrev" class="pager-router-link"
326326
:to="{ name: 'api', params: { version: prev.version }, query: { operationid: prev.id } }">{{ prev.title }}
327327
</RouterLink>
328328
</el-col>
329329
<el-col :span="12" class="pager-right">
330-
<RouterLink v-show="displayNext" class="pager-router-link"
330+
<RouterLink v-if="displayNext" class="pager-router-link"
331331
:to="{ name: 'api', params: { version: next.version }, query: { operationid: next.id } }">{{ next.title }}
332332
</RouterLink>
333-
<el-icon v-show="displayNext">
333+
<el-icon v-if="displayNext">
334334
<ArrowRightBold />
335335
</el-icon>
336336
</el-col>

src/obp/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,10 @@ export async function getMyAPICollectionsEndpoint(collectionName: string): Promi
200200
)
201201
}
202202

203+
export async function getAPICollectionEndpoints(collectionId: string): Promise<any> {
204+
return await get(`obp/v6.0.0/api-collections/${collectionId}/api-collection-endpoints`)
205+
}
206+
203207
export async function getOBPBanks(): Promise<any> {
204208
return await get(`obp/v6.0.0/banks`)
205209
}

src/router/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import MessageDocsListView from '../views/MessageDocsListView.vue'
3333
import MessageDocsJsonSchemaView from '../views/MessageDocsJsonSchemaView.vue'
3434
import MessageDocsJsonSchemaListView from '../views/MessageDocsJsonSchemaListView.vue'
3535
import BodyView from '../views/BodyView.vue'
36+
import CollectionView from '../views/CollectionView.vue'
3637
import Content from '../components/Content.vue'
3738
import Preview from '../components/Preview.vue'
3839
import NotFoundView from '../views/NotFoundView.vue'
@@ -140,6 +141,21 @@ export default async function router(): Promise<any> {
140141
name: 'callback',
141142
component: isServerActive ? BodyView : InternalServerErrorView
142143
},
144+
{
145+
path: '/collections/:id',
146+
name: 'collection-view',
147+
component: isServerActive ? CollectionView : InternalServerErrorView,
148+
children: [
149+
{
150+
path: '',
151+
name: 'collection-api',
152+
components: {
153+
body: Content,
154+
preview: Preview
155+
}
156+
}
157+
]
158+
},
143159
{ path: '/api-server-error', name: 'apiServerError', component: APIServerErrorView },
144160
{ path: '/:pathMatch(.*)*', name: 'notFound', component: NotFoundView }
145161
]

src/views/CollectionView.vue

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
<script setup lang="ts">
2+
import Menu from '../components/Menu.vue'
3+
import AutoLogout from '../components/AutoLogout.vue'
4+
import ChatWidget from '../components/ChatWidget.vue'
5+
import { Search } from '@element-plus/icons-vue'
6+
import { onMounted, ref, computed, inject, reactive, nextTick } from 'vue'
7+
import { getCurrentUser, getAPICollectionEndpoints, OBP_API_DEFAULT_RESOURCE_DOC_VERSION } from '../obp'
8+
import { obpResourceDocsKey } from '@/obp/keys'
9+
import { useRoute, useRouter } from 'vue-router'
10+
import { SEARCH_LINKS_COLOR as searchLinksColorSetting } from '../obp/style-setting'
11+
12+
const route = useRoute()
13+
const router = useRouter()
14+
const isLoggedIn = ref(false)
15+
const loading = ref(true)
16+
const errorMessage = ref('')
17+
const collectionId = ref('')
18+
const groups = ref<Record<string, any[]>>({})
19+
const sortedKeys = ref<string[]>([])
20+
const activeKeys = ref<string[]>([])
21+
const searchLinksColor = ref(searchLinksColorSetting)
22+
const form = reactive({ search: '' })
23+
const allDocs = ref<Record<string, any[]>>({})
24+
25+
const resourceDocs = inject(obpResourceDocsKey)
26+
const version = OBP_API_DEFAULT_RESOURCE_DOC_VERSION
27+
28+
const hasOperationId = computed(() => {
29+
return !!route.query.operationid
30+
})
31+
32+
const endpointCount = computed(() => {
33+
return Object.values(groups.value).reduce((acc: number, items: any[]) => acc + items.length, 0)
34+
})
35+
36+
const isChatbotEnabled = import.meta.env.VITE_CHATBOT_ENABLED === 'true'
37+
38+
const sortLinks = (items: any[]) => {
39+
const uniqueLinks: Record<string, string> = {}
40+
for (const { summary, operation_id } of items) {
41+
if (!Object.keys(uniqueLinks).includes(summary.trim()))
42+
uniqueLinks[summary.trim()] = operation_id
43+
}
44+
return Object.fromEntries(
45+
Object.entries(uniqueLinks).sort((a, b) => a[0].localeCompare(b[0]))
46+
)
47+
}
48+
49+
const clearActiveTab = () => {
50+
const activeTabs = document.querySelectorAll('.active-api-router-tab')
51+
activeTabs.forEach((tab) => {
52+
tab.classList.remove('active-api-router-tab')
53+
})
54+
}
55+
56+
const setTabActive = (id: string) => {
57+
const tabs = document.querySelectorAll('.api-router-link')
58+
clearActiveTab()
59+
tabs.forEach((tab) => {
60+
if (tab.id === id) {
61+
tab.parentElement?.classList.add('active-api-router-tab')
62+
}
63+
})
64+
}
65+
66+
const setActive = (event: Event) => {
67+
const target = event.target as HTMLElement
68+
if (target.tagName.toLowerCase() === 'a') {
69+
setTabActive(target.id)
70+
}
71+
}
72+
73+
const isKeyFound = (keys: string[], item: string) => keys.every((k) => item.toLowerCase().includes(k))
74+
75+
const searchEvent = (value: string) => {
76+
if (value) {
77+
const splitKey = value.split(' ').map((k) => k.toLowerCase())
78+
sortedKeys.value = Object.keys(allDocs.value).filter((title) => {
79+
const isGroupFound = isKeyFound(splitKey, title)
80+
const items = allDocs.value[title].filter(
81+
(item: any) => isGroupFound || isKeyFound(splitKey, item.summary)
82+
)
83+
groups.value[title] = items
84+
return isGroupFound || items.length > 0
85+
})
86+
} else {
87+
groups.value = JSON.parse(JSON.stringify(allDocs.value))
88+
sortedKeys.value = Object.keys(groups.value).sort()
89+
}
90+
}
91+
92+
onMounted(async () => {
93+
const currentUser = await getCurrentUser()
94+
const currentResponseKeys = Object.keys(currentUser)
95+
isLoggedIn.value = currentResponseKeys.includes('username')
96+
97+
collectionId.value = route.params.id as string
98+
if (!collectionId.value) {
99+
errorMessage.value = 'No collection ID provided.'
100+
loading.value = false
101+
return
102+
}
103+
104+
const response = await getAPICollectionEndpoints(collectionId.value)
105+
106+
if (response.error) {
107+
errorMessage.value = typeof response.error === 'string'
108+
? response.error
109+
: response.error.message || JSON.stringify(response.error)
110+
loading.value = false
111+
return
112+
}
113+
114+
const endpoints = response.api_collection_endpoints
115+
if (!endpoints || endpoints.length === 0) {
116+
errorMessage.value = 'This collection has no endpoints.'
117+
loading.value = false
118+
return
119+
}
120+
121+
const operationIds = new Set(endpoints.map((ep: any) => ep.operation_id))
122+
123+
const versionDocs = resourceDocs?.[version]?.resource_docs || []
124+
const matchedDocs = versionDocs.filter((doc: any) => operationIds.has(doc.operation_id))
125+
126+
if (matchedDocs.length === 0) {
127+
errorMessage.value = 'No matching endpoints found in the resource documentation.'
128+
loading.value = false
129+
return
130+
}
131+
132+
const grouped = matchedDocs.reduce((acc: Record<string, any[]>, doc: any) => {
133+
const tag = doc.tags[0]
134+
;(acc[tag] = acc[tag] || []).push(doc)
135+
return acc
136+
}, {})
137+
138+
allDocs.value = JSON.parse(JSON.stringify(grouped))
139+
groups.value = JSON.parse(JSON.stringify(grouped))
140+
activeKeys.value = Object.keys(groups.value)
141+
sortedKeys.value = activeKeys.value.sort()
142+
loading.value = false
143+
144+
await nextTick()
145+
if (route.query.operationid) {
146+
setTabActive(route.query.operationid as string)
147+
}
148+
})
149+
</script>
150+
151+
<template>
152+
<AutoLogout v-if="isLoggedIn" />
153+
<el-container class="root">
154+
<el-aside class="search-nav" width="20%">
155+
<el-container class="search-nav-container">
156+
<el-header class="collection-header">
157+
<div class="collection-title">Collection</div>
158+
<div class="collection-id">{{ collectionId }}</div>
159+
<div v-if="!loading && !errorMessage" class="collection-count">{{ endpointCount }} endpoints</div>
160+
</el-header>
161+
<el-header class="search-nav-search-bar" v-if="!loading && !errorMessage">
162+
<el-col :span="24">
163+
<el-input v-model="form.search" placeholder="Search" :prefix-icon="Search" @input="searchEvent" />
164+
</el-col>
165+
</el-header>
166+
<el-main>
167+
<div v-if="loading" class="loading-state">
168+
<el-icon class="is-loading"><i class="el-icon-loading"></i></el-icon>
169+
Loading collection...
170+
</div>
171+
<div v-else-if="errorMessage" class="error-state">
172+
<p>{{ errorMessage }}</p>
173+
</div>
174+
<el-collapse v-else v-model="activeKeys" class="search-nav-collapse">
175+
<el-collapse-item v-for="key in sortedKeys" :title="key" :key="key" :name="key">
176+
<div class="el-tabs--right">
177+
<div v-for="(value, k) of sortLinks(groups[key])" :key="value" class="api-router-tab" @click="setActive">
178+
<RouterLink active-class="active-api-router-link" class="api-router-link" :id="value"
179+
:to="{ name: 'collection-api', params: { id: collectionId }, query: { operationid: value } }">{{ k }}</RouterLink>
180+
</div>
181+
</div>
182+
</el-collapse-item>
183+
</el-collapse>
184+
</el-main>
185+
</el-container>
186+
</el-aside>
187+
<el-main>
188+
<el-container class="main">
189+
<el-header class="menu">
190+
<Menu />
191+
</el-header>
192+
<el-container class="middle">
193+
<el-aside class="summary" :width="hasOperationId ? '50%' : '100%'">
194+
<RouterView name="body" />
195+
</el-aside>
196+
<el-main v-if="hasOperationId" class="preview">
197+
<RouterView class="preview" name="preview" />
198+
</el-main>
199+
</el-container>
200+
</el-container>
201+
<ChatWidget v-if="isChatbotEnabled" />
202+
</el-main>
203+
</el-container>
204+
</template>
205+
206+
<style scoped>
207+
.root {
208+
height: 100%;
209+
}
210+
.summary {
211+
max-height: 100%;
212+
}
213+
.main {
214+
height: 100%;
215+
overflow: hidden;
216+
}
217+
.search-nav {
218+
height: 100%;
219+
max-height: 100%;
220+
overflow: hidden;
221+
background-color: #f8f9fb;
222+
border-right: solid 1px var(--el-menu-border-color);
223+
}
224+
.middle {
225+
height: 100%;
226+
overflow: hidden;
227+
}
228+
.preview {
229+
color: white;
230+
background-color: #151d30;
231+
max-height: 100%;
232+
}
233+
.search-nav-container {
234+
height: 100%;
235+
max-height: 100%;
236+
overflow: hidden;
237+
padding: 0;
238+
}
239+
.collection-header {
240+
padding: 15px 20px;
241+
height: auto;
242+
border-bottom: 1px solid var(--el-menu-border-color);
243+
}
244+
.collection-title {
245+
font-family: 'Roboto';
246+
font-size: 12px;
247+
color: #909399;
248+
text-transform: uppercase;
249+
letter-spacing: 0.5px;
250+
}
251+
.collection-id {
252+
font-family: 'Roboto';
253+
font-size: 13px;
254+
color: #39455f;
255+
font-weight: 500;
256+
margin-top: 4px;
257+
word-break: break-all;
258+
}
259+
.collection-count {
260+
font-family: 'Roboto';
261+
font-size: 12px;
262+
color: #909399;
263+
margin-top: 4px;
264+
}
265+
.search-nav-search-bar {
266+
box-shadow: rgba(0, 0, 0, 0.40) 0px 25px 50px -20px;
267+
height: auto;
268+
}
269+
.search-nav-collapse {
270+
height: 100%;
271+
max-height: 99%;
272+
margin-right: -10px;
273+
width: 100%;
274+
overflow-x: hidden;
275+
min-height: unset;
276+
}
277+
.api-router-link {
278+
width: 100%;
279+
margin-left: 15px;
280+
font-family: 'Roboto';
281+
text-decoration: none;
282+
color: #39455f;
283+
display: inline-block;
284+
}
285+
.api-router-tab {
286+
border-left: 2px solid var(--el-menu-border-color);
287+
}
288+
.api-router-tab:hover,
289+
.active-api-router-tab {
290+
border-left: 2px solid v-bind(searchLinksColor);
291+
}
292+
.api-router-tab:hover .api-router-link,
293+
.active-api-router-link {
294+
color: v-bind(searchLinksColor);
295+
}
296+
.loading-state {
297+
padding: 20px;
298+
text-align: center;
299+
color: #909399;
300+
font-family: 'Roboto';
301+
}
302+
.error-state {
303+
padding: 20px;
304+
color: #f56c6c;
305+
font-family: 'Roboto';
306+
font-size: 14px;
307+
}
308+
.menu {
309+
/* match BodyView menu styling */
310+
}
311+
</style>

0 commit comments

Comments
 (0)