This commit is contained in:
2024-09-01 18:54:23 +05:00
parent 76d18365a5
commit 061f09eca1
1597 changed files with 109451 additions and 1 deletions

View File

@@ -0,0 +1,186 @@
<template>
<LoadingCard :loading="loading" class="px-6 py-4">
<h3 class="h-6 flex mb-3 text-sm font-bold">
{{ title }}
<span class="ml-auto font-semibold text-gray-400 text-xs"
>({{ formattedTotal }} {{ __('total') }})</span
>
</h3>
<HelpTextTooltip :text="helpText" :width="helpWidth" />
<div class="flex min-h-[90px]">
<div
class="flex-1 overflow-hidden overflow-y-auto"
:class="{
'max-h-[90px]': legendsHeight === 'fixed',
}"
>
<ul>
<li
v-for="item in formattedItems"
:key="item.color"
class="text-xs leading-normal"
>
<span
class="inline-block rounded-full w-2 h-2 mr-2"
:style="{
backgroundColor: item.color,
}"
/>{{ item.label }} ({{ item.value }} - {{ item.percentage }}%)
</li>
</ul>
</div>
<div
ref="chart"
class="flex-none rounded-b-lg ct-chart mr-4 w-[90px] h-[90px]"
:class="{ invisible: this.currentTotal <= 0 }"
/>
</div>
</LoadingCard>
</template>
<script>
import debounce from 'lodash/debounce'
import map from 'lodash/map'
import sumBy from 'lodash/sumBy'
import Chartist from 'chartist'
import 'chartist/dist/chartist.min.css'
const colorForIndex = index =>
[
'#F5573B',
'#F99037',
'#F2CB22',
'#8FC15D',
'#098F56',
'#47C1BF',
'#1693EB',
'#6474D7',
'#9C6ADE',
'#E471DE',
][index]
export default {
name: 'BasePartitionMetric',
props: {
loading: Boolean,
title: String,
helpText: {},
helpWidth: {},
chartData: Array,
legendsHeight: { type: String, default: 'fixed' },
},
data: () => ({
chartist: null,
resizeObserver: null,
}),
watch: {
chartData: function (newData, oldData) {
this.renderChart()
},
},
created() {
const debouncer = debounce(callback => callback(), Nova.config('debounce'))
this.resizeObserver = new ResizeObserver(entries => {
debouncer(() => {
this.renderChart()
})
})
},
mounted() {
this.chartist = new Chartist.Pie(
this.$refs.chart,
this.formattedChartData,
{
donut: true,
donutWidth: 10,
donutSolid: true,
startAngle: 270,
showLabel: false,
}
)
this.chartist.on('draw', context => {
if (context.type === 'slice') {
context.element.attr({
style: `fill: ${context.meta.color} !important`,
})
}
})
this.resizeObserver.observe(this.$refs.chart)
},
beforeUnmount() {
this.resizeObserver.unobserve(this.$refs.chart)
},
methods: {
renderChart() {
this.chartist.update(this.formattedChartData)
},
getItemColor(item, index) {
return typeof item.color === 'string' ? item.color : colorForIndex(index)
},
},
computed: {
chartClasses() {
return []
},
formattedChartData() {
return { labels: this.formattedLabels, series: this.formattedData }
},
formattedItems() {
return map(this.chartData, (item, index) => {
return {
label: item.label,
value: Nova.formatNumber(item.value),
color: this.getItemColor(item, index),
percentage: Nova.formatNumber(String(item.percentage)),
}
})
},
formattedLabels() {
return map(this.chartData, item => item.label)
},
formattedData() {
return map(this.chartData, (item, index) => {
return {
value: item.value,
meta: { color: this.getItemColor(item, index) },
}
})
},
formattedTotal() {
let total = this.currentTotal.toFixed(2)
let roundedTotal = Math.round(total)
if (roundedTotal.toFixed(2) == total) {
return Nova.formatNumber(new String(roundedTotal))
}
return Nova.formatNumber(new String(total))
},
currentTotal() {
return sumBy(this.chartData, 'value')
},
},
}
</script>

View File

