Skip to content

Commit 5baac44

Browse files
authored
ui: add UI too to view and download usage records (#8615)
This PR adds a new UI tool for admins for viewing and downloading usage records. This PR also makes startdate and enddate as non required params for generateUsageRecords. (Fixes: #7133)
1 parent 7214c13 commit 5baac44

File tree

12 files changed

+983
-43
lines changed

12 files changed

+983
-43
lines changed

api/src/main/java/org/apache/cloudstack/api/command/admin/usage/GenerateUsageRecordsCmd.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,13 @@ public class GenerateUsageRecordsCmd extends BaseCmd {
4747

4848
@Parameter(name = ApiConstants.END_DATE,
4949
type = CommandType.DATE,
50-
required = true,
50+
required = false,
5151
description = "End date range for usage record query. Use yyyy-MM-dd as the date format, e.g. startDate=2009-06-03.")
5252
private Date endDate;
5353

5454
@Parameter(name = ApiConstants.START_DATE,
5555
type = CommandType.DATE,
56-
required = true,
56+
required = false,
5757
description = "Start date range for usage record query. Use yyyy-MM-dd as the date format, e.g. startDate=2009-06-01.")
5858
private Date startDate;
5959

ui/public/locales/en.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,7 @@
618618
"label.datetime.filter.starting": "Starting <b>{startDate}</b>.",
619619
"label.datetime.filter.up.to": "Up to <b>{endDate}</b>.",
620620
"label.day": "Day",
621+
"label.days": "Days",
621622
"label.day.of.month": "Day of month",
622623
"label.day.of.week": "Day of week",
623624
"label.db.usage.metrics": "DB/Usage server",
@@ -806,6 +807,7 @@
806807
"label.done": "Done",
807808
"label.down": "Down",
808809
"label.download": "Download",
810+
"label.download.csv": "Download CSV",
809811
"label.download.kubeconfig.cluster": "Download kubeconfig for the cluster <br><br> The <code><b>kubectl</b></code> command-line tool uses kubeconfig files to find the information it needs to choose a cluster and communicate with the API server of a cluster.",
810812
"label.download.kubectl": "Download <code><b>kubectl</b></code> tool for cluster's Kubernetes version",
811813
"label.download.kubernetes.cluster.config": "Download Kubernetes cluster config",
@@ -924,6 +926,7 @@
924926
"label.fetch.instances": "Fetch Instances",
925927
"label.fetch.latest": "Fetch latest",
926928
"label.filename": "File Name",
929+
"label.fetched": "Fetched",
927930
"label.files": "Alternate files to retrieve",
928931
"label.filter": "Filter",
929932
"label.filter.annotations.all": "All comments",
@@ -1229,6 +1232,8 @@
12291232
"label.label": "Label",
12301233
"label.last.updated": "Last update",
12311234
"label.lastannotated": "Last annotation date",
1235+
"label.lastheartbeat": "Last heartbeat",
1236+
"label.lastsuccessfuljob": "Last successful job",
12321237
"label.lastboottime": "Boot time of the management server machine",
12331238
"label.lastname": "Last name",
12341239
"label.lastname.lower": "lastname",
@@ -1483,6 +1488,7 @@
14831488
"label.no.items": "No available Items",
14841489
"label.no.matching.offering": "No matching offering found",
14851490
"label.no.matching.network": "No matching Networks found",
1491+
"label.no.usage.records": "No usage records found",
14861492
"label.noderootdisksize": "Node root disk size (in GB)",
14871493
"label.nodiskcache": "No disk cache",
14881494
"label.none": "None",
@@ -1523,6 +1529,7 @@
15231529
"label.of": "of",
15241530
"label.of.month": "of month",
15251531
"label.offerha": "Offer HA",
1532+
"label.offeringid": "Offering ID",
15261533
"label.offeringtype": "Compute offering type",
15271534
"label.ok": "OK",
15281535
"label.only.end.date.and.time": "Only end date and time",
@@ -1700,6 +1707,8 @@
17001707
"label.publicnetwork": "Public Network",
17011708
"label.publicport": "Public port",
17021709
"label.purgeresources": "Purge Resources",
1710+
"label.purge.usage.records.success": "Successfuly purged usage records",
1711+
"label.purge.usage.records.error": "Failed while purging usage records",
17031712
"label.purpose": "Purpose",
17041713
"label.qostype": "QoS type",
17051714
"label.quickview": "Quick view",
@@ -1731,7 +1740,14 @@
17311740
"label.rados.secret": "RADOS secret",
17321741
"label.rados.user": "RADOS user",
17331742
"label.ram": "RAM",
1743+
"label.range.today": "Today",
1744+
"label.range.yesterday": "Yesterday",
1745+
"label.range.last.1week": "Last 1 week",
1746+
"label.range.last.2week": "Last 2 weeks",
1747+
"label.range.last.1month": "Last 1 month",
1748+
"label.range.last.3month": "Last 3 months",
17341749
"label.raw.data": "Raw data",
1750+
"label.rawusage": "Raw usage (in hours)",
17351751
"label.rbd": "RBD",
17361752
"label.rbdid": "Cephx user",
17371753
"label.rbdmonitor": "Ceph monitor",
@@ -1975,6 +1991,7 @@
19751991
"label.sharedrouteripv6": "IPv6 address for the VR in this shared Network.",
19761992
"label.sharewith": "Share with",
19771993
"label.showing": "Showing",
1994+
"label.show.usage.records": "Show usage records",
19781995
"label.shrinkok": "Shrink OK",
19791996
"label.shutdown": "Shutdown",
19801997
"label.shutdown.provider": "Shutdown provider",
@@ -2283,8 +2300,22 @@
22832300
"label.upload.volume.from.url": "Upload volume from URL",
22842301
"label.url": "URL",
22852302
"label.usage.explanation": "Note: Only the usage server that owns the active usage job is shown here.",
2303+
"label.usage": "Usage",
2304+
"label.usage.records.downloading": "Downloading usage records",
2305+
"label.usage.records.fetch.child.domains": "Fetch usage records for child domains",
2306+
"label.usage.records.usagetype.required": "Usage type is required with resource ID",
2307+
"label.usage.records.generate": "Generate usage records",
2308+
"label.usage.records.generate.after": "Usage records will be created for the period after ",
2309+
"label.usage.records.generated": "A job has been created to generate usage records.",
2310+
"label.usage.records.generate.description": "If the scheduled usage job was not run or failed, this will generate records(only if there any records to be generated)",
2311+
"label.usage.records.purge": "Purge usage records",
2312+
"label.usage.records.purge.days": "Purge records older than",
2313+
"label.usage.records.purge.days.description": "Purge records older than the specified number of days.",
2314+
"label.usage.records.purge.alert": "Purging usage records will permanently delete the records from the database. Depending on the data being deleted, this can increase load on the database and may take a while. Are you sure you want to continue?",
2315+
"label.usageid": "Resource ID",
22862316
"label.usageinterface": "Usage interface",
22872317
"label.usagename": "Usage type",
2318+
"label.usagetype": "Usage type",
22882319
"label.usageunit": "Unit",
22892320
"label.usageislocal": "A Usage Server is installed locally",
22902321
"label.usagetypedescription": "Usage description",

ui/src/components/view/ListView.vue

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
:pagination="false"
2626
:rowSelection="explicitlyAllowRowSelection || enableGroupAction() || $route.name === 'event' ? {selectedRowKeys: selectedRowKeys, onChange: onSelectChange, columnWidth: 30} : null"
2727
:rowClassName="getRowClassName"
28+
@resizeColumn="handleResizeColumn"
2829
style="overflow-y: auto"
2930
>
3031
<template #customFilterDropdown>
@@ -98,6 +99,9 @@
9899
<template v-if="column.key === 'templatetype'">
99100
<span>{{ text }}</span>
100101
</template>
102+
<template v-if="column.key === 'templateid'">
103+
<router-link :to="{ path: '/template/' + record.templateid }">{{ text }}</router-link>
104+
</template>
101105
<template v-if="column.key === 'type'">
102106
<span v-if="['USER.LOGIN', 'USER.LOGOUT', 'ROUTER.HEALTH.CHECKS', 'FIREWALL.CLOSE', 'ALERT.SERVICE.DOMAINROUTER'].includes(text)">{{ $t(text.toLowerCase()) }}</span>
103107
<span v-else>{{ text }}</span>
@@ -245,7 +249,7 @@
245249
</template>
246250
<template v-if="column.key === 'vpcname'">
247251
<a v-if="record.vpcid">
248-
<router-link :to="{ path: '/vpc/' + record.vpcid }">{{ text }}</router-link>
252+
<router-link :to="{ path: '/vpc/' + record.vpcid }">{{ text || record.vpcid }}</router-link>
249253
</a>
250254
<span v-else>{{ text }}</span>
251255
</template>
@@ -276,6 +280,9 @@
276280
<template v-if="column.key === 'level'">
277281
<router-link :to="{ path: '/event/' + record.id }">{{ text }}</router-link>
278282
</template>
283+
<template v-if="column.key === 'usageType'">
284+
{{ usageTypeMap[record.usagetype] }}
285+
</template>
279286

280287
<template v-if="column.key === 'clustername'">
281288
<router-link :to="{ path: '/cluster/' + record.clusterid }">{{ text }}</router-link>
@@ -319,7 +326,7 @@
319326
<span v-else>{{ text }}</span>
320327
</template>
321328
<template v-if="column.key === 'zone'">
322-
<router-link v-if="record.zoneid && !record.zoneid.includes(',') && $router.resolve('/zone/' + record.zoneid).matched[0].redirect !== '/exception/404'" :to="{ path: '/zone/' + record.zoneid }">{{ text }}</router-link>
329+
<router-link v-if="record.zoneid && !record.zoneid.includes(',') && $router.resolve('/zone/' + record.zoneid).matched[0].redirect !== '/exception/404'" :to="{ path: '/zone/' + record.zoneid }">{{ text || record.zoneid }}</router-link>
323330
<span v-else>{{ text }}</span>
324331
</template>
325332
<template v-if="column.key === 'zonename'">
@@ -374,6 +381,9 @@
374381
<template v-if="column.key === 'payloadurl'">
375382
<copy-label :label="text" />
376383
</template>
384+
<template v-if="column.key === 'usageid'">
385+
<copy-label :label="text" />
386+
</template>
377387
<template v-if="column.key === 'eventtype'">
378388
<router-link v-if="$router.resolve('/event/' + record.eventid).matched[0].redirect !== '/exception/404'" :to="{ path: '/event/' + record.eventid }">{{ text }}</router-link>
379389
<span v-else>{{ text }}</span>
@@ -406,6 +416,9 @@
406416
<template v-if="column.key === 'duration' && ['webhook', 'webhookdeliveries'].includes($route.path.split('/')[1])">
407417
<span> {{ getDuration(record.startdate, record.enddate) }} </span>
408418
</template>
419+
<template v-if="['startdate', 'enddate'].includes(column.key) && ['usage'].includes($route.path.split('/')[1])">
420+
{{ $toLocaleDate(text.replace('\'T\'', ' ')) }}
421+
</template>
409422
<template v-if="column.key === 'order'">
410423
<div class="shift-btns">
411424
<a-tooltip :name="text" placement="top">
@@ -482,6 +495,13 @@
482495
icon="reload-outlined"
483496
:disabled="!('updateConfiguration' in $store.getters.apis)" />
484497
</template>
498+
<template v-if="column.key === 'usageActions'">
499+
<tooltip-button
500+
:tooltip="$t('label.view')"
501+
icon="search-outlined"
502+
@onClick="$emit('view-usage-record', record)" />
503+
<slot></slot>
504+
</template>
485505
<template v-if="column.key === 'tariffActions'">
486506
<tooltip-button
487507
:tooltip="$t('label.edit')"
@@ -618,6 +638,7 @@ export default {
618638
disable: 'storageallocateddisablethreshold'
619639
}
620640
},
641+
usageTypeMap: {},
621642
resourceIdToValidLinksMap: {}
622643
}
623644
},
@@ -632,6 +653,9 @@ export default {
632653
}
633654
}
634655
},
656+
created () {
657+
this.getUsageTypes()
658+
},
635659
computed: {
636660
hasSelected () {
637661
return this.selectedRowKeys.length > 0
@@ -942,6 +966,9 @@ export default {
942966
}
943967
return name
944968
},
969+
handleResizeColumn (w, col) {
970+
col.width = w
971+
},
945972
updateSelectedColumns (name) {
946973
this.$emit('update-selected-columns', name)
947974
},
@@ -965,6 +992,24 @@ export default {
965992
}
966993
var duration = Date.parse(enddate) - Date.parse(startdate)
967994
return (duration > 0 ? duration / 1000.0 : 0) + ''
995+
},
996+
getUsageTypes () {
997+
if (this.$route.path.split('/')[1] === 'usage') {
998+
api('listUsageTypes').then(json => {
999+
if (json && json.listusagetypesresponse && json.listusagetypesresponse.usagetype) {
1000+
this.usageTypes = json.listusagetypesresponse.usagetype.map(x => {
1001+
return {
1002+
id: x.usagetypeid,
1003+
value: x.description
1004+
}
1005+
})
1006+
this.usageTypeMap = {}
1007+
for (var usageType of this.usageTypes) {
1008+
this.usageTypeMap[usageType.id] = usageType.value
1009+
}
1010+
}
1011+
})
1012+
}
9681013
}
9691014
}
9701015
}

