mirror of
https://github.com/apache/cloudstack.git
synced 2025-10-26 08:42:29 +01:00
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:
parent
56c661c1df
commit
f24fb20e6b
1
ui/public/config.json
vendored
1
ui/public/config.json
vendored
@ -92,6 +92,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"plugins": [],
|
"plugins": [],
|
||||||
|
"apidocs": true,
|
||||||
"basicZoneEnabled": true,
|
"basicZoneEnabled": true,
|
||||||
"multipleServer": false,
|
"multipleServer": false,
|
||||||
"allowSettingTheme": true,
|
"allowSettingTheme": true,
|
||||||
|
|||||||
@ -348,6 +348,9 @@
|
|||||||
"label.annotation.everyone": "Visible to everyone",
|
"label.annotation.everyone": "Visible to everyone",
|
||||||
"label.anti.affinity": "Anti-affinity",
|
"label.anti.affinity": "Anti-affinity",
|
||||||
"label.anti.affinity.group": "Anti-affinity group",
|
"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.api.version": "API version",
|
||||||
"label.apikey": "API key",
|
"label.apikey": "API key",
|
||||||
"label.app.cookie": "AppCookie",
|
"label.app.cookie": "AppCookie",
|
||||||
@ -1796,6 +1799,7 @@
|
|||||||
"label.replace.acl": "Replace ACL",
|
"label.replace.acl": "Replace ACL",
|
||||||
"label.replace.acl.list": "Replace ACL list",
|
"label.replace.acl.list": "Replace ACL list",
|
||||||
"label.report.bug": "Ask a question or Report an issue",
|
"label.report.bug": "Ask a question or Report an issue",
|
||||||
|
"label.request": "Request",
|
||||||
"label.required": "Required",
|
"label.required": "Required",
|
||||||
"label.requireshvm": "HVM",
|
"label.requireshvm": "HVM",
|
||||||
"label.requiresupgrade": "Requires upgrade",
|
"label.requiresupgrade": "Requires upgrade",
|
||||||
|
|||||||
@ -19,6 +19,7 @@
|
|||||||
import { UserLayout, BasicLayout, RouteView } from '@/layouts'
|
import { UserLayout, BasicLayout, RouteView } from '@/layouts'
|
||||||
import AutogenView from '@/views/AutogenView.vue'
|
import AutogenView from '@/views/AutogenView.vue'
|
||||||
import IFramePlugin from '@/views/plugins/IFramePlugin.vue'
|
import IFramePlugin from '@/views/plugins/IFramePlugin.vue'
|
||||||
|
import ApiDocsPlugin from '@/views/plugins/ApiDocsPlugin.vue'
|
||||||
|
|
||||||
import { shallowRef } from 'vue'
|
import { shallowRef } from 'vue'
|
||||||
import { vueProps } from '@/vue-app'
|
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
|
return routerMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -61,6 +61,7 @@ import {
|
|||||||
Tree,
|
Tree,
|
||||||
Calendar,
|
Calendar,
|
||||||
Slider,
|
Slider,
|
||||||
|
Result,
|
||||||
AutoComplete,
|
AutoComplete,
|
||||||
Collapse,
|
Collapse,
|
||||||
Space,
|
Space,
|
||||||
@ -133,5 +134,6 @@ export default {
|
|||||||
app.use(Descriptions)
|
app.use(Descriptions)
|
||||||
app.use(Space)
|
app.use(Space)
|
||||||
app.use(Statistic)
|
app.use(Statistic)
|
||||||
|
app.use(Result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -314,7 +314,10 @@ const user = {
|
|||||||
const apiName = api.name
|
const apiName = api.name
|
||||||
apis[apiName] = {
|
apis[apiName] = {
|
||||||
params: api.params,
|
params: api.params,
|
||||||
response: api.response
|
response: api.response,
|
||||||
|
isasync: api.isasync,
|
||||||
|
since: api.since,
|
||||||
|
description: api.description
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
commit('SET_APIS', apis)
|
commit('SET_APIS', apis)
|
||||||
|
|||||||
@ -471,6 +471,10 @@ a {
|
|||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-list-item.selected-item {
|
||||||
|
background-color: @primary-color-light;
|
||||||
|
}
|
||||||
|
|
||||||
.ant-select-arrow .anticon {
|
.ant-select-arrow .anticon {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|||||||
222
ui/src/views/plugins/ApiDocsPlugin.vue
Normal file
222
ui/src/views/plugins/ApiDocsPlugin.vue
Normal 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>
|
||||||
Loading…
x
Reference in New Issue
Block a user