blob: cefc169c8d4a0c228c24b2042a735a7a181eeaa8 [file]
// 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>
<filter-quota-data-by-period-view @fetchData="fetchData"/>
<div v-if="dataSource.length > 0">
<hr class="m-20-0" />
<div class="chart-row">
<a-space direction="vertical">
<div>
<a-radio-group
v-model:value="graphType"
buttonStyle="solid">
<a-radio-button value="bar_chart">
{{ $t('label.total') }}
</a-radio-button>
<a-radio-button value="line_chart">
{{ $t('label.quota.statement.history') }}
</a-radio-button>
<a-radio-button value="incremental_chart">
{{ $t('label.quota.statement.cumulative.history') }}
</a-radio-button>
</a-radio-group>
</div>
</a-space>
</div>
<div style="font-size: 18px">
<strong> {{ $t('label.quota.usage.types.summary') }} </strong>
</div>
<export-to-csv-button :action="exportDataToCsv" />
<bar-chart v-if="graphType === 'bar_chart'" :chart-options="getBarChartOptions()" :chart-data="getUsageTypeBarChartData()"/>
<resource-stats-line-chart
v-else
:chart-labels="usageLineChartLabels"
:chart-data="getEntryForCurrentGraphType(this.usageLineChartData)"
:yAxisIncrementValue="getYAxisIncrement(getEntryForCurrentGraphType(this.YAxisMax.usageTypes))"
:yAxisMeasurementUnit="''"
/>
<a-table
size="small"
:loading="loading"
:columns="columns"
:dataSource="dataSource.filter(row => row.quota > 0)"
:rowKey="record => record.name"
:pagination="false"
:scroll="{ y: '55vh' }">
<template #bodyCell="{ column, text, record }">
<template v-if="column.dataIndex === 'name'">
<a @click="handleSelectedTypeChange(`${record.type}-${record.name}`)">{{ $t(text) }}</a>
</template>
<template v-if="column.dataIndex === 'unit'">
{{ $t(text) }}
</template>
<template v-if="column.dataIndex === 'quota'">
<a-tooltip placement="right">
<template #title>
{{ text }}
</template>
<span class="dotted-underline">{{ parseFloat(text).toFixed(2) }}</span>
</a-tooltip>
</template>
</template>
<template #footer >
<div style="text-align: right;">
{{ $t('label.currency') }}: <b>{{ currency }}</b><br/>
{{ $t('label.quota.total.consumption') }}:
<a-tooltip placement="bottom">
<template #title>
{{ totalQuota }}
</template>
<b class="dotted-underline">{{ parseFloat(totalQuota).toFixed(2) }}</b>
</a-tooltip>
</div>
</template>
</a-table>
<hr class="m-20-0" id="resource-by-type" />
<strong>
<tooltip-label style="font-size: 18px" :title="$t('label.quota.usage.resources.by.type')" :tooltip="$t('message.quota.usage.resource.warn')"/>
</strong>
<a-select
v-model:value="selectedType"
class="w-100"
style="margin: 5px 0 10px 0px"
show-search
@change="handleSelectedTypeChange">
<a-select-option
v-for="quotaType of getQuotaTypesFiltered()"
:value="`${quotaType.id}-${quotaType.type}`"
:key="quotaType.id">
{{ $t(quotaType.type) }}
</a-select-option>
</a-select>
<export-to-csv-button v-if="dataSourceResource.length > 0" :action="exportResourcesToCsv" :label="`label.export.resources.csv`" />
<bar-chart v-if="dataSourceResource.length > 0 && graphType === 'bar_chart'" :chart-options="getBarChartOptions()" :chart-data="getResourceBarChartData()"/>
<resource-stats-line-chart
v-else-if="dataSourceResource.length > 0 && graphType !== 'bar_chart'"
:chart-labels="resourceLineChartLabels"
:chart-data="getEntryForCurrentGraphType(this.resourceLineChartData)"
:yAxisIncrementValue="getYAxisIncrement(getEntryForCurrentGraphType(this.YAxisMax.resources))"
:yAxisMeasurementUnit="''"
/>
<a-table
size="small"
:loading="loadingResources"
:columns="resourceColumns"
:dataSource="dataSourceResource"
:rowKey="(record) => record.displayname"
:pagination="false"
:scroll="{ y: '55vh' }">
<template #title v-if="dataSourceResource.length > 0">
<div>{{ $t('label.currency') }}: <b>{{ currency }}</b></div>
</template>
<template #bodyCell="{ column, text, record }">
<template v-if="column.dataIndex === 'displayname'">
<span v-if="!text">
-
</span>
<span v-if="text === '<untraceable>' || !record.resourceid">
{{ text }}
</span>
<a v-else @click="handleSelectedResourceChange(record.resourceid)">
{{ text }}
</a>
</template>
<template v-if="column.dataIndex === 'quotaconsumed'">
<a-tooltip placement="right">
<template #title>
{{ text }}
</template>
<span class="dotted-underline">{{ parseFloat(text).toFixed(2) }}</span>
</a-tooltip>
</template>
</template>
</a-table>
<hr class="m-20-0" id="details-by-resource" />
<strong>
<tooltip-label style="font-size: 18px" :title="$t('label.quota.usage.details.by.resource')"/>
</strong>
<a-select
v-model:value="selectedResource"
class="w-100"
style="margin: 5px 0 10px 0px"
show-search
@change="handleSelectedResourceChange"
:disabled="getResources().length == 0">
<a-select-option
v-for="item of getResources()"
:value="item.id"
:key="item.id">
{{ $t(item.name) }}
</a-select-option>
</a-select>
<export-to-csv-button v-if="dataSourceTariffs.length > 0" :action="exportResourceDetailsToCsv" :label="`label.export.details.csv`" />
<bar-chart v-if="dataSourceTariffs.length > 0 && graphType === 'bar_chart'" :chart-options="getBarChartOptions()" :chart-data="getTariffsBarChartData()"/>
<resource-stats-line-chart
v-else-if="dataSourceTariffs.length > 0 && graphType !== 'bar_chart'"
:chart-labels="tariffLineChartLabels"
:chart-data="getEntryForCurrentGraphType(this.tariffLineChartData)"
:yAxisIncrementValue="getYAxisIncrement(getEntryForCurrentGraphType(this.YAxisMax.tariffs))"
:yAxisMeasurementUnit="''"
/>
<a-table
size="small"
:loading="loadingTariffs"
:columns="resourceDetailsColumns"
:dataSource="dataSourceTariffs"
:rowKey="record => record.tariffname + '-' + record.startdate"
:pagination="false"
:scroll="{ y: '55vh' }">
<template #title v-if="dataSourceTariffs.length > 0">
<div>{{ $t('label.currency') }}: <b>{{ currency }}</b></div>
</template>
<template #bodyCell="{ column, text, record }">
<template v-if="column.dataIndex === 'tariffname'">
<a v-if="'quotaTariffList' in $store.getters.apis" :href="`#/quotatariff/${record.tariffid}`" target="_blank">
{{ text }}
</a>
<span v-else>
{{ text }}
</span>
</template>
<template v-if="column.dataIndex === 'enddate'">
{{ $toLocaleDate(text) }}
</template>
<template v-if="column.dataIndex === 'startdate'">
{{ $toLocaleDate(text) }}
</template>
<template v-if="column.dataIndex === 'quotaconsumed'">
<a-tooltip placement="right">
<template #title>
{{ text }}
</template>
<span class="dotted-underline">{{ parseFloat(text).toFixed(2) }}</span>
</a-tooltip>
</template>
</template>
</a-table>
</div>
</div>
</template>
<script>
import { getAPI } from '@/api'
import FilterQuotaDataByPeriodView from './FilterQuotaDataByPeriodView.vue'
import BarChart from '@/components/view/charts/BarChart.vue'
import ResourceStatsLineChart from '@/components/view/stats/ResourceStatsLineChart.vue'
import ExportToCsvButton from '@/components/view/buttons/ExportToCsvButton.vue'
import { getChartColorObject } from '@/utils/chart'
import { getQuotaTypeByName, getQuotaTypes } from '@/utils/quota'
import TooltipLabel from '@/components/widgets/TooltipLabel'
import { downloadDataAsCsv } from '@/utils/util.js'
import * as dateUtils from '@/utils/date'
export default {
name: 'QuotaUsageTab',
components: {
FilterQuotaDataByPeriodView,
BarChart,
ExportToCsvButton,
ResourceStatsLineChart,
TooltipLabel
},
data () {
return {
dataSource: [],
selectedType: '',
loadingResources: false,
dataSourceResource: [],
selectedResource: '',
loadingTariffs: false,
dataSourceTariffs: [],
startDate: undefined,
endDate: undefined,
graphType: 'bar_chart',
usageLineChartLabels: [],
resourceLineChartLabels: [],
tariffLineChartLabels: [],
usageLineChartData: {},
resourceLineChartData: {},
tariffLineChartData: {},
YAxisMax: {}
}
},
watch: {
graphType (newGraphType) {
if (newGraphType === 'bar_chart') {
return
}
this.prepareDataForUsageTypeLineGraph()
if (!this.selectedType) {
return
}
this.prepareDataForResourceLineGraph()
if (!this.selectedResource) {
return
}
this.prepareDataForTariffLineGraph()
}
},
computed: {
columns () {
return [
{
title: this.$t('label.quota.type.name'),
dataIndex: 'name',
width: 'calc(100% / 3)',
sorter: (a, b) => a.name.localeCompare(b.name)
},
{
title: this.$t('label.quota.type.unit'),
dataIndex: 'unit',
width: 'calc(100% / 3)',
sorter: (a, b) => a.unit.localeCompare(b.unit)
},
{
title: this.$t('label.quota.consumed'),
dataIndex: 'quota',
width: 'calc(100% / 3)',
sorter: (a, b) => a.quota - b.quota,
defaultSortOrder: 'descend'
}
]
},
resourceColumns () {
return [
{
title: this.$t('label.resource'),
dataIndex: 'displayname',
width: '50%',
sorter: (a, b) => a.displayname.localeCompare(b.displayname),
defaultSortOrder: 'ascend'
},
{
title: this.$t('label.quota.consumed'),
dataIndex: 'quotaconsumed',
width: '50%',
sorter: (a, b) => a.quotaconsumed - b.quotaconsumed
}
]
},
resourceDetailsColumns () {
return [
{
title: this.$t('label.quota.tariff'),
dataIndex: 'tariffname',
sorter: (a, b) => a.tariffname.localeCompare(b.tariffname)
},
{
title: this.$t('label.start.date'),
dataIndex: 'startdate',
sorter: (a, b) => a.startdate.localeCompare(b.startdate),
defaultSortOrder: 'descend'
},
{
title: this.$t('label.end.date'),
dataIndex: 'enddate',
sorter: (a, b) => a.enddate.localeCompare(b.enddate)
},
{
title: this.$t('label.quota.consumed'),
dataIndex: 'quotaconsumed',
sorter: (a, b) => a.quotaconsumed - b.quotaconsumed
}
]
}
},
methods: {
async fetchData (startDate, endDate, keepMoment = true) {
if (this.loading) return
this.startDate = dateUtils.parseDayJsObject({ value: startDate, keepMoment: keepMoment })
this.endDate = dateUtils.parseDayJsObject({ value: endDate, keepMoment: keepMoment })
this.loading = true
this.dataSource = []
this.dataSourceResource = []
this.dataSourceTariffs = []
this.selectedResource = ''
this.selectedType = ''
try {
const quotaStatement = await this.getQuotaStatement({
startdate: this.startDate,
enddate: this.endDate
})
if (!quotaStatement) {
return
}
this.dataSource = quotaStatement.quotausage.filter(row => row.quota !== 0)
if (this.dataSource.length === 0) {
this.$notification.info({ message: this.$t('message.request.no.data') })
}
this.currency = quotaStatement.currency
this.totalQuota = quotaStatement.totalquota
if (this.graphType !== 'bar_chart') {
this.prepareDataForUsageTypeLineGraph()
}
} finally {
this.loading = false
}
},
async fetchResourceData () {
if (this.selectedType === '' || this.loadingResources) return
this.dataSourceResource = []
this.loadingResources = true
try {
const quotaStatement = await this.getQuotaStatement({
startdate: this.startDate,
enddate: this.endDate,
showresources: true,
type: this.selectedType.split('-')[0]
})
this.dataSourceResource = quotaStatement.quotausage[0].resources.filter(row => row.quotaconsumed !== 0)
if (this.dataSourceResource.length === 0) {
this.$notification.info({ message: this.$t('message.request.no.data') })
}
if (this.graphType !== 'bar_chart') {
this.prepareDataForResourceLineGraph()
}
} finally {
this.loadingResources = false
}
},
async fetchTariffData () {
if (this.selectedResource === '' || this.loadingTariffs) return
this.dataSourceTariffs = []
this.loadingTariffs = true
try {
const quotaResourceStatement = await getAPI('quotaResourceStatement', {
startdate: this.startDate,
enddate: this.endDate,
usagetype: this.selectedType.split('-')[0],
id: this.selectedResource,
accountid: this.$route.params?.id,
ignoreproject: true
}).then(json => json.quotaresourcestatementresponse?.quotaresourcestatement?.items || [])
this.dataSourceTariffs = quotaResourceStatement.map(quotaUsage => ({
...quotaUsage,
startdate: dateUtils.parseDayJsObject({ value: quotaUsage.startdate, keepMoment: false }),
enddate: dateUtils.parseDayJsObject({ value: quotaUsage.enddate, keepMoment: false })
})).filter(row => row.quotaconsumed !== 0)
if (this.dataSourceTariffs.length === 0) {
this.$notification.info({ message: this.$t('message.request.no.data') })
}
if (this.graphType !== 'bar_chart') {
this.prepareDataForTariffLineGraph()
}
} finally {
this.loadingTariffs = false
}
},
async getQuotaStatement (apiParams) {
const params = {
ignoreproject: true,
accountid: this.$route.params?.id,
...apiParams
}
return await getAPI('quotaStatement', params)
.then(json => json.quotastatementresponse.statement || {})
},
getBarChartOptions () {
return { responsive: true }
},
getUsageTypeBarChartData () {
const datasets = []
for (const row of this.dataSource) {
datasets.push({
label: this.$t(row.name),
data: [row.quota],
...this.getColor(row)
})
}
return { labels: [this.$t('label.quota.type.name')], datasets }
},
getResourceBarChartData () {
const datasets = []
for (const row of this.dataSourceResource) {
datasets.push({
label: row.displayname,
data: [row.quotaconsumed],
...this.getColor(row)
})
}
return { labels: [this.$t('label.resource')], datasets }
},
getTariffsBarChartData () {
const aggregatedTariffs = this.aggregateTariffQuotas()
const datasets = []
for (const key in aggregatedTariffs) {
datasets.push({
label: key,
data: [aggregatedTariffs[key]],
...this.getColor({ tariffname: key })
})
}
return { labels: [this.$t('label.quota.tariff')], datasets }
},
aggregateTariffQuotas () {
const tariffs = {}
for (const row of this.dataSourceTariffs) {
const currentValue = tariffs[row.tariffname] ?? 0
tariffs[row.tariffname] = currentValue + row.quotaconsumed
}
return tariffs
},
setUsageTypeLineChartData () {
this.usageLineChartLabels = this.getLineChartLabelsForData(this.dataSource)
this.usageLineChartData = this.prepareLineChartData(this.dataSource, this.usageLineChartLabels)
},
setResourceLineChartData () {
this.resourceLineChartLabels = this.getLineChartLabelsForData(this.dataSourceResource)
this.resourceLineChartData = this.prepareLineChartData(this.dataSourceResource, this.resourceLineChartLabels)
},
setTariffLineChartData () {
this.dataSourceTariffs.sort((a, b) => new Date(a.enddate) - new Date(b.enddate))
const usageGroupedByTariffName = this.groupUsageByTariffName()
const transformedData = Object.values(usageGroupedByTariffName)
this.tariffLineChartLabels = this.getLineChartLabelsForData(transformedData)
this.tariffLineChartData = this.prepareLineChartData(transformedData, this.tariffLineChartLabels)
},
groupUsageByTariffName () {
const groupedData = {}
this.dataSourceTariffs.forEach((obj) => {
if (!(obj.tariffname in groupedData)) {
groupedData[obj.tariffname] = { tariffname: obj.tariffname, history: [] }
}
groupedData[obj.tariffname].history.push(obj)
})
return groupedData
},
getLineChartLabelForDate (date) {
return this.$toLocalDate(date)
},
getLineChartLabelsForData (data) {
const lineChartLabels = [this.getLineChartLabelForDate(this.startDate)]
for (const row of data) {
let isPreviousZero = true
for (let i = 0; i < row.history.length; i++) {
const item = row.history[i]
const isCurrentZero = item.quotaconsumed === 0
if (isCurrentZero && isPreviousZero) {
continue
}
if (isPreviousZero) {
// Previous was zero, but we current is not. Push our startdate to have an accurate curve
this.pushDateToLabelsIfNotPresent(lineChartLabels, this.getLineChartLabelForDate(item.startdate))
}
this.pushDateToLabelsIfNotPresent(lineChartLabels, this.getLineChartLabelForDate(item.enddate))
isPreviousZero = isCurrentZero
}
}
lineChartLabels.sort((a, b) => new Date(a) - new Date(b))
return lineChartLabels
},
pushDateToLabelsIfNotPresent (lineChartLabels, date) {
const hasNearDate = lineChartLabels.some(d => {
const diff = Math.abs(new Date(date) - new Date(d).getTime())
return diff < 5 * 1000 * 60 // Do not push the label if there is already one within 5 minutes
})
if (!hasNearDate) {
lineChartLabels.push(date)
return true
}
return false
},
calculatePaddedMax (values) {
return Math.ceil(Math.max(...values) * 1.2)
},
getChartDataMaxValues (chartData) {
return Object.values(chartData).map(item => Math.max(...item.data.map(obj => obj.stat)))
},
setYAxisMaxForUsageTypes () {
this.YAxisMax.usageTypes = {
incremental: this.calculatePaddedMax(this.getChartDataMaxValues(this.usageLineChartData.incremental)),
history: this.calculatePaddedMax(this.getChartDataMaxValues(this.usageLineChartData.history))
}
},
setYAxisMaxForResources () {
this.YAxisMax.resources = {
incremental: this.calculatePaddedMax(this.getChartDataMaxValues(this.resourceLineChartData.incremental)),
history: this.calculatePaddedMax(this.getChartDataMaxValues(this.resourceLineChartData.history))
}
},
setYAxisMaxForTariffs () {
this.YAxisMax.tariffs = {
incremental: this.calculatePaddedMax(this.getChartDataMaxValues(this.tariffLineChartData.incremental)),
history: this.calculatePaddedMax(this.getChartDataMaxValues(this.tariffLineChartData.history))
}
},
getEntryForCurrentGraphType (data) {
if (this.graphType === 'line_chart') {
return data.history
}
return data.incremental
},
prepareDataForUsageTypeLineGraph () {
this.setUsageTypeLineChartData()
this.setYAxisMaxForUsageTypes()
},
prepareDataForResourceLineGraph () {
this.setResourceLineChartData()
this.setYAxisMaxForResources()
},
prepareDataForTariffLineGraph () {
this.setTariffLineChartData()
this.setYAxisMaxForTariffs()
},
prepareLineChartData (data, lineChartLabels) {
const chartData = { history: [], incremental: [] }
for (const row of data) {
const datasetName = this.$t(this.getName(row))
const color = this.getColor(row)
const historyDataset = { label: datasetName, data: [], fill: false, ...color }
const incrementalDataset = { label: datasetName, data: [], fill: false, ...color }
let historyIndex = 0
let accumulatedQuota = 0
for (const label of lineChartLabels) {
const periodQuota = this.calculateQuotaForPeriod(row.history, label, historyIndex)
historyIndex = periodQuota.nextIndex
accumulatedQuota += periodQuota.value
historyDataset.data.push({ stat: periodQuota.value })
incrementalDataset.data.push({ stat: accumulatedQuota })
}
chartData.history.push(historyDataset)
chartData.incremental.push(incrementalDataset)
}
return chartData
},
calculateQuotaForPeriod (historyItems, label, startIndex) {
let quota = 0
let currentIndex = startIndex
while (currentIndex < historyItems.length) {
const item = historyItems[currentIndex]
if (this.getLineChartLabelForDate(item.enddate) > label) {
break
}
quota += item.quotaconsumed
currentIndex++
}
return { value: quota, nextIndex: currentIndex }
},
getColor (row) {
const quotaType = getQuotaTypeByName(row.name)
if (quotaType?.chartColor) {
return getChartColorObject(quotaType.chartColor)
}
return getChartColorObject(this.textToDeterministicColor(this.getName(row)))
},
textToDeterministicColor (text) {
let hash = 0
for (let i = text.length - 1; i >= 0; i--) {
hash = ((hash << 5) - hash) + text.charCodeAt(i)
hash = hash & hash
}
const hash1 = Math.abs(hash)
const hash2 = Math.abs(hash * 73)
const hash3 = Math.abs(hash * 97)
let color = '#'
color += (hash1 & 0xFF).toString(16).padStart(2, '0')
color += (hash2 & 0xFF).toString(16).padStart(2, '0')
color += (hash3 & 0xFF).toString(16).padStart(2, '0')
return color
},
getName (row) {
if (row.name) {
// Translate the usage type's name
return this.$t(row.name)
}
return row.displayname || row.tariffname
},
getYAxisIncrement (max) {
if (max < 1) {
return 1
}
return Math.pow(10, Math.floor(Math.log10(max)))
},
exportDataToCsv () {
downloadDataAsCsv({
data: this.dataSource,
keys: ['type', 'name', 'unit', 'quota'],
fileName: `quota-usage-of-account-${this.$route.params.id}-between-${this.startDate}-and-${this.endDate}`
})
},
exportResourcesToCsv () {
downloadDataAsCsv({
data: this.dataSourceResource.map(row => ({ ...row, name: row.displayname })),
keys: ['resourceid', 'name', 'quotaconsumed'],
fileName: `quota-usage-of-resources-of-type-${this.selectedType}-of-account-${this.$route.params.id}-between-${this.startDate}-and-${this.endDate}`
})
},
exportResourceDetailsToCsv () {
downloadDataAsCsv({
data: this.dataSourceTariffs,
keys: ['tariffid', 'tariffname', 'startdate', 'enddate', 'quotaconsumed'],
fileName: `detailed-quota-usage-of-resource-${this.selectedResource}-of-type-${this.selectedType}-of-account-${this.$route.params.id}-between-${this.startDate}-and-${this.endDate}`
})
},
getQuotaTypesFiltered () {
const quotaTypesRetrieved = this.dataSource.map(item => item.name)
return getQuotaTypes().filter((item) => quotaTypesRetrieved.includes(item.type))
},
getResources () {
return this.dataSourceResource.filter(item => item.resourceid).map(item => ({ id: item.resourceid, name: item.displayname }))
},
async handleSelectedTypeChange (value) {
this.selectedType = value
this.selectedResource = ''
this.dataSourceTariffs = []
document.getElementById('resource-by-type').scrollIntoView({ behavior: 'smooth' })
await this.fetchResourceData()
},
async handleSelectedResourceChange (value) {
this.selectedResource = value
document.getElementById('details-by-resource').scrollIntoView({ behavior: 'smooth' })
await this.fetchTariffData()
}
}
}
</script>
<style lang="scss" scoped>
@import '@/style/common/common.scss';
</style>