@@ -0,0 +1,85 @@
<template>
<LoadingCard :loading="loading" class="flex flex-col px-6 py-4">
<div class="h-6 flex items-center mb-4">
<h3 class="flex-1 mr-3 leading-tight text-sm font-bold">{{ title }}</h3>
<HelpTextTooltip :text="helpText" :width="helpWidth" />
<div class="flex-none text-right">
<span class="text-gray-500 font-medium inline-block">
{{ formattedValue }}
<span v-if="suffix" class="text-sm">{{ formattedSuffix }}</span>
</span>
</div>
</div>
<p class="flex items-center text-4xl mb-4">{{ percentage }}%</p>
<div class="flex h-full justify-center items-center flex-grow-1 mb-4">
<ProgressBar
:title="formattedValue"
:color="bgClass"
:value="percentage"
/>
</div>
</LoadingCard>
</template>
<script>
import { singularOrPlural } from '@/util'
export default {
name: 'BaseProgressMetric',
props: {
loading: { default: true },
title: {},
helpText: {},
helpWidth: {},
maxWidth: {},
target: {},
value: {},
percentage: {},
format: {
type: String,
default: '(0[.]00a)',
},
avoid: { type: Boolean, default: false },
prefix: '',
suffix: '',
suffixInflection: { type: Boolean, default: true },
},
computed: {
isNullValue() {
return this.value == null
},
formattedValue() {
if (!this.isNullValue) {
const value = Nova.formatNumber(new String(this.value), this.format)
return `${this.prefix}${value}`
}
return ''
},
formattedSuffix() {
if (this.suffixInflection === false) {
return this.suffix
}
return singularOrPlural(this.value, this.suffix)
},
bgClass() {
if (this.avoid) {
return this.percentage > 60 ? 'bg-yellow-500' : 'bg-green-300'
}
return this.percentage > 60 ? 'bg-green-500' : 'bg-yellow-300'
},
},
}
</script>

View File

