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 <rohit.yadav@shapeblue.com>

* 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 <rohit.yadav@shapeblue.com>

* Update ui/src/views/plugins/ApiDocsPlugin.vue

Co-authored-by: Suresh Kumar Anaparti <sureshkumar.anaparti@gmail.com>

* Update ui/public/locales/en.json

Co-authored-by: Suresh Kumar Anaparti <sureshkumar.anaparti@gmail.com>

* address Suresh's feedback

Signed-off-by: Rohit Yadav <rohit.yadav@shapeblue.com>

* filter example/options as we type

Signed-off-by: Rohit Yadav <rohit.yadav@shapeblue.com>

* Address Joao's comments

Signed-off-by: Rohit Yadav <rohit.yadav@shapeblue.com>

---------

Signed-off-by: Rohit Yadav <rohit.yadav@shapeblue.com>
Co-authored-by: Suresh Kumar Anaparti <sureshkumar.anaparti@gmail.com>
This commit is contained in:
Rohit Yadav 2024-07-22 10:46:40 +05:30 committed by GitHub
parent 56c661c1df
commit f24fb20e6b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 248 additions and 1 deletions

View File

@ -92,6 +92,7 @@
]
},
"plugins": [],
"apidocs": true,
"basicZoneEnabled": true,
"multipleServer": false,
"allowSettingTheme": true,

View File

@ -348,6 +348,9 @@
"label.annotation.everyone": "Visible to everyone",
"label.anti.affinity": "Anti-affinity",
"label.anti.affinity.group": "Anti-affinity group",
"label.api.docs": "API Docs",
"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.",
"label.api.docs.count": "APIs available for your account",
"label.api.version": "API version",
"label.apikey": "API key",
"label.app.cookie": "AppCookie",
@ -1796,6 +1799,7 @@
"label.replace.acl": "Replace ACL",
"label.replace.acl.list": "Replace ACL list",
"label.report.bug": "Ask a question or Report an issue",
"label.request": "Request",
"label.required": "Required",
"label.requireshvm": "HVM",
"label.requiresupgrade": "Requires upgrade",

View File

@ -19,6 +19,7 @@
import { UserLayout, BasicLayout, RouteView } from '@/layouts'
import AutogenView from '@/views/AutogenView.vue'
import IFramePlugin from '@/views/plugins/IFramePlugin.vue'
import ApiDocsPlugin from '@/views/plugins/ApiDocsPlugin.vue'
import { shallowRef } from 'vue'
import { vueProps } from '@/vue-app'
@ -275,6 +276,16 @@ export function asyncRouterMap () {
})
}
const apidocs = vueProps.$config.apidocs
if (apidocs !== false) {
routerMap[0].children.push({
path: '/apidocs/',
name: 'apidocs',
component: shallowRef(ApiDocsPlugin),
meta: { title: 'label.api.docs', icon: 'read-outlined' }
})
}
return routerMap
}

View File

@ -61,6 +61,7 @@ import {
Tree,
Calendar,
Slider,
Result,
AutoComplete,
Collapse,
Space,
@ -133,5 +134,6 @@ export default {
app.use(Descriptions)
app.use(Space)
app.use(Statistic)
app.use(Result)
}
}

View File

@ -314,7 +314,10 @@ const user = {
const apiName = api.name
apis[apiName] = {
params: api.params,
response: api.response
response: api.response,
isasync: api.isasync,
since: api.since,
description: api.description
}
}
commit('SET_APIS', apis)

View File

@ -471,6 +471,10 @@ a {
width: auto;
}
.ant-list-item.selected-item {
background-color: @primary-color-light;
}
.ant-select-arrow .anticon {
vertical-align: top;
}

View File