ui/src/components/widgets/Status.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@ export default {
8787
case 'InProgress':
8888
state = this.$t('state.inprogress')
8989
break
90+
case 'Down':
91+
state = this.$t('state.down')
92+
break
93+
case 'Up':
94+
state = this.$t('state.up')
95+
break
9096
}
9197
return state.charAt(0).toUpperCase() + state.slice(1)
9298
}

ui/src/config/router.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,6 @@ export function asyncRouterMap () {
224224
generateRouterMap(tools),
225225
generateRouterMap(quota),
226226
generateRouterMap(cloudian),
227-
228227
{
229228
path: '/exception',
230229
name: 'exception',

ui/src/config/section/tools.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ export default {
6161
}
6262
]
6363
},
64+
{
65+
name: 'usage',
66+
title: 'label.usage',
67+
icon: 'ContainerOutlined',
68+
permission: ['listUsageRecords'],
69+
meta: { title: 'label.usage', icon: 'ContainerOutlined' },
70+
component: () => import('@/views/infra/UsageRecords.vue')
71+
},
6472
{
6573
name: 'manageinstances',
6674
title: 'label.action.import.export.instances',

ui/src/core/lazy_lib/icons_use.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import {
5656
ClusterOutlined,
5757
CodeOutlined,
5858
CompassOutlined,
59+
ContainerOutlined,
5960
ControlOutlined,
6061
CopyOutlined,
6162
CreditCardOutlined,
@@ -220,6 +221,7 @@ export default {
220221
app.component('CloudUploadOutlined', CloudUploadOutlined)
221222
app.component('ClusterOutlined', ClusterOutlined)
222223
app.component('CodeOutlined', CodeOutlined)
224+
app.component('ContainerOutlined', ContainerOutlined)
223225
app.component('ControlOutlined', ControlOutlined)
224226
app.component('CompassOutlined', CompassOutlined)
225227
app.component('CopyOutlined', CopyOutlined)

ui/src/utils/util.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,27 @@ export function sanitizeReverse (value) {
6868
.replace(/&lt;/g, '<')
6969
.replace(/&gt;/g, '>')
7070
}
71+
72+
export function toCsv ({ keys = null, data = null, columnDelimiter = ',', lineDelimiter = '\n' }) {
73+
if (data === null || !data.length) {
74+
return null
75+
}
76+
77+
let result = ''
78+
result += keys.join(columnDelimiter)
79+
result += lineDelimiter
80+
81+
data.forEach(item => {
82+
keys.forEach(key => {
83+
if (item[key] === undefined) {
84+
item[key] = ''
85+
}
86+
result += typeof item[key] === 'string' && item[key].includes(columnDelimiter) ? `"${item[key]}"` : item[key]
87+
result += columnDelimiter
88+
})
89+
result = result.slice(0, -1)
90+
result += lineDelimiter
91+
})
92+
93+
return result
94+
}

ui/src/views/AutogenView.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -738,7 +738,7 @@ export default {
738738
})
739739
},
740740
fetchData (params = {}) {
741-
if (this.$route.name === 'deployVirtualMachine') {
741+
if (['deployVirtualMachine', 'usage'].includes(this.$route.name)) {
742742
return
743743
}
744744
if (this.routeName !== this.$route.name) {

ui/src/views/iam/RolePermissionTab.vue

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ import draggable from 'vuedraggable'
111111
import PermissionEditable from './PermissionEditable'
112112
import RuleDelete from './RuleDelete'
113113
import TooltipButton from '@/components/widgets/TooltipButton'
114+
import { toCsv } from '@/utils/util.js'
114115
115116
export default {
116117
name: 'RolePermissionTab',
@@ -249,32 +250,8 @@ export default {
249250
this.updateTable = false
250251
})
251252
},
252-
rulesDataToCsv ({ data = null, columnDelimiter = ',', lineDelimiter = '\n' }) {
253-
if (data === null || !data.length) {
254-
return null
255-
}
256-
257-
const keys = ['rule', 'permission', 'description']
258-
let result = ''
259-
result += keys.join(columnDelimiter)
260-
result += lineDelimiter
261-
262-
data.forEach(item => {
263-
keys.forEach(key => {
264-
if (item[key] === undefined) {
265-
item[key] = ''
266-
}
267-
result += typeof item[key] === 'string' && item[key].includes(columnDelimiter) ? `"${item[key]}"` : item[key]
268-
result += columnDelimiter
269-
})
270-
result = result.slice(0, -1)
271-
result += lineDelimiter
272-
})
273-
274-
return result
275-
},
276253
exportRolePermissions () {
277-
const rulesCsvData = this.rulesDataToCsv({ data: this.rules })
254+
const rulesCsvData = toCsv({ keys: ['rule', 'permission', 'description'], data: this.rules })
278255
const hiddenElement = document.createElement('a')
279256
hiddenElement.href = 'data:text/csv;charset=utf-8,' + encodeURI(rulesCsvData)
280257
hiddenElement.target = '_blank'

0 commit comments

Comments
 (0)