@@ -0,0 +1,231 @@
<template>
<LoadingCard :loading="loading" class="px-6 py-4">
<div class="h-6 flex items-center mb-4">
<h3 class="mr-3 leading-tight text-sm font-bold">{{ title }}</h3>
<HelpTextTooltip :text="helpText" :width="helpWidth" />
<SelectControl
v-if="ranges.length > 0"
class="ml-auto w-[6rem] shrink-0"
size="xxs"
:options="ranges"
:selected="selectedRangeKey"
@change="handleChange"
:aria-label="__('Select Ranges')"
/>
</div>
<p class="flex items-center text-4xl mb-4">
{{ formattedValue }}
<span v-if="suffix" class="ml-2 text-sm font-bold">{{
formattedSuffix
}}</span>
</p>
<div
ref="chart"
class="absolute inset-0 rounded-b-lg ct-chart"
style="top: 60%"
/>
</LoadingCard>
</template>
<script>
import debounce from 'lodash/debounce'
import Chartist from 'chartist'
import 'chartist/dist/chartist.min.css'
import { singularOrPlural } from '@/util'
import ChartistTooltip from 'chartist-plugin-tooltips-updated'
import 'chartist-plugin-tooltips-updated/dist/chartist-plugin-tooltip.css'
export default {
name: 'BaseTrendMetric',
emits: ['selected'],
props: {
loading: Boolean,
title: {},
helpText: {},
helpWidth: {},
value: {},
chartData: {},
maxWidth: {},
prefix: '',
suffix: '',
suffixInflection: { type: Boolean, default: true },
ranges: { type: Array, default: () => [] },
selectedRangeKey: [String, Number],
format: {
type: String,
default: '0[.]00a',
},
},
data: () => ({
chartist: null,
resizeObserver: null,
}),
watch: {
selectedRangeKey: function (newRange, oldRange) {
this.renderChart()
},
chartData: function (newData, oldData) {
this.renderChart()
},
},
created() {
const debouncer = debounce(callback => callback(), Nova.config('debounce'))
this.resizeObserver = new ResizeObserver(entries => {
debouncer(() => {
this.renderChart()
})
})
},
mounted() {
const low = Math.min(...this.chartData)
const high = Math.max(...this.chartData)
// Use zero as the graph base if the lowest value is greater than or equal to zero.
// This avoids the awkward situation where the chart doesn't appear filled in.
const areaBase = low >= 0 ? 0 : low
this.chartist = new Chartist.Line(this.$refs.chart, this.chartData, {
lineSmooth: Chartist.Interpolation.none(),
fullWidth: true,
showPoint: true,
showLine: true,
showArea: true,
chartPadding: {
top: 10,
right: 0,
bottom: 0,
left: 0,
},
low,
high,
areaBase,
axisX: {
showGrid: false,
showLabel: true,
offset: 0,
},
axisY: {
showGrid: false,
showLabel: true,
offset: 0,
},
plugins: [
ChartistTooltip({
pointClass: 'ct-point',
anchorToPoint: false,
}),
ChartistTooltip({
pointClass: 'ct-point__left',
anchorToPoint: false,
tooltipOffset: {
x: 50,
y: -20,
},
}),
ChartistTooltip({
pointClass: 'ct-point__right',
anchorToPoint: false,
tooltipOffset: {
x: -50,
y: -20,
},
}),
],
})
this.chartist.on('draw', data => {
if (data.type === 'point') {
data.element.attr({
'ct:value': this.transformTooltipText(data.value.y),
})
data.element.addClass(
this.transformTooltipClass(data.axisX.ticks.length, data.index) ?? ''
)
}
})
this.resizeObserver.observe(this.$refs.chart)
},
beforeUnmount() {
this.resizeObserver.unobserve(this.$refs.chart)
},
methods: {
renderChart() {
this.chartist.update(this.chartData)
},
handleChange(event) {
const value = event?.target?.value || event
this.$emit('selected', value)
},
transformTooltipText(value) {
let formattedValue = Nova.formatNumber(new String(value), this.format)
if (this.prefix) {
return `${this.prefix}${formattedValue}`
}
if (this.suffix) {
const suffix = this.suffixInflection
? singularOrPlural(value, this.suffix)
: this.suffix
return `${formattedValue} ${suffix}`
}
return `${formattedValue}`
},
transformTooltipClass(total, index) {
if (index < 2) {
return 'ct-point__left'
} else if (index > total - 3) {
return 'ct-point__right'
}
return 'ct-point'
},
},
computed: {
isNullValue() {
return this.value == null
},
formattedValue() {
if (!this.isNullValue) {
const value = Nova.formatNumber(new String(this.value), this.format)
return `${this.prefix}${value}`
}
return ''
},
formattedSuffix() {
if (this.suffixInflection === false) {
return this.suffix
}
return singularOrPlural(this.value, this.suffix)
},
},
}
</script>

View File