@ -0,0 +1,222 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
<template>
<div>
<resource-layout>
<template #left>
<a-card :bordered="false">
<a-auto-complete
v-model:value="query"
:options="options.filter(value => value.value.toLowerCase().includes(query.toLowerCase()))"
style="width: 100%"
>
<a-input-search
size="default"
:placeholder="$t('label.search')"
v-model:value="query"
allow-clear
enter-button
>
<template #prefix><search-outlined /></template>
</a-input-search>
</a-auto-complete>
<a-list style="margin-top: 12px; height:580px; overflow-y: scroll;" size="small" :data-source="Object.keys($store.getters.apis).sort()">
<template #renderItem="{ item }">
<a>
<a-list-item
v-if="item.toLowerCase().includes(query.toLowerCase())"
@click="showApi(item)"
style="padding-left: 12px"
:class="selectedApi === item ? 'selected-item' : ''">
{{ item }} <a-tag v-if="$store.getters.apis[item].isasync" color="blue">async</a-tag>
</a-list-item>
</a>
</template>
</a-list>
<a-divider style="margin-bottom: 12px" />
<span>{{ Object.keys($store.getters.apis).length }} {{ $t('label.api.docs.count') }}</span>
</a-card>
</template>
<template #right>
<a-card
class="spin-content"
:bordered="true"
style="width: 100%; overflow-x: auto">
<span v-if="selectedApi && selectedApi in $store.getters.apis">
<h2>{{ selectedApi }}
<a-tag v-if="$store.getters.apis[selectedApi].isasync" color="blue">Asynchronous API</a-tag>
<a-tag v-if="$store.getters.apis[selectedApi].since">Since {{ $store.getters.apis[selectedApi].since }}</a-tag>
<tooltip-button
tooltipPlacement="right"
:tooltip="$t('label.copy') + ' ' + selectedApi"
icon="CopyOutlined"
type="outlined"
size="small"
@onClick="$message.success($t('label.copied.clipboard'))"
:copyResource="selectedApi" />
</h2>
<p>{{ $store.getters.apis[selectedApi].description }}</p>
<h3>{{ $t('label.request') }} {{ $t('label.params') }}:</h3>
<a-table
: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'}]"
:data-source="selectedParams"
:pagination="false"
size="small">
<template #bodyCell="{text, column, record}">
<a-tag v-if="record.since && column.dataIndex === 'description'">Since {{ record.since }}</a-tag>
<span v-if="record.required === true"><strong>{{ text }}</strong></span>
<span v-else>{{ text }}</span>
</template>
</a-table>
<br/>
<h3>{{ $t('label.response') }} {{ $t('label.params') }}:</h3>
<a-table
:columns="[{title: $t('label.name'), dataIndex: 'name'}, {title: $t('label.type'), dataIndex: 'type'}, {title: $t('label.description'), dataIndex: 'description'}]"
:data-source="selectedResponse"
:pagination="false"
size="small" />
</span>
<span v-else>
<a-alert
:message="$t('label.api.docs')"
type="info"
show-icon
banner>
<template #description>
<a href="https://docs.cloudstack.apache.org/en/latest/developersguide/dev.html" target="_blank">{{ $t('label.api.docs.description') }}</a>
</template>
</a-alert>
<a-result
status="success"
:title="$t('label.download') + ' CloudStack CloudMonkey CLI'"
sub-title="For API automation and orchestration"
>
<template #extra>
<a-button type="primary"><a href="https://github.com/apache/cloudstack-cloudmonkey/releases" target="_blank">{{ $t('label.download') }} CLI</a></a-button>
<a-button><a href="https://github.com/apache/cloudstack-cloudmonkey/wiki/Usage" target="_blank">{{ $t('label.open.documentation') }} (CLI)</a></a-button>
<br/>
<br/>
<div v-if="showKeys">
<key-outlined />
<strong>
{{ $t('label.apikey') }}
<tooltip-button
tooltipPlacement="right"
:tooltip="$t('label.copy') + ' ' + $t('label.apikey')"
icon="CopyOutlined"
type="dashed"
size="small"
@onClick="$message.success($t('label.copied.clipboard'))"
:copyResource="userkeys.apikey" />
</strong>
<div>
{{ userkeys.apikey.substring(0, 20) }}...
</div>
<br/>
<lock-outlined />
<strong>
{{ $t('label.secretkey') }}
<tooltip-button
tooltipPlacement="right"
:tooltip="$t('label.copy') + ' ' + $t('label.secretkey')"
icon="CopyOutlined"
type="dashed"
size="small"
@onClick="$message.success($t('label.copied.clipboard'))"
:copyResource="userkeys.secretkey" />
</strong>
<div>
{{ userkeys.secretkey.substring(0, 20) }}...
</div>
</div>
</template>
</a-result>
</span>
</a-card>
</template>
</resource-layout>
</div>
</template>
<script>
import { api } from '@/api'
import ResourceLayout from '@/layouts/ResourceLayout'
import TooltipButton from '@/components/widgets/TooltipButton'
export default {
name: 'ApiDocsPlugin',
components: {
ResourceLayout,
TooltipButton
},
data () {
return {
query: '',
selectedApi: '',
selectedParams: [],
selectedResponse: [],
showKeys: false,
userkeys: {},
options: [
{ value: 'VirtualMachine', label: 'Instance' },
{ value: 'Kubernetes', label: 'Kubernetes' },
{ value: 'Volume', label: 'Volume' },
{ value: 'Snapshot', label: 'Snapshot' },
{ value: 'Backup', label: 'Backup' },
{ value: 'Network', label: 'Network' },
{ value: 'IpAddress', label: 'IP Address' },
{ value: 'VPN', label: 'VPN' },
{ value: 'VPC', label: 'VPC' },
{ value: 'NetworkACL', label: 'Network ACL' },
{ value: 'SecurityGroup', label: 'Security Group' },
{ value: 'Template', label: 'Template' },
{ value: 'ISO', label: 'ISO' },
{ value: 'SSH', label: 'SSH' },
{ value: 'Project', label: 'Project' },
{ value: 'Account', label: 'Account' },
{ value: 'User', label: 'User' },
{ value: 'Event', label: 'Event' },
{ value: 'Offering', label: 'Offering' },
{ value: 'Zone', label: 'Zone' }
]
}
},
created () {
if (!('getUserKeys' in this.$store.getters.apis)) {
return
}
api('getUserKeys', { id: this.$store.getters.userInfo.id }).then(json => {
this.userkeys = json.getuserkeysresponse.userkeys
if (this.userkeys && this.userkeys.secretkey) {
this.showKeys = true
}
})
},
methods: {
showApi (api) {
this.selectedApi = api
this.selectedParams = this.$store.getters.apis[api].params
.sort((a, b) => (a.name > b.name) ? 1 : ((b.name > a.name) ? -1 : 0))
.sort((a, b) => (a.required > b.required) ? -1 : ((b.required > a.required) ? 1 : 0))
.filter(value => Object.keys(value).length > 0)
this.selectedResponse = this.$store.getters.apis[api].response.filter(value => Object.keys(value).length > 0)
}
}
}
</script>