aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/components/services
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/services')
-rw-r--r--src/components/services/AdGuardHome.vue4
-rw-r--r--src/components/services/Healthchecks.vue2
-rw-r--r--src/components/services/Immich.vue30
-rw-r--r--src/components/services/Mealie.vue2
-rw-r--r--src/components/services/OctoPrint.vue18
-rw-r--r--src/components/services/PaperlessNG.vue2
-rw-r--r--src/components/services/PiAlert.vue174
-rw-r--r--src/components/services/PiHole.vue5
-rw-r--r--src/components/services/Portainer.vue4
-rw-r--r--src/components/services/Prometheus.vue6
-rw-r--r--src/components/services/Proxmox.vue6
-rw-r--r--src/components/services/Rtorrent.vue8
-rw-r--r--src/components/services/SABnzbd.vue2
-rw-r--r--src/components/services/Tautulli.vue2
-rw-r--r--src/components/services/Tdarr.vue18
-rw-r--r--src/components/services/qBittorrent.vue2
16 files changed, 153 insertions, 132 deletions
diff --git a/src/components/services/AdGuardHome.vue b/src/components/services/AdGuardHome.vue
index 4c53398..d5b84f6 100644
--- a/src/components/services/AdGuardHome.vue
+++ b/src/components/services/AdGuardHome.vue
@@ -63,12 +63,12 @@ export default {
63 methods: { 63 methods: {
64 fetchStatus: async function () { 64 fetchStatus: async function () {
65 this.status = await this.fetch("/control/status").catch((e) => 65 this.status = await this.fetch("/control/status").catch((e) =>
66 console.log(e) 66 console.log(e),
67 ); 67 );
68 }, 68 },
69 fetchStats: async function () { 69 fetchStats: async function () {
70 this.stats = await this.fetch("/control/stats").catch((e) => 70 this.stats = await this.fetch("/control/stats").catch((e) =>
71 console.log(e) 71 console.log(e),
72 ); 72 );
73 }, 73 },
74 }, 74 },
diff --git a/src/components/services/Healthchecks.vue b/src/components/services/Healthchecks.vue
index c60f241..0aefb74 100644
--- a/src/components/services/Healthchecks.vue
+++ b/src/components/services/Healthchecks.vue
@@ -66,7 +66,7 @@ export default {
66 const apikey = this.item.apikey; 66 const apikey = this.item.apikey;
67 if (!apikey) { 67 if (!apikey) {
68 console.error( 68 console.error(
69 "apikey is not present in config.yml for the Healthchecks entry!" 69 "apikey is not present in config.yml for the Healthchecks entry!",
70 ); 70 );
71 return; 71 return;
72 } 72 }
diff --git a/src/components/services/Immich.vue b/src/components/services/Immich.vue
index e86ecd6..7cde306 100644
--- a/src/components/services/Immich.vue
+++ b/src/components/services/Immich.vue
@@ -40,7 +40,7 @@ export default {
40 }, 40 },
41 data: () => { 41 data: () => {
42 return { 42 return {
43 users: null, 43 users: null,
44 photos: null, 44 photos: null,
45 videos: null, 45 videos: null,
46 usage: null, 46 usage: null,
@@ -56,25 +56,27 @@ export default {
56 }, 56 },
57 computed: { 57 computed: {
58 humanizeSize: function () { 58 humanizeSize: function () {
59 let bytes = this.usage; 59 let bytes = this.usage;
60 if (Math.abs(bytes) < 1024) 60 if (Math.abs(bytes) < 1024) return bytes + " B";
61 return bytes + ' B';
62 61
63 const units = ['KiB', 'MiB', 'GiB', 'TiB']; 62 const units = ["KiB", "MiB", "GiB", "TiB"];
64 let u = -1; 63 let u = -1;
65 do { 64 do {
66 bytes /= 1024; 65 bytes /= 1024;
67 ++u; 66 ++u;
68 } while (Math.round(Math.abs(bytes) * 100) / 100 >= 1024 && u < units.length - 1); 67 } while (
68 Math.round(Math.abs(bytes) * 100) / 100 >= 1024 &&
69 u < units.length - 1
70 );
69 71
70 return bytes.toFixed(2) + ' ' + units[u]; 72 return bytes.toFixed(2) + " " + units[u];
71 }, 73 },
72 }, 74 },
73 methods: { 75 methods: {
74 fetchConfig: function () { 76 fetchConfig: function () {
75 const headers = { 77 const headers = {
76 "x-api-key": this.item.apikey, 78 "x-api-key": this.item.apikey,
77 }; 79 };
78 80
79 this.fetch(`/api/server-info/stats`, { headers }) 81 this.fetch(`/api/server-info/stats`, { headers })
80 .then((stats) => { 82 .then((stats) => {
@@ -82,7 +84,7 @@ export default {
82 this.videos = stats.videos; 84 this.videos = stats.videos;
83 this.usage = stats.usage; 85 this.usage = stats.usage;
84 this.users = stats.usageByUser.length; 86 this.users = stats.usageByUser.length;
85 }) 87 })
86 .catch((e) => { 88 .catch((e) => {
87 console.error(e); 89 console.error(e);
88 this.serverError = true; 90 this.serverError = true;
@@ -123,4 +125,4 @@ export default {
123 } 125 }
124 } 126 }
125} 127}
126</style> \ No newline at end of file 128</style>
diff --git a/src/components/services/Mealie.vue b/src/components/services/Mealie.vue
index b5b2255..43550d8 100644
--- a/src/components/services/Mealie.vue
+++ b/src/components/services/Mealie.vue
@@ -45,7 +45,7 @@ export default {
45 if (this.item.subtitle != null) return; 45 if (this.item.subtitle != null) return;
46 46
47 this.meal = await this.fetch("/api/meal-plans/today/", { headers }).catch( 47 this.meal = await this.fetch("/api/meal-plans/today/", { headers }).catch(
48 (e) => console.log(e) 48 (e) => console.log(e),
49 ); 49 );
50 this.stats = await this.fetch("/api/debug/statistics/", { 50 this.stats = await this.fetch("/api/debug/statistics/", {
51 headers, 51 headers,
diff --git a/src/components/services/OctoPrint.vue b/src/components/services/OctoPrint.vue
index 1428d9a..aceb2d8 100644
--- a/src/components/services/OctoPrint.vue
+++ b/src/components/services/OctoPrint.vue
@@ -6,7 +6,9 @@
6 <template v-if="item.subtitle && !state"> 6 <template v-if="item.subtitle && !state">
7 {{ item.subtitle }} 7 {{ item.subtitle }}
8 </template> 8 </template>
9 <template v-if="!error && display == 'text' && statusClass == 'in-progress'"> 9 <template
10 v-if="!error && display == 'text' && statusClass == 'in-progress'"
11 >
10 <i class="fa-solid fa-gear mr-1"></i> 12 <i class="fa-solid fa-gear mr-1"></i>
11 <b v-if="completion">{{ completion.toFixed() }}%</b> 13 <b v-if="completion">{{ completion.toFixed() }}%</b>
12 <span class="separator mx-1"> | </span> 14 <span class="separator mx-1"> | </span>
@@ -17,9 +19,13 @@
17 </template> 19 </template>
18 <template v-if="!error && display == 'text' && statusClass == 'ready'"> 20 <template v-if="!error && display == 'text' && statusClass == 'ready'">
19 <i class="fa-solid fa-temperature-half mr-1"></i> 21 <i class="fa-solid fa-temperature-half mr-1"></i>
20 <b v-if="printer.temperature.bed">{{ printer.temperature.bed.actual.toFixed() }} C</b> 22 <b v-if="printer.temperature.bed"
23 >{{ printer.temperature.bed.actual.toFixed() }} C</b
24 >
21 <span class="separator mx-1"> | </span> 25 <span class="separator mx-1"> | </span>
22 <b v-if="printer.temperature.tool0">{{ printer.temperature.tool0.actual.toFixed() }} C</b> 26 <b v-if="printer.temperature.tool0"
27 >{{ printer.temperature.tool0.actual.toFixed() }} C</b
28 >
23 </template> 29 </template>
24 <template v-if="!error && display == 'bar'"> 30 <template v-if="!error && display == 'bar'">
25 <progress 31 <progress
@@ -28,7 +34,7 @@
28 :value="completion" 34 :value="completion"
29 max="100" 35 max="100"
30 :title="`${state} - ${completion.toFixed()}%, ${toTime( 36 :title="`${state} - ${completion.toFixed()}%, ${toTime(
31 printTimeLeft 37 printTimeLeft,
32 )} left`" 38 )} left`"
33 > 39 >
34 {{ completion }}% 40 {{ completion }}%
@@ -99,7 +105,9 @@ export default {
99 }, 105 },
100 fetchPrinterStatus: async function () { 106 fetchPrinterStatus: async function () {
101 try { 107 try {
102 const response = await this.fetch(`api/printer?apikey=${this.item.apikey}`); 108 const response = await this.fetch(
109 `api/printer?apikey=${this.item.apikey}`,
110 );
103 this.printer = response; 111 this.printer = response;
104 this.error = response.error; 112 this.error = response.error;
105 } catch (e) { 113 } catch (e) {
diff --git a/src/components/services/PaperlessNG.vue b/src/components/services/PaperlessNG.vue
index 69f2437..8f7122a 100644
--- a/src/components/services/PaperlessNG.vue
+++ b/src/components/services/PaperlessNG.vue
@@ -40,7 +40,7 @@ export default {
40 const apikey = this.item.apikey; 40 const apikey = this.item.apikey;
41 if (!apikey) { 41 if (!apikey) {
42 console.error( 42 console.error(
43 "apikey is not present in config.yml for the paperless entry!" 43 "apikey is not present in config.yml for the paperless entry!",
44 ); 44 );
45 return; 45 return;
46 } 46 }
diff --git a/src/components/services/PiAlert.vue b/src/components/services/PiAlert.vue
index fb0d9ed..fd1fd9e 100644
--- a/src/components/services/PiAlert.vue
+++ b/src/components/services/PiAlert.vue
@@ -1,104 +1,108 @@
1<template> 1<template>
2 <Generic :item="item"> 2 <Generic :item="item">
3 <template #indicator> 3 <template #indicator>
4 <div class="notifs"> 4 <div class="notifs">
5 <strong class="notif total" title="Total Devices"> 5 <strong class="notif total" title="Total Devices">
6 {{ total }} 6 {{ total }}
7 </strong> 7 </strong>
8 <strong class="notif connected" title="Connected Devices"> 8 <strong class="notif connected" title="Connected Devices">
9 {{ connected }} 9 {{ connected }}
10 </strong> 10 </strong>
11 <strong class="notif newdevices" title="New Devices"> 11 <strong class="notif newdevices" title="New Devices">
12 {{ newdevices }} 12 {{ newdevices }}
13 </strong> 13 </strong>
14 <strong class="notif alert" title="Down Alerts"> 14 <strong class="notif alert" title="Down Alerts">
15 {{ downalert }} 15 {{ downalert }}
16 </strong> 16 </strong>
17 <strong v-if="serverError" class="notif alert" 17 <strong
18 title="Connection error to PiAlert server, check the url in config.yml">?</strong> 18 v-if="serverError"
19 </div> 19 class="notif alert"
20 </template> 20 title="Connection error to PiAlert server, check the url in config.yml"
21 </Generic> 21 >?</strong
22 >
23 </div>
24 </template>
25 </Generic>
22</template> 26</template>
23 27
24<script> 28<script>
25import service from "@/mixins/service.js"; 29import service from "@/mixins/service.js";
26import Generic from "./Generic.vue"; 30import Generic from "./Generic.vue";
27 31
28export default { 32export default {
29 name: "PiAlert", 33 name: "PiAlert",
30 mixins: [service], 34 mixins: [service],
31 props: { 35 props: {
32 item: Object, 36 item: Object,
33 }, 37 },
34 components: { 38 components: {
35 Generic, 39 Generic,
36 }, 40 },
37 data: () => { 41 data: () => {
38 return { 42 return {
39 total: 0, 43 total: 0,
40 connected: 0, 44 connected: 0,
41 newdevices: 0, 45 newdevices: 0,
42 downalert: 0, 46 downalert: 0,
43 serverError: false, 47 serverError: false,
44 }; 48 };
45 }, 49 },
46 created() { 50 created() {
47 const updateInterval = parseInt(this.item.updateInterval, 10) || 0; 51 const updateInterval = parseInt(this.item.updateInterval, 10) || 0;
48 if (updateInterval > 0) { 52 if (updateInterval > 0) {
49 setInterval(() => this.fetchStatus(), updateInterval); 53 setInterval(() => this.fetchStatus(), updateInterval);
50 } 54 }
51 this.fetchStatus(); 55 this.fetchStatus();
52 }, 56 },
53 methods: { 57 methods: {
54 fetchStatus: async function () { 58 fetchStatus: async function () {
55 this.fetch("/php/server/devices.php?action=getDevicesTotals") 59 this.fetch("/php/server/devices.php?action=getDevicesTotals")
56 .then((response) => { 60 .then((response) => {
57 this.total = response[0]; 61 this.total = response[0];
58 this.connected = response[1]; 62 this.connected = response[1];
59 this.newdevices = response[3]; 63 this.newdevices = response[3];
60 this.downalert = response[4]; 64 this.downalert = response[4];
61 }) 65 })
62 .catch((e) => { 66 .catch((e) => {
63 console.log(e); 67 console.log(e);
64 this.serverError = true; 68 this.serverError = true;
65 }); 69 });
66 },
67 }, 70 },
71 },
68}; 72};
69</script> 73</script>
70 74
71<style scoped lang="scss"> 75<style scoped lang="scss">
72.notifs { 76.notifs {
73 position: absolute; 77 position: absolute;
74 color: white; 78 color: white;
75 font-family: sans-serif; 79 font-family: sans-serif;
76 top: 0.3em; 80 top: 0.3em;
77 right: 0.5em; 81 right: 0.5em;
78 82
79 .notif { 83 .notif {
80 display: inline-block; 84 display: inline-block;
81 padding: 0.2em 0.35em; 85 padding: 0.2em 0.35em;
82 border-radius: 0.25em; 86 border-radius: 0.25em;
83 position: relative; 87 position: relative;
84 margin-left: 0.3em; 88 margin-left: 0.3em;
85 font-size: 0.8em; 89 font-size: 0.8em;
86 90
87 &.total { 91 &.total {
88 background-color: #4fb5d6; 92 background-color: #4fb5d6;
89 } 93 }
90 94
91 &.connected { 95 &.connected {
92 background-color: #4fd671; 96 background-color: #4fd671;
93 } 97 }
94 98
95 &.newdevices { 99 &.newdevices {
96 background-color: #d08d2e; 100 background-color: #d08d2e;
97 } 101 }
98 102
99 &.alert { 103 &.alert {
100 background-color: #e51111; 104 background-color: #e51111;
101 }
102 } 105 }
106 }
103} 107}
104</style> \ No newline at end of file 108</style>
diff --git a/src/components/services/PiHole.vue b/src/components/services/PiHole.vue
index 237cb12..b111456 100644
--- a/src/components/services/PiHole.vue
+++ b/src/components/services/PiHole.vue
@@ -52,8 +52,9 @@ export default {
52 const authQueryParams = this.item.apikey 52 const authQueryParams = this.item.apikey
53 ? `?summaryRaw&auth=${this.item.apikey}` 53 ? `?summaryRaw&auth=${this.item.apikey}`
54 : ""; 54 : "";
55 const result = await this.fetch(`/api.php${authQueryParams}`) 55 const result = await this.fetch(`/api.php${authQueryParams}`).catch((e) =>
56 .catch((e) => console.log(e)); 56 console.log(e),
57 );
57 58
58 this.status = result.status; 59 this.status = result.status;
59 this.ads_percentage_today = result.ads_percentage_today; 60 this.ads_percentage_today = result.ads_percentage_today;
diff --git a/src/components/services/Portainer.vue b/src/components/services/Portainer.vue
index d101ecc..cfc762e 100644
--- a/src/components/services/Portainer.vue
+++ b/src/components/services/Portainer.vue
@@ -78,7 +78,7 @@ export default {
78 this.endpoints = await this.fetch("/api/endpoints", { headers }).catch( 78 this.endpoints = await this.fetch("/api/endpoints", { headers }).catch(
79 (e) => { 79 (e) => {
80 console.error(e); 80 console.error(e);
81 } 81 },
82 ); 82 );
83 83
84 let containers = []; 84 let containers = [];
@@ -93,7 +93,7 @@ export default {
93 const endpointContainers = await this.fetch(uri, { headers }).catch( 93 const endpointContainers = await this.fetch(uri, { headers }).catch(
94 (e) => { 94 (e) => {
95 console.error(e); 95 console.error(e);
96 } 96 },
97 ); 97 );
98 98
99 if (endpointContainers) { 99 if (endpointContainers) {
diff --git a/src/components/services/Prometheus.vue b/src/components/services/Prometheus.vue
index 6efcb34..7f661ca 100644
--- a/src/components/services/Prometheus.vue
+++ b/src/components/services/Prometheus.vue
@@ -72,7 +72,7 @@ export default {
72 countFiring: function () { 72 countFiring: function () {
73 if (this.api) { 73 if (this.api) {
74 return this.api.data?.alerts?.filter( 74 return this.api.data?.alerts?.filter(
75 (alert) => alert.state === AlertsStatus.firing 75 (alert) => alert.state === AlertsStatus.firing,
76 ).length; 76 ).length;
77 } 77 }
78 return 0; 78 return 0;
@@ -80,7 +80,7 @@ export default {
80 countPending: function () { 80 countPending: function () {
81 if (this.api) { 81 if (this.api) {
82 return this.api.data?.alerts?.filter( 82 return this.api.data?.alerts?.filter(
83 (alert) => alert.state === AlertsStatus.pending 83 (alert) => alert.state === AlertsStatus.pending,
84 ).length; 84 ).length;
85 } 85 }
86 return 0; 86 return 0;
@@ -88,7 +88,7 @@ export default {
88 countInactive: function () { 88 countInactive: function () {
89 if (this.api) { 89 if (this.api) {
90 return this.api.data?.alerts?.filter( 90 return this.api.data?.alerts?.filter(
91 (alert) => alert.state === AlertsStatus.pending 91 (alert) => alert.state === AlertsStatus.pending,
92 ).length; 92 ).length;
93 } 93 }
94 return 0; 94 return 0;
diff --git a/src/components/services/Proxmox.vue b/src/components/services/Proxmox.vue
index 8136050..5d0c950 100644
--- a/src/components/services/Proxmox.vue
+++ b/src/components/services/Proxmox.vue
@@ -122,7 +122,7 @@ export default {
122 }; 122 };
123 const status = await this.fetch( 123 const status = await this.fetch(
124 `/api2/json/nodes/${this.item.node}/status`, 124 `/api2/json/nodes/${this.item.node}/status`,
125 options 125 options,
126 ); 126 );
127 // main metrics: 127 // main metrics:
128 const decimalsToShow = this.item.hide_decimals ? 0 : 1; 128 const decimalsToShow = this.item.hide_decimals ? 0 : 1;
@@ -139,7 +139,7 @@ export default {
139 if (this.isValueShown("vms")) { 139 if (this.isValueShown("vms")) {
140 const vms = await this.fetch( 140 const vms = await this.fetch(
141 `/api2/json/nodes/${this.item.node}/qemu`, 141 `/api2/json/nodes/${this.item.node}/qemu`,
142 options 142 options,
143 ); 143 );
144 this.parseVMsAndLXCs(vms, this.vms); 144 this.parseVMsAndLXCs(vms, this.vms);
145 } 145 }
@@ -147,7 +147,7 @@ export default {
147 if (this.isValueShown("lxcs")) { 147 if (this.isValueShown("lxcs")) {
148 const lxcs = await this.fetch( 148 const lxcs = await this.fetch(
149 `/api2/json/nodes/${this.item.node}/lxc`, 149 `/api2/json/nodes/${this.item.node}/lxc`,
150 options 150 options,
151 ); 151 );
152 this.parseVMsAndLXCs(lxcs, this.lxcs); 152 this.parseVMsAndLXCs(lxcs, this.lxcs);
153 } 153 }
diff --git a/src/components/services/Rtorrent.vue b/src/components/services/Rtorrent.vue
index ed8e7a6..59dd843 100644
--- a/src/components/services/Rtorrent.vue
+++ b/src/components/services/Rtorrent.vue
@@ -41,7 +41,7 @@ const displayRate = (rate) => {
41 41
42 return ( 42 return (
43 Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }).format( 43 Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }).format(
44 rate || 0 44 rate || 0,
45 ) + ` ${units[i]}/s` 45 ) + ` ${units[i]}/s`
46 ); 46 );
47}; 47};
@@ -105,8 +105,8 @@ export default {
105 return this.getXml(methodName).then((xml) => 105 return this.getXml(methodName).then((xml) =>
106 parseInt( 106 parseInt(
107 xml.getElementsByTagName("value")[0].firstChild.textContent, 107 xml.getElementsByTagName("value")[0].firstChild.textContent,
108 10 108 10,
109 ) 109 ),
110 ); 110 );
111 }, 111 },
112 // Fetch the numer of torrents by requesting the download list 112 // Fetch the numer of torrents by requesting the download list
@@ -143,7 +143,7 @@ export default {
143 return response.text(); 143 return response.text();
144 }) 144 })
145 .then((text) => 145 .then((text) =>
146 Promise.resolve(new DOMParser().parseFromString(text, "text/xml")) 146 Promise.resolve(new DOMParser().parseFromString(text, "text/xml")),
147 ); 147 );
148 }, 148 },
149 }, 149 },
diff --git a/src/components/services/SABnzbd.vue b/src/components/services/SABnzbd.vue
index 2f93c71..e92e587 100644
--- a/src/components/services/SABnzbd.vue
+++ b/src/components/services/SABnzbd.vue
@@ -56,7 +56,7 @@ export default {
56 fetchStatus: async function () { 56 fetchStatus: async function () {
57 try { 57 try {
58 const response = await this.fetch( 58 const response = await this.fetch(
59 `/api?output=json&apikey=${this.item.apikey}&mode=queue` 59 `/api?output=json&apikey=${this.item.apikey}&mode=queue`,
60 ); 60 );
61 this.error = false; 61 this.error = false;
62 this.stats = response.queue; 62 this.stats = response.queue;
diff --git a/src/components/services/Tautulli.vue b/src/components/services/Tautulli.vue
index 0f27ff1..db27c7f 100644
--- a/src/components/services/Tautulli.vue
+++ b/src/components/services/Tautulli.vue
@@ -51,7 +51,7 @@ export default {
51 fetchStatus: async function () { 51 fetchStatus: async function () {
52 try { 52 try {
53 const response = await this.fetch( 53 const response = await this.fetch(
54 `/api/v2?apikey=${this.item.apikey}&cmd=get_activity` 54 `/api/v2?apikey=${this.item.apikey}&cmd=get_activity`,
55 ); 55 );
56 this.error = false; 56 this.error = false;
57 this.stats = response.response.data; 57 this.stats = response.response.data;
diff --git a/src/components/services/Tdarr.vue b/src/components/services/Tdarr.vue
index a2734f0..22341d9 100644
--- a/src/components/services/Tdarr.vue
+++ b/src/components/services/Tdarr.vue
@@ -72,14 +72,20 @@ export default {
72 method: "POST", 72 method: "POST",
73 headers: { 73 headers: {
74 "Content-Type": "application/json", 74 "Content-Type": "application/json",
75 "Accept": "application/json", 75 Accept: "application/json",
76 }, 76 },
77 body: JSON.stringify({"headers":{"content-Type":"application/json"},"data":{"collection":"StatisticsJSONDB","mode":"getById","docID":"statistics","obj":{}},"timeout":1000}), 77 body: JSON.stringify({
78 headers: { "content-Type": "application/json" },
79 data: {
80 collection: "StatisticsJSONDB",
81 mode: "getById",
82 docID: "statistics",
83 obj: {},
84 },
85 timeout: 1000,
86 }),
78 }; 87 };
79 const response = await this.fetch( 88 const response = await this.fetch(`/api/v2/cruddb`, options);
80 `/api/v2/cruddb`,
81 options
82 );
83 this.error = false; 89 this.error = false;
84 this.stats = response; 90 this.stats = response;
85 } catch (e) { 91 } catch (e) {
diff --git a/src/components/services/qBittorrent.vue b/src/components/services/qBittorrent.vue
index 06dd47c..d47ed0b 100644
--- a/src/components/services/qBittorrent.vue
+++ b/src/components/services/qBittorrent.vue
@@ -43,7 +43,7 @@ const displayRate = (rate) => {
43 } 43 }
44 return ( 44 return (
45 Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }).format( 45 Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }).format(
46 rate || 0 46 rate || 0,
47 ) + ` ${units[i]}/s` 47 ) + ` ${units[i]}/s`
48 ); 48 );
49}; 49};