@@ -0,0 +1,236 @@
<template>
<LoadingCard :loading="loading" class="px-6 py-4">
<div class="h-6 flex items-center mb-4">
<h3 class="mr-3 leading-tight text-sm font-bold">{{ title }}</h3>
<HelpTextTooltip :text="helpText" :width="helpWidth" />
<SelectControl
v-if="ranges.length > 0"
class="ml-auto w-[6rem] shrink-0"
size="xxs"
:options="ranges"
:selected="selectedRangeKey"
@change="handleChange"
:aria-label="__('Select Ranges')"
/>
</div>
<div class="flex items-center mb-4 space-x-4">
<div
v-if="icon"
class="rounded-lg bg-primary-500 text-white h-14 w-14 flex items-center justify-center"
>
<Icon :type="icon" width="24" height="24" />
</div>
<div>
<component
:is="copyable ? 'CopyButton' : 'p'"
@click="handleCopyClick"
class="flex items-center text-4xl"
:rounded="false"
>
<span v-tooltip="`${tooltipFormattedValue}`">
{{ formattedValue }}
</span>
<span v-if="suffix" class="ml-2 text-sm font-bold">
{{ formattedSuffix }}
</span>
</component>
<div v-tooltip="`${tooltipFormattedPreviousValue}`">
<p class="flex items-center font-bold text-sm">
<svg
v-if="increaseOrDecreaseLabel === 'Decrease'"
xmlns="http://www.w3.org/2000/svg"
class="text-red-500 stroke-current mr-2"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 17h8m0 0V9m0 8l-8-8-4 4-6-6"
/>
</svg>
<svg
v-if="increaseOrDecreaseLabel === 'Increase'"
class="text-green-500 stroke-current mr-2"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"
/>
</svg>
<span v-if="!(increaseOrDecrease === 0)">
<span v-if="growthPercentage !== 0">
{{ growthPercentage }}%
{{ __(increaseOrDecreaseLabel) }}
</span>
<span v-else>{{ __('No Increase') }}</span>
</span>
<span class="text-gray-400 font-semibold" v-else>
<span v-if="previous === '0' && value !== '0'">
{{ __('No Prior Data') }}
</span>
<span v-if="value === '0' && previous !== '0' && !zeroResult">
{{ __('No Current Data') }}
</span>
<span v-if="value == '0' && previous == '0' && !zeroResult">
{{ __('No Data') }}
</span>
</span>
</p>
</div>
</div>
</div>
</LoadingCard>
</template>
<script>
import { increaseOrDecrease, singularOrPlural } from '@/util'
import { CopiesToClipboard } from '@/mixins'
export default {
name: 'BaseValueMetric',
mixins: [CopiesToClipboard],
emits: ['selected'],
props: {
loading: { default: true },
copyable: { default: false },
title: {},
helpText: {},
helpWidth: {},
icon: { type: String },
maxWidth: {},
previous: {},
value: {},
prefix: '',
suffix: '',
suffixInflection: { default: true },
selectedRangeKey: [String, Number],
ranges: { type: Array, default: () => [] },
format: { type: String, default: '(0[.]00a)' },
tooltipFormat: { type: String, default: '(0[.]00)' },
zeroResult: { default: false },
},
data: () => ({ copied: false }),
methods: {
handleChange(event) {
let value = event?.target?.value || event
this.$emit('selected', value)
},
handleCopyClick() {
if (this.copyable) {
this.copied = true
this.copyValueToClipboard(this.tooltipFormattedValue)
setTimeout(() => {
this.copied = false
}, 2000)
}
},
},
computed: {
growthPercentage() {
return Math.abs(this.increaseOrDecrease)
},
increaseOrDecrease() {
if (this.previous === 0 || this.previous == null || this.value === 0)
return 0
return increaseOrDecrease(this.value, this.previous).toFixed(2)
},
increaseOrDecreaseLabel() {
switch (Math.sign(this.increaseOrDecrease)) {
case 1:
return 'Increase'
case 0:
return 'Constant'
case -1:
return 'Decrease'
}
},
sign() {
switch (Math.sign(this.increaseOrDecrease)) {
case 1:
return '+'
case 0:
return ''
case -1:
return '-'
}
},
isNullValue() {
return this.value == null
},
isNullPreviousValue() {
return this.previous == null
},
formattedValue() {
if (!this.isNullValue) {
return (
this.prefix + Nova.formatNumber(new String(this.value), this.format)
)
}
return ''
},
tooltipFormattedValue() {
if (!this.isNullValue) {
return this.value
}
return ''
},
tooltipFormattedPreviousValue() {
if (!this.isNullPreviousValue) {
return this.previous
}
return ''
},
formattedSuffix() {
if (this.suffixInflection === false) {
return this.suffix
}
return singularOrPlural(this.value, this.suffix)
},
},
}
</script>

View File

