Skip to content

Commit f24fb20

Browse files
ui: add new API docs tab (#9409)
* ui: add new API docs tab This introduces a new API docs table which is enabled by default but the admin can disable it via config.json. This uses the discovered APIs for logged in user/account to show them the APIs accessible to them and generates dynamic API docs based on them which are searchable. Also introduces some common auto-completed API groups that are available to most roles. Signed-off-by: Rohit Yadav <[email protected]> * Update ui/src/views/plugins/ApiDocsPlugin.vue * Update ui/src/views/plugins/ApiDocsPlugin.vue * Update ui/src/views/plugins/ApiDocsPlugin.vue * Update ui/src/views/plugins/ApiDocsPlugin.vue * Update ui/src/views/plugins/ApiDocsPlugin.vue * fix performance issues Signed-off-by: Rohit Yadav <[email protected]> * Update ui/src/views/plugins/ApiDocsPlugin.vue Co-authored-by: Suresh Kumar Anaparti <[email protected]> * Update ui/public/locales/en.json Co-authored-by: Suresh Kumar Anaparti <[email protected]> * address Suresh's feedback Signed-off-by: Rohit Yadav <[email protected]> * filter example/options as we type Signed-off-by: Rohit Yadav <[email protected]> * Address Joao's comments Signed-off-by: Rohit Yadav <[email protected]> --------- Signed-off-by: Rohit Yadav <[email protected]> Co-authored-by: Suresh Kumar Anaparti <[email protected]>
1 parent 56c661c commit f24fb20

File tree

7 files changed

+248
-1
lines changed

7 files changed

+248
-1
lines changed

ui/public/config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
]
9393
},
9494
"plugins": [],
95+
"apidocs": true,
9596
"basicZoneEnabled": true,
9697
"multipleServer": false,
9798
"allowSettingTheme": true,

ui/public/locales/en.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,9 @@
348348
"label.annotation.everyone": "Visible to everyone",
349349
"label.anti.affinity": "Anti-affinity",
350350
"label.anti.affinity.group": "Anti-affinity group",
351+
"label.api.docs": "API Docs",
352+
"label.api.docs.description": "For information about how the APIs work, and tips on how to use them, click here to see the Developer's Guide.",
353+
"label.api.docs.count": "APIs available for your account",
351354
"label.api.version": "API version",
352355
"label.apikey": "API key",
353356
"label.app.cookie": "AppCookie",
@@ -1796,6 +1799,7 @@
17961799
"label.replace.acl": "Replace ACL",
17971800
"label.replace.acl.list": "Replace ACL list",
17981801
"label.report.bug": "Ask a question or Report an issue",
1802+
"label.request": "Request",
17991803
"label.required": "Required",
18001804
"label.requireshvm": "HVM",
18011805
"label.requiresupgrade": "Requires upgrade",

ui/src/config/router.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import { UserLayout, BasicLayout, RouteView } from '@/layouts'
2020
import AutogenView from '@/views/AutogenView.vue'
2121
import IFramePlugin from '@/views/plugins/IFramePlugin.vue'
22+
import ApiDocsPlugin from '@/views/plugins/ApiDocsPlugin.vue'
2223

2324
import { shallowRef } from 'vue'
2425
import { vueProps } from '@/vue-app'
@@ -275,6 +276,16 @@ export function asyncRouterMap () {
275276
})
276277
}
277278

279+
const apidocs = vueProps.$config.apidocs
280+
if (apidocs !== false) {
281+
routerMap[0].children.push({
282+
path: '/apidocs/',
283+
name: 'apidocs',
284+
component: shallowRef(ApiDocsPlugin),
285+
meta: { title: 'label.api.docs', icon: 'read-outlined' }
286+
})
287+
}
288+
278289
return routerMap
279290
}
280291

ui/src/core/lazy_lib/components_use.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import {
6161
Tree,
6262
Calendar,
6363
Slider,
64+
Result,
6465
AutoComplete,
6566
Collapse,
6667
Space,
@@ -133,5 +134,6 @@ export default {
133134
app.use(Descriptions)
134135
app.use(Space)
135136
app.use(Statistic)
137+
app.use(Result)
136138
}
137139
}

ui/src/store/modules/user.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,10 @@ const user = {
314314
const apiName = api.name
315315
apis[apiName] = {
316316
params: api.params,
317-
response: api.response
317+
response: api.response,
318+
isasync: api.isasync,
319+
since: api.since,
320+
description: api.description
318321
}
319322
}
320323
commit('SET_APIS', apis)

ui/src/style/vars.less

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,10 @@ a {
471471
width: auto;
472472
}
473473

