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>

Reply via email to