@@ -0,0 +1,119 @@
<template>
<tr class="group">
<td
v-if="row.icon"
class="pl-6 w-8 pr-2 w-max"
:class="{
[row.iconClass]: true,
[rowClasses]: true,
'text-gray-400 dark:text-gray-600': !row.iconClass,
}"
>
<Heroicon :type="row.icon" />
</td>
<td
class="px-2 w-auto"
:class="{
[rowClasses]: true,
'pl-6': !row.icon,
'pr-6': !row.editUrl || !row.viewUrl,
}"
>
<h2 class="text-base text-gray-500 truncate">
{{ row.title }}
</h2>
<p class="text-gray-400 text-xs truncate">{{ row.subtitle }}</p>
</td>
<td
v-if="row.actions.length > 0"
class="text-right pr-4 w-max"
:class="rowClasses"
>
<div class="flex justify-end items-center text-gray-400">
<Dropdown>
<Button
icon="ellipsis-horizontal"
variant="action"
:aria-label="__('Resource Row Dropdown')"
/>
<template #menu>
<DropdownMenu width="auto" class="px-1">
<ScrollWrap
:height="250"
class="divide-y divide-gray-100 dark:divide-gray-800 divide-solid"
>
<div class="py-1">
<DropdownMenuItem
v-bind="actionAttributes(action)"
v-for="action in row.actions"
>
{{ action.name }}
</DropdownMenuItem>
</div>
</ScrollWrap>
</DropdownMenu>
</template>
</Dropdown>
</div>
</td>
</tr>
</template>
<script>
import isNull from 'lodash/isNull'
import omitBy from 'lodash/omitBy'
import { Button, Icon } from 'laravel-nova-ui'
import Heroicon from '@/components/Icons/Icon'
export default {
components: {
Button,
Icon,
Heroicon,
},
props: {
row: {
type: Object,
required: true,
},
},
methods: {
actionAttributes(item) {
let method = item.method || 'GET'
if (item.external && item.method == 'GET') {
return {
as: 'external',
href: item.path,
name: item.name,
title: item.name,
target: item.target || null,
external: true,
}
}
return omitBy(
{
as: method === 'GET' ? 'link' : 'form-button',
href: item.path,
method: method !== 'GET' ? method : null,
data: item.data || null,
headers: item.headers || null,
},
isNull
)
},
},
computed: {
rowClasses() {
return ['py-2']
},
},
}
</script>

View File

@@ -0,0 +1,112 @@
<template>
<BasePartitionMetric
:title="card.name"
:help-text="card.helpText"
:help-width="card.helpWidth"
:chart-data="chartData"
:loading="loading"
:legends-height="card.height"
/>
</template>
<script>
import { MetricBehavior } from '@/mixins'
import { minimum } from '@/util'
export default {
mixins: [MetricBehavior],
props: {
card: {
type: Object,
required: true,
},
resourceName: {
type: String,
default: '',
},
resourceId: {
type: [Number, String],
default: '',
},
lens: {
type: String,
default: '',
},
},
data: () => ({
loading: true,
chartData: [],
}),
watch: {
resourceId() {
this.fetch()
},
},
created() {
this.fetch()
},
mounted() {
if (this.card && this.card.refreshWhenFiltersChange === true) {
Nova.$on('filter-changed', this.fetch)
}
},
beforeUnmount() {
if (this.card && this.card.refreshWhenFiltersChange === true) {
Nova.$off('filter-changed', this.fetch)
}
},
methods: {
fetch() {
this.loading = true
minimum(Nova.request().get(this.metricEndpoint, this.metricPayload)).then(
({
data: {
value: { value },
},
}) => {
this.chartData = value
this.loading = false
}
)
},
},
computed: {
metricEndpoint() {
const lens = this.lens !== '' ? `/lens/${this.lens}` : ''
if (this.resourceName && this.resourceId) {
return `/nova-api/${this.resourceName}${lens}/${this.resourceId}/metrics/${this.card.uriKey}`
} else if (this.resourceName) {
return `/nova-api/${this.resourceName}${lens}/metrics/${this.card.uriKey}`
} else {
return `/nova-api/metrics/${this.card.uriKey}`
}
},
metricPayload() {
const payload = { params: {} }
if (
!Nova.missingResource(this.resourceName) &&
this.card &&
this.card.refreshWhenFiltersChange === true
) {
payload.params.filter =
this.$store.getters[`${this.resourceName}/currentEncodedFilters`]
}
return payload
},
},
}
</script>

View File

