This is an automated email from the ASF dual-hosted git repository.
rohit pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudstack.git
The following commit(s) were added to refs/heads/main by this push:
new f24fb20e6b3 ui: add new API docs tab (#9409)
f24fb20e6b3 is described below
commit f24fb20e6b371ecb5abed2f6975a3ebb3b932e60
Author: Rohit Yadav <[email protected]>
AuthorDate: Mon Jul 22 10:46:40 2024 +0530
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]>
---
ui/public/config.json | 1 +
ui/public/locales/en.json | 4 +
ui/src/config/router.js | 11 ++
ui/src/core/lazy_lib/components_use.js | 2 +
ui/src/store/modules/user.js | 5 +-
ui/src/style/vars.less | 4 +
ui/src/views/plugins/ApiDocsPlugin.vue | 222 +++++++++++++++++++++++++++++++++
7 files changed, 248 insertions(+), 1 deletion(-)
diff --git a/ui/public/config.json b/ui/public/config.json
index 639ed4f97f1..774e414af0a 100644
--- a/ui/public/config.json
+++ b/ui/public/config.json
@@ -92,6 +92,7 @@
]
},
"plugins": [],
+ "apidocs": true,
"basicZoneEnabled": true,
"multipleServer": false,
"allowSettingTheme": true,
diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json
index cec0b641ebe..253f20294f5 100644
--- a/ui/public/locales/en.json
+++ b/ui/public/locales/en.json
@@ -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",
diff --git a/ui/src/config/router.js b/ui/src/config/router.js
index c358d215577..9d9cd0d4491 100644
--- a/ui/src/config/router.js
+++ b/ui/src/config/router.js
@@ -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
}
diff --git a/ui/src/core/lazy_lib/components_use.js
b/ui/src/core/lazy_lib/components_use.js
index 98fc9e0c816..3ee5d07a49d 100644
--- a/ui/src/core/lazy_lib/components_use.js
+++ b/ui/src/core/lazy_lib/components_use.js
@@ -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)
}
}
diff --git a/ui/src/store/modules/user.js b/ui/src/store/modules/user.js
index fb5b6ff5e0b..24302a94033 100644
--- a/ui/src/store/modules/user.js
+++ b/ui/src/store/modules/user.js
@@ -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)
diff --git a/ui/src/style/vars.less b/ui/src/style/vars.less
index fc6fdf75170..de2d494c878 100644
--- a/ui/src/style/vars.less
+++ b/ui/src/style/vars.less
@@ -471,6 +471,10 @@ a {
width: auto;
}
+.ant-list-item.selected-item {
+ background-color: @primary-color-light;
+}
+
.ant-select-arrow .anticon {
vertical-align: top;
}
diff --git a/ui/src/views/plugins/ApiDocsPlugin.vue
b/ui/src/views/plugins/ApiDocsPlugin.vue
new file mode 100644
index 00000000000..ba7f547572b
--- /dev/null
+++ b/ui/src/views/plugins/ApiDocsPlugin.vue
@@ -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>