Create frontend PoC

This commit is contained in:
Sandra
2022-01-24 19:46:21 +01:00
parent 01627e87b7
commit 6095e00fc1
19 changed files with 10226 additions and 0 deletions

23
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

24
frontend/README.md Normal file
View File

@@ -0,0 +1,24 @@
# frontend
## Project setup
```
yarn install
```
### Compiles and hot-reloads for development
```
yarn serve
```
### Compiles and minifies for production
```
yarn build
```
### Lints and fixes files
```
yarn lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

5
frontend/babel.config.js Normal file
View File

@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

50
frontend/package.json Normal file
View File

@@ -0,0 +1,50 @@
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/vue-fontawesome": "^3.0.0-5",
"apexcharts": "^3.33.0",
"core-js": "^3.6.5",
"mdb-vue-ui-kit": "^1.9.0",
"typeface-roboto": "^1.1.13",
"vue": "^3.0.0",
"vue3-apexcharts": "^1.4.1"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.0.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^7.0.0"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>Okaeri Timings</title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@@ -0,0 +1,50 @@
#!/bin/sh
print_metadata() {
echo "#"
echo "# Okaeri Timings 1.0"
echo "#"
echo "# Hostname: $(hostname)"
# echo "# IP: $(curl -s https://checkip.amazonaws.com/)"
echo "# User: $(whoami)"
echo "#"
echo "# Kernel: $(uname -r)"
echo "# OS: $(cat /etc/os-release | grep PRETTY_NAME | awk -F '"' '{print $2}')"
echo "#"
}
print_header() {
echo "timestamp,cpu/user,cpu/nice,cpu/system,cpu/idle,cpu/iowait,cpu/irq,cpu/softirq,cpu/steal,cpu/guest,cpu/guest_nice,mem/total,mem/free,mem/available,mem/buffers,mem/cached,swap/cached,swap/total,swap/free"
}
print_data() {
timestamp=$(date --iso-8601=seconds)
procstatout=$(cat /proc/stat | grep -m1 ^cpu | cut -d ' ' -f 3-)
user=$(echo "$procstatout" | awk '{print $1}')
nice=$(echo "$procstatout" | awk '{print $2}')
system=$(echo "$procstatout" | awk '{print $3}')
idle=$(echo "$procstatout" | awk '{print $4}')
iowait=$(echo "$procstatout" | awk '{print $5}')
irq=$(echo "$procstatout" | awk '{print $6}')
softirq=$(echo "$procstatout" | awk '{print $7}')
steal=$(echo "$procstatout" | awk '{print $8}')
guest=$(echo "$procstatout" | awk '{print $9}')
guest_nice=$(echo "$procstatout" | awk '{print $10}')
meminfout=$(cat /proc/meminfo)
memtotal=$(echo "$meminfout" | grep ^MemTotal: | awk '{print $2}')
memfree=$(echo "$meminfout" | grep ^MemFree: | awk '{print $2}')
membuffers=$(echo "$meminfout" | grep ^Buffers: | awk '{print $2}')
memcached=$(echo "$meminfout" | grep ^Cached: | awk '{print $2}')
memavailable=$(echo "$meminfout" | grep ^MemAvailable: | awk '{print $2}')
swapcached=$(echo "$meminfout" | grep ^SwapCached: | awk '{print $2}')
swaptotal=$(echo "$meminfout" | grep ^SwapTotal: | awk '{print $2}')
swapfree=$(echo "$meminfout" | grep ^SwapFree: | awk '{print $2}')
echo "$timestamp,$user,$nice,$system,$idle,$iowait,$irq,$softirq,$steal,$guest,$guest_nice,$memtotal,$memfree,$memavailable,$membuffers,$memcached,$swapcached,$swaptotal,$swapfree"
}
print_metadata
print_header
print_data

271
frontend/src/App.vue Normal file
View File

@@ -0,0 +1,271 @@
<template>
<MDBNavbar expand="lg" dark bg="dark" container>
<MDBNavbarNav class="mb-lg-0">
<MDBNavbarItem href="#" class="font-weight-bold" active>Okaeri Timings</MDBNavbarItem>
</MDBNavbarNav>
<MDBNavbarNav right class="mb-lg-0">
<MDBNavbarItem href="https://github.com/OkaeriPoland/okaeri-timings" active>
<font-awesome-icon :icon="['fab', 'github']"/>
GitHub
</MDBNavbarItem>
</MDBNavbarNav>
</MDBNavbar>
<MDBContainer class="my-5">
<MDBRow class="gy-4">
<MDBCol md="12" v-if="!summaryLoaded">
<MDBCard>
<MDBCardBody>
<MDBCardTitle>1. Generate report</MDBCardTitle>
<MDBCardText>
Execute this command on your Linux based system:
<pre class="mt-2 mb-4">curl -s https://timings.okaeri.eu/otimings.sh | bash -s 60</pre>
<strong>Note:</strong> report generation takes a long time. Make sure it runs till the end (e.g. using <code>screen</code>)
for the best results. The default run-time is <code>60</code> minutes and cannot be shorter than 5 minutes. After the script
finalizes its run, the file named 'okaeri-timings-XXX.csv' (e.g. 'okaeri-timings-1642975054.csv') will be available.
</MDBCardText>
</MDBCardBody>
</MDBCard>
</MDBCol>
<MDBCol md="12" v-if="!summaryLoaded">
<MDBCard>
<MDBCardBody>
<MDBCardTitle>2. Upload report</MDBCardTitle>
<MDBFile size="lg" label="Please upload 'okaeri-timings-XXX.csv' file" accept=".csv" v-model="files"/>
</MDBCardBody>
</MDBCard>
</MDBCol>
<MDBCol md="12" v-if="summaryLoaded">
<MDBCard>
<MDBCardBody>
<MDBCardTitle class="d-flex">
<span class="me-1">Summary</span>
<MDBBadge color="primary">/proc/stat</MDBBadge>
</MDBCardTitle>
<MDBCardText>
This section contains raw data from <code>/proc/stat</code>, visualised and with calculated relative (percent) share of the total CPU time.
<ul>
<li><em>Current run</em> represents the data between the start and the end of the report generation.</li>
<li><em>All time</em> represents absolute values from the last system restart until the end of the report generation.</li>
</ul>
</MDBCardText>
<MDBTabs v-model="summaryTab">
<MDBTabNav tabsClasses="mb-3">
<MDBTabItem tabId="summary-current-run" href="summary-current-run">Current run</MDBTabItem>
<MDBTabItem tabId="summary-all-time" href="summary-all-time">All time</MDBTabItem>
</MDBTabNav>
<MDBTabContent>
<MDBTabPane tabId="summary-current-run">
<Summary :key="summaryTab" :stats="summaryStats" :chart-series="summarySeries"/>
</MDBTabPane>
<MDBTabPane tabId="summary-all-time">
<Summary :key="summaryTab" :stats="summaryStatsAll" :chart-series="summarySeriesAll"/>
</MDBTabPane>
</MDBTabContent>
</MDBTabs>
</MDBCardBody>
</MDBCard>
</MDBCol>
<MDBCol md="12" v-if="summaryLoaded">
<MDBCard>
<MDBCardBody>
<MDBCardTitle class="d-flex">
<span class="me-1">History</span>
<MDBBadge color="primary">/proc/stat</MDBBadge>
</MDBCardTitle>
<MDBCardText>
This section contains data (currently <code>{{ historyTab.replace('history-', '') }}</code>) changes over time represented as a relative (percent) share of the total CPU time.
</MDBCardText>
<MDBTabs v-model="historyTab">
<MDBTabNav tabsClasses="mb-3">
<MDBTabItem v-for="label in dataLabels" :key="label" :tabId="`history-${label}`" :href="`history-${label}`">{{ label }}</MDBTabItem>
</MDBTabNav>
<MDBTabContent>
<MDBTabPane v-for="label in dataLabels" :key="label" :tabId="`history-${label}`">
<History :key="historyTab" :name="label" :chart-series="historySeries[label]"/>
</MDBTabPane>
</MDBTabContent>
</MDBTabs>
</MDBCardBody>
</MDBCard>
</MDBCol>
<MDBCol md="12" v-if="summaryLoaded">
<MDBCard>
<MDBCardBody>
<MDBCardTitle class="d-flex">
<span class="me-1">History</span>
<MDBBadge color="primary">/proc/meminfo</MDBBadge>
</MDBCardTitle>
<MDBCardText>
This section contains memory usage changes over time.
</MDBCardText>
<code>TODO</code>
</MDBCardBody>
</MDBCard>
</MDBCol>
</MDBRow>
</MDBContainer>
</template>
<script>
import {
MDBBadge,
MDBCard,
MDBCardBody,
MDBCardText,
MDBCardTitle,
MDBCol,
MDBContainer,
MDBFile,
MDBNavbar,
MDBNavbarItem,
MDBNavbarNav,
MDBRow,
MDBTabContent,
MDBTabItem,
MDBTabNav,
MDBTabPane,
MDBTabs
} from 'mdb-vue-ui-kit';
import Summary from "@/components/Summary";
import History from "@/components/History";
import {ref} from "vue";
export default {
components: {
History,
Summary,
MDBNavbar,
MDBNavbarNav,
MDBNavbarItem,
MDBContainer,
MDBCard,
MDBCardBody,
MDBCardTitle,
MDBFile,
MDBRow,
MDBCol,
MDBTabs,
MDBTabNav,
MDBTabItem,
MDBTabContent,
MDBTabPane,
MDBCardText,
MDBBadge
},
props: {
msg: String
},
watch: {
files: async function (value) {
this.rawData = (await this.fileToString(value[0])).split(/\r?\n/).map(l => l.split(" "));
const firstLineData = this.rawData[0].slice(1).map(i => parseInt(i));
const lastLineData = this.rawData[this.rawData.length - 1].slice(1).map(i => parseInt(i));
// update summary
this.summarySeriesAll = lastLineData;
this.summarySeries = lastLineData.map((i, index) => i - firstLineData[index]);
this.summaryLoaded = true;
// update history
this.dataLabels.forEach((label, index) => {
this.historySeries[label] = this.calculateHistory(this.rawData, index);
});
},
summarySeries: function (value) {
this.summaryStats = this.calculateSummaryStats(value);
},
summarySeriesAll: function (value) {
this.summaryStatsAll = this.calculateSummaryStats(value);
}
},
setup: function () {
return {
dataLabels: ['user', 'nice', 'system', 'idle', 'iowait', 'irq', 'softirq', 'steal', 'guest', 'guest_nice']
}
},
data: function () {
return {
files: ref([]),
rawData: undefined,
// summary
summaryTab: ref('summary-current-run'),
summaryLoaded: false,
summaryStats: [],
summaryStatsAll: [],
summarySeries: [],
summarySeriesAll: [],
// steal
historyTab: ref('history-steal'),
historySeries: {},
};
},
methods: {
fileToString(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsText(file, "UTF-8");
reader.onload = event => resolve(event.target.result)
reader.onerror = event => reject(event);
});
},
calculateSummaryStats(data) {
const dataTotal = data.reduce((a, b) => a + b, 0);
let stats = [];
this.dataLabels.forEach((label, index) => {
stats[index] = {
name: label,
value: data[index],
percent: (data[index] / dataTotal) * 100
}
});
stats.sort((a, b) => {
return (a.value > b.value) ? -1 : 1;
});
return stats;
},
calculateHistory(rawData, index) {
let history = [];
let lastTotal = 0;
let lastValue = 0;
rawData.forEach(line => {
const date = new Date(line[0]);
const rawValue = parseInt(line[index + 1]);
const value = rawValue - lastValue;
const rawTotal = line.slice(1).map(i => parseInt(i)).reduce((a, b) => a + b, 0);
const total = rawTotal - lastTotal;
history.push([date, ((value / total) * 100)])
lastTotal = rawTotal;
lastValue = rawValue;
})
return history;
}
}
}
</script>
<style>
body {
min-height: 100vh;
background-color: #eaeaea;
}
#app {
font-family: 'Roboto', Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
pre {
border: 1px solid;
border-radius: 2px;
background-color: rgba(0, 0, 0, 0.05);
padding: 5px;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -0,0 +1,42 @@
<template>
<apexchart type="line" height="300" :options="chartOptions" :series="[{name: name, data: chartSeries}]"></apexchart>
</template>
<script>
export default {
components: {
},
props: {
name: {
type: String
},
chartOptions: {
type: Object,
default: function () {
return {
fill: {type: 'solid'},
xaxis: {type: 'datetime'},
yaxis: {
labels: {
formatter: function (value) {
return value.toFixed(3) + '%';
}
},
},
tooltip: {
x: {
format: "yyyy-MM-dd HH:mm:ss"
}
},
dataLabels: {enabled: false},
animations: {enabled: false},
stroke: {curve: 'straight'}
};
}
},
chartSeries: {
type: Array
}
}
}
</script>

View File

@@ -0,0 +1,239 @@
<template>
<MDBRow>
<MDBCol md="7">
<MDBTable>
<thead>
<tr>
<th scope="col" style="width: 30%">#</th>
<th scope="col" style="width: 40%">Value</th>
<th scope="col" style="width: 30%">Share</th>
</tr>
</thead>
<tbody>
<tr v-for="stat in stats" :key="stat.name">
<th scope="row">{{ stat.name }}</th>
<td>{{ stat.value }}</td>
<td>
<template v-if="warningPoints[stat.name] && warningPoints[stat.name](stat.percent) !== null">
<MDBBadge :color="warningPoints[stat.name](stat.percent)">
{{ (stat.percent).toFixed(3) }}%
<font-awesome-icon icon="exclamation-triangle"/>
</MDBBadge>
</template>
<template v-else>
{{ (stat.percent).toFixed(3) }}%
</template>
</td>
</tr>
</tbody>
</MDBTable>
</MDBCol>
<MDBCol md="5" class="d-flex flex-column">
<apexchart type="pie" height="300" class="mb-4" :options="chartOptions" :series="chartSeries"></apexchart>
<MDBBtn outline="primary" class="mt-auto ms-auto" aria-controls="summaryHelp" @click="summaryHelpModal = true">
<font-awesome-icon icon="question-circle"/>
Help
</MDBBtn>
</MDBCol>
<MDBModal id="summaryHelpModal" tabindex="-1" labelledby="summaryHelpModalLabel" class="modal-big" v-model="summaryHelpModal">
<MDBModalHeader>
<MDBModalTitle id="summaryHelpModalLabel"><font-awesome-icon icon="question-circle"/> Help</MDBModalTitle>
</MDBModalHeader>
<MDBModalBody>
<h6>user</h6>
<p>
Time spent in user mode.<br>
(e.g. <mark>applications</mark>)
</p>
<h6>nice</h6>
<p>
Time spent in user mode with low priority (nice).<br>
(e.g. <mark>applications running with <a href="https://en.wikipedia.org/wiki/Nice_(Unix)" target="_blank" rel="noopener nofollow noreferrer">nice > 0</a></mark>)
</p>
<h6>system</h6>
<p>
Time spent in system mode.<br>
(e.g. <mark>networking</mark>, <mark>firewall</mark>, <mark>drivers</mark>)
</p>
<h6>idle</h6>
<p>Time spent in the idle task. This value should be USER_HZ times the second entry in the /proc/uptime pseudo-file.</p>
<h6>iowait <small>(since Linux 2.5.41)</small></h6>
<p>Time waiting for I/O to complete. This value is not reliable, for the following reasons:</p>
<ol>
<li>
The CPU will not wait for I/O to complete; iowait is the time that a task is waiting for I/O to complete.
When a CPU goes into idle state for outstanding task I/O, another task will be scheduled on this CPU.
</li>
<li>
On a multi-core CPU, the task waiting for I/O to complete is not running on any CPU, so the iowait
of each CPU is difficult to calculate.
</li>
<li>
The value in this field may decrease in certain conditions.
</li>
</ol>
<h6>irq <small>(since Linux 2.6.0)</small></h6>
<p>Time servicing interrupts.</p>
<h6>softirq <small>(since Linux 2.6.0)</small></h6>
<p>Time servicing softirqs.</p>
<h6>steal <small>(since Linux 2.6.11)</small></h6>
<p>Stolen time, which is the time spent in other operating systems when running in a virtualized environment.</p>
<h6>guest <small>(since Linux 2.6.24)</small></h6>
<p>Time spent running a virtual CPU for guest operating systems under the control of the Linux kernel.</p>
<h6>guest_nice <small>(since Linux 2.6.33)</small></h6>
<p>Time spent running a niced guest (virtual CPU for guest operating systems under the control of the Linux kernel).</p>
</MDBModalBody>
<MDBModalFooter>
<p class="mb-0 me-auto">
This help page comes from the Linux manual for proc:
<a href="#" @click="copyrightCollapse = !copyrightCollapse" aria-controls="copyrightCollapse" :aria-expanded="copyrightCollapse">Copyright</a>,
<a href="#" @click="licenseCollapse = !licenseCollapse" aria-controls="licenseCollapse" :aria-expanded="licenseCollapse">License</a>
</p>
<MDBBtn color="primary" @click="summaryHelpModal = false">Close</MDBBtn>
<MDBCollapse id="copyrightCollapse" v-model="copyrightCollapse" class="w-100">
<pre class="mt-2 mb-0">
Copyright (C) 1994, 1995 by Daniel Quinlan (quinlan@yggdrasil.com)
and Copyright (C) 2002-2008,2017 Michael Kerrisk &lt;mtk.manpages@gmail.com&gt;
with networking additions from Alan Cox (A.Cox@swansea.ac.uk)
and scsi additions from Michael Neuffer (neuffer@mail.uni-mainz.de)
and sysctl additions from Andries Brouwer (aeb@cwi.nl)
and System V IPC (as well as various other) additions from
Michael Kerrisk &lt;mtk.manpages@gmail.com&gt;
</pre>
</MDBCollapse>
<MDBCollapse id="licenseCollapse" v-model="licenseCollapse" class="w-100">
<pre class="mt-2 mb-0">
This is free documentation; you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of
the License, or (at your option) any later version.
The GNU General Public License's references to "object code"
and "executables" are to be interpreted as the output of any
document formatting or typesetting system, including
intermediate and printed output.
This manual is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public
License along with this manual; if not, see
&lt;http://www.gnu.org/licenses/&gt;.
</pre>
</MDBCollapse>
</MDBModalFooter>
</MDBModal>
</MDBRow>
</template>
<script>
import {MDBBadge, MDBBtn, MDBCol, MDBModal, MDBModalBody, MDBModalFooter, MDBModalHeader, MDBModalTitle, MDBRow, MDBTable, MDBCollapse} from "mdb-vue-ui-kit";
import {ref} from "vue";
export default {
components: {
MDBRow,
MDBCol,
MDBTable,
MDBBadge,
MDBBtn,
MDBModal,
MDBModalHeader,
MDBModalTitle,
MDBModalBody,
MDBModalFooter,
MDBCollapse
},
watch: {
copyrightCollapse: function (value) {
if (value) {
setTimeout(() => document.getElementById("copyrightCollapse").scrollIntoView({behavior: 'smooth'}), 250);
}
},
licenseCollapse: function (value) {
if (value) {
setTimeout(() => document.getElementById("licenseCollapse").scrollIntoView({behavior: 'smooth'}), 250);
}
},
},
props: {
stats: {
type: Array
},
warningPoints: {
type: Object,
default: function () {
return {
user: function (value) {
return (value > 90) ? 'danger' : (value > 70) ? 'warning' : null
},
system: function (value) {
return (value > 20) ? 'danger' : (value > 10) ? 'warning' : null
},
idle: function (value) {
return (value < 10) ? 'danger' : (value < 30) ? 'warning' : null
},
iowait: function (value) {
return (value > 10) ? 'danger' : (value > 3) ? 'warning' : null
},
steal: function (value) {
return (value > 20) ? 'danger' : (value > 5) ? 'warning' : null
},
guest: function (value) {
return (value > 90) ? 'danger' : (value > 70) ? 'warning' : null
},
};
}
},
chartOptions: {
type: Object,
default: function () {
return {
labels: ['user', 'nice', 'system', 'idle', 'iowait', 'irq', 'softirq', 'steal', 'guest', 'guest_nice'],
colors: ['#00aa00', '#0000aa', '#aa0000', '#aaaaaa', '#ff5555', '#ffaa00', '#ff00ff', '#44dddd', '#00aaaa', '#00aaaa'],
animations: {enabled: false},
};
}
},
chartSeries: {
type: Array
}
},
data: function () {
return {
summaryHelpModal: ref(false),
copyrightCollapse: ref(false),
licenseCollapse: ref(false),
}
}
}
</script>
<style scoped>
.badge {
font-size: unset;
}
</style>
<style>
.modal-big .modal-dialog {
max-width: 1000px;
}
</style>

13
frontend/src/main.js Normal file
View File

@@ -0,0 +1,13 @@
import 'mdb-vue-ui-kit/css/mdb.min.css'
import 'typeface-roboto/index.css'
import {createApp} from 'vue'
import App from './App.vue'
import fontAwesome from "@/plugins/font-awesome";
import VueApexCharts from "vue3-apexcharts";
const app = createApp(App);
app.use(fontAwesome);
app.use(VueApexCharts);
app.mount('#app')

View File

@@ -0,0 +1,15 @@
import {library} from '@fortawesome/fontawesome-svg-core'
import {FontAwesomeIcon, FontAwesomeLayers} from '@fortawesome/vue-fontawesome'
import {faGithub} from "@fortawesome/free-brands-svg-icons/faGithub";
import {faExclamationTriangle} from "@fortawesome/free-solid-svg-icons/faExclamationTriangle";
import {faQuestionCircle} from "@fortawesome/free-solid-svg-icons/faQuestionCircle";
library.add(faGithub);
library.add(faExclamationTriangle);
library.add(faQuestionCircle);
export default (app) => {
app.component('font-awesome-icon', FontAwesomeIcon);
app.component('font-awesome-layers', FontAwesomeLayers);
}

8796
frontend/yarn.lock Normal file

File diff suppressed because it is too large Load Diff