@@ -0,0 +1,154 @@
<template>
<BaseProgressMetric
:title="card.name"
:help-text="card.helpText"
:help-width="card.helpWidth"
:target="target"
:value="value"
:percentage="percentage"
:prefix="prefix"
:suffix="suffix"
:suffix-inflection="suffixInflection"
:format="format"
:avoid="avoid"
:loading="loading"
/>
</template>
<script>
import { minimum } from '@/util'
import { InteractsWithDates, MetricBehavior } from '@/mixins'
export default {
name: 'ProgressMetric',
mixins: [InteractsWithDates, MetricBehavior],
props: {
card: {
type: Object,
required: true,
},
resourceName: {
type: String,
default: '',
},
resourceId: {
type: [Number, String],
default: '',
},
lens: {
type: String,
default: '',
},
},
data: () => ({
loading: true,
format: '(0[.]00a)',
avoid: false,
prefix: '',
suffix: '',
suffixInflection: true,
value: 0,
target: 0,
percentage: 0,
zeroResult: false,
}),
watch: {
resourceId() {
this.fetch()
},
},
created() {
if (this.hasRanges) {
this.selectedRangeKey =
this.card.selectedRangeKey || this.card.ranges[0].value
}
this.fetch()
},
mounted() {
if (this.card && this.card.refreshWhenFiltersChange === true) {
Nova.$on('filter-changed', this.fetch)
}
},
beforeUnmount() {
if (this.card && this.card.refreshWhenFiltersChange === true) {
Nova.$off('filter-changed', this.fetch)
}
},
methods: {
fetch() {
this.loading = true
minimum(Nova.request().get(this.metricEndpoint, this.metricPayload)).then(
({
data: {
value: {
value,
target,
percentage,
prefix,
suffix,
suffixInflection,
format,
avoid,
},
},
}) => {
this.value = value
this.target = target
this.percentage = percentage
this.format = format || this.format
this.avoid = avoid
this.prefix = prefix || this.prefix
this.suffix = suffix || this.suffix
this.suffixInflection = suffixInflection
this.loading = false
}
)
},
},
computed: {
metricPayload() {
const payload = {
params: {
timezone: this.userTimezone,
},
}
if (
!Nova.missingResource(this.resourceName) &&
this.card &&
this.card.refreshWhenFiltersChange === true
) {
payload.params.filter =
this.$store.getters[`${this.resourceName}/currentEncodedFilters`]
}
return payload
},
metricEndpoint() {
const lens = this.lens !== '' ? `/lens/${this.lens}` : ''
if (this.resourceName && this.resourceId) {
return `/nova-api/${this.resourceName}${lens}/${this.resourceId}/metrics/${this.card.uriKey}`
} else if (this.resourceName) {
return `/nova-api/${this.resourceName}${lens}/metrics/${this.card.uriKey}`
} else {
return `/nova-api/metrics/${this.card.uriKey}`
}
},
},
}
</script>

View File

@@ -0,0 +1,133 @@
<template>
<LoadingCard :loading="loading" class="pt-4">
<div class="h-6 flex items-center px-6 mb-4">
<h3 class="mr-3 leading-tight text-sm font-bold">{{ card.name }}</h3>
<HelpTextTooltip :text="card.helpText" :width="card.helpWidth" />
</div>
<div class="mb-5 pb-4">
<div
v-if="value.length > 0"
class="overflow-hidden overflow-x-auto relative"
>
<table class="w-full table-default table-fixed">
<tbody
class="border-t border-b border-gray-100 dark:border-gray-700 divide-y divide-gray-100 dark:divide-gray-700"
>
<MetricTableRow v-for="row in value" :row="row" />
</tbody>
</table>
</div>
<div v-else class="flex flex-col items-center justify-between px-6 gap-2">
<p class="font-normal text-center py-4">
{{ card.emptyText }}
</p>
</div>
</div>
</LoadingCard>
</template>
<script>
import { minimum } from '@/util'
import { InteractsWithDates, MetricBehavior } from '@/mixins'
export default {
name: 'TableCard',
mixins: [InteractsWithDates, MetricBehavior],
props: {
card: {
type: Object,
required: true,
},
resourceName: {
type: String,
default: '',
},
resourceId: {
type: [Number, String],
default: '',
},
lens: {
type: String,
default: '',
},
},
data: () => ({
loading: true,
value: [],
}),
watch: {
resourceId() {
this.fetch()
},
},
created() {
this.fetch()
},
mounted() {
if (this.card && this.card.refreshWhenFiltersChange === true) {
Nova.$on('filter-changed', this.fetch)
}
},
beforeUnmount() {
if (this.card && this.card.refreshWhenFiltersChange === true) {
Nova.$off('filter-changed', this.fetch)
}
},
methods: {
fetch() {
this.loading = true
minimum(Nova.request().get(this.metricEndpoint, this.metricPayload)).then(
({ data: { value } }) => {
this.value = value
this.loading = false
}
)
},
},
computed: {
metricPayload() {
const payload = {
params: {
timezone: this.userTimezone,
},
}
if (
!Nova.missingResource(this.resourceName) &&
this.card &&
this.card.refreshWhenFiltersChange === true
) {
payload.params.filter =
this.$store.getters[`${this.resourceName}/currentEncodedFilters`]
}
return payload
},
metricEndpoint() {
const lens = this.lens !== '' ? `/lens/${this.lens}` : ''
if (this.resourceName && this.resourceId) {
return `/nova-api/${this.resourceName}${lens}/${this.resourceId}/metrics/${this.card.uriKey}`
} else if (this.resourceName) {
return `/nova-api/${this.resourceName}${lens}/metrics/${this.card.uriKey}`
} else {
return `/nova-api/metrics/${this.card.uriKey}`
}
},
},
}
</script>