474+
.ant-list-item.selected-item {
475+
background-color: @primary-color-light;
476+
}
477+
474478
.ant-select-arrow .anticon {
475479
vertical-align: top;
476480
}
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
<template>
19+
<div>
20+
<resource-layout>
21+
<template #left>
22+
<a-card :bordered="false">
23+
<a-auto-complete
24+
v-model:value="query"
25+
:options="options.filter(value => value.value.toLowerCase().includes(query.toLowerCase()))"
26+
style="width: 100%"
27+
>
28+
<a-input-search
29+
size="default"
30+
:placeholder="$t('label.search')"
31+
v-model:value="query"
32+
allow-clear
33+
enter-button
34+
>
35+
<template #prefix><search-outlined /></template>
36+
</a-input-search>
37+
</a-auto-complete>
38+
<a-list style="margin-top: 12px; height:580px; overflow-y: scroll;" size="small" :data-source="Object.keys($store.getters.apis).sort()">
39+
<template #renderItem="{ item }">
40+
<a>
41+
<a-list-item
42+
v-if="item.toLowerCase().includes(query.toLowerCase())"
43+
@click="showApi(item)"
44+
style="padding-left: 12px"
45+
:class="selectedApi === item ? 'selected-item' : ''">
46+
{{ item }} <a-tag v-if="$store.getters.apis[item].isasync" color="blue">async</a-tag>
47+
</a-list-item>
48+
</a>
49+
</template>
50+
</a-list>
51+
<a-divider style="margin-bottom: 12px" />
52+
<span>{{ Object.keys($store.getters.apis).length }} {{ $t('label.api.docs.count') }}</span>
53+
</a-card>
54+
</template>
55+
<template #right>
56+
<a-card
57+
class="spin-content"
58+
:bordered="true"
59+
style="width: 100%; overflow-x: auto">
60+
<span v-if="selectedApi && selectedApi in $store.getters.apis">
61+
<h2>{{ selectedApi }}
62+
<a-tag v-if="$store.getters.apis[selectedApi].isasync" color="blue">Asynchronous API</a-tag>
63+
<a-tag v-if="$store.getters.apis[selectedApi].since">Since {{ $store.getters.apis[selectedApi].since }}</a-tag>
64+
<tooltip-button
65+
tooltipPlacement="right"
66+
:tooltip="$t('label.copy') + ' ' + selectedApi"
67+
icon="CopyOutlined"
68+
type="outlined"
69+
size="small"
70+
@onClick="$message.success($t('label.copied.clipboard'))"
71+
:copyResource="selectedApi" />
72+
</h2>
73+
<p>{{ $store.getters.apis[selectedApi].description }}</p>
74+
<h3>{{ $t('label.request') }} {{ $t('label.params') }}:</h3>
75+
<a-table
76+
:columns="[{title: $t('label.name'), dataIndex: 'name'}, {title: $t('label.required'), dataIndex: 'required'}, {title: $t('label.type'), dataIndex: 'type'}, {title: $t('label.description'), dataIndex: 'description'}]"
77+
:data-source="selectedParams"
78+
:pagination="false"
79+
size="small">
80+
<template #bodyCell="{text, column, record}">
81+
<a-tag v-if="record.since && column.dataIndex === 'description'">Since {{ record.since }}</a-tag>
82+
<span v-if="record.required === true"><strong>{{ text }}</strong></span>
83+
<span v-else>{{ text }}</span>
84+
</template>
85+
</a-table>
86+
<br/>
87+
<h3>{{ $t('label.response') }} {{ $t('label.params') }}:</h3>
88+
<a-table
89+
:columns="[{title: $t('label.name'), dataIndex: 'name'}, {title: $t('label.type'), dataIndex: 'type'}, {title: $t('label.description'), dataIndex: 'description'}]"
90+
:data-source="selectedResponse"
91+
:pagination="false"
92+
size="small" />
93+
</span>
94+
<span v-else>
95+
<a-alert
96+
:message="$t('label.api.docs')"
97+
type="info"
98+
show-icon
99+
banner>
100+
<template #description>
101+
<a href="https://docs.cloudstack.apache.org/en/latest/developersguide/dev.html" target="_blank">{{ $t('label.api.docs.description') }}</a>
102+
</template>
103+
</a-alert>
104+
<a-result
105+
status="success"
106+
:title="$t('label.download') + ' CloudStack CloudMonkey CLI'"
107+
sub-title="For API automation and orchestration"
108+
>
109+
<template #extra>
110+
<a-button type="primary"><a href="https://github.com/apache/cloudstack-cloudmonkey/releases" target="_blank">{{ $t('label.download') }} CLI</a></a-button>
111+
<a-button><a href="https://github.com/apache/cloudstack-cloudmonkey/wiki/Usage" target="_blank">{{ $t('label.open.documentation') }} (CLI)</a></a-button>
112+
<br/>
113+
<br/>
114+
<div v-if="showKeys">
115+
<key-outlined />
116+
<strong>
117+
{{ $t('label.apikey') }}
118+
<tooltip-button
119+
tooltipPlacement="right"
120+
:tooltip="$t('label.copy') + ' ' + $t('label.apikey')"
121+
icon="CopyOutlined"
122+
type="dashed"
123+
size="small"
124+
@onClick="$message.success($t('label.copied.clipboard'))"
125+
:copyResource="userkeys.apikey" />
126+
</strong>
127+
<div>
128+
{{ userkeys.apikey.substring(0, 20) }}...
129+
</div>
130+
<br/>
131+
<lock-outlined />
132+
<strong>
133+
{{ $t('label.secretkey') }}
134+
<tooltip-button
135+
tooltipPlacement="right"
136+
:tooltip="$t('label.copy') + ' ' + $t('label.secretkey')"
137+
icon="CopyOutlined"
138+
type="dashed"
139+
size="small"
140+
@onClick="$message.success($t('label.copied.clipboard'))"
141+
:copyResource="userkeys.secretkey" />
142+
</strong>
143+
<div>
144+
{{ userkeys.secretkey.substring(0, 20) }}...
145+
</div>
146+
</div>
147+
</template>
148+
</a-result>
149+
</span>
150+
</a-card>
151+
</template>
152+
</resource-layout>
153+
</div>
154+
</template>
155+
156+
<script>
157+
import { api } from '@/api'
158+
159+
import ResourceLayout from '@/layouts/ResourceLayout'
160+
import TooltipButton from '@/components/widgets/TooltipButton'
161+
162+
export default {
163+
name: 'ApiDocsPlugin',
164+
components: {
165+
ResourceLayout,
166+
TooltipButton
167+
},
168+
data () {
169+
return {
170+
query: '',
171+
selectedApi: '',
172+
selectedParams: [],
173+
selectedResponse: [],
174+
showKeys: false,
175+
userkeys: {},
176+
options: [
177+
{ value: 'VirtualMachine', label: 'Instance' },
178+
{ value: 'Kubernetes', label: 'Kubernetes' },
179+
{ value: 'Volume', label: 'Volume' },
180+
{ value: 'Snapshot', label: 'Snapshot' },
181+
{ value: 'Backup', label: 'Backup' },
182+
{ value: 'Network', label: 'Network' },
183+
{ value: 'IpAddress', label: 'IP Address' },
184+
{ value: 'VPN', label: 'VPN' },
185+
{ value: 'VPC', label: 'VPC' },
186+
{ value: 'NetworkACL', label: 'Network ACL' },
187+
{ value: 'SecurityGroup', label: 'Security Group' },
188+
{ value: 'Template', label: 'Template' },
189+
{ value: 'ISO', label: 'ISO' },
190+
{ value: 'SSH', label: 'SSH' },
191+
{ value: 'Project', label: 'Project' },
192+
{ value: 'Account', label: 'Account' },
193+
{ value: 'User', label: 'User' },
194+
{ value: 'Event', label: 'Event' },
195+
{ value: 'Offering', label: 'Offering' },
196+
{ value: 'Zone', label: 'Zone' }
197+
]
198+
}
199+
},
200+
created () {
201+
if (!('getUserKeys' in this.$store.getters.apis)) {
202+
return
203+
}
204+
api('getUserKeys', { id: this.$store.getters.userInfo.id }).then(json => {
205+
this.userkeys = json.getuserkeysresponse.userkeys
206+
if (this.userkeys && this.userkeys.secretkey) {
207+
this.showKeys = true
208+
}
209+
})
210+
},
211+
methods: {
212+
showApi (api) {
213+
this.selectedApi = api
214+
this.selectedParams = this.$store.getters.apis[api].params
215+
.sort((a, b) => (a.name > b.name) ? 1 : ((b.name > a.name) ? -1 : 0))
216+
.sort((a, b) => (a.required > b.required) ? -1 : ((b.required > a.required) ? 1 : 0))
217+
.filter(value => Object.keys(value).length > 0)
218+
this.selectedResponse = this.$store.getters.apis[api].response.filter(value => Object.keys(value).length > 0)
219+
}
220+
}
221+
}
222+
</script>

0 commit comments

Comments
 (0)