mirror of
https://github.com/amir20/dozzle.git
synced 2025-12-24 22:39:18 +01:00
feat: handles container exits better and uses the real time when container has exited (#3504)
This commit is contained in:
@@ -1,15 +1,25 @@
|
||||
<template>
|
||||
<div>
|
||||
<span class="font-light capitalize"> RUNNING </span>
|
||||
<span class="font-light capitalize"> STATE </span>
|
||||
<span class="font-semibold uppercase"> {{ container.state }} </span>
|
||||
</div>
|
||||
<div v-if="container.startedAt.getFullYear() > 0">
|
||||
<span class="font-light capitalize"> STARTED </span>
|
||||
<span class="font-semibold">
|
||||
<DistanceTime :date="container.created" strict :suffix="false" />
|
||||
<DistanceTime :date="container.startedAt" strict />
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="container.state != 'running' && container.finishedAt.getFullYear() > 0">
|
||||
<span class="font-light capitalize"> Finished </span>
|
||||
<span class="font-semibold">
|
||||
<DistanceTime :date="container.finishedAt" strict />
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="container.state == 'running'">
|
||||
<span class="font-light capitalize"> Load </span>
|
||||
<span class="font-semibold"> {{ container.stat.cpu.toFixed(2) }}% </span>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="container.state == 'running'">
|
||||
<span class="font-light capitalize"> MEM </span>
|
||||
<span class="font-semibold"> {{ formatBytes(container.stat.memoryUsage) }} </span>
|
||||
</div>
|
||||
|
||||
@@ -31,9 +31,45 @@ function createFuzzySearchModal() {
|
||||
initialState: {
|
||||
container: {
|
||||
containers: [
|
||||
new Container("123", new Date(), "image", "test", "command", "host", {}, "running", []),
|
||||
new Container("345", new Date(), "image", "foo bar", "command", "host", {}, "running", []),
|
||||
new Container("567", new Date(), "image", "baz", "command", "host", {}, "running", []),
|
||||
new Container(
|
||||
"123",
|
||||
new Date(),
|
||||
new Date(),
|
||||
new Date(),
|
||||
"image",
|
||||
"test",
|
||||
"command",
|
||||
"host",
|
||||
{},
|
||||
"running",
|
||||
[],
|
||||
),
|
||||
new Container(
|
||||
"345",
|
||||
new Date(),
|
||||
new Date(),
|
||||
new Date(),
|
||||
"image",
|
||||
"foo bar",
|
||||
"command",
|
||||
"host",
|
||||
{},
|
||||
"running",
|
||||
[],
|
||||
),
|
||||
new Container(
|
||||
"567",
|
||||
new Date(),
|
||||
new Date(),
|
||||
new Date(),
|
||||
"image",
|
||||
"baz",
|
||||
"command",
|
||||
"host",
|
||||
{},
|
||||
"running",
|
||||
[],
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
</span>
|
||||
</router-link>
|
||||
<template #content>
|
||||
<ContainerPopup :container="item as Container" />
|
||||
<ContainerPopup :container="item" />
|
||||
</template>
|
||||
</Popup>
|
||||
</li>
|
||||
|
||||
@@ -94,7 +94,19 @@ describe("<ContainerEventSource />", () => {
|
||||
},
|
||||
props: {
|
||||
streamSource: useContainerStream,
|
||||
entity: new Container("abc", new Date(), "image", "name", "command", "localhost", {}, "created", []),
|
||||
entity: new Container(
|
||||
"abc",
|
||||
new Date(), // created
|
||||
new Date(), // started
|
||||
new Date(), // finished
|
||||
"image",
|
||||
"name",
|
||||
"command",
|
||||
"localhost",
|
||||
{},
|
||||
"created",
|
||||
[],
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -137,11 +137,12 @@ function useLogStream(url: Ref<string>, loadMoreUrl?: Ref<string>) {
|
||||
const event = JSON.parse((e as MessageEvent).data) as {
|
||||
actorId: string;
|
||||
name: "container-stopped" | "container-started";
|
||||
time: string;
|
||||
};
|
||||
const containerEvent = new ContainerEventLogEntry(
|
||||
event.name == "container-started" ? "Container started" : "Container stopped",
|
||||
event.actorId,
|
||||
new Date(),
|
||||
new Date(event.time),
|
||||
event.name,
|
||||
);
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ export class Container {
|
||||
constructor(
|
||||
public readonly id: string,
|
||||
public readonly created: Date,
|
||||
public startedAt: Date,
|
||||
public finishedAt: Date,
|
||||
public readonly image: string,
|
||||
name: string,
|
||||
public readonly command: string,
|
||||
|
||||
@@ -61,12 +61,13 @@ export const useContainerStore = defineStore("container", () => {
|
||||
}
|
||||
});
|
||||
es.addEventListener("container-event", (e) => {
|
||||
const event = JSON.parse((e as MessageEvent).data) as { actorId: string; name: string };
|
||||
const event = JSON.parse((e as MessageEvent).data) as { actorId: string; name: string; time: string };
|
||||
const container = allContainersById.value[event.actorId];
|
||||
if (container) {
|
||||
switch (event.name) {
|
||||
case "die":
|
||||
container.state = "exited";
|
||||
container.finishedAt = new Date(event.time);
|
||||
break;
|
||||
case "destroy":
|
||||
container.state = "deleted";
|
||||
@@ -75,6 +76,18 @@ export const useContainerStore = defineStore("container", () => {
|
||||
}
|
||||
});
|
||||
|
||||
es.addEventListener("container-updated", (e) => {
|
||||
const container = JSON.parse((e as MessageEvent).data) as ContainerJson;
|
||||
const existing = allContainersById.value[container.id];
|
||||
if (existing) {
|
||||
existing.name = container.name;
|
||||
existing.state = container.state;
|
||||
existing.health = container.health;
|
||||
existing.startedAt = new Date(container.startedAt);
|
||||
existing.finishedAt = new Date(container.finishedAt);
|
||||
}
|
||||
});
|
||||
|
||||
es.addEventListener("update-host", (e) => {
|
||||
const host = JSON.parse((e as MessageEvent).data) as Host;
|
||||
updateHost(host);
|
||||
@@ -133,6 +146,8 @@ export const useContainerStore = defineStore("container", () => {
|
||||
return new Container(
|
||||
c.id,
|
||||
new Date(c.created),
|
||||
new Date(c.startedAt),
|
||||
new Date(c.finishedAt),
|
||||
c.image,
|
||||
c.name,
|
||||
c.command,
|
||||
|
||||
4
assets/types/Container.d.ts
vendored
4
assets/types/Container.d.ts
vendored
@@ -7,7 +7,9 @@ export interface ContainerStat {
|
||||
|
||||
export type ContainerJson = {
|
||||
readonly id: string;
|
||||
readonly created: number;
|
||||
readonly created: string;
|
||||
readonly startedAt: string;
|
||||
readonly finishedAt: string;
|
||||
readonly image: string;
|
||||
readonly name: string;
|
||||
readonly command: string;
|
||||
|
||||
@@ -385,12 +385,16 @@ func newContainerFromJSON(c types.ContainerJSON, host string) Container {
|
||||
Tty: c.Config.Tty,
|
||||
}
|
||||
|
||||
if createdAt, err := time.Parse(time.RFC3339Nano, c.Created); err == nil {
|
||||
container.Created = createdAt.UTC()
|
||||
}
|
||||
|
||||
if startedAt, err := time.Parse(time.RFC3339Nano, c.State.StartedAt); err == nil {
|
||||
container.StartedAt = startedAt.UTC()
|
||||
}
|
||||
|
||||
if createdAt, err := time.Parse(time.RFC3339Nano, c.Created); err == nil {
|
||||
container.Created = createdAt.UTC()
|
||||
if stoppedAt, err := time.Parse(time.RFC3339Nano, c.State.FinishedAt); err == nil {
|
||||
container.FinishedAt = stoppedAt.UTC()
|
||||
}
|
||||
|
||||
if c.State.Health != nil {
|
||||
|
||||
@@ -154,6 +154,34 @@ func (s *ContainerStore) FindContainer(id string, filter ContainerFilter) (Conta
|
||||
}
|
||||
|
||||
if container, ok := s.containers.Load(id); ok {
|
||||
if container.StartedAt.IsZero() {
|
||||
log.Debug().Str("id", id).Msg("container doesn't have detailed information, fetching it")
|
||||
if newContainer, ok := s.containers.Compute(id, func(c *Container, loaded bool) (*Container, bool) {
|
||||
if loaded {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
if newContainer, err := s.client.FindContainer(ctx, id); err == nil {
|
||||
return &newContainer, false
|
||||
}
|
||||
}
|
||||
return c, false
|
||||
}); ok {
|
||||
event := ContainerEvent{
|
||||
Name: "update",
|
||||
Host: s.client.Host().ID,
|
||||
ActorID: id,
|
||||
}
|
||||
s.subscribers.Range(func(c context.Context, events chan<- ContainerEvent) bool {
|
||||
select {
|
||||
case events <- event:
|
||||
case <-c.Done():
|
||||
s.subscribers.Delete(c)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return *newContainer, nil
|
||||
}
|
||||
}
|
||||
return *container, nil
|
||||
} else {
|
||||
log.Warn().Str("id", id).Msg("container not found")
|
||||
|
||||
@@ -12,19 +12,20 @@ import (
|
||||
|
||||
// Container represents an internal representation of docker containers
|
||||
type Container struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Image string `json:"image"`
|
||||
Command string `json:"command"`
|
||||
Created time.Time `json:"created"`
|
||||
StartedAt time.Time `json:"startedAt,omitempty"`
|
||||
State string `json:"state"`
|
||||
Health string `json:"health,omitempty"`
|
||||
Host string `json:"host,omitempty"`
|
||||
Tty bool `json:"-"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
Stats *utils.RingBuffer[ContainerStat] `json:"stats,omitempty"`
|
||||
Group string `json:"group,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Image string `json:"image"`
|
||||
Command string `json:"command"`
|
||||
Created time.Time `json:"created"`
|
||||
StartedAt time.Time `json:"startedAt"`
|
||||
FinishedAt time.Time `json:"finishedAt"`
|
||||
State string `json:"state"`
|
||||
Health string `json:"health,omitempty"`
|
||||
Host string `json:"host,omitempty"`
|
||||
Tty bool `json:"-"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
Stats *utils.RingBuffer[ContainerStat] `json:"stats,omitempty"`
|
||||
Group string `json:"group,omitempty"`
|
||||
}
|
||||
|
||||
// ContainerStat represent stats instant for a container
|
||||
@@ -41,6 +42,7 @@ type ContainerEvent struct {
|
||||
Host string `json:"host"`
|
||||
ActorID string `json:"actorId"`
|
||||
ActorAttributes map[string]string `json:"actorAttributes,omitempty"`
|
||||
Time time.Time `json:"time"`
|
||||
}
|
||||
|
||||
type ContainerFilter map[string][]string
|
||||
|
||||
@@ -81,23 +81,15 @@ Content-Type: text/html
|
||||
<pre>dev</pre>
|
||||
|
||||
/* snapshot: Test_handler_between_dates */
|
||||
HTTP/1.1 200 OK
|
||||
Connection: close
|
||||
Content-Security-Policy: default-src 'self' 'wasm-unsafe-eval' blob: https://cdn.jsdelivr.net https://*.duckdb.org; style-src 'self' 'unsafe-inline'; img-src 'self' data:;
|
||||
Content-Type: application/x-jsonl; charset=UTF-8
|
||||
|
||||
{"m":"INFO Testing stdout logs...","ts":1589396137772,"id":466600245,"l":"info","s":"stdout","c":"123456"}
|
||||
{"m":"INFO Testing stderr logs...","ts":1589396197772,"id":1101501603,"l":"info","s":"stderr","c":"123456"}
|
||||
|
||||
|
||||
/* snapshot: Test_handler_between_dates_with_fill */
|
||||
HTTP/1.1 200 OK
|
||||
Connection: close
|
||||
Content-Security-Policy: default-src 'self' 'wasm-unsafe-eval' blob: https://cdn.jsdelivr.net https://*.duckdb.org; style-src 'self' 'unsafe-inline'; img-src 'self' data:;
|
||||
Content-Type: application/x-jsonl; charset=UTF-8
|
||||
|
||||
{"m":"INFO Testing stdout logs...","ts":1589396137772,"id":466600245,"l":"info","s":"stdout","c":"123456"}
|
||||
{"m":"INFO Testing stderr logs...","ts":1589396197772,"id":1101501603,"l":"info","s":"stderr","c":"123456"}
|
||||
|
||||
|
||||
/* snapshot: Test_handler_download_logs */
|
||||
INFO Testing logs...
|
||||
|
||||
@@ -145,7 +137,7 @@ data: []
|
||||
|
||||
|
||||
event: container-event
|
||||
data: {"name":"start","host":"localhost","actorId":"1234"}
|
||||
data: {"name":"start","host":"localhost","actorId":"1234","time":"0001-01-01T00:00:00Z"}
|
||||
|
||||
/* snapshot: Test_handler_streamLogs_error_finding_container */
|
||||
HTTP/1.1 404 Not Found
|
||||
@@ -157,16 +149,9 @@ X-Content-Type-Options: nosniff
|
||||
error finding container
|
||||
|
||||
/* snapshot: Test_handler_streamLogs_error_reading */
|
||||
HTTP/1.1 200 OK
|
||||
Connection: close
|
||||
Cache-Control: no-transform
|
||||
Cache-Control: no-cache
|
||||
Connection: keep-alive
|
||||
Content-Security-Policy: default-src 'self' 'wasm-unsafe-eval' blob: https://cdn.jsdelivr.net https://*.duckdb.org; style-src 'self' 'unsafe-inline'; img-src 'self' data:;
|
||||
Content-Type: text/event-stream
|
||||
X-Accel-Buffering: no
|
||||
|
||||
:ping
|
||||
:ping
|
||||
|
||||
|
||||
|
||||
/* snapshot: Test_handler_streamLogs_error_std */
|
||||
HTTP/1.1 400 Bad Request
|
||||
@@ -178,48 +163,27 @@ X-Content-Type-Options: nosniff
|
||||
stdout or stderr is required
|
||||
|
||||
/* snapshot: Test_handler_streamLogs_happy */
|
||||
HTTP/1.1 200 OK
|
||||
Connection: close
|
||||
Cache-Control: no-transform
|
||||
Cache-Control: no-cache
|
||||
Connection: keep-alive
|
||||
Content-Security-Policy: default-src 'self' 'wasm-unsafe-eval' blob: https://cdn.jsdelivr.net https://*.duckdb.org; style-src 'self' 'unsafe-inline'; img-src 'self' data:;
|
||||
Content-Type: text/event-stream
|
||||
X-Accel-Buffering: no
|
||||
|
||||
:ping
|
||||
|
||||
data: {"m":"INFO Testing logs...","ts":0,"id":4256192898,"l":"info","s":"stdout","c":"123456"}
|
||||
|
||||
|
||||
event: container-event
|
||||
data: {"name":"container-stopped","host":"localhost","actorId":"123456"}
|
||||
data: {"name":"container-stopped","host":"localhost","actorId":"123456","time":"<removed>"}
|
||||
|
||||
|
||||
|
||||
|
||||
/* snapshot: Test_handler_streamLogs_happy_container_stopped */
|
||||
HTTP/1.1 200 OK
|
||||
Connection: close
|
||||
Cache-Control: no-transform
|
||||
Cache-Control: no-cache
|
||||
Connection: keep-alive
|
||||
Content-Security-Policy: default-src 'self' 'wasm-unsafe-eval' blob: https://cdn.jsdelivr.net https://*.duckdb.org; style-src 'self' 'unsafe-inline'; img-src 'self' data:;
|
||||
Content-Type: text/event-stream
|
||||
X-Accel-Buffering: no
|
||||
|
||||
:ping
|
||||
|
||||
event: container-event
|
||||
data: {"name":"container-stopped","host":"localhost","actorId":"123456"}
|
||||
data: {"name":"container-stopped","host":"localhost","actorId":"123456","time":"<removed>"}
|
||||
|
||||
|
||||
|
||||
|
||||
/* snapshot: Test_handler_streamLogs_happy_with_id */
|
||||
HTTP/1.1 200 OK
|
||||
Connection: close
|
||||
Cache-Control: no-transform
|
||||
Cache-Control: no-cache
|
||||
Connection: keep-alive
|
||||
Content-Security-Policy: default-src 'self' 'wasm-unsafe-eval' blob: https://cdn.jsdelivr.net https://*.duckdb.org; style-src 'self' 'unsafe-inline'; img-src 'self' data:;
|
||||
Content-Type: text/event-stream
|
||||
X-Accel-Buffering: no
|
||||
|
||||
:ping
|
||||
|
||||
data: {"m":"INFO Testing logs...","ts":1589396137772,"id":1469707724,"l":"info","s":"stdout","c":"123456"}
|
||||
@@ -227,4 +191,4 @@ id: 1589396137772
|
||||
|
||||
|
||||
event: container-event
|
||||
data: {"name":"container-stopped","host":"localhost","actorId":"123456"}
|
||||
data: {"name":"container-stopped","host":"localhost","actorId":"123456","time":"<removed>"}
|
||||
@@ -86,6 +86,14 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
case "update":
|
||||
log.Debug().Str("id", event.ActorID).Msg("container updated")
|
||||
if containerService, err := h.multiHostService.FindContainer(event.Host, event.ActorID, usersFilter); err == nil {
|
||||
if err := sseWriter.Event("container-updated", containerService.Container); err != nil {
|
||||
log.Error().Err(err).Msg("error writing event to event stream")
|
||||
return
|
||||
}
|
||||
}
|
||||
case "health_status: healthy", "health_status: unhealthy":
|
||||
log.Debug().Str("container", event.ActorID).Str("health", event.Name).Msg("container health status")
|
||||
healthy := "unhealthy"
|
||||
|
||||
@@ -342,12 +342,22 @@ func (h *handler) streamLogsForContainers(w http.ResponseWriter, r *http.Request
|
||||
log.Error().Err(err).Msg("error while finding container")
|
||||
return
|
||||
}
|
||||
container = containerService.Container
|
||||
start := utils.Max(absoluteTime, container.StartedAt)
|
||||
err = containerService.StreamLogs(r.Context(), start, stdTypes, liveLogs)
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
log.Debug().Str("container", container.ID).Msg("streaming ended")
|
||||
events <- &docker.ContainerEvent{ActorID: container.ID, Name: "container-stopped", Host: container.Host}
|
||||
finishedAt := container.FinishedAt
|
||||
if container.FinishedAt.IsZero() {
|
||||
finishedAt = time.Now()
|
||||
}
|
||||
events <- &docker.ContainerEvent{
|
||||
ActorID: container.ID,
|
||||
Name: "container-stopped",
|
||||
Host: container.Host,
|
||||
Time: finishedAt,
|
||||
}
|
||||
} else if !errors.Is(err, context.Canceled) {
|
||||
log.Error().Err(err).Str("container", container.ID).Msg("unknown error while streaming logs")
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"net/http"
|
||||
@@ -60,7 +61,8 @@ func Test_handler_streamLogs_happy(t *testing.T) {
|
||||
handler := createDefaultHandler(mockedClient)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
reader := strings.NewReader(regexp.MustCompile(`"time":"[^"]*"`).ReplaceAllString(rr.Body.String(), `"time":"<removed>"`))
|
||||
abide.AssertReader(t, t.Name(), reader)
|
||||
mockedClient.AssertExpectations(t)
|
||||
}
|
||||
|
||||
@@ -105,7 +107,8 @@ func Test_handler_streamLogs_happy_with_id(t *testing.T) {
|
||||
handler := createDefaultHandler(mockedClient)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
reader := strings.NewReader(regexp.MustCompile(`"time":"[^"]*"`).ReplaceAllString(rr.Body.String(), `"time":"<removed>"`))
|
||||
abide.AssertReader(t, t.Name(), reader)
|
||||
mockedClient.AssertExpectations(t)
|
||||
}
|
||||
|
||||
@@ -141,7 +144,8 @@ func Test_handler_streamLogs_happy_container_stopped(t *testing.T) {
|
||||
handler := createDefaultHandler(mockedClient)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
reader := strings.NewReader(regexp.MustCompile(`"time":"[^"]*"`).ReplaceAllString(rr.Body.String(), `"time":"<removed>"`))
|
||||
abide.AssertReader(t, t.Name(), reader)
|
||||
mockedClient.AssertExpectations(t)
|
||||
}
|
||||
|
||||
@@ -178,7 +182,8 @@ func Test_handler_streamLogs_error_reading(t *testing.T) {
|
||||
handler := createDefaultHandler(mockedClient)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
reader := strings.NewReader(regexp.MustCompile(`"time":"[^"]*"`).ReplaceAllString(rr.Body.String(), `"time":"<removed>"`))
|
||||
abide.AssertReader(t, t.Name(), reader)
|
||||
mockedClient.AssertExpectations(t)
|
||||
}
|
||||
|
||||
@@ -243,7 +248,8 @@ func Test_handler_between_dates(t *testing.T) {
|
||||
handler := createDefaultHandler(mockedClient)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
reader := strings.NewReader(regexp.MustCompile(`"time":"[^"]*"`).ReplaceAllString(rr.Body.String(), `"time":"<removed>"`))
|
||||
abide.AssertReader(t, t.Name(), reader)
|
||||
mockedClient.AssertExpectations(t)
|
||||
}
|
||||
|
||||
@@ -289,7 +295,8 @@ func Test_handler_between_dates_with_fill(t *testing.T) {
|
||||
handler := createDefaultHandler(mockedClient)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
|
||||
reader := strings.NewReader(regexp.MustCompile(`"time":"[^"]*"`).ReplaceAllString(rr.Body.String(), `"time":"<removed>"`))
|
||||
abide.AssertReader(t, t.Name(), reader)
|
||||
mockedClient.AssertExpectations(t)
|
||||
}
|
||||
|
||||
|
||||
@@ -30,11 +30,6 @@ export default defineConfig(() => ({
|
||||
target: "esnext",
|
||||
},
|
||||
plugins: [
|
||||
VueMacros({
|
||||
plugins: {
|
||||
vue: Vue(),
|
||||
},
|
||||
}),
|
||||
VueRouter({
|
||||
routesFolder: {
|
||||
src: "./assets/pages",
|
||||
@@ -42,6 +37,11 @@ export default defineConfig(() => ({
|
||||
dts: "./assets/typed-router.d.ts",
|
||||
importMode: "sync",
|
||||
}),
|
||||
VueMacros({
|
||||
plugins: {
|
||||
vue: Vue(),
|
||||
},
|
||||
}),
|
||||
Icons({
|
||||
autoInstall: true,
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user