View File

@@ -0,0 +1,176 @@
<template>
<BaseTrendMetric
@selected="handleRangeSelected"
:title="card.name"
:help-text="card.helpText"
:help-width="card.helpWidth"
:value="value"
:chart-data="data"
:ranges="card.ranges"
:format="format"
:prefix="prefix"
:suffix="suffix"
:suffix-inflection="suffixInflection"
:selected-range-key="selectedRangeKey"
:loading="loading"
/>
</template>
<script>
import map from 'lodash/map'
import { InteractsWithDates, MetricBehavior } from '@/mixins'
import { minimum } from '@/util'
export default {
name: 'TrendMetric',
mixins: [InteractsWithDates, MetricBehavior],
props: {
card: {
type: Object,
required: true,
},
resourceName: {
type: String,
default: '',
},
resourceId: {
type: [Number, String],
default: '',
},
lens: {
type: String,
default: '',
},
},
data: () => ({
loading: true,
value: '',
data: [],
format: '(0[.]00a)',
prefix: '',
suffix: '',
suffixInflection: true,
selectedRangeKey: null,
}),
watch: {
resourceId() {
this.fetch()
},
},
created() {
if (this.hasRanges) {
this.selectedRangeKey =
this.card.selectedRangeKey || this.card.ranges[0].value
}
this.fetch()
},
mounted() {
if (this.card && this.card.refreshWhenFiltersChange === true) {
Nova.$on('filter-changed', this.fetch)
}
},
beforeUnmount() {
if (this.card && this.card.refreshWhenFiltersChange === true) {
Nova.$off('filter-changed', this.fetch)
}
},
methods: {
handleRangeSelected(key) {
this.selectedRangeKey = key
this.fetch()
},
fetch() {
this.loading = true
minimum(Nova.request().get(this.metricEndpoint, this.metricPayload)).then(
({
data: {
value: {
labels,
trend,
value,
prefix,
suffix,
suffixInflection,
format,
},
},
}) => {
this.value = value
this.labels = Object.keys(trend)
this.data = {
labels: Object.keys(trend),
series: [
map(trend, (value, label) => {
return {
meta: label,
value: value,
}
}),
],
}
this.format = format || this.format
this.prefix = prefix || this.prefix
this.suffix = suffix || this.suffix
this.suffixInflection = suffixInflection
this.loading = false
}
)
},
},
computed: {
hasRanges() {
return this.card.ranges.length > 0
},
metricPayload() {
const payload = {
params: {
timezone: this.userTimezone,
twelveHourTime: this.usesTwelveHourTime,
},
}
if (
!Nova.missingResource(this.resourceName) &&
this.card &&
this.card.refreshWhenFiltersChange === true
) {
payload.params.filter =
this.$store.getters[`${this.resourceName}/currentEncodedFilters`]
}
if (this.hasRanges) {
payload.params.range = this.selectedRangeKey
}
return payload
},
metricEndpoint() {
const lens = this.lens !== '' ? `/lens/${this.lens}` : ''
if (this.resourceName && this.resourceId) {
return `/nova-api/${this.resourceName}${lens}/${this.resourceId}/metrics/${this.card.uriKey}`
} else if (this.resourceName) {
return `/nova-api/${this.resourceName}${lens}/metrics/${this.card.uriKey}`
} else {
return `/nova-api/metrics/${this.card.uriKey}`
}
},
},
}
</script>

