mirror of
https://github.com/TwiN/gatus.git
synced 2024-11-21 23:43:27 +01:00
Add events to service detail page
This commit is contained in:
parent
119b80edc0
commit
fbb5d48bf7
@ -124,7 +124,14 @@ func serviceStatusHandler(writer http.ResponseWriter, r *http.Request) {
|
|||||||
_, _ = writer.Write([]byte("not found"))
|
_, _ = writer.Write([]byte("not found"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data, err := json.Marshal(serviceStatus)
|
data := map[string]interface{}{
|
||||||
|
"serviceStatus": serviceStatus,
|
||||||
|
// This is my lazy way of exposing events even though they're not visible from the json annotation
|
||||||
|
// present in ServiceStatus. We do this because creating a separate object for each endpoints
|
||||||
|
// would be wasteful (one with and one without Events)
|
||||||
|
"events": serviceStatus.Events,
|
||||||
|
}
|
||||||
|
output, err := json.Marshal(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[controller][serviceStatusHandler] Unable to marshal object to JSON: %s", err.Error())
|
log.Printf("[controller][serviceStatusHandler] Unable to marshal object to JSON: %s", err.Error())
|
||||||
writer.WriteHeader(http.StatusInternalServerError)
|
writer.WriteHeader(http.StatusInternalServerError)
|
||||||
@ -133,7 +140,7 @@ func serviceStatusHandler(writer http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
writer.Header().Add("Content-Type", "application/json")
|
writer.Header().Add("Content-Type", "application/json")
|
||||||
writer.WriteHeader(http.StatusOK)
|
writer.WriteHeader(http.StatusOK)
|
||||||
_, _ = writer.Write(data)
|
_, _ = writer.Write(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
func healthHandler(writer http.ResponseWriter, _ *http.Request) {
|
func healthHandler(writer http.ResponseWriter, _ *http.Request) {
|
||||||
|
26
core/event.go
Normal file
26
core/event.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Event is something that happens at a specific time
|
||||||
|
type Event struct {
|
||||||
|
// Type is the kind of event
|
||||||
|
Type EventType `json:"type"`
|
||||||
|
|
||||||
|
// Timestamp is the moment at which the event happened
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EventType is, uh, the types of events?
|
||||||
|
type EventType string
|
||||||
|
|
||||||
|
var (
|
||||||
|
// EventStart is a type of event that represents when a service starts being monitored
|
||||||
|
EventStart EventType = "START"
|
||||||
|
|
||||||
|
// EventHealthy is a type of event that represents a service passing all of its conditions
|
||||||
|
EventHealthy EventType = "HEALTHY"
|
||||||
|
|
||||||
|
// EventUnhealthy is a type of event that represents a service failing one or more of its conditions
|
||||||
|
EventUnhealthy EventType = "UNHEALTHY"
|
||||||
|
)
|
@ -1,6 +1,10 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import "github.com/TwinProduction/gatus/util"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/TwinProduction/gatus/util"
|
||||||
|
)
|
||||||
|
|
||||||
// ServiceStatus contains the evaluation Results of a Service
|
// ServiceStatus contains the evaluation Results of a Service
|
||||||
type ServiceStatus struct {
|
type ServiceStatus struct {
|
||||||
@ -16,6 +20,13 @@ type ServiceStatus struct {
|
|||||||
// Results is the list of service evaluation results
|
// Results is the list of service evaluation results
|
||||||
Results []*Result `json:"results"`
|
Results []*Result `json:"results"`
|
||||||
|
|
||||||
|
// Events is a list of events
|
||||||
|
//
|
||||||
|
// We don't expose this through JSON, because the main dashboard doesn't need to have these events.
|
||||||
|
// However, the detailed service page does leverage this by including it to a map that will be
|
||||||
|
// marshalled alongside the ServiceStatus.
|
||||||
|
Events []*Event `json:"-"`
|
||||||
|
|
||||||
// Uptime information on the service's uptime
|
// Uptime information on the service's uptime
|
||||||
Uptime *Uptime `json:"uptime"`
|
Uptime *Uptime `json:"uptime"`
|
||||||
}
|
}
|
||||||
@ -27,13 +38,33 @@ func NewServiceStatus(service *Service) *ServiceStatus {
|
|||||||
Group: service.Group,
|
Group: service.Group,
|
||||||
Key: util.ConvertGroupAndServiceToKey(service.Group, service.Name),
|
Key: util.ConvertGroupAndServiceToKey(service.Group, service.Name),
|
||||||
Results: make([]*Result, 0),
|
Results: make([]*Result, 0),
|
||||||
Uptime: NewUptime(),
|
Events: []*Event{{
|
||||||
|
Type: EventStart,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
}},
|
||||||
|
Uptime: NewUptime(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddResult adds a Result to ServiceStatus.Results and makes sure that there are
|
// AddResult adds a Result to ServiceStatus.Results and makes sure that there are
|
||||||
// no more than 20 results in the Results slice
|
// no more than 20 results in the Results slice
|
||||||
func (ss *ServiceStatus) AddResult(result *Result) {
|
func (ss *ServiceStatus) AddResult(result *Result) {
|
||||||
|
if len(ss.Results) > 0 {
|
||||||
|
// Check if there's any change since the last result
|
||||||
|
// OR there's only 1 event, which only happens when there's a start event
|
||||||
|
if ss.Results[len(ss.Results)-1].Success != result.Success || len(ss.Events) == 1 {
|
||||||
|
event := &Event{Timestamp: result.Timestamp}
|
||||||
|
if result.Success {
|
||||||
|
event.Type = EventHealthy
|
||||||
|
} else {
|
||||||
|
event.Type = EventUnhealthy
|
||||||
|
}
|
||||||
|
ss.Events = append(ss.Events, event)
|
||||||
|
if len(ss.Events) > 20 {
|
||||||
|
ss.Events = ss.Events[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
ss.Results = append(ss.Results, result)
|
ss.Results = append(ss.Results, result)
|
||||||
if len(ss.Results) > 20 {
|
if len(ss.Results) > 20 {
|
||||||
ss.Results = ss.Results[1:]
|
ss.Results = ss.Results[1:]
|
||||||
|
@ -217,7 +217,7 @@ func TestInMemoryStore_GetAllAsJSON(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("shouldn't have returned an error, got", err.Error())
|
t.Fatal("shouldn't have returned an error, got", err.Error())
|
||||||
}
|
}
|
||||||
expectedOutput := `{"group_name":{"name":"name","group":"group","results":[{"status":200,"hostname":"example.org","duration":150000000,"errors":null,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"},{"status":200,"hostname":"example.org","duration":750000000,"errors":["error-1","error-2"],"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":false},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":false}],"success":false,"timestamp":"0001-01-01T00:00:00Z"}],"uptime":{"7d":0.5,"24h":0.5,"1h":0.5}}}`
|
expectedOutput := `{"group_name":{"name":"name","group":"group","key":"group_name","results":[{"status":200,"hostname":"example.org","duration":150000000,"errors":null,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"},{"status":200,"hostname":"example.org","duration":750000000,"errors":["error-1","error-2"],"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":false},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":false}],"success":false,"timestamp":"0001-01-01T00:00:00Z"}],"uptime":{"7d":0.5,"24h":0.5,"1h":0.5}}}`
|
||||||
if string(output) != expectedOutput {
|
if string(output) != expectedOutput {
|
||||||
t.Errorf("expected:\n %s\n\ngot:\n %s", expectedOutput, string(output))
|
t.Errorf("expected:\n %s\n\ngot:\n %s", expectedOutput, string(output))
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,11 @@
|
|||||||
<div class="container container-xs relative mx-auto rounded shadow-xl border my-3 p-5 text-left" id="global">
|
<div class="container container-xs relative mx-auto rounded shadow-xl border my-3 p-5 text-left" id="global">
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<div class="flex flex-wrap">
|
<div class="flex flex-wrap">
|
||||||
<div class="w-2/3 text-left my-auto">
|
<div class="w-3/4 text-left my-auto">
|
||||||
<div class="title text-5xl font-light">Health Status</div>
|
<div class="title text-5xl font-light">Health Status</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/3 flex justify-end">
|
<div class="w-1/4 flex justify-end">
|
||||||
<img src="./assets/logo.png" alt="Gatus" style="min-width: 50px; max-width: 200px; width: 20%;"/>
|
<img src="./assets/logo.png" alt="Gatus" class="object-scale-down" style="max-width: 100px; min-width: 50px; min-height:50px;"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -42,9 +42,11 @@ export default {
|
|||||||
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
html, body {
|
html, body {
|
||||||
background-color: #f7f9fb;
|
background-color: #f7f9fb;
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#global, #results {
|
#global, #results {
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<div class='service container px-3 py-3 border-l border-r border-t rounded-none' v-if="data && data.results && data.results.length">
|
<div class='service container px-3 py-3 border-l border-r border-t rounded-none' v-if="data && data.results && data.results.length">
|
||||||
<div class='flex flex-wrap mb-2'>
|
<div class='flex flex-wrap mb-2'>
|
||||||
<div class='w-3/4'>
|
<div class='w-3/4'>
|
||||||
<router-link :to="generatePath()" class="font-bold transition duration-200 ease-in-out hover:text-blue-900">{{ data.name }}</router-link> <span class='text-gray-500 font-light'>- {{ data.results[data.results.length - 1].hostname }}</span>
|
<router-link :to="generatePath()" class="inline-block font-bold transform hover:scale-110 transition duration-100 ease-in-out hover:text-blue-800" title="View detailed service health">{{ data.name }}</router-link> <span class='text-gray-500 font-light'>| {{ data.results[data.results.length - 1].hostname }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class='w-1/4 text-right'>
|
<div class='w-1/4 text-right'>
|
||||||
<span class='font-light status-min-max-ms'>
|
<span class='font-light status-min-max-ms'>
|
||||||
@ -35,6 +35,8 @@
|
|||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import {helper} from "@/mixins/helper";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Service',
|
name: 'Service',
|
||||||
props: {
|
props: {
|
||||||
@ -42,6 +44,7 @@ export default {
|
|||||||
data: Object,
|
data: Object,
|
||||||
},
|
},
|
||||||
emits: ['showTooltip'],
|
emits: ['showTooltip'],
|
||||||
|
mixins: [helper],
|
||||||
methods: {
|
methods: {
|
||||||
updateMinAndMaxResponseTimes() {
|
updateMinAndMaxResponseTimes() {
|
||||||
let minResponseTime = null;
|
let minResponseTime = null;
|
||||||
@ -62,18 +65,6 @@ export default {
|
|||||||
this.maxResponseTime = maxResponseTime;
|
this.maxResponseTime = maxResponseTime;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
generatePrettyTimeAgo(t) {
|
|
||||||
let differenceInMs = new Date().getTime() - new Date(t).getTime();
|
|
||||||
if (differenceInMs > 3600000) {
|
|
||||||
let hours = (differenceInMs/3600000).toFixed(0);
|
|
||||||
return hours + " hour" + (hours !== "1" ? "s" : "") + " ago";
|
|
||||||
}
|
|
||||||
if (differenceInMs > 60000) {
|
|
||||||
let minutes = (differenceInMs/60000).toFixed(0);
|
|
||||||
return minutes + " minute" + (minutes !== "1" ? "s" : "") + " ago";
|
|
||||||
}
|
|
||||||
return (differenceInMs/1000).toFixed(0) + " seconds ago";
|
|
||||||
},
|
|
||||||
generatePath() {
|
generatePath() {
|
||||||
if (!this.data) {
|
if (!this.data) {
|
||||||
return "/";
|
return "/";
|
||||||
|
16
web/app/src/mixins/helper.js
Normal file
16
web/app/src/mixins/helper.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
export const helper = {
|
||||||
|
methods: {
|
||||||
|
generatePrettyTimeAgo(t) {
|
||||||
|
let differenceInMs = new Date().getTime() - new Date(t).getTime();
|
||||||
|
if (differenceInMs > 3600000) {
|
||||||
|
let hours = (differenceInMs / 3600000).toFixed(0);
|
||||||
|
return hours + " hour" + (hours !== "1" ? "s" : "") + " ago";
|
||||||
|
}
|
||||||
|
if (differenceInMs > 60000) {
|
||||||
|
let minutes = (differenceInMs / 60000).toFixed(0);
|
||||||
|
return minutes + " minute" + (minutes !== "1" ? "s" : "") + " ago";
|
||||||
|
}
|
||||||
|
return (differenceInMs / 1000).toFixed(0) + " seconds ago";
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<router-link to="/" class="absolute top-2 left-2 inline-block px-2 py-0 text-lg text-black transition bg-gray-100 rounded shadow ripple hover:shadow-lg hover:bg-gray-200 focus:outline-none">
|
<router-link to="/" class="absolute top-2 left-2 inline-block px-2 py-0 text-lg text-black transition bg-gray-100 rounded shadow ripple hover:shadow-lg hover:bg-gray-200 focus:outline-none">
|
||||||
←
|
←
|
||||||
</router-link>
|
</router-link>
|
||||||
<div class="container mx-auto">
|
<div class="container mx-auto">
|
||||||
<slot v-if="serviceStatus">
|
<slot v-if="serviceStatus">
|
||||||
@ -8,7 +8,7 @@
|
|||||||
<hr class="mb-4" />
|
<hr class="mb-4" />
|
||||||
<Service :data="serviceStatus" :maximumNumberOfResults="20" @showTooltip="showTooltip" />
|
<Service :data="serviceStatus" :maximumNumberOfResults="20" @showTooltip="showTooltip" />
|
||||||
</slot>
|
</slot>
|
||||||
<div v-if="serviceStatus.uptime" class="mt-5">
|
<div v-if="serviceStatus.uptime" class="mt-12">
|
||||||
<h1 class="text-3xl text-monospace text-gray-400">UPTIME</h1>
|
<h1 class="text-3xl text-monospace text-gray-400">UPTIME</h1>
|
||||||
<hr />
|
<hr />
|
||||||
<div class="flex space-x-4 text-center text-2xl mt-5">
|
<div class="flex space-x-4 text-center text-2xl mt-5">
|
||||||
@ -25,20 +25,44 @@
|
|||||||
<h2 class="text-sm text-gray-400">Last hour</h2>
|
<h2 class="text-sm text-gray-400">Last hour</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-xl text-monospace text-gray-400">BADGES</h3>
|
<hr class="mt-1"/>
|
||||||
<hr />
|
<h3 class="text-xl text-monospace text-gray-400 mt-1 text-right">BADGES</h3>
|
||||||
<div class="flex space-x-4 text-center text-2xl mt-5">
|
<div class="flex space-x-4 text-center text-2xl mt-6 relative bottom-12">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<img :src="generateBadgeImageURL('7d')" alt="7d uptime badge" class="mx-auto" />
|
<img :src="generateBadgeImageURL('7d')" alt="7d uptime badge" class="mx-auto" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<img :src="generateBadgeImageURL('24h')" alt="7d uptime badge" class="mx-auto" />
|
<img :src="generateBadgeImageURL('24h')" alt="24h uptime badge" class="mx-auto" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<img :src="generateBadgeImageURL('1h')" alt="7d uptime badge" class="mx-auto" />
|
<img :src="generateBadgeImageURL('1h')" alt="1h uptime badge" class="mx-auto" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl text-monospace text-gray-400 mt-4">EVENTS</h1>
|
||||||
|
<hr class="mb-4" />
|
||||||
|
<div>
|
||||||
|
<slot v-for="event in events" :key="event">
|
||||||
|
<div class="p-3 my-4">
|
||||||
|
<h2 class="text-lg">
|
||||||
|
<span v-if="event.type === 'HEALTHY'" class="border border-green-600 rounded-full px-1 text-green-700 opacity-75 bg-green-100 mr-2"><span class="relative bottom-0.5">🡡</span></span>
|
||||||
|
<span v-else-if="event.type === 'UNHEALTHY'" class="border border-red-500 rounded-full px-1 text-red-700 opacity-75 bg-red-100 mr-2">🡣</span>
|
||||||
|
<span v-else-if="event.type === 'START'" class="mr-2">▶</span>
|
||||||
|
{{ event.fancyText }}
|
||||||
|
</h2>
|
||||||
|
<div class="flex mt-1 text-sm text-gray-400">
|
||||||
|
<div class="flex-1 text-left pl-10">
|
||||||
|
{{ new Date(event.timestamp).toISOString() }}
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 text-right">
|
||||||
|
{{ event.fancyTimeAgo }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Settings @refreshData="fetchData"/>
|
<Settings @refreshData="fetchData"/>
|
||||||
</template>
|
</template>
|
||||||
@ -48,6 +72,7 @@
|
|||||||
import Settings from '@/components/Settings.vue'
|
import Settings from '@/components/Settings.vue'
|
||||||
import Service from '@/components/Service.vue';
|
import Service from '@/components/Service.vue';
|
||||||
import {SERVER_URL} from "@/main.js";
|
import {SERVER_URL} from "@/main.js";
|
||||||
|
import {helper} from "@/mixins/helper.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Details',
|
name: 'Details',
|
||||||
@ -56,6 +81,7 @@ export default {
|
|||||||
Settings,
|
Settings,
|
||||||
},
|
},
|
||||||
emits: ['showTooltip'],
|
emits: ['showTooltip'],
|
||||||
|
mixins: [helper],
|
||||||
methods: {
|
methods: {
|
||||||
fetchData() {
|
fetchData() {
|
||||||
console.log("[Details][fetchData] Fetching data");
|
console.log("[Details][fetchData] Fetching data");
|
||||||
@ -63,8 +89,34 @@ export default {
|
|||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (JSON.stringify(this.serviceStatus) !== JSON.stringify(data)) {
|
if (JSON.stringify(this.serviceStatus) !== JSON.stringify(data)) {
|
||||||
console.log(data);
|
this.serviceStatus = data.serviceStatus;
|
||||||
this.serviceStatus = data;
|
let events = [];
|
||||||
|
for (let i = data.events.length-1; i >= 0; i--) {
|
||||||
|
let event = data.events[i];
|
||||||
|
if (i === data.events.length-1) {
|
||||||
|
if (event.type === "UNHEALTHY") {
|
||||||
|
event.fancyText = "Service is unhealthy";
|
||||||
|
} else {
|
||||||
|
event.fancyText = "Service is healthy";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let nextEvent = data.events[i+1];
|
||||||
|
if (event.type === "HEALTHY") {
|
||||||
|
event.fancyText = "Service became healthy again";
|
||||||
|
} else if (event.type === "UNHEALTHY") {
|
||||||
|
if (nextEvent) {
|
||||||
|
event.fancyText = "Service was unhealthy for " + this.prettifyTimeDifference(nextEvent.timestamp, event.timestamp);
|
||||||
|
} else {
|
||||||
|
event.fancyText = "Service became unhealthy";
|
||||||
|
}
|
||||||
|
} else if (event.type === "START") {
|
||||||
|
event.fancyText = "Monitoring started";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event.fancyTimeAgo = this.generatePrettyTimeAgo(event.timestamp);
|
||||||
|
events.push(event);
|
||||||
|
}
|
||||||
|
this.events = events;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -77,13 +129,18 @@ export default {
|
|||||||
}
|
}
|
||||||
return (uptime * 100).toFixed(2) + "%"
|
return (uptime * 100).toFixed(2) + "%"
|
||||||
},
|
},
|
||||||
|
prettifyTimeDifference(start, end) {
|
||||||
|
let minutes = Math.ceil((new Date(start) - new Date(end))/1000/60);
|
||||||
|
return minutes + (minutes === 1 ? " minute" : " minutes");
|
||||||
|
},
|
||||||
showTooltip(result, event) {
|
showTooltip(result, event) {
|
||||||
this.$emit('showTooltip', result, event);
|
this.$emit('showTooltip', result, event);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
serviceStatus: {}
|
serviceStatus: {},
|
||||||
|
events: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
4
web/app/vue.config.js
Normal file
4
web/app/vue.config.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
module.exports = {
|
||||||
|
filenameHashing: false,
|
||||||
|
productionSourceMap: false
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user