View File

@@ -0,0 +1,175 @@
<template>
<BaseValueMetric
@selected="handleRangeSelected"
:title="card.name"
:copyable="copyable"
:help-text="card.helpText"
:help-width="card.helpWidth"
:icon="card.icon"
:previous="previous"
:value="value"
:ranges="card.ranges"
:format="format"
:tooltip-format="tooltipFormat"
:prefix="prefix"
:suffix="suffix"
:suffix-inflection="suffixInflection"
:selected-range-key="selectedRangeKey"
:loading="loading"
:zero-result="zeroResult"
/>
</template>
<script>
import { minimum } from '@/util'
import { InteractsWithDates, MetricBehavior } from '@/mixins'
export default {
name: 'ValueMetric',
mixins: [InteractsWithDates, MetricBehavior],
props: {
card: {
type: Object,
required: true,
},
resourceName: {
type: String,
default: '',
},
resourceId: {
type: [Number, String],
default: '',
},
lens: {
type: String,
default: '',
},
},
data: () => ({
loading: true,
copyable: false,
format: '(0[.]00a)',
tooltipFormat: '(0[.]00)',
value: 0,
previous: 0,
prefix: '',
suffix: '',
suffixInflection: true,
selectedRangeKey: null,
zeroResult: false,
}),
watch: {
resourceId() {
this.fetch()
},
},
created() {
if (this.hasRanges) {
this.selectedRangeKey =
this.card.selectedRangeKey || this.card.ranges[0].value
}
this.fetch()
},
mounted() {
if (this.card && this.card.refreshWhenFiltersChange === true) {
Nova.$on('filter-changed', this.fetch)
}
},
beforeUnmount() {
if (this.card && this.card.refreshWhenFiltersChange === true) {
Nova.$off('filter-changed', this.fetch)
}
},
methods: {
handleRangeSelected(key) {
this.selectedRangeKey = key
this.fetch()
},
fetch() {
this.loading = true
minimum(Nova.request().get(this.metricEndpoint, this.metricPayload)).then(
({
data: {
value: {
copyable,
value,
previous,
prefix,
suffix,
suffixInflection,
format,
tooltipFormat,
zeroResult,
},
},
}) => {
this.copyable = copyable
this.value = value
this.format = format || this.format
this.tooltipFormat = tooltipFormat || this.tooltipFormat
this.prefix = prefix || this.prefix
this.suffix = suffix || this.suffix
this.suffixInflection = suffixInflection
this.zeroResult = zeroResult || this.zeroResult
this.previous = previous
this.loading = false
}
)
},
},
computed: {
hasRanges() {
return this.card.ranges.length > 0
},
metricPayload() {
const payload = {
params: {
timezone: this.userTimezone,
},
}
if (
!Nova.missingResource(this.resourceName) &&
this.card &&
this.card.refreshWhenFiltersChange === true
) {
payload.params.filter =
this.$store.getters[`${this.resourceName}/currentEncodedFilters`]
}
if (this.hasRanges) {
payload.params.range = this.selectedRangeKey
}
return payload
},
metricEndpoint() {
const lens = this.lens !== '' ? `/lens/${this.lens}` : ''
if (this.resourceName && this.resourceId) {
return `/nova-api/${this.resourceName}${lens}/${this.resourceId}/metrics/${this.card.uriKey}`
} else if (this.resourceName) {
return `/nova-api/${this.resourceName}${lens}/metrics/${this.card.uriKey}`
} else {
return `/nova-api/metrics/${this.card.uriKey}`
}
},
},
}
</script>