Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79f0e2127a | ||
|
|
163b1c7e28 | ||
|
|
db01579f04 | ||
|
|
be7c154d6b | ||
|
|
b1bc706de2 | ||
|
|
40f5cb1301 | ||
|
|
cedfbee983 | ||
|
|
c835f51cc4 | ||
|
|
5ab06d5906 | ||
|
|
d44316fa9c | ||
|
|
6ef3da9abd | ||
|
|
752495ed6f | ||
|
|
8f895e40bc | ||
|
|
cd9ddcf427 | ||
|
|
bbc7794006 | ||
|
|
7dc37f130c | ||
|
|
0711bc1c76 | ||
|
|
0aa24386b2 | ||
|
|
ca35b93671 | ||
|
|
a6220e4d38 | ||
|
|
4ed64a7cce | ||
|
|
0f27e11084 | ||
|
|
85eafc9c40 | ||
|
|
332cc384ea | ||
|
|
72fd31f85b | ||
|
|
a0ce370e9e | ||
|
|
e823904865 | ||
|
|
22bbfe1592 | ||
|
|
770e1818f0 | ||
|
|
d6fab75f8f | ||
|
|
17c18c156e | ||
|
|
5eca19840e | ||
|
|
b1d7b8ba55 | ||
|
|
e2ee430bbd | ||
|
|
0755a71dc2 | ||
|
|
60758db9c8 | ||
|
|
7b96196904 | ||
|
|
efcfa0e375 | ||
|
|
66f9204ae6 | ||
|
|
73c023ce22 | ||
|
|
261517ac3f | ||
|
|
2e0a546aa2 | ||
|
|
72ed7b50ba | ||
|
|
486bcec363 | ||
|
|
3db0ad42fe | ||
|
|
c1a75e21ba | ||
|
|
96c5e24501 | ||
|
|
c1a16fd76e | ||
|
|
42fab58c9f | ||
|
|
400cef767f |
2
.github/workflows/deploy.yml
vendored
2
.github/workflows/deploy.yml
vendored
@@ -62,7 +62,7 @@ jobs:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3.1.0
|
||||
uses: docker/build-push-action@v3.1.1
|
||||
with:
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
|
||||
|
||||
4
.github/workflows/dev.yml
vendored
4
.github/workflows/dev.yml
vendored
@@ -27,10 +27,10 @@ jobs:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3.1.0
|
||||
uses: docker/build-push-action@v3.1.1
|
||||
with:
|
||||
push: true
|
||||
platforms: linux/amd64
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
build-args: TAG=${{ steps.meta.outputs.version }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@ static
|
||||
dozzle
|
||||
coverage
|
||||
.pnpm-debug.log
|
||||
.vscode
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Build assets
|
||||
FROM --platform=$BUILDPLATFORM node:18-alpine as node
|
||||
|
||||
RUN apk add --no-cache git openssh make g++ util-linux python3 && npm install -g pnpm
|
||||
RUN npm install -g pnpm
|
||||
|
||||
|
||||
WORKDIR /build
|
||||
@@ -19,9 +19,9 @@ COPY assets ./assets
|
||||
# Install dependencies
|
||||
RUN pnpm install -r --offline --prod --ignore-scripts && pnpm build
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:1.18.4-alpine AS builder
|
||||
FROM --platform=$BUILDPLATFORM golang:1.19.0-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache git ca-certificates && mkdir /dozzle
|
||||
RUN apk add --no-cache ca-certificates && mkdir /dozzle
|
||||
|
||||
WORKDIR /dozzle
|
||||
|
||||
@@ -54,8 +54,6 @@ ENV PATH /bin
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
COPY --from=builder /dozzle/dozzle /dozzle
|
||||
|
||||
HEALTHCHECK --start-period=4s --interval=2s CMD [ "/dozzle", "healthcheck" ]
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT ["/dozzle"]
|
||||
|
||||
24
README.md
24
README.md
@@ -58,6 +58,30 @@ Dozzle will be available at [http://localhost:8888/](http://localhost:8888/). Yo
|
||||
ports:
|
||||
- 9999:8080
|
||||
|
||||
|
||||
### Enabling health check
|
||||
|
||||
Dozzle doesn't enable healthcheck by default as it adds extra CPU usage. `healthcheck` can be enabled manually.
|
||||
|
||||
version: "3"
|
||||
services:
|
||||
dozzle:
|
||||
container_name: dozzle
|
||||
image: amir20/dozzle:latest
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
ports:
|
||||
- 8080:8080
|
||||
environment:
|
||||
DOZZLE_LEVEL: trace
|
||||
healthcheck:
|
||||
test: [ "CMD", "/dozzle", "healthcheck" ]
|
||||
interval: 3s
|
||||
timeout: 30s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
|
||||
|
||||
#### Security
|
||||
|
||||
You can control the device Dozzle binds to by passing `--addr` parameter. For example,
|
||||
|
||||
@@ -69,20 +69,6 @@ const containerStore = useContainerStore();
|
||||
const { activeContainers, visibleContainers } = storeToRefs(containerStore);
|
||||
|
||||
onMounted(() => {
|
||||
if (smallerScrollbars.value) {
|
||||
document.documentElement.classList.add("has-custom-scrollbars");
|
||||
}
|
||||
switch (lightTheme.value) {
|
||||
case "dark":
|
||||
document.documentElement.setAttribute("data-theme", "dark");
|
||||
break;
|
||||
case "light":
|
||||
document.documentElement.setAttribute("data-theme", "light");
|
||||
break;
|
||||
default:
|
||||
document.documentElement.removeAttribute("data-theme");
|
||||
}
|
||||
|
||||
hotkeys("command+k, ctrl+k", (event, handler) => {
|
||||
event.preventDefault();
|
||||
showFuzzySearch();
|
||||
@@ -121,7 +107,7 @@ function showFuzzySearch() {
|
||||
active: true,
|
||||
});
|
||||
}
|
||||
function onResized(e) {
|
||||
function onResized(e: any) {
|
||||
if (e.length == 2) {
|
||||
menuWidth.value = e[0].size;
|
||||
}
|
||||
|
||||
2
assets/components.d.ts
vendored
2
assets/components.d.ts
vendored
@@ -13,8 +13,10 @@ declare module '@vue/runtime-core' {
|
||||
ContainerStat: typeof import('./components/ContainerStat.vue')['default']
|
||||
ContainerTitle: typeof import('./components/ContainerTitle.vue')['default']
|
||||
DropdownMenu: typeof import('./components/DropdownMenu.vue')['default']
|
||||
FieldList: typeof import('./components/FieldList.vue')['default']
|
||||
FuzzySearchModal: typeof import('./components/FuzzySearchModal.vue')['default']
|
||||
InfiniteLoader: typeof import('./components/InfiniteLoader.vue')['default']
|
||||
JSONPayload: typeof import('./components/JSONPayload.vue')['default']
|
||||
LogActionsToolbar: typeof import('./components/LogActionsToolbar.vue')['default']
|
||||
LogContainer: typeof import('./components/LogContainer.vue')['default']
|
||||
LogEventSource: typeof import('./components/LogEventSource.vue')['default']
|
||||
|
||||
@@ -1,34 +1,28 @@
|
||||
<template>
|
||||
<div class="is-size-7 is-uppercase columns is-marginless is-mobile">
|
||||
<div class="is-size-7 is-uppercase columns is-marginless is-mobile" v-if="container.stat">
|
||||
<div class="column is-narrow has-text-weight-bold">
|
||||
{{ state }}
|
||||
{{ container.state }}
|
||||
</div>
|
||||
<div class="column is-narrow" v-if="stat.memoryUsage !== null">
|
||||
<div class="column is-narrow" v-if="container.stat.memoryUsage !== null">
|
||||
<span class="has-text-weight-light has-spacer">mem</span>
|
||||
<span class="has-text-weight-bold">
|
||||
{{ formatBytes(stat.memoryUsage) }}
|
||||
{{ formatBytes(container.stat.memoryUsage) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="column is-narrow" v-if="stat.cpu !== null">
|
||||
<div class="column is-narrow" v-if="container.stat.cpu !== null">
|
||||
<span class="has-text-weight-light has-spacer">load</span>
|
||||
<span class="has-text-weight-bold"> {{ stat.cpu }}% </span>
|
||||
<span class="has-text-weight-bold"> {{ container.stat.cpu }}% </span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ContainerStat } from "@/types/Container";
|
||||
import { PropType } from "vue";
|
||||
import { Container } from "@/types/Container";
|
||||
import { ComputedRef, inject } from "vue";
|
||||
import { formatBytes } from "@/utils";
|
||||
|
||||
defineProps({
|
||||
stat: {
|
||||
type: Object as PropType<ContainerStat>,
|
||||
required: true,
|
||||
},
|
||||
state: String,
|
||||
});
|
||||
const container = inject("container") as ComputedRef<Container>;
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -9,13 +9,8 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Container } from "@/types/Container";
|
||||
import { PropType } from "vue";
|
||||
defineProps({
|
||||
container: {
|
||||
type: Object as PropType<Container>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
import { inject, ComputedRef } from "vue";
|
||||
const container = inject("container") as ComputedRef<Container>;
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
84
assets/components/FieldList.vue
Normal file
84
assets/components/FieldList.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<ul v-if="expanded" ref="root">
|
||||
<li v-for="(value, name) in fields">
|
||||
<template v-if="isObject(value)">
|
||||
<span class="has-text-grey">{{ name }}=</span>
|
||||
<field-list
|
||||
:fields="value"
|
||||
:parent-key="parentKey.concat(name)"
|
||||
:visible-keys="visibleKeys"
|
||||
expanded
|
||||
></field-list>
|
||||
</template>
|
||||
<template v-else-if="Array.isArray(value)">
|
||||
<a @click="toggleField(name)"> {{ hasField(name) ? "remove" : "add" }} </a>
|
||||
<span class="has-text-grey">{{ name }}=</span>[
|
||||
<span class="has-text-weight-bold" v-for="(item, index) in value">
|
||||
{{ item }}
|
||||
<span v-if="index !== value.length - 1">,</span>
|
||||
</span>
|
||||
]
|
||||
</template>
|
||||
<template v-else>
|
||||
<a @click="toggleField(name)"> {{ hasField(name) ? "remove" : "add" }} </a>
|
||||
<span class="has-text-grey">{{ name }}=</span><span class="has-text-weight-bold">{{ value }}</span>
|
||||
</template>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { arrayEquals, isObject } from "@/utils";
|
||||
import { nextTick, PropType, ref, toRaw } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
fields: {
|
||||
type: Object as PropType<Record<string, any>>,
|
||||
required: true,
|
||||
},
|
||||
expanded: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
parentKey: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: [],
|
||||
},
|
||||
visibleKeys: {
|
||||
type: Array as PropType<string[][]>,
|
||||
default: [],
|
||||
},
|
||||
});
|
||||
|
||||
const root = ref<HTMLElement>();
|
||||
|
||||
async function toggleField(field: string) {
|
||||
const index = fieldIndex(field);
|
||||
|
||||
if (index > -1) {
|
||||
props.visibleKeys.splice(index, 1);
|
||||
} else {
|
||||
props.visibleKeys.push(props.parentKey.concat(field));
|
||||
}
|
||||
|
||||
await nextTick();
|
||||
|
||||
root.value?.scrollIntoView({
|
||||
block: "center",
|
||||
});
|
||||
}
|
||||
|
||||
function hasField(field: string) {
|
||||
return fieldIndex(field) > -1;
|
||||
}
|
||||
|
||||
function fieldIndex(field: string) {
|
||||
const path = props.parentKey.concat(field);
|
||||
return props.visibleKeys.findIndex((keys) => arrayEquals(toRaw(keys), toRaw(path)));
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
ul {
|
||||
margin-left: 2em;
|
||||
}
|
||||
</style>
|
||||
@@ -22,7 +22,7 @@ const root = ref<HTMLElement>();
|
||||
const observer = new IntersectionObserver(async (entries) => {
|
||||
if (entries[0].intersectionRatio <= 0) return;
|
||||
if (props.onLoadMore && props.enabled) {
|
||||
const scrollingParent = root.value.closest("[data-scrolling]") || document.documentElement;
|
||||
const scrollingParent = root.value?.closest("[data-scrolling]") || document.documentElement;
|
||||
const previousHeight = scrollingParent.scrollHeight;
|
||||
isLoading.value = true;
|
||||
await props.onLoadMore();
|
||||
@@ -32,7 +32,7 @@ const observer = new IntersectionObserver(async (entries) => {
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => observer.observe(root.value));
|
||||
onMounted(() => observer.observe(root.value!));
|
||||
onUnmounted(() => observer.disconnect());
|
||||
</script>
|
||||
|
||||
|
||||
55
assets/components/JSONPayload.vue
Normal file
55
assets/components/JSONPayload.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<ul class="fields" @click="expanded = !expanded">
|
||||
<li v-for="(value, name) in logEntry.message">
|
||||
<template v-if="value">
|
||||
<span class="has-text-grey">{{ name }}=</span>
|
||||
<span class="has-text-weight-bold" v-html="markSearch(value)"></span>
|
||||
</template>
|
||||
</li>
|
||||
</ul>
|
||||
<field-list :fields="logEntry.unfilteredPayload" :expanded="expanded" :visible-keys="visibleKeys"></field-list>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useSearchFilter } from "@/composables/search";
|
||||
import { VisibleLogEntry } from "@/types/VisibleLogEntry";
|
||||
|
||||
import { PropType, ref } from "vue";
|
||||
|
||||
const { markSearch } = useSearchFilter();
|
||||
|
||||
defineProps({
|
||||
logEntry: {
|
||||
type: Object as PropType<VisibleLogEntry>,
|
||||
required: true,
|
||||
},
|
||||
visibleKeys: {
|
||||
type: Array as PropType<string[][]>,
|
||||
default: [],
|
||||
},
|
||||
});
|
||||
|
||||
const expanded = ref(false);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.fields {
|
||||
display: inline-block;
|
||||
list-style: none;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
&::after {
|
||||
content: "expand json";
|
||||
color: var(--secondary-color);
|
||||
display: inline-block;
|
||||
margin-left: 0.5em;
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -41,11 +41,11 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, PropType } from "vue";
|
||||
import { inject, onMounted, onUnmounted, PropType, ComputedRef } from "vue";
|
||||
import hotkeys from "hotkeys-js";
|
||||
import config from "@/stores/config";
|
||||
import { Container } from "@/types/Container";
|
||||
import { useSearchFilter } from "@/composables/search";
|
||||
import { Container } from "@/types/Container";
|
||||
|
||||
const { showSearch } = useSearchFilter();
|
||||
|
||||
@@ -56,10 +56,6 @@ const props = defineProps({
|
||||
type: Function as PropType<(e: Event) => void>,
|
||||
default: (e: Event) => {},
|
||||
},
|
||||
container: {
|
||||
type: Object as () => Container,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const onHotkey = (event: Event) => {
|
||||
@@ -67,6 +63,8 @@ const onHotkey = (event: Event) => {
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const container = inject("container") as ComputedRef<Container>;
|
||||
|
||||
onMounted(() => hotkeys("shift+command+l, shift+ctrl+l", onHotkey));
|
||||
onUnmounted(() => hotkeys.unbind("shift+command+l, shift+ctrl+l", onHotkey));
|
||||
</script>
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
<template #header v-if="showTitle">
|
||||
<div class="mr-0 columns is-vcentered is-marginless is-hidden-mobile">
|
||||
<div class="column is-clipped is-paddingless">
|
||||
<container-title :container="container" @close="$emit('close')" />
|
||||
<container-title @close="$emit('close')" />
|
||||
</div>
|
||||
<div class="column is-narrow is-paddingless">
|
||||
<container-stat :stat="container.stat" :state="container.state" v-if="container.stat" />
|
||||
<container-stat v-if="container.stat" />
|
||||
</div>
|
||||
|
||||
<div class="mr-2 column is-narrow is-paddingless">
|
||||
<log-actions-toolbar :container="container" :onClearClicked="onClearClicked" />
|
||||
<log-actions-toolbar :onClearClicked="onClearClicked" />
|
||||
</div>
|
||||
<div class="mr-2 column is-narrow is-paddingless" v-if="closable">
|
||||
<button class="delete is-medium" @click="emit('close')"></button>
|
||||
@@ -18,13 +18,13 @@
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ setLoading }">
|
||||
<log-viewer-with-source ref="viewer" :id="id" @loading-more="setLoading($event)" />
|
||||
<log-viewer-with-source ref="viewer" @loading-more="setLoading($event)" />
|
||||
</template>
|
||||
</scrollable-view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, toRefs } from "vue";
|
||||
import { provide, ref, toRefs } from "vue";
|
||||
import LogViewerWithSource from "./LogViewerWithSource.vue";
|
||||
import { useContainerStore } from "@/stores/container";
|
||||
|
||||
@@ -54,6 +54,8 @@ const store = useContainerStore();
|
||||
|
||||
const container = store.currentContainer(id);
|
||||
|
||||
provide("container", container);
|
||||
|
||||
const viewer = ref<InstanceType<typeof LogViewerWithSource>>();
|
||||
|
||||
function onClearClicked() {
|
||||
|
||||
@@ -6,29 +6,10 @@ import LogEventSource from "./LogEventSource.vue";
|
||||
import LogViewer from "./LogViewer.vue";
|
||||
import { settings } from "../composables/settings";
|
||||
import { useSearchFilter } from "@/composables/search";
|
||||
import { vi, describe, expect, beforeEach, test, beforeAll, afterAll } from "vitest";
|
||||
import { computed, Ref } from "vue";
|
||||
import { vi, describe, expect, beforeEach, test, beforeAll, afterAll, afterEach } from "vitest";
|
||||
import { computed, nextTick } from "vue";
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
|
||||
vi.mock("lodash.debounce", () => ({
|
||||
__esModule: true,
|
||||
default: vi.fn((fn) => {
|
||||
fn.cancel = () => {};
|
||||
return fn;
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/stores/container", () => ({
|
||||
__esModule: true,
|
||||
useContainerStore() {
|
||||
return {
|
||||
currentContainer(id: Ref<string>) {
|
||||
return computed(() => ({ id: id.value }));
|
||||
},
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/stores/config", () => ({
|
||||
__esModule: true,
|
||||
default: { base: "" },
|
||||
@@ -47,6 +28,13 @@ describe("<LogEventSource />", () => {
|
||||
observe: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
}));
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(1560336942459);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
function createLogEventSource(
|
||||
@@ -78,13 +66,16 @@ describe("<LogEventSource />", () => {
|
||||
components: {
|
||||
LogViewer,
|
||||
},
|
||||
provide: {
|
||||
container: computed(() => ({ id: "abc", image: "test:v123" })),
|
||||
},
|
||||
},
|
||||
slots: {
|
||||
default: `
|
||||
<template #scoped="params"><log-viewer :messages="params.messages"></log-viewer></template>
|
||||
`,
|
||||
},
|
||||
props: { id: "abc" },
|
||||
props: {},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -111,68 +102,27 @@ describe("<LogEventSource />", () => {
|
||||
const wrapper = createLogEventSource();
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
|
||||
data: `2019-06-12T10:55:42.459034602Z "This is a message."`,
|
||||
data: `{"ts":1560336942459, "m":"This is a message.", "id":1}`,
|
||||
});
|
||||
|
||||
const [message, _] = wrapper.vm.messages;
|
||||
const { key, ...messageWithoutKey } = message;
|
||||
|
||||
expect(key).toBe("2019-06-12T10:55:42.459034602Z");
|
||||
expect(messageWithoutKey).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("should parse messages with loki's timestamp format", async () => {
|
||||
const wrapper = createLogEventSource();
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({ data: `2020-04-27T12:35:43.272974324+02:00 xxxxx` });
|
||||
vi.runAllTimers();
|
||||
await nextTick();
|
||||
|
||||
const [message, _] = wrapper.vm.messages;
|
||||
const { key, ...messageWithoutKey } = message;
|
||||
|
||||
expect(key).toBe("2020-04-27T12:35:43.272974324+02:00");
|
||||
expect(messageWithoutKey).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("should pass messages to slot", async () => {
|
||||
const wrapper = createLogEventSource();
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
|
||||
data: `2019-06-12T10:55:42.459034602Z "This is a message."`,
|
||||
});
|
||||
const [message, _] = wrapper.getComponent(LogViewer).vm.messages;
|
||||
|
||||
const { key, ...messageWithoutKey } = message;
|
||||
|
||||
expect(key).toBe("2019-06-12T10:55:42.459034602Z");
|
||||
|
||||
expect(messageWithoutKey).toMatchSnapshot();
|
||||
expect(message).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("render html correctly", () => {
|
||||
const RealDate = Date;
|
||||
beforeAll(() => {
|
||||
// @ts-ignore
|
||||
global.Date = class extends RealDate {
|
||||
constructor(arg: any | number) {
|
||||
super(arg);
|
||||
if (arg) {
|
||||
return new RealDate(arg);
|
||||
} else {
|
||||
return new RealDate(1560336936000);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
afterAll(() => (global.Date = RealDate));
|
||||
|
||||
test("should render messages", async () => {
|
||||
const wrapper = createLogEventSource();
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
|
||||
data: `2019-06-12T10:55:42.459034602Z "This is a message."`,
|
||||
data: `{"ts":1560336942459, "m":"This is a message.", "id":1}`,
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
vi.runAllTimers();
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -180,10 +130,12 @@ describe("<LogEventSource />", () => {
|
||||
const wrapper = createLogEventSource();
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
|
||||
data: `2019-06-12T10:55:42.459034602Z \x1b[30mblack\x1b[37mwhite`,
|
||||
data: '{"ts":1560336942459,"m":"\\u001b[30mblack\\u001b[37mwhite", "id":1}',
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
vi.runAllTimers();
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -191,10 +143,12 @@ describe("<LogEventSource />", () => {
|
||||
const wrapper = createLogEventSource();
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
|
||||
data: `2019-06-12T10:55:42.459034602Z <test>foo bar</test>`,
|
||||
data: `{"ts":1560336942459, "m":"<test>foo bar</test>", "id":1}`,
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
vi.runAllTimers();
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -202,10 +156,12 @@ describe("<LogEventSource />", () => {
|
||||
const wrapper = createLogEventSource({ hourStyle: "12" });
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
|
||||
data: `2019-06-12T23:55:42.459034602Z <test>foo bar</test>`,
|
||||
data: `{"ts":1560336942459, "m":"<test>foo bar</test>", "id":1}`,
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
vi.runAllTimers();
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -213,10 +169,12 @@ describe("<LogEventSource />", () => {
|
||||
const wrapper = createLogEventSource({ hourStyle: "24" });
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
|
||||
data: `2019-06-12T23:55:42.459034602Z <test>foo bar</test>`,
|
||||
data: `{"ts":1560336942459, "m":"<test>foo bar</test>", "id":1}`,
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
vi.runAllTimers();
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -224,13 +182,15 @@ describe("<LogEventSource />", () => {
|
||||
const wrapper = createLogEventSource({ searchFilter: "test" });
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
|
||||
data: `2019-06-11T10:55:42.459034602Z Foo bar`,
|
||||
data: `{"ts":1560336942459, "m":"foo bar", "id":1}`,
|
||||
});
|
||||
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
|
||||
data: `2019-06-12T10:55:42.459034602Z This is a test <hi></hi>`,
|
||||
data: `{"ts":1560336942459, "m":"test bar", "id":2}`,
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
vi.runAllTimers();
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.find("ul.events").html()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,133 +1,25 @@
|
||||
<template>
|
||||
<infinite-loader :onLoadMore="loadOlderLogs" :enabled="messages.length > 100"></infinite-loader>
|
||||
<infinite-loader :onLoadMore="fetchMore" :enabled="messages.length > 100"></infinite-loader>
|
||||
<slot :messages="messages"></slot>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, ref, watch, onUnmounted } from "vue";
|
||||
import debounce from "lodash.debounce";
|
||||
import { useEventSource } from "@/composables/eventsource";
|
||||
import { Container } from "@/types/Container";
|
||||
import { inject, ComputedRef } from "vue";
|
||||
|
||||
import { LogEntry } from "@/types/LogEntry";
|
||||
import InfiniteLoader from "./InfiniteLoader.vue";
|
||||
import config from "@/stores/config";
|
||||
import { useContainerStore } from "@/stores/container";
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { id } = toRefs(props);
|
||||
const emit = defineEmits(["loading-more"]);
|
||||
const store = useContainerStore();
|
||||
const container = store.currentContainer(id);
|
||||
const container = inject("container") as ComputedRef<Container>;
|
||||
const { connect, messages, loadOlderLogs } = useEventSource(container);
|
||||
|
||||
const messages = ref<LogEntry[]>([]);
|
||||
const buffer = ref<LogEntry[]>([]);
|
||||
|
||||
function flushNow() {
|
||||
messages.value.push(...buffer.value);
|
||||
buffer.value = [];
|
||||
}
|
||||
|
||||
const flushBuffer = debounce(flushNow, 250, { maxWait: 1000 });
|
||||
|
||||
let es: EventSource | null = null;
|
||||
let lastEventId = "";
|
||||
|
||||
function connect({ clear } = { clear: true }) {
|
||||
es?.close();
|
||||
|
||||
if (clear) {
|
||||
flushBuffer.cancel();
|
||||
messages.value = [];
|
||||
buffer.value = [];
|
||||
lastEventId = "";
|
||||
}
|
||||
|
||||
es = new EventSource(`${config.base}/api/logs/stream?id=${props.id}&lastEventId=${lastEventId}`);
|
||||
es.addEventListener("container-stopped", () => {
|
||||
es?.close();
|
||||
es = null;
|
||||
buffer.value.push({
|
||||
event: "container-stopped",
|
||||
message: "Container stopped",
|
||||
date: new Date(),
|
||||
key: new Date().toString(),
|
||||
});
|
||||
flushBuffer();
|
||||
flushBuffer.flush();
|
||||
});
|
||||
es.addEventListener("error", (e) => console.error("EventSource failed: " + JSON.stringify(e)));
|
||||
es.onmessage = (e) => {
|
||||
lastEventId = e.lastEventId;
|
||||
if (e.data) {
|
||||
buffer.value.push(parseMessage(e.data));
|
||||
flushBuffer();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function loadOlderLogs() {
|
||||
if (messages.value.length < 300) return;
|
||||
|
||||
emit("loading-more", true);
|
||||
const to = messages.value[0].date;
|
||||
const last = messages.value[299].date;
|
||||
const delta = to.getTime() - last.getTime();
|
||||
const from = new Date(to.getTime() + delta);
|
||||
const logs = await (
|
||||
await fetch(`${config.base}/api/logs?id=${props.id}&from=${from.toISOString()}&to=${to.toISOString()}`)
|
||||
).text();
|
||||
if (logs) {
|
||||
const newMessages = logs
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => parseMessage(line));
|
||||
messages.value.unshift(...newMessages);
|
||||
}
|
||||
emit("loading-more", false);
|
||||
}
|
||||
|
||||
function parseMessage(data: String): LogEntry {
|
||||
let i = data.indexOf(" ");
|
||||
if (i == -1) {
|
||||
i = data.length;
|
||||
}
|
||||
const key = data.substring(0, i);
|
||||
const date = new Date(key);
|
||||
const message = data.substring(i + 1);
|
||||
return { key, date, message };
|
||||
}
|
||||
|
||||
watch(
|
||||
() => container.value.state,
|
||||
(newValue, oldValue) => {
|
||||
console.log("LogEventSource: container changed", newValue, oldValue);
|
||||
if (newValue == "running" && newValue != oldValue) {
|
||||
buffer.value.push({
|
||||
event: "container-started",
|
||||
message: "Container started",
|
||||
date: new Date(),
|
||||
key: new Date().toString(),
|
||||
});
|
||||
connect({ clear: false });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
if (es) {
|
||||
es.close();
|
||||
}
|
||||
});
|
||||
|
||||
connect();
|
||||
watch(id, () => connect());
|
||||
const beforeLoading = () => emit("loading-more", true);
|
||||
const afterLoading = () => emit("loading-more", false);
|
||||
|
||||
defineExpose({
|
||||
clear: () => (messages.value = []),
|
||||
});
|
||||
|
||||
const fetchMore = () => loadOlderLogs({ beforeLoading, afterLoading });
|
||||
|
||||
connect();
|
||||
</script>
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
<ul class="events" ref="events" :class="{ 'disable-wrap': !softWrap, [size]: true }">
|
||||
<li
|
||||
v-for="(item, index) in filtered"
|
||||
:key="item.key"
|
||||
:data-key="item.key"
|
||||
:key="item.id"
|
||||
:data-key="item.id"
|
||||
:data-event="item.event"
|
||||
:class="{ selected: item.selected }"
|
||||
:class="{ selected: toRaw(item) === toRaw(lastSelectedItem) }"
|
||||
>
|
||||
<div class="line-options" v-show="isSearching()">
|
||||
<dropdown-menu :class="{ 'is-last': index === filtered.length - 1 }" class="is-top minimal">
|
||||
<a class="dropdown-item" @click="handleJumpLineSelected($event, item)" :href="`#${item.key}`">
|
||||
<a class="dropdown-item" @click="handleJumpLineSelected($event, item)" :href="`#${item.id}`">
|
||||
<div class="level is-justify-content-start">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
@@ -25,20 +25,27 @@
|
||||
</div>
|
||||
<div class="line">
|
||||
<span class="date" v-if="showTimestamp"> <relative-time :date="item.date"></relative-time></span>
|
||||
<span class="text" v-html="colorize(item.message)"></span>
|
||||
<JSONPayload :log-entry="item" :visible-keys="visibleKeys.value" v-if="item.isComplex()"></JSONPayload>
|
||||
<span class="text" v-html="colorize(item.message)" v-if="item.isSimple()"></span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { PropType, ref, toRefs, watch } from "vue";
|
||||
import { ComputedRef, inject, PropType, ref, toRefs, watch, toRaw } from "vue";
|
||||
import { useRouteHash } from "@vueuse/router";
|
||||
import { size, showTimestamp, softWrap } from "@/composables/settings";
|
||||
import RelativeTime from "./RelativeTime.vue";
|
||||
import AnsiConvertor from "ansi-to-html";
|
||||
import { VisibleLogEntry } from "@/types/VisibleLogEntry";
|
||||
import { LogEntry } from "@/types/LogEntry";
|
||||
import { useSearchFilter } from "@/composables/search";
|
||||
import { useVisibleFilter } from "@/composables/visible";
|
||||
import { Container } from "@/types/Container";
|
||||
import { persistentVisibleKeys } from "@/utils";
|
||||
|
||||
import RelativeTime from "./RelativeTime.vue";
|
||||
import AnsiConvertor from "ansi-to-html";
|
||||
import JSONPayload from "./JSONPayload.vue";
|
||||
|
||||
const props = defineProps({
|
||||
messages: {
|
||||
@@ -48,18 +55,22 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const ansiConvertor = new AnsiConvertor({ escapeXML: true });
|
||||
const { filteredMessages, resetSearch, markSearch, isSearching } = useSearchFilter();
|
||||
const colorize = (value: string) => markSearch(ansiConvertor.toHtml(value));
|
||||
|
||||
const { messages } = toRefs(props);
|
||||
const filtered = filteredMessages(messages);
|
||||
let visibleKeys = persistentVisibleKeys(inject("container") as ComputedRef<Container>);
|
||||
|
||||
const { filteredPayload } = useVisibleFilter(visibleKeys);
|
||||
const { filteredMessages, resetSearch, markSearch, isSearching } = useSearchFilter();
|
||||
|
||||
const visible = filteredPayload(messages);
|
||||
const filtered = filteredMessages(visible);
|
||||
|
||||
const events = ref<HTMLElement>();
|
||||
let lastSelectedItem: LogEntry | undefined = undefined;
|
||||
function handleJumpLineSelected(e: Event, item: LogEntry) {
|
||||
if (lastSelectedItem) {
|
||||
lastSelectedItem.selected = false;
|
||||
}
|
||||
lastSelectedItem = item;
|
||||
item.selected = true;
|
||||
let lastSelectedItem = ref<VisibleLogEntry>();
|
||||
|
||||
function handleJumpLineSelected(e: Event, item: VisibleLogEntry) {
|
||||
lastSelectedItem.value = item;
|
||||
resetSearch();
|
||||
}
|
||||
|
||||
@@ -84,6 +95,13 @@ watch(
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
white-space: pre-wrap;
|
||||
&::before {
|
||||
content: " ";
|
||||
}
|
||||
}
|
||||
|
||||
& > li {
|
||||
display: flex;
|
||||
word-wrap: break-word;
|
||||
@@ -167,13 +185,6 @@ watch(
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.text {
|
||||
white-space: pre-wrap;
|
||||
&::before {
|
||||
content: " ";
|
||||
}
|
||||
}
|
||||
|
||||
:deep(mark) {
|
||||
border-radius: 2px;
|
||||
background-color: var(--secondary-color);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<log-event-source ref="source" :id="id" #default="{ messages }" @loading-more="emit('loading-more', $event)">
|
||||
<log-event-source ref="source" #default="{ messages }" @loading-more="emit('loading-more', $event)">
|
||||
<log-viewer :messages="messages"></log-viewer>
|
||||
</log-event-source>
|
||||
</template>
|
||||
@@ -7,12 +7,6 @@
|
||||
<script lang="ts" setup>
|
||||
import LogViewer from "./LogViewer.vue";
|
||||
import { ref } from "vue";
|
||||
defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["loading-more"]);
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<div class="is-scrollbar-notification">
|
||||
<transition name="fade">
|
||||
<button class="button" :class="hasMore ? 'has-more' : ''" @click="scrollToBottom('instant')" v-show="paused">
|
||||
<button class="button" :class="hasMore ? 'has-more' : ''" @click="scrollToBottom()" v-show="paused">
|
||||
<mdi-light-chevron-double-down />
|
||||
</button>
|
||||
</transition>
|
||||
@@ -24,61 +24,51 @@
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
props: {
|
||||
scrollable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
|
||||
name: "ScrollableView",
|
||||
data() {
|
||||
return {
|
||||
paused: false,
|
||||
hasMore: false,
|
||||
loading: false,
|
||||
mutationObserver: null,
|
||||
intersectionObserver: null,
|
||||
};
|
||||
defineProps({
|
||||
scrollable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
mounted() {
|
||||
const { scrollableContent } = this.$refs;
|
||||
this.mutationObserver = new MutationObserver((e) => {
|
||||
if (!this.paused) {
|
||||
this.scrollToBottom("instant");
|
||||
} else {
|
||||
const record = e[e.length - 1];
|
||||
if (
|
||||
record.target.children[record.target.children.length - 1] == record.addedNodes[record.addedNodes.length - 1]
|
||||
) {
|
||||
this.hasMore = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
this.mutationObserver.observe(scrollableContent, { childList: true, subtree: true });
|
||||
});
|
||||
|
||||
this.intersectionObserver = new IntersectionObserver(
|
||||
(entries) => (this.paused = entries[0].intersectionRatio == 0),
|
||||
{ threshholds: [0, 1], rootMargin: "80px 0px" }
|
||||
);
|
||||
this.intersectionObserver.observe(this.$refs.scrollObserver);
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.mutationObserver.disconnect();
|
||||
this.intersectionObserver.disconnect();
|
||||
},
|
||||
methods: {
|
||||
scrollToBottom(behavior = "instant") {
|
||||
this.$refs.scrollObserver.scrollIntoView({ behavior });
|
||||
this.hasMore = false;
|
||||
},
|
||||
setLoading(loading) {
|
||||
this.loading = loading;
|
||||
},
|
||||
},
|
||||
};
|
||||
const paused = ref(false);
|
||||
const hasMore = ref(false);
|
||||
const loading = ref(false);
|
||||
const scrollObserver = ref<HTMLElement>();
|
||||
const scrollableContent = ref<HTMLElement>();
|
||||
|
||||
const mutationObserver = new MutationObserver((e) => {
|
||||
if (!paused.value) {
|
||||
scrollToBottom();
|
||||
} else {
|
||||
const record = e[e.length - 1];
|
||||
if (record.target.children[record.target.children.length - 1] == record.addedNodes[record.addedNodes.length - 1]) {
|
||||
hasMore.value = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const intersectionObserver = new IntersectionObserver((entries) => (paused.value = entries[0].intersectionRatio == 0), {
|
||||
threshholds: [0, 1],
|
||||
rootMargin: "80px 0px",
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
mutationObserver.observe(scrollableContent.value!, { childList: true, subtree: true });
|
||||
intersectionObserver.observe(scrollObserver.value!);
|
||||
});
|
||||
|
||||
function scrollToBottom(behavior: "auto" | "smooth" = "auto") {
|
||||
scrollObserver.value?.scrollIntoView({ behavior });
|
||||
hasMore.value = false;
|
||||
}
|
||||
|
||||
function setLoading(value: boolean) {
|
||||
loading.value = value;
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
section {
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
exports[`<LogEventSource /> > render html correctly > should render dates with 12 hour style 1`] = `
|
||||
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
|
||||
<li data-key=\\"2019-06-12T23:55:42.459034602Z\\" class=\\"\\" data-v-cce5b553=\\"\\">
|
||||
<li data-key=\\"1\\" class=\\"\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
|
||||
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
|
||||
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
|
||||
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#2019-06-12T23:55:42.459034602Z\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"level-left\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
|
||||
@@ -23,19 +23,21 @@ exports[`<LogEventSource /> > render html correctly > should render dates with 1
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T23:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 11:55:42 PM</time></span><span class=\\"text\\" data-v-cce5b553=\\"\\"><test>foo bar</test></span></div>
|
||||
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 10:55:42 AM</time></span>
|
||||
<!--v-if--><span class=\\"text\\" data-v-cce5b553=\\"\\"><test>foo bar</test></span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>"
|
||||
`;
|
||||
|
||||
exports[`<LogEventSource /> > render html correctly > should render dates with 24 hour style 1`] = `
|
||||
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
|
||||
<li data-key=\\"2019-06-12T23:55:42.459034602Z\\" class=\\"\\" data-v-cce5b553=\\"\\">
|
||||
<li data-key=\\"1\\" class=\\"\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
|
||||
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
|
||||
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
|
||||
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#2019-06-12T23:55:42.459034602Z\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"level-left\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
|
||||
@@ -51,19 +53,21 @@ exports[`<LogEventSource /> > render html correctly > should render dates with 2
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T23:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 23:55:42</time></span><span class=\\"text\\" data-v-cce5b553=\\"\\"><test>foo bar</test></span></div>
|
||||
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 10:55:42</time></span>
|
||||
<!--v-if--><span class=\\"text\\" data-v-cce5b553=\\"\\"><test>foo bar</test></span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>"
|
||||
`;
|
||||
|
||||
exports[`<LogEventSource /> > render html correctly > should render messages 1`] = `
|
||||
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
|
||||
<li data-key=\\"2019-06-12T10:55:42.459034602Z\\" class=\\"\\" data-v-cce5b553=\\"\\">
|
||||
<li data-key=\\"1\\" class=\\"\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
|
||||
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
|
||||
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
|
||||
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#2019-06-12T10:55:42.459034602Z\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"level-left\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
|
||||
@@ -79,19 +83,21 @@ exports[`<LogEventSource /> > render html correctly > should render messages 1`]
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 10:55:42 AM</time></span><span class=\\"text\\" data-v-cce5b553=\\"\\">\\"This is a message.\\"</span></div>
|
||||
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 10:55:42 AM</time></span>
|
||||
<!--v-if--><span class=\\"text\\" data-v-cce5b553=\\"\\">This is a message.</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>"
|
||||
`;
|
||||
|
||||
exports[`<LogEventSource /> > render html correctly > should render messages with color 1`] = `
|
||||
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
|
||||
<li data-key=\\"2019-06-12T10:55:42.459034602Z\\" class=\\"\\" data-v-cce5b553=\\"\\">
|
||||
<li data-key=\\"1\\" class=\\"\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
|
||||
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
|
||||
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
|
||||
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#2019-06-12T10:55:42.459034602Z\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"level-left\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
|
||||
@@ -107,19 +113,21 @@ exports[`<LogEventSource /> > render html correctly > should render messages wit
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 10:55:42 AM</time></span><span class=\\"text\\" data-v-cce5b553=\\"\\"><span style=\\"color:#000\\">black<span style=\\"color:#AAA\\">white</span></span></span></div>
|
||||
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 10:55:42 AM</time></span>
|
||||
<!--v-if--><span class=\\"text\\" data-v-cce5b553=\\"\\"><span style=\\"color:#000\\">black<span style=\\"color:#AAA\\">white</span></span></span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>"
|
||||
`;
|
||||
|
||||
exports[`<LogEventSource /> > render html correctly > should render messages with filter 1`] = `
|
||||
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
|
||||
<li data-key=\\"2019-06-12T10:55:42.459034602Z\\" class=\\"\\" data-v-cce5b553=\\"\\">
|
||||
<li data-key=\\"2\\" class=\\"\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
|
||||
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
|
||||
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
|
||||
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#2019-06-12T10:55:42.459034602Z\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#2\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"level-left\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
|
||||
@@ -135,19 +143,21 @@ exports[`<LogEventSource /> > render html correctly > should render messages wit
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 10:55:42 AM</time></span><span class=\\"text\\" data-v-cce5b553=\\"\\">This is a <mark>test</mark> <hi></hi></span></div>
|
||||
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 10:55:42 AM</time></span>
|
||||
<!--v-if--><span class=\\"text\\" data-v-cce5b553=\\"\\"><mark>test</mark> bar</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>"
|
||||
`;
|
||||
|
||||
exports[`<LogEventSource /> > render html correctly > should render messages with html entities 1`] = `
|
||||
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
|
||||
<li data-key=\\"2019-06-12T10:55:42.459034602Z\\" class=\\"\\" data-v-cce5b553=\\"\\">
|
||||
<li data-key=\\"1\\" class=\\"\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
|
||||
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
|
||||
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
|
||||
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#2019-06-12T10:55:42.459034602Z\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"level-left\\" data-v-cce5b553=\\"\\">
|
||||
<div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
|
||||
@@ -163,7 +173,9 @@ exports[`<LogEventSource /> > render html correctly > should render messages wit
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 10:55:42 AM</time></span><span class=\\"text\\" data-v-cce5b553=\\"\\"><test>foo bar</test></span></div>
|
||||
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 10:55:42 AM</time></span>
|
||||
<!--v-if--><span class=\\"text\\" data-v-cce5b553=\\"\\"><test>foo bar</test></span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>"
|
||||
`;
|
||||
@@ -182,20 +194,7 @@ exports[`<LogEventSource /> > renders correctly 1`] = `
|
||||
exports[`<LogEventSource /> > should parse messages 1`] = `
|
||||
{
|
||||
"date": 2019-06-12T10:55:42.459Z,
|
||||
"message": "\\"This is a message.\\"",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`<LogEventSource /> > should parse messages with loki's timestamp format 1`] = `
|
||||
{
|
||||
"date": 2020-04-27T10:35:43.272Z,
|
||||
"message": "xxxxx",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`<LogEventSource /> > should pass messages to slot 1`] = `
|
||||
{
|
||||
"date": 2019-06-12T10:55:42.459Z,
|
||||
"message": "\\"This is a message.\\"",
|
||||
"id": 1,
|
||||
"message": "This is a message.",
|
||||
}
|
||||
`;
|
||||
|
||||
111
assets/composables/eventsource.ts
Normal file
111
assets/composables/eventsource.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { ref, watch, onUnmounted, ComputedRef } from "vue";
|
||||
import debounce from "lodash.debounce";
|
||||
|
||||
import type { LogEntry, LogEvent } from "@/types/LogEntry";
|
||||
|
||||
import config from "@/stores/config";
|
||||
import { Container } from "@/types/Container";
|
||||
|
||||
function parseMessage(data: string): LogEntry {
|
||||
const e = JSON.parse(data) as LogEvent;
|
||||
|
||||
const id = e.id;
|
||||
const date = new Date(e.ts);
|
||||
return { id, date, message: e.m };
|
||||
}
|
||||
|
||||
export function useEventSource(container: ComputedRef<Container>) {
|
||||
const messages = ref<LogEntry[]>([]);
|
||||
const buffer = ref<LogEntry[]>([]);
|
||||
|
||||
function flushNow() {
|
||||
messages.value.push(...buffer.value);
|
||||
buffer.value = [];
|
||||
}
|
||||
const flushBuffer = debounce(flushNow, 250, { maxWait: 1000 });
|
||||
let es: EventSource | null = null;
|
||||
let lastEventId = "";
|
||||
|
||||
function connect({ clear } = { clear: true }) {
|
||||
es?.close();
|
||||
|
||||
if (clear) {
|
||||
flushBuffer.cancel();
|
||||
messages.value = [];
|
||||
buffer.value = [];
|
||||
lastEventId = "";
|
||||
}
|
||||
|
||||
es = new EventSource(`${config.base}/api/logs/stream?id=${container.value.id}&lastEventId=${lastEventId}`);
|
||||
es.addEventListener("container-stopped", () => {
|
||||
es?.close();
|
||||
es = null;
|
||||
buffer.value.push({
|
||||
event: "container-stopped",
|
||||
message: "Container stopped",
|
||||
date: new Date(),
|
||||
id: new Date().getTime(),
|
||||
});
|
||||
flushBuffer();
|
||||
flushBuffer.flush();
|
||||
});
|
||||
es.addEventListener("error", (e) => console.error("EventSource failed: " + JSON.stringify(e)));
|
||||
es.onmessage = (e) => {
|
||||
lastEventId = e.lastEventId;
|
||||
if (e.data) {
|
||||
buffer.value.push(parseMessage(e.data));
|
||||
flushBuffer();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function loadOlderLogs({ beforeLoading, afterLoading } = { beforeLoading: () => {}, afterLoading: () => {} }) {
|
||||
if (messages.value.length < 300) return;
|
||||
|
||||
beforeLoading();
|
||||
const to = messages.value[0].date;
|
||||
const last = messages.value[299].date;
|
||||
const delta = to.getTime() - last.getTime();
|
||||
const from = new Date(to.getTime() + delta);
|
||||
const logs = await (
|
||||
await fetch(`${config.base}/api/logs?id=${container.value.id}&from=${from.toISOString()}&to=${to.toISOString()}`)
|
||||
).text();
|
||||
if (logs) {
|
||||
const newMessages = logs
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => parseMessage(line));
|
||||
messages.value.unshift(...newMessages);
|
||||
}
|
||||
afterLoading();
|
||||
}
|
||||
|
||||
watch(
|
||||
() => container.value.state,
|
||||
(newValue, oldValue) => {
|
||||
console.log("LogEventSource: container changed", newValue, oldValue);
|
||||
if (newValue == "running" && newValue != oldValue) {
|
||||
buffer.value.push({
|
||||
event: "container-started",
|
||||
message: "Container started",
|
||||
date: new Date(),
|
||||
id: new Date().getTime(),
|
||||
});
|
||||
connect({ clear: false });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
if (es) {
|
||||
es.close();
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => container.value.id,
|
||||
() => connect()
|
||||
);
|
||||
|
||||
return { connect, messages, loadOlderLogs };
|
||||
}
|
||||
@@ -1,21 +1,42 @@
|
||||
import { ref, computed, Ref } from "vue";
|
||||
import { useDebounce } from "@vueuse/core";
|
||||
import { VisibleLogEntry } from "@/types/VisibleLogEntry";
|
||||
|
||||
const searchFilter = ref<string>("");
|
||||
const debouncedSearchFilter = useDebounce(searchFilter);
|
||||
const showSearch = ref(false);
|
||||
|
||||
import type { LogEntry } from "@/types/LogEntry";
|
||||
function matchRecord(record: Record<string, any>, regex: RegExp): boolean {
|
||||
for (const key in record) {
|
||||
const value = record[key];
|
||||
if (typeof value === "string" && regex.test(value)) {
|
||||
return true;
|
||||
}
|
||||
if (Array.isArray(value) && matchRecord(value, regex)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function useSearchFilter() {
|
||||
const regex = computed(() => {
|
||||
const isSmartCase = searchFilter.value === searchFilter.value.toLowerCase();
|
||||
return isSmartCase ? new RegExp(searchFilter.value, "i") : new RegExp(searchFilter.value);
|
||||
const isSmartCase = debouncedSearchFilter.value === debouncedSearchFilter.value.toLowerCase();
|
||||
return isSmartCase ? new RegExp(debouncedSearchFilter.value, "i") : new RegExp(debouncedSearchFilter.value);
|
||||
});
|
||||
|
||||
function filteredMessages(messages: Ref<LogEntry[]>) {
|
||||
function filteredMessages(messages: Ref<VisibleLogEntry[]>) {
|
||||
return computed(() => {
|
||||
if (searchFilter && searchFilter.value) {
|
||||
if (debouncedSearchFilter.value) {
|
||||
try {
|
||||
return messages.value.filter((d) => d.message.match(regex.value));
|
||||
return messages.value.filter((d) => {
|
||||
if (d.isSimple()) {
|
||||
return regex.value.test(d.message);
|
||||
} else if (d.isComplex()) {
|
||||
return matchRecord(d.message, regex.value);
|
||||
}
|
||||
throw new Error("Unknown message type");
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof SyntaxError) {
|
||||
console.info(`Ignoring SyntaxError from search.`, e);
|
||||
@@ -29,11 +50,17 @@ export function useSearchFilter() {
|
||||
});
|
||||
}
|
||||
|
||||
function markSearch(log: string) {
|
||||
if (searchFilter && searchFilter.value) {
|
||||
return log.replace(regex.value, `<mark>$&</mark>`);
|
||||
function markSearch(log: string): string;
|
||||
function markSearch(log: string[]): string[];
|
||||
function markSearch(log: string | string[]) {
|
||||
if (!debouncedSearchFilter.value) {
|
||||
return log;
|
||||
}
|
||||
return log;
|
||||
if (Array.isArray(log)) {
|
||||
return log.map((d) => markSearch(d));
|
||||
}
|
||||
|
||||
return log.toString().replace(regex.value, (match) => `<mark>${match}</mark>`);
|
||||
}
|
||||
|
||||
function resetSearch() {
|
||||
|
||||
13
assets/composables/visible.ts
Normal file
13
assets/composables/visible.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { LogEntry } from "@/types/LogEntry";
|
||||
import { VisibleLogEntry } from "@/types/VisibleLogEntry";
|
||||
import { computed, ComputedRef, Ref } from "vue";
|
||||
|
||||
export function useVisibleFilter(visibleKeys: ComputedRef<Ref<string[][]>>) {
|
||||
function filteredPayload(messages: Ref<LogEntry[]>) {
|
||||
return computed(() => {
|
||||
return messages.value.map((d) => new VisibleLogEntry(d, visibleKeys.value));
|
||||
});
|
||||
}
|
||||
|
||||
return { filteredPayload };
|
||||
}
|
||||
@@ -17,6 +17,7 @@ $menu-item-hover-color: var(--menu-item-hover-color);
|
||||
|
||||
$text-strong: var(--text-strong-color);
|
||||
$text: var(--text-color);
|
||||
$text-light: var(--text-light-color);
|
||||
|
||||
$panel-heading-background-color: var(--panel-heading-background-color);
|
||||
$panel-heading-color: var(--panel-heading-color);
|
||||
@@ -38,8 +39,7 @@ $light-toolbar-color: rgba($grey-darker, 0.7);
|
||||
@import "@oruga-ui/theme-bulma/dist/scss/components/skeleton.scss";
|
||||
@import "splitpanes/dist/splitpanes.css";
|
||||
|
||||
html,
|
||||
[data-theme="dark"] {
|
||||
@mixin dark {
|
||||
--scheme-main: #{$black};
|
||||
--scheme-main-bis: #{$black-bis};
|
||||
--scheme-main-ter: #{$black-ter};
|
||||
@@ -64,93 +64,57 @@ html,
|
||||
|
||||
--text-strong-color: #{$grey-lightest};
|
||||
--text-color: #{$grey-lighter};
|
||||
--text-light-color: #{$grey};
|
||||
}
|
||||
|
||||
@mixin light {
|
||||
--scheme-main: #{$white};
|
||||
--scheme-main-bis: #{$white-bis};
|
||||
--scheme-main-ter: #{$white-ter};
|
||||
|
||||
--border-color: #{$grey-lighter};
|
||||
--border-hover-color: var(--secondary-color);
|
||||
--logo-color: #{$grey-darker};
|
||||
|
||||
--primary-color: #{$turquoise};
|
||||
--secondary-color: #d8f0ca;
|
||||
|
||||
--body-background-color: #{$white-bis};
|
||||
--action-toolbar-background-color: #{$light-toolbar-color};
|
||||
--body-color: #{$grey-darker};
|
||||
|
||||
--menu-item-color: #{$grey-dark};
|
||||
--menu-item-hover-background-color: #eee8e7;
|
||||
--menu-item-hover-color: #{black-ter};
|
||||
|
||||
--panel-heading-background-color: var(--secondary-color);
|
||||
--panel-heading-color: var(--text-strong-color);
|
||||
|
||||
--text-strong-color: #{$grey-dark};
|
||||
--text-color: #{$grey-darker};
|
||||
--text-light-color: #{$grey};
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
@include dark;
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
@include light;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
--scheme-main: #{$black};
|
||||
--scheme-main-bis: #{$black-bis};
|
||||
--scheme-main-ter: #{$black-ter};
|
||||
|
||||
--border-color: #{$grey-darker};
|
||||
--border-hover-color: var(--secondary-color);
|
||||
--logo-color: var(--secondary-color);
|
||||
|
||||
--primary-color: #{$turquoise};
|
||||
--secondary-color: #{$yellow};
|
||||
|
||||
--body-background-color: #{$black-bis};
|
||||
--action-toolbar-background-color: #{$dark-toolbar-color};
|
||||
|
||||
--menu-item-active-background-color: var(--primary-color);
|
||||
--menu-item-color: hsl(0, 6%, 87%);
|
||||
--menu-item-hover-background-color: #{$white-ter};
|
||||
--menu-item-hover-color: #{$black-ter};
|
||||
|
||||
--panel-heading-background-color: var(--secondary-color);
|
||||
--panel-heading-color: var(--scheme-main-bis);
|
||||
|
||||
--text-strong-color: #{$grey-lightest};
|
||||
--text-color: #{$grey-lighter};
|
||||
@include dark;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
--scheme-main: #{$white};
|
||||
--scheme-main-bis: #{$white-bis};
|
||||
--scheme-main-ter: #{$white-ter};
|
||||
|
||||
--border-color: #{$grey-lighter};
|
||||
--border-hover-color: var(--secondary-color);
|
||||
--logo-color: #{$grey-darker};
|
||||
|
||||
--primary-color: #{$turquoise};
|
||||
--secondary-color: #d8f0ca;
|
||||
|
||||
--body-background-color: #{$white-bis};
|
||||
--action-toolbar-background-color: #{$light-toolbar-color};
|
||||
--body-color: #{$grey-darker};
|
||||
|
||||
--menu-item-color: #{$grey-dark};
|
||||
--menu-item-hover-background-color: #eee8e7;
|
||||
--menu-item-hover-color: #{black-ter};
|
||||
|
||||
--panel-heading-background-color: var(--secondary-color);
|
||||
--panel-heading-color: var(--text-strong-color);
|
||||
|
||||
--text-strong-color: #{$grey-dark};
|
||||
--text-color: #{$grey-darker};
|
||||
@include light;
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
--scheme-main: #{$white};
|
||||
--scheme-main-bis: #{$white-bis};
|
||||
--scheme-main-ter: #{$white-ter};
|
||||
|
||||
--border-color: #{$grey-lighter};
|
||||
--border-hover-color: var(--secondary-color);
|
||||
--logo-color: #{$grey-darker};
|
||||
|
||||
--primary-color: #{$turquoise};
|
||||
--secondary-color: #d8f0ca;
|
||||
|
||||
--body-background-color: #{$white-bis};
|
||||
--action-toolbar-background-color: #{$light-toolbar-color};
|
||||
--body-color: #{$grey-darker};
|
||||
|
||||
--menu-item-color: #{$grey-dark};
|
||||
--menu-item-hover-background-color: #eee8e7;
|
||||
--menu-item-hover-color: #{black-ter};
|
||||
|
||||
--panel-heading-background-color: var(--secondary-color);
|
||||
--panel-heading-color: var(--text-strong-color);
|
||||
|
||||
--text-strong-color: #{$grey-dark};
|
||||
--text-color: #{$grey-darker};
|
||||
}
|
||||
|
||||
html {
|
||||
overflow-x: unset;
|
||||
overflow-y: unset;
|
||||
|
||||
1
assets/types/Container.d.ts
vendored
1
assets/types/Container.d.ts
vendored
@@ -4,6 +4,7 @@ export interface Container {
|
||||
readonly image: string;
|
||||
readonly name: string;
|
||||
readonly status: string;
|
||||
readonly command: string;
|
||||
state: "created" | "running" | "exited" | "dead" | "paused" | "restarting";
|
||||
stat?: ContainerStat;
|
||||
}
|
||||
|
||||
15
assets/types/LogEntry.d.ts
vendored
15
assets/types/LogEntry.d.ts
vendored
@@ -1,7 +1,16 @@
|
||||
export interface LogEntry {
|
||||
date: Date;
|
||||
message: string;
|
||||
key: string;
|
||||
readonly date: Date;
|
||||
readonly message: string | JSONObject;
|
||||
readonly id: number;
|
||||
event?: string;
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
export interface LogEvent {
|
||||
readonly m: string | JSONObject;
|
||||
readonly ts: number;
|
||||
readonly id: number;
|
||||
}
|
||||
|
||||
export type JSONValue = string | number | boolean | JSONObject | Array<JSONValue>;
|
||||
export type JSONObject = { [x: string]: JSONValue };
|
||||
|
||||
54
assets/types/VisibleLogEntry.ts
Normal file
54
assets/types/VisibleLogEntry.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { computed, ComputedRef, Ref } from "vue";
|
||||
import { flattenJSON, getDeep } from "@/utils";
|
||||
import type { JSONObject, LogEntry } from "./LogEntry";
|
||||
|
||||
export class VisibleLogEntry implements LogEntry {
|
||||
private readonly entry: LogEntry;
|
||||
filteredMessage: undefined | ComputedRef<Record<string, any>>;
|
||||
|
||||
constructor(entry: LogEntry, visibleKeys: Ref<string[][]>) {
|
||||
this.entry = entry;
|
||||
this.filteredMessage = undefined;
|
||||
if (this.isComplex()) {
|
||||
const message = this.message;
|
||||
this.filteredMessage = computed(() => {
|
||||
if (!visibleKeys.value.length) {
|
||||
return flattenJSON(message);
|
||||
} else {
|
||||
return visibleKeys.value.reduce((acc, attr) => ({ ...acc, [attr.join(".")]: getDeep(message, attr) }), {});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public isComplex(): this is { message: JSONObject } {
|
||||
return typeof this.entry.message === "object";
|
||||
}
|
||||
|
||||
public isSimple(): this is { message: string } {
|
||||
return !this.isComplex();
|
||||
}
|
||||
|
||||
public get unfilteredPayload(): JSONObject {
|
||||
if (typeof this.entry.message === "string") {
|
||||
throw new Error("Cannot get unfiltered payload of a simple message");
|
||||
}
|
||||
return this.entry.message;
|
||||
}
|
||||
|
||||
public get date(): Date {
|
||||
return this.entry.date;
|
||||
}
|
||||
|
||||
public get message(): string | JSONObject {
|
||||
return this.filteredMessage?.value ?? this.entry.message;
|
||||
}
|
||||
|
||||
public get id(): number {
|
||||
return this.entry.id;
|
||||
}
|
||||
|
||||
public get event(): string | undefined {
|
||||
return this.entry.event;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
import { Container } from "@/types/Container";
|
||||
import { useStorage } from "@vueuse/core";
|
||||
import { computed, ComputedRef } from "vue";
|
||||
|
||||
export function formatBytes(bytes: number, decimals = 2) {
|
||||
if (bytes === 0) return "0 Bytes";
|
||||
const k = 1024;
|
||||
@@ -6,3 +10,38 @@ export function formatBytes(bytes: number, decimals = 2) {
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
|
||||
}
|
||||
|
||||
export function getDeep(obj: Record<string, any>, path: string[]) {
|
||||
return path.reduce((acc, key) => acc?.[key], obj);
|
||||
}
|
||||
|
||||
export function isObject(value: any): value is Record<string, any> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
export function flattenJSON(obj: Record<string, any>, path: string[] = []) {
|
||||
const result: Record<string, any> = {};
|
||||
Object.keys(obj).forEach((key) => {
|
||||
const value = obj[key];
|
||||
const newPath = path.concat(key);
|
||||
if (isObject(value)) {
|
||||
Object.assign(result, flattenJSON(value, newPath));
|
||||
} else {
|
||||
result[newPath.join(".")] = value;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export function arrayEquals(a: string[], b: string[]): boolean {
|
||||
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((val, index) => val === b[index]);
|
||||
}
|
||||
|
||||
export function persistentVisibleKeys(container: ComputedRef<Container>) {
|
||||
return computed(() => useStorage(stripVersion(container.value.image) + ":" + container.value.command, []));
|
||||
}
|
||||
|
||||
export function stripVersion(label: string) {
|
||||
const [name, _] = label.split(":");
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -138,10 +138,15 @@ func (d *dockerClient) ContainerStats(ctx context.Context, id string, stats chan
|
||||
log.Errorf("decoder for stats api returned an unknown error %v", err)
|
||||
}
|
||||
|
||||
ncpus := uint8(v.CPUStats.OnlineCPUs)
|
||||
if ncpus == 0 {
|
||||
ncpus = uint8(len(v.CPUStats.CPUUsage.PercpuUsage))
|
||||
}
|
||||
|
||||
var (
|
||||
cpuDelta = float64(v.CPUStats.CPUUsage.TotalUsage) - float64(v.PreCPUStats.CPUUsage.TotalUsage)
|
||||
systemDelta = float64(v.CPUStats.SystemUsage) - float64(v.PreCPUStats.SystemUsage)
|
||||
cpuPercent = int64((cpuDelta / systemDelta) * float64(len(v.CPUStats.CPUUsage.PercpuUsage)) * 100)
|
||||
cpuPercent = int64((cpuDelta / systemDelta) * float64(ncpus) * 100)
|
||||
memUsage = int64(v.MemoryStats.Usage - v.MemoryStats.Stats["cache"])
|
||||
memPercent = int64(float64(memUsage) / float64(v.MemoryStats.Limit) * 100)
|
||||
)
|
||||
@@ -167,6 +172,14 @@ func (d *dockerClient) ContainerStats(ctx context.Context, id string, stats chan
|
||||
func (d *dockerClient) ContainerLogs(ctx context.Context, id string, tailSize int, since string) (io.ReadCloser, error) {
|
||||
log.WithField("id", id).WithField("since", since).Debug("streaming logs for container")
|
||||
|
||||
if since != "" {
|
||||
if millis, err := strconv.ParseInt(since, 10, 64); err == nil {
|
||||
since = time.UnixMicro(millis).Add(time.Millisecond).Format(time.RFC3339Nano)
|
||||
} else {
|
||||
log.WithError(err).Debug("unable to parse since")
|
||||
}
|
||||
}
|
||||
|
||||
options := types.ContainerLogsOptions{
|
||||
ShowStdout: true,
|
||||
ShowStderr: true,
|
||||
|
||||
@@ -26,3 +26,9 @@ type ContainerEvent struct {
|
||||
ActorID string `json:"actorId"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type LogEvent struct {
|
||||
Message any `json:"m,omitempty"`
|
||||
Timestamp int64 `json:"ts"`
|
||||
Id uint32 `json:"id,omitempty"`
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM cypress/included:10.3.0
|
||||
FROM cypress/included:10.6.0
|
||||
|
||||
RUN apt install curl && curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defineConfig } from "cypress";
|
||||
import { initPlugin } from '@frsource/cypress-plugin-visual-regression-diff/dist/plugins';
|
||||
|
||||
export default defineConfig({
|
||||
fixturesFolder: false,
|
||||
@@ -6,7 +7,7 @@ export default defineConfig({
|
||||
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {
|
||||
// implement node event listeners here
|
||||
initPlugin(on, config);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"DOZZLE_DEFAULT": "http://localhost:3000/"
|
||||
"DOZZLE_DEFAULT": "http://localhost:8080/"
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
@@ -5,8 +5,8 @@ context("Dozzle default mode", { baseUrl: Cypress.env("DOZZLE_DEFAULT") }, () =>
|
||||
cy.visit("/");
|
||||
});
|
||||
|
||||
it.skip("home screen", () => {
|
||||
cy.get("li.running", { timeout: 10000 }).removeDates().replaceSkippedElements().matchImageSnapshot();
|
||||
it("home screen", () => {
|
||||
cy.get("li.running", { timeout: 10000 }).removeDates().replaceSkippedElements().matchImage();
|
||||
});
|
||||
|
||||
it("correct title", () => {
|
||||
12
e2e/cypress/e2e/dozzle_dark.cy.js
Normal file
12
e2e/cypress/e2e/dozzle_dark.cy.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context("Dozzle dark mode", { baseUrl: Cypress.env("DOZZLE_DEFAULT") }, () => {
|
||||
beforeEach(() => {
|
||||
cy.visit("/");
|
||||
cy.window().then((win) => win.document.documentElement.setAttribute("data-theme", "dark"));
|
||||
});
|
||||
|
||||
it("home screen", () => {
|
||||
cy.get("li.running", { timeout: 10000 }).removeDates().replaceSkippedElements().matchImage();
|
||||
});
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context.skip("Dozzle light mode", { baseUrl: Cypress.env("DOZZLE_DEFAULT") }, () => {
|
||||
before(() => {
|
||||
cy.visit("/settings");
|
||||
cy.contains("Use light theme").click();
|
||||
});
|
||||
beforeEach(() => {
|
||||
cy.visit("/");
|
||||
});
|
||||
|
||||
it("home screen", () => {
|
||||
cy.get("li.running", { timeout: 10000 }).removeDates().matchImageSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************************
|
||||
// This example plugins/index.js can be used to load plugins
|
||||
//
|
||||
// You can change the location of this file or turn off loading
|
||||
// the plugins file with the 'pluginsFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/plugins-guide
|
||||
// ***********************************************************
|
||||
|
||||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
|
||||
const { addMatchImageSnapshotPlugin } = require("cypress-image-snapshot/plugin");
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
|
||||
addMatchImageSnapshotPlugin(on, config);
|
||||
};
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands'
|
||||
import '@frsource/cypress-plugin-visual-regression-diff/dist/support';
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
// require('./commands')
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
"scripts": {},
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cypress": "^10.3.0",
|
||||
"cypress-image-snapshot": "^4.0.1",
|
||||
"@frsource/cypress-plugin-visual-regression-diff": "^1.9.2",
|
||||
"cypress": "^10.6.0",
|
||||
"typescript": "^4.7.4"
|
||||
}
|
||||
}
|
||||
|
||||
585
e2e/pnpm-lock.yaml
generated
585
e2e/pnpm-lock.yaml
generated
@@ -1,13 +1,13 @@
|
||||
lockfileVersion: 5.4
|
||||
|
||||
specifiers:
|
||||
cypress: ^10.3.0
|
||||
cypress-image-snapshot: ^4.0.1
|
||||
'@frsource/cypress-plugin-visual-regression-diff': ^1.9.2
|
||||
cypress: ^10.6.0
|
||||
typescript: ^4.7.4
|
||||
|
||||
dependencies:
|
||||
cypress: 10.3.0
|
||||
cypress-image-snapshot: 4.0.1_cypress@10.3.0
|
||||
'@frsource/cypress-plugin-visual-regression-diff': 1.9.2_cypress@10.6.0
|
||||
cypress: 10.6.0
|
||||
typescript: 4.7.4
|
||||
|
||||
packages:
|
||||
@@ -52,8 +52,21 @@ packages:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/@types/node/14.18.21:
|
||||
resolution: {integrity: sha512-x5W9s+8P4XteaxT/jKF0PSb7XEvo5VmqEWgsMlyeY4ZlLK8I6aH6g5TPPyDlLAep+GYf4kefb7HFyc7PAO3m+Q==}
|
||||
/@frsource/cypress-plugin-visual-regression-diff/1.9.2_cypress@10.6.0:
|
||||
resolution: {integrity: sha512-jtriX93BwHC1AIetlOU3xKKTmm/q1a+zysBOZqmAQ3e9uv/vD34F09uF/7JXKLoeza3OZf7eWQIERjA3yPa+bA==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
cypress: '>=4.5.0'
|
||||
dependencies:
|
||||
cypress: 10.6.0
|
||||
move-file: 2.1.0
|
||||
pixelmatch: 5.3.0
|
||||
pngjs: 6.0.0
|
||||
sharp: 0.30.7
|
||||
dev: false
|
||||
|
||||
/@types/node/14.18.24:
|
||||
resolution: {integrity: sha512-aJdn8XErcSrfr7k8ZDDfU6/2OgjZcB2Fu9d+ESK8D7Oa5mtsv8Fa8GpcwTA0v60kuZBaalKPzuzun4Ov1YWO/w==}
|
||||
dev: false
|
||||
|
||||
/@types/sinonjs__fake-timers/8.1.1:
|
||||
@@ -68,7 +81,7 @@ packages:
|
||||
resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==}
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
'@types/node': 14.18.21
|
||||
'@types/node': 14.18.24
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
@@ -92,28 +105,11 @@ packages:
|
||||
type-fest: 0.21.3
|
||||
dev: false
|
||||
|
||||
/ansi-regex/2.1.1:
|
||||
resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/ansi-regex/5.0.1:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
engines: {node: '>=8'}
|
||||
dev: false
|
||||
|
||||
/ansi-styles/2.2.1:
|
||||
resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/ansi-styles/3.2.1:
|
||||
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
|
||||
engines: {node: '>=4'}
|
||||
dependencies:
|
||||
color-convert: 1.9.3
|
||||
dev: false
|
||||
|
||||
/ansi-styles/4.3.0:
|
||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -121,13 +117,6 @@ packages:
|
||||
color-convert: 2.0.1
|
||||
dev: false
|
||||
|
||||
/app-path/3.3.0:
|
||||
resolution: {integrity: sha512-EAgEXkdcxH1cgEePOSsmUtw9ItPl0KTxnh/pj9ZbhvbKbij9x0oX6PWpGnorDr0DS5AosLgoa5n3T/hZmKQpYA==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
execa: 1.0.0
|
||||
dev: false
|
||||
|
||||
/arch/2.2.0:
|
||||
resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==}
|
||||
dev: false
|
||||
@@ -183,6 +172,14 @@ packages:
|
||||
tweetnacl: 0.14.5
|
||||
dev: false
|
||||
|
||||
/bl/4.1.0:
|
||||
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
||||
dependencies:
|
||||
buffer: 5.7.1
|
||||
inherits: 2.0.4
|
||||
readable-stream: 3.6.0
|
||||
dev: false
|
||||
|
||||
/blob-util/2.0.2:
|
||||
resolution: {integrity: sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==}
|
||||
dev: false
|
||||
@@ -218,26 +215,6 @@ packages:
|
||||
resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==}
|
||||
dev: false
|
||||
|
||||
/chalk/1.1.3:
|
||||
resolution: {integrity: sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dependencies:
|
||||
ansi-styles: 2.2.1
|
||||
escape-string-regexp: 1.0.5
|
||||
has-ansi: 2.0.0
|
||||
strip-ansi: 3.0.1
|
||||
supports-color: 2.0.0
|
||||
dev: false
|
||||
|
||||
/chalk/2.4.2:
|
||||
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
|
||||
engines: {node: '>=4'}
|
||||
dependencies:
|
||||
ansi-styles: 3.2.1
|
||||
escape-string-regexp: 1.0.5
|
||||
supports-color: 5.5.0
|
||||
dev: false
|
||||
|
||||
/chalk/4.1.2:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -251,6 +228,10 @@ packages:
|
||||
engines: {node: '>= 0.8.0'}
|
||||
dev: false
|
||||
|
||||
/chownr/1.1.4:
|
||||
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
|
||||
dev: false
|
||||
|
||||
/ci-info/3.3.2:
|
||||
resolution: {integrity: sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==}
|
||||
dev: false
|
||||
@@ -284,12 +265,6 @@ packages:
|
||||
string-width: 4.2.3
|
||||
dev: false
|
||||
|
||||
/color-convert/1.9.3:
|
||||
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
|
||||
dependencies:
|
||||
color-name: 1.1.3
|
||||
dev: false
|
||||
|
||||
/color-convert/2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
@@ -297,14 +272,25 @@ packages:
|
||||
color-name: 1.1.4
|
||||
dev: false
|
||||
|
||||
/color-name/1.1.3:
|
||||
resolution: {integrity: sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=}
|
||||
dev: false
|
||||
|
||||
/color-name/1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
dev: false
|
||||
|
||||
/color-string/1.9.1:
|
||||
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
simple-swizzle: 0.2.2
|
||||
dev: false
|
||||
|
||||
/color/4.2.3:
|
||||
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
|
||||
engines: {node: '>=12.5.0'}
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
color-string: 1.9.1
|
||||
dev: false
|
||||
|
||||
/colorette/2.0.19:
|
||||
resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==}
|
||||
dev: false
|
||||
@@ -334,17 +320,6 @@ packages:
|
||||
resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==}
|
||||
dev: false
|
||||
|
||||
/cross-spawn/6.0.5:
|
||||
resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==}
|
||||
engines: {node: '>=4.8'}
|
||||
dependencies:
|
||||
nice-try: 1.0.5
|
||||
path-key: 2.0.1
|
||||
semver: 5.7.1
|
||||
shebang-command: 1.2.0
|
||||
which: 1.3.1
|
||||
dev: false
|
||||
|
||||
/cross-spawn/7.0.3:
|
||||
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -354,32 +329,15 @@ packages:
|
||||
which: 2.0.2
|
||||
dev: false
|
||||
|
||||
/cypress-image-snapshot/4.0.1_cypress@10.3.0:
|
||||
resolution: {integrity: sha512-PBpnhX/XItlx3/DAk5ozsXQHUi72exybBNH5Mpqj1DVmjq+S5Jd9WE5CRa4q5q0zuMZb2V2VpXHth6MjFpgj9Q==}
|
||||
engines: {node: '>=8'}
|
||||
peerDependencies:
|
||||
cypress: ^4.5.0
|
||||
dependencies:
|
||||
chalk: 2.4.2
|
||||
cypress: 10.3.0
|
||||
fs-extra: 7.0.1
|
||||
glob: 7.2.0
|
||||
jest-image-snapshot: 4.2.0
|
||||
pkg-dir: 3.0.0
|
||||
term-img: 4.1.0
|
||||
transitivePeerDependencies:
|
||||
- jest
|
||||
dev: false
|
||||
|
||||
/cypress/10.3.0:
|
||||
resolution: {integrity: sha512-txkQWKzvBVnWdCuKs5Xc08gjpO89W2Dom2wpZgT9zWZT5jXxqPIxqP/NC1YArtkpmp3fN5HW8aDjYBizHLUFvg==}
|
||||
/cypress/10.6.0:
|
||||
resolution: {integrity: sha512-6sOpHjostp8gcLO34p6r/Ci342lBs8S5z9/eb3ZCQ22w2cIhMWGUoGKkosabPBfKcvRS9BE4UxybBtlIs8gTQA==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
hasBin: true
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
'@cypress/request': 2.88.10
|
||||
'@cypress/xvfb': 1.2.4_supports-color@8.1.1
|
||||
'@types/node': 14.18.21
|
||||
'@types/node': 14.18.24
|
||||
'@types/sinonjs__fake-timers': 8.1.1
|
||||
'@types/sizzle': 2.3.3
|
||||
arch: 2.2.0
|
||||
@@ -393,10 +351,10 @@ packages:
|
||||
cli-table3: 0.6.2
|
||||
commander: 5.1.0
|
||||
common-tags: 1.8.2
|
||||
dayjs: 1.11.3
|
||||
dayjs: 1.11.5
|
||||
debug: 4.3.4_supports-color@8.1.1
|
||||
enquirer: 2.3.6
|
||||
eventemitter2: 6.4.5
|
||||
eventemitter2: 6.4.7
|
||||
execa: 4.1.0
|
||||
executable: 4.1.1
|
||||
extract-zip: 2.0.1_supports-color@8.1.1
|
||||
@@ -428,8 +386,8 @@ packages:
|
||||
assert-plus: 1.0.0
|
||||
dev: false
|
||||
|
||||
/dayjs/1.11.3:
|
||||
resolution: {integrity: sha512-xxwlswWOlGhzgQ4TKzASQkUhqERI3egRNqgV4ScR8wlANA/A9tZ7miXa44vTTKEq5l7vWoL5G57bG3zA+Kow0A==}
|
||||
/dayjs/1.11.5:
|
||||
resolution: {integrity: sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA==}
|
||||
dev: false
|
||||
|
||||
/debug/3.2.7_supports-color@8.1.1:
|
||||
@@ -457,11 +415,28 @@ packages:
|
||||
supports-color: 8.1.1
|
||||
dev: false
|
||||
|
||||
/decompress-response/6.0.0:
|
||||
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
mimic-response: 3.1.0
|
||||
dev: false
|
||||
|
||||
/deep-extend/0.6.0:
|
||||
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
dev: false
|
||||
|
||||
/delayed-stream/1.0.0:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
dev: false
|
||||
|
||||
/detect-libc/2.0.1:
|
||||
resolution: {integrity: sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==}
|
||||
engines: {node: '>=8'}
|
||||
dev: false
|
||||
|
||||
/ecc-jsbn/0.1.2:
|
||||
resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==}
|
||||
dependencies:
|
||||
@@ -487,25 +462,12 @@ packages:
|
||||
dev: false
|
||||
|
||||
/escape-string-regexp/1.0.5:
|
||||
resolution: {integrity: sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=}
|
||||
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
dev: false
|
||||
|
||||
/eventemitter2/6.4.5:
|
||||
resolution: {integrity: sha512-bXE7Dyc1i6oQElDG0jMRZJrRAn9QR2xyyFGmBdZleNmyQX0FqGYmhZIrIrpPfm/w//LTo4tVQGOGQcGCb5q9uw==}
|
||||
dev: false
|
||||
|
||||
/execa/1.0.0:
|
||||
resolution: {integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==}
|
||||
engines: {node: '>=6'}
|
||||
dependencies:
|
||||
cross-spawn: 6.0.5
|
||||
get-stream: 4.1.0
|
||||
is-stream: 1.1.0
|
||||
npm-run-path: 2.0.2
|
||||
p-finally: 1.0.0
|
||||
signal-exit: 3.0.7
|
||||
strip-eof: 1.0.0
|
||||
/eventemitter2/6.4.7:
|
||||
resolution: {integrity: sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==}
|
||||
dev: false
|
||||
|
||||
/execa/4.1.0:
|
||||
@@ -530,6 +492,11 @@ packages:
|
||||
pify: 2.3.0
|
||||
dev: false
|
||||
|
||||
/expand-template/2.0.3:
|
||||
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
|
||||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/extend/3.0.2:
|
||||
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
|
||||
dev: false
|
||||
@@ -566,13 +533,6 @@ packages:
|
||||
escape-string-regexp: 1.0.5
|
||||
dev: false
|
||||
|
||||
/find-up/3.0.0:
|
||||
resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==}
|
||||
engines: {node: '>=6'}
|
||||
dependencies:
|
||||
locate-path: 3.0.0
|
||||
dev: false
|
||||
|
||||
/forever-agent/0.6.1:
|
||||
resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==}
|
||||
dev: false
|
||||
@@ -586,13 +546,8 @@ packages:
|
||||
mime-types: 2.1.35
|
||||
dev: false
|
||||
|
||||
/fs-extra/7.0.1:
|
||||
resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==}
|
||||
engines: {node: '>=6 <7 || >=8'}
|
||||
dependencies:
|
||||
graceful-fs: 4.2.8
|
||||
jsonfile: 4.0.0
|
||||
universalify: 0.1.2
|
||||
/fs-constants/1.0.0:
|
||||
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
||||
dev: false
|
||||
|
||||
/fs-extra/9.1.0:
|
||||
@@ -606,19 +561,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
/fs.realpath/1.0.0:
|
||||
resolution: {integrity: sha1-FQStJSMVjKpA20onh8sBQRmU6k8=}
|
||||
dev: false
|
||||
|
||||
/get-stdin/5.0.1:
|
||||
resolution: {integrity: sha1-Ei4WFZHiH/TFJTAwVpPyDmOTo5g=}
|
||||
engines: {node: '>=0.12.0'}
|
||||
dev: false
|
||||
|
||||
/get-stream/4.1.0:
|
||||
resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==}
|
||||
engines: {node: '>=6'}
|
||||
dependencies:
|
||||
pump: 3.0.0
|
||||
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
||||
dev: false
|
||||
|
||||
/get-stream/5.2.0:
|
||||
@@ -640,15 +583,8 @@ packages:
|
||||
assert-plus: 1.0.0
|
||||
dev: false
|
||||
|
||||
/glob/7.2.0:
|
||||
resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==}
|
||||
dependencies:
|
||||
fs.realpath: 1.0.0
|
||||
inflight: 1.0.6
|
||||
inherits: 2.0.4
|
||||
minimatch: 3.0.4
|
||||
once: 1.4.0
|
||||
path-is-absolute: 1.0.1
|
||||
/github-from-package/0.0.0:
|
||||
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
|
||||
dev: false
|
||||
|
||||
/glob/7.2.3:
|
||||
@@ -669,30 +605,10 @@ packages:
|
||||
ini: 2.0.0
|
||||
dev: false
|
||||
|
||||
/glur/1.1.2:
|
||||
resolution: {integrity: sha1-8g6jbbEDv8KSNDkh8fkeg8NGdok=}
|
||||
dev: false
|
||||
|
||||
/graceful-fs/4.2.10:
|
||||
resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==}
|
||||
dev: false
|
||||
|
||||
/graceful-fs/4.2.8:
|
||||
resolution: {integrity: sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==}
|
||||
dev: false
|
||||
|
||||
/has-ansi/2.0.0:
|
||||
resolution: {integrity: sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dependencies:
|
||||
ansi-regex: 2.1.1
|
||||
dev: false
|
||||
|
||||
/has-flag/3.0.0:
|
||||
resolution: {integrity: sha1-tdRU3CGZriJWmfNGfloH87lVuv0=}
|
||||
engines: {node: '>=4'}
|
||||
dev: false
|
||||
|
||||
/has-flag/4.0.0:
|
||||
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -722,7 +638,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
/inflight/1.0.6:
|
||||
resolution: {integrity: sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=}
|
||||
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
|
||||
dependencies:
|
||||
once: 1.4.0
|
||||
wrappy: 1.0.2
|
||||
@@ -732,11 +648,19 @@ packages:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
dev: false
|
||||
|
||||
/ini/1.3.8:
|
||||
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
|
||||
dev: false
|
||||
|
||||
/ini/2.0.0:
|
||||
resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==}
|
||||
engines: {node: '>=10'}
|
||||
dev: false
|
||||
|
||||
/is-arrayish/0.3.2:
|
||||
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
|
||||
dev: false
|
||||
|
||||
/is-ci/3.0.1:
|
||||
resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==}
|
||||
hasBin: true
|
||||
@@ -762,11 +686,6 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
dev: false
|
||||
|
||||
/is-stream/1.1.0:
|
||||
resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/is-stream/2.0.1:
|
||||
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -789,31 +708,6 @@ packages:
|
||||
resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==}
|
||||
dev: false
|
||||
|
||||
/iterm2-version/4.2.0:
|
||||
resolution: {integrity: sha512-IoiNVk4SMPu6uTcK+1nA5QaHNok2BMDLjSl5UomrOixe5g4GkylhPwuiGdw00ysSCrXAKNMfFTu+u/Lk5f6OLQ==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
app-path: 3.3.0
|
||||
plist: 3.0.4
|
||||
dev: false
|
||||
|
||||
/jest-image-snapshot/4.2.0:
|
||||
resolution: {integrity: sha512-6aAqv2wtfOgxiJeBayBCqHo1zX+A12SUNNzo7rIxiXh6W6xYVu8QyHWkada8HeRi+QUTHddp0O0Xa6kmQr+xbQ==}
|
||||
engines: {node: '>= 10.14.2'}
|
||||
peerDependencies:
|
||||
jest: '>=20 <=26'
|
||||
dependencies:
|
||||
chalk: 1.1.3
|
||||
get-stdin: 5.0.1
|
||||
glur: 1.1.2
|
||||
lodash: 4.17.21
|
||||
mkdirp: 0.5.5
|
||||
pixelmatch: 5.2.1
|
||||
pngjs: 3.4.0
|
||||
rimraf: 2.7.1
|
||||
ssim.js: 3.5.0
|
||||
dev: false
|
||||
|
||||
/jsbn/0.1.1:
|
||||
resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==}
|
||||
dev: false
|
||||
@@ -826,12 +720,6 @@ packages:
|
||||
resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==}
|
||||
dev: false
|
||||
|
||||
/jsonfile/4.0.0:
|
||||
resolution: {integrity: sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=}
|
||||
optionalDependencies:
|
||||
graceful-fs: 4.2.8
|
||||
dev: false
|
||||
|
||||
/jsonfile/6.1.0:
|
||||
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
||||
dependencies:
|
||||
@@ -870,19 +758,11 @@ packages:
|
||||
log-update: 4.0.0
|
||||
p-map: 4.0.0
|
||||
rfdc: 1.3.0
|
||||
rxjs: 7.5.5
|
||||
rxjs: 7.5.6
|
||||
through: 2.3.8
|
||||
wrap-ansi: 7.0.0
|
||||
dev: false
|
||||
|
||||
/locate-path/3.0.0:
|
||||
resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==}
|
||||
engines: {node: '>=6'}
|
||||
dependencies:
|
||||
p-locate: 3.0.0
|
||||
path-exists: 3.0.0
|
||||
dev: false
|
||||
|
||||
/lodash.once/4.1.1:
|
||||
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
|
||||
dev: false
|
||||
@@ -937,10 +817,9 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/minimatch/3.0.4:
|
||||
resolution: {integrity: sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==}
|
||||
dependencies:
|
||||
brace-expansion: 1.1.11
|
||||
/mimic-response/3.1.0:
|
||||
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
|
||||
engines: {node: '>=10'}
|
||||
dev: false
|
||||
|
||||
/minimatch/3.1.2:
|
||||
@@ -953,11 +832,15 @@ packages:
|
||||
resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==}
|
||||
dev: false
|
||||
|
||||
/mkdirp/0.5.5:
|
||||
resolution: {integrity: sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==}
|
||||
hasBin: true
|
||||
/mkdirp-classic/0.5.3:
|
||||
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
|
||||
dev: false
|
||||
|
||||
/move-file/2.1.0:
|
||||
resolution: {integrity: sha512-i9qLW6gqboJ5Ht8bauZi7KlTnQ3QFpBCvMvFfEcHADKgHGeJ9BZMO7SFCTwHPV9Qa0du9DYY1Yx3oqlGt30nXA==}
|
||||
engines: {node: '>=10.17'}
|
||||
dependencies:
|
||||
minimist: 1.2.6
|
||||
path-exists: 4.0.0
|
||||
dev: false
|
||||
|
||||
/ms/2.1.2:
|
||||
@@ -968,15 +851,19 @@ packages:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
dev: false
|
||||
|
||||
/nice-try/1.0.5:
|
||||
resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==}
|
||||
/napi-build-utils/1.0.2:
|
||||
resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==}
|
||||
dev: false
|
||||
|
||||
/npm-run-path/2.0.2:
|
||||
resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==}
|
||||
engines: {node: '>=4'}
|
||||
/node-abi/3.24.0:
|
||||
resolution: {integrity: sha512-YPG3Co0luSu6GwOBsmIdGW6Wx0NyNDLg/hriIyDllVsNwnI6UeqaWShxC3lbH4LtEQUgoLP3XR1ndXiDAWvmRw==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
path-key: 2.0.1
|
||||
semver: 7.3.7
|
||||
dev: false
|
||||
|
||||
/node-addon-api/5.0.0:
|
||||
resolution: {integrity: sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==}
|
||||
dev: false
|
||||
|
||||
/npm-run-path/4.0.1:
|
||||
@@ -987,7 +874,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
/once/1.4.0:
|
||||
resolution: {integrity: sha1-WDsap3WWHUsROsF9nFC6753Xa9E=}
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
dependencies:
|
||||
wrappy: 1.0.2
|
||||
dev: false
|
||||
@@ -1003,25 +890,6 @@ packages:
|
||||
resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==}
|
||||
dev: false
|
||||
|
||||
/p-finally/1.0.0:
|
||||
resolution: {integrity: sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=}
|
||||
engines: {node: '>=4'}
|
||||
dev: false
|
||||
|
||||
/p-limit/2.3.0:
|
||||
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
|
||||
engines: {node: '>=6'}
|
||||
dependencies:
|
||||
p-try: 2.2.0
|
||||
dev: false
|
||||
|
||||
/p-locate/3.0.0:
|
||||
resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==}
|
||||
engines: {node: '>=6'}
|
||||
dependencies:
|
||||
p-limit: 2.3.0
|
||||
dev: false
|
||||
|
||||
/p-map/4.0.0:
|
||||
resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -1029,26 +897,16 @@ packages:
|
||||
aggregate-error: 3.1.0
|
||||
dev: false
|
||||
|
||||
/p-try/2.2.0:
|
||||
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
|
||||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/path-exists/3.0.0:
|
||||
resolution: {integrity: sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=}
|
||||
engines: {node: '>=4'}
|
||||
/path-exists/4.0.0:
|
||||
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||
engines: {node: '>=8'}
|
||||
dev: false
|
||||
|
||||
/path-is-absolute/1.0.1:
|
||||
resolution: {integrity: sha1-F0uSaHNVNP+8es5r9TpanhtcX18=}
|
||||
resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/path-key/2.0.1:
|
||||
resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==}
|
||||
engines: {node: '>=4'}
|
||||
dev: false
|
||||
|
||||
/path-key/3.1.1:
|
||||
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1067,36 +925,35 @@ packages:
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/pixelmatch/5.2.1:
|
||||
resolution: {integrity: sha512-WjcAdYSnKrrdDdqTcVEY7aB7UhhwjYQKYhHiBXdJef0MOaQeYpUdQ+iVyBLa5YBKS8MPVPPMX7rpOByISLpeEQ==}
|
||||
/pixelmatch/5.3.0:
|
||||
resolution: {integrity: sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
pngjs: 4.0.1
|
||||
pngjs: 6.0.0
|
||||
dev: false
|
||||
|
||||
/pkg-dir/3.0.0:
|
||||
resolution: {integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==}
|
||||
engines: {node: '>=6'}
|
||||
/pngjs/6.0.0:
|
||||
resolution: {integrity: sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==}
|
||||
engines: {node: '>=12.13.0'}
|
||||
dev: false
|
||||
|
||||
/prebuild-install/7.1.1:
|
||||
resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
find-up: 3.0.0
|
||||
dev: false
|
||||
|
||||
/plist/3.0.4:
|
||||
resolution: {integrity: sha512-ksrr8y9+nXOxQB2osVNqrgvX/XQPOXaU4BQMKjYq8PvaY1U18mo+fKgBSwzK+luSyinOuPae956lSVcBwxlAMg==}
|
||||
engines: {node: '>=6'}
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
xmlbuilder: 9.0.7
|
||||
dev: false
|
||||
|
||||
/pngjs/3.4.0:
|
||||
resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
dev: false
|
||||
|
||||
/pngjs/4.0.1:
|
||||
resolution: {integrity: sha512-rf5+2/ioHeQxR6IxuYNYGFytUyG3lma/WW1nsmjeHlWwtb2aByla6dkVc8pmJ9nplzkTA0q2xx7mMWrOTqT4Gg==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
detect-libc: 2.0.1
|
||||
expand-template: 2.0.3
|
||||
github-from-package: 0.0.0
|
||||
minimist: 1.2.6
|
||||
mkdirp-classic: 0.5.3
|
||||
napi-build-utils: 1.0.2
|
||||
node-abi: 3.24.0
|
||||
pump: 3.0.0
|
||||
rc: 1.2.8
|
||||
simple-get: 4.0.1
|
||||
tar-fs: 2.1.1
|
||||
tunnel-agent: 0.6.0
|
||||
dev: false
|
||||
|
||||
/pretty-bytes/5.6.0:
|
||||
@@ -1108,8 +965,8 @@ packages:
|
||||
resolution: {integrity: sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==}
|
||||
dev: false
|
||||
|
||||
/psl/1.8.0:
|
||||
resolution: {integrity: sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==}
|
||||
/psl/1.9.0:
|
||||
resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==}
|
||||
dev: false
|
||||
|
||||
/pump/3.0.0:
|
||||
@@ -1129,6 +986,25 @@ packages:
|
||||
engines: {node: '>=0.6'}
|
||||
dev: false
|
||||
|
||||
/rc/1.2.8:
|
||||
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
deep-extend: 0.6.0
|
||||
ini: 1.3.8
|
||||
minimist: 1.2.6
|
||||
strip-json-comments: 2.0.1
|
||||
dev: false
|
||||
|
||||
/readable-stream/3.6.0:
|
||||
resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==}
|
||||
engines: {node: '>= 6'}
|
||||
dependencies:
|
||||
inherits: 2.0.4
|
||||
string_decoder: 1.3.0
|
||||
util-deprecate: 1.0.2
|
||||
dev: false
|
||||
|
||||
/request-progress/3.0.0:
|
||||
resolution: {integrity: sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==}
|
||||
dependencies:
|
||||
@@ -1147,13 +1023,6 @@ packages:
|
||||
resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==}
|
||||
dev: false
|
||||
|
||||
/rimraf/2.7.1:
|
||||
resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
glob: 7.2.0
|
||||
dev: false
|
||||
|
||||
/rimraf/3.0.2:
|
||||
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
|
||||
hasBin: true
|
||||
@@ -1161,8 +1030,8 @@ packages:
|
||||
glob: 7.2.3
|
||||
dev: false
|
||||
|
||||
/rxjs/7.5.5:
|
||||
resolution: {integrity: sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==}
|
||||
/rxjs/7.5.6:
|
||||
resolution: {integrity: sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==}
|
||||
dependencies:
|
||||
tslib: 2.4.0
|
||||
dev: false
|
||||
@@ -1175,11 +1044,6 @@ packages:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
dev: false
|
||||
|
||||
/semver/5.7.1:
|
||||
resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/semver/7.3.7:
|
||||
resolution: {integrity: sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -1188,11 +1052,19 @@ packages:
|
||||
lru-cache: 6.0.0
|
||||
dev: false
|
||||
|
||||
/shebang-command/1.2.0:
|
||||
resolution: {integrity: sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=}
|
||||
engines: {node: '>=0.10.0'}
|
||||
/sharp/0.30.7:
|
||||
resolution: {integrity: sha512-G+MY2YW33jgflKPTXXptVO28HvNOo9G3j0MybYAHeEmby+QuD2U98dT6ueht9cv/XDqZspSpIhoSW+BAKJ7Hig==}
|
||||
engines: {node: '>=12.13.0'}
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
shebang-regex: 1.0.0
|
||||
color: 4.2.3
|
||||
detect-libc: 2.0.1
|
||||
node-addon-api: 5.0.0
|
||||
prebuild-install: 7.1.1
|
||||
semver: 7.3.7
|
||||
simple-get: 4.0.1
|
||||
tar-fs: 2.1.1
|
||||
tunnel-agent: 0.6.0
|
||||
dev: false
|
||||
|
||||
/shebang-command/2.0.0:
|
||||
@@ -1202,11 +1074,6 @@ packages:
|
||||
shebang-regex: 3.0.0
|
||||
dev: false
|
||||
|
||||
/shebang-regex/1.0.0:
|
||||
resolution: {integrity: sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/shebang-regex/3.0.0:
|
||||
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1216,6 +1083,24 @@ packages:
|
||||
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
|
||||
dev: false
|
||||
|
||||
/simple-concat/1.0.1:
|
||||
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
|
||||
dev: false
|
||||
|
||||
/simple-get/4.0.1:
|
||||
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
|
||||
dependencies:
|
||||
decompress-response: 6.0.0
|
||||
once: 1.4.0
|
||||
simple-concat: 1.0.1
|
||||
dev: false
|
||||
|
||||
/simple-swizzle/0.2.2:
|
||||
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
|
||||
dependencies:
|
||||
is-arrayish: 0.3.2
|
||||
dev: false
|
||||
|
||||
/slice-ansi/3.0.0:
|
||||
resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1250,10 +1135,6 @@ packages:
|
||||
tweetnacl: 0.14.5
|
||||
dev: false
|
||||
|
||||
/ssim.js/3.5.0:
|
||||
resolution: {integrity: sha512-Aj6Jl2z6oDmgYFFbQqK7fght19bXdOxY7Tj03nF+03M9gCBAjeIiO8/PlEGMfKDwYpw4q6iBqVq2YuREorGg/g==}
|
||||
dev: false
|
||||
|
||||
/string-width/4.2.3:
|
||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1263,11 +1144,10 @@ packages:
|
||||
strip-ansi: 6.0.1
|
||||
dev: false
|
||||
|
||||
/strip-ansi/3.0.1:
|
||||
resolution: {integrity: sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=}
|
||||
engines: {node: '>=0.10.0'}
|
||||
/string_decoder/1.3.0:
|
||||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||
dependencies:
|
||||
ansi-regex: 2.1.1
|
||||
safe-buffer: 5.2.1
|
||||
dev: false
|
||||
|
||||
/strip-ansi/6.0.1:
|
||||
@@ -1277,26 +1157,14 @@ packages:
|
||||
ansi-regex: 5.0.1
|
||||
dev: false
|
||||
|
||||
/strip-eof/1.0.0:
|
||||
resolution: {integrity: sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/strip-final-newline/2.0.0:
|
||||
resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
|
||||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/supports-color/2.0.0:
|
||||
resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
dev: false
|
||||
|
||||
/supports-color/5.5.0:
|
||||
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
|
||||
engines: {node: '>=4'}
|
||||
dependencies:
|
||||
has-flag: 3.0.0
|
||||
/strip-json-comments/2.0.1:
|
||||
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/supports-color/7.2.0:
|
||||
@@ -1313,12 +1181,24 @@ packages:
|
||||
has-flag: 4.0.0
|
||||
dev: false
|
||||
|
||||
/term-img/4.1.0:
|
||||
resolution: {integrity: sha512-DFpBhaF5j+2f7kheKFc1ajsAUUDGOaNPpKPtiIMxlbfud6mvfFZuWGnTRpaujUa5J7yl6cIw/h6nyr4mSsENPg==}
|
||||
engines: {node: '>=8'}
|
||||
/tar-fs/2.1.1:
|
||||
resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==}
|
||||
dependencies:
|
||||
ansi-escapes: 4.3.2
|
||||
iterm2-version: 4.2.0
|
||||
chownr: 1.1.4
|
||||
mkdirp-classic: 0.5.3
|
||||
pump: 3.0.0
|
||||
tar-stream: 2.2.0
|
||||
dev: false
|
||||
|
||||
/tar-stream/2.2.0:
|
||||
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
|
||||
engines: {node: '>=6'}
|
||||
dependencies:
|
||||
bl: 4.1.0
|
||||
end-of-stream: 1.4.4
|
||||
fs-constants: 1.0.0
|
||||
inherits: 2.0.4
|
||||
readable-stream: 3.6.0
|
||||
dev: false
|
||||
|
||||
/throttleit/1.0.0:
|
||||
@@ -1340,7 +1220,7 @@ packages:
|
||||
resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==}
|
||||
engines: {node: '>=0.8'}
|
||||
dependencies:
|
||||
psl: 1.8.0
|
||||
psl: 1.9.0
|
||||
punycode: 2.1.1
|
||||
dev: false
|
||||
|
||||
@@ -1369,11 +1249,6 @@ packages:
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/universalify/0.1.2:
|
||||
resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
|
||||
engines: {node: '>= 4.0.0'}
|
||||
dev: false
|
||||
|
||||
/universalify/2.0.0:
|
||||
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
@@ -1384,6 +1259,10 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
dev: false
|
||||
|
||||
/util-deprecate/1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
dev: false
|
||||
|
||||
/uuid/8.3.2:
|
||||
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
||||
hasBin: true
|
||||
@@ -1398,13 +1277,6 @@ packages:
|
||||
extsprintf: 1.3.0
|
||||
dev: false
|
||||
|
||||
/which/1.3.1:
|
||||
resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
dev: false
|
||||
|
||||
/which/2.0.2:
|
||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -1432,12 +1304,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
/wrappy/1.0.2:
|
||||
resolution: {integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=}
|
||||
dev: false
|
||||
|
||||
/xmlbuilder/9.0.7:
|
||||
resolution: {integrity: sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=}
|
||||
engines: {node: '>=4.0'}
|
||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
dev: false
|
||||
|
||||
/yallist/4.0.0:
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { Config } from "@jest/types";
|
||||
|
||||
const config: Config.InitialOptions = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "jsdom",
|
||||
testPathIgnorePatterns: ["node_modules", "<rootDir>/integration/", "<rootDir>/e2e/"],
|
||||
transform: {
|
||||
"^.+\\.vue$": "@vue/vue3-jest",
|
||||
},
|
||||
moduleNameMapper: {
|
||||
"@/(.*)": ["<rootDir>/assets/$1"],
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
45
package.json
45
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dozzle",
|
||||
"version": "3.12.10",
|
||||
"version": "4.0.1",
|
||||
"description": "Realtime log viewer for docker containers. ",
|
||||
"homepage": "https://github.com/amir20/dozzle#readme",
|
||||
"bugs": {
|
||||
@@ -22,43 +22,43 @@
|
||||
"postinstall": "husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify-json/carbon": "^1.1.6",
|
||||
"@iconify-json/carbon": "^1.1.7",
|
||||
"@iconify-json/cil": "^1.1.2",
|
||||
"@iconify-json/mdi": "^1.1.29",
|
||||
"@iconify-json/mdi": "^1.1.30",
|
||||
"@iconify-json/mdi-light": "^1.1.2",
|
||||
"@iconify-json/octicon": "^1.1.13",
|
||||
"@iconify-json/octicon": "^1.1.16",
|
||||
"@oruga-ui/oruga-next": "^0.5.4",
|
||||
"@oruga-ui/theme-bulma": "^0.2.6",
|
||||
"@vitejs/plugin-vue": "3.0.1",
|
||||
"@vitejs/plugin-vue": "3.0.3",
|
||||
"@vue/compiler-sfc": "^3.2.37",
|
||||
"@vueuse/core": "^8.9.4",
|
||||
"@vueuse/router": "^8.9.4",
|
||||
"@vueuse/core": "^9.1.0",
|
||||
"@vueuse/router": "^9.1.0",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"bulma": "^0.9.4",
|
||||
"date-fns": "^2.28.0",
|
||||
"date-fns": "^2.29.2",
|
||||
"fuzzysort": "^2.0.1",
|
||||
"hotkeys-js": "^3.9.4",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"pinia": "^2.0.16",
|
||||
"sass": "^1.53.0",
|
||||
"pinia": "^2.0.20",
|
||||
"sass": "^1.54.4",
|
||||
"semver": "^7.3.7",
|
||||
"splitpanes": "^3.1.1",
|
||||
"typescript": "^4.7.4",
|
||||
"unplugin-auto-import": "^0.9.3",
|
||||
"unplugin-icons": "^0.14.7",
|
||||
"unplugin-vue-components": "^0.21.1",
|
||||
"vite": "3.0.2",
|
||||
"unplugin-auto-import": "^0.11.2",
|
||||
"unplugin-icons": "^0.14.8",
|
||||
"unplugin-vue-components": "^0.22.4",
|
||||
"vite": "3.0.9",
|
||||
"vue": "^3.2.37",
|
||||
"vue-router": "^4.1.2"
|
||||
"vue-router": "^4.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pinia/testing": "^0.0.12",
|
||||
"@types/jest": "^28.1.6",
|
||||
"@pinia/testing": "^0.0.14",
|
||||
"@types/jest": "^28.1.7",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/node": "^18.0.6",
|
||||
"@types/semver": "^7.3.10",
|
||||
"@types/node": "^18.7.7",
|
||||
"@types/semver": "^7.3.12",
|
||||
"@vue/test-utils": "^2.0.2",
|
||||
"c8": "^7.11.3",
|
||||
"c8": "^7.12.0",
|
||||
"eventsourcemock": "^2.0.0",
|
||||
"husky": "^8.0.1",
|
||||
"jest-serializer-vue": "^2.0.2",
|
||||
@@ -66,9 +66,10 @@
|
||||
"lint-staged": "^13.0.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.7.1",
|
||||
"release-it": "^15.1.2",
|
||||
"release-it": "^15.3.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"vitest": "^0.18.1"
|
||||
"vitest": "^0.22.1",
|
||||
"vue-tsc": "^0.40.1"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,vue,css}": [
|
||||
|
||||
1043
pnpm-lock.yaml
generated
1043
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,8 @@
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"paths": {
|
||||
"@/*": ["assets/*"]
|
||||
}
|
||||
},
|
||||
"jsx": "preserve",
|
||||
},
|
||||
"include": ["assets/**/*.ts", "assets/**/*.d.ts", "assets/**/*.vue"],
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ Connection: keep-alive
|
||||
Content-Type: text/event-stream
|
||||
X-Accel-Buffering: no
|
||||
|
||||
data: INFO Testing logs...
|
||||
data: {"m":"INFO Testing logs...","ts":0,"id":4256192898}
|
||||
|
||||
event: container-stopped
|
||||
data: end of stream
|
||||
@@ -170,8 +170,8 @@ Connection: keep-alive
|
||||
Content-Type: text/event-stream
|
||||
X-Accel-Buffering: no
|
||||
|
||||
data: 2020-05-13T18:55:37.772853839Z INFO Testing logs...
|
||||
id: 2020-05-13T18:55:37.772853839Z
|
||||
data: {"m":"INFO Testing logs...","ts":1589396137772,"id":1469707724}
|
||||
id: 1589396137772
|
||||
|
||||
event: container-stopped
|
||||
data: end of stream
|
||||
114
web/logs.go
114
web/logs.go
@@ -4,6 +4,8 @@ import (
|
||||
"bufio"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"hash/fnv"
|
||||
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -13,29 +15,12 @@ import (
|
||||
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/docker"
|
||||
"github.com/dustin/go-humanize"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
|
||||
|
||||
from, _ := time.Parse(time.RFC3339, r.URL.Query().Get("from"))
|
||||
to, _ := time.Parse(time.RFC3339, r.URL.Query().Get("to"))
|
||||
id := r.URL.Query().Get("id")
|
||||
|
||||
reader, err := h.client.ContainerLogsBetweenDates(r.Context(), id, from, to)
|
||||
defer reader.Close()
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
io.Copy(w, reader)
|
||||
}
|
||||
|
||||
func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.URL.Query().Get("id")
|
||||
container, err := h.client.FindContainer(id)
|
||||
@@ -63,6 +48,64 @@ func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
|
||||
io.Copy(zw, reader)
|
||||
}
|
||||
|
||||
func logEventIterator(reader *bufio.Reader) func() (docker.LogEvent, error) {
|
||||
return func() (docker.LogEvent, error) {
|
||||
message, readerError := reader.ReadString('\n')
|
||||
|
||||
h := fnv.New32a()
|
||||
h.Write([]byte(message))
|
||||
|
||||
logEvent := docker.LogEvent{Id: h.Sum32(), Message: message}
|
||||
|
||||
if index := strings.IndexAny(message, " "); index != -1 {
|
||||
logId := message[:index]
|
||||
if timestamp, err := time.Parse(time.RFC3339Nano, logId); err == nil {
|
||||
logEvent.Timestamp = timestamp.UnixMilli()
|
||||
message = strings.TrimSuffix(message[index+1:], "\n")
|
||||
logEvent.Message = message
|
||||
if strings.HasPrefix(message, "{") && strings.HasSuffix(message, "}") {
|
||||
var data map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(message), &data); err != nil {
|
||||
log.Errorf("json unmarshal error while streaming %v", err.Error())
|
||||
}
|
||||
logEvent.Message = data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return logEvent, readerError
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/ld+json; charset=UTF-8")
|
||||
|
||||
from, _ := time.Parse(time.RFC3339, r.URL.Query().Get("from"))
|
||||
to, _ := time.Parse(time.RFC3339, r.URL.Query().Get("to"))
|
||||
id := r.URL.Query().Get("id")
|
||||
|
||||
reader, err := h.client.ContainerLogsBetweenDates(r.Context(), id, from, to)
|
||||
defer reader.Close()
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
buffered := bufio.NewReader(reader)
|
||||
eventIterator := logEventIterator(buffered)
|
||||
|
||||
for {
|
||||
logEvent, readerError := eventIterator()
|
||||
if readerError != nil {
|
||||
break
|
||||
}
|
||||
if err := json.NewEncoder(w).Encode(logEvent); err != nil {
|
||||
log.Errorf("json encoding error while streaming %v", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.URL.Query().Get("id")
|
||||
if id == "" {
|
||||
@@ -105,17 +148,36 @@ func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
fmt.Fprintf(w, ":ping \n\n")
|
||||
f.Flush()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
buffered := bufio.NewReader(reader)
|
||||
var readerError error
|
||||
var message string
|
||||
eventIterator := logEventIterator(buffered)
|
||||
|
||||
for {
|
||||
message, readerError = buffered.ReadString('\n')
|
||||
fmt.Fprintf(w, "data: %s\n", message)
|
||||
if index := strings.IndexAny(message, " "); index != -1 {
|
||||
id := message[:index]
|
||||
if _, err := time.Parse(time.RFC3339Nano, id); err == nil {
|
||||
fmt.Fprintf(w, "id: %s\n", id)
|
||||
}
|
||||
|
||||
var logEvent docker.LogEvent
|
||||
logEvent, readerError = eventIterator()
|
||||
if buf, err := json.Marshal(logEvent); err != nil {
|
||||
log.Errorf("json encoding error while streaming %v", err.Error())
|
||||
} else {
|
||||
fmt.Fprintf(w, "data: %s\n", buf)
|
||||
}
|
||||
if logEvent.Timestamp > 0 {
|
||||
fmt.Fprintf(w, "id: %d\n", logEvent.Timestamp)
|
||||
}
|
||||
fmt.Fprintf(w, "\n")
|
||||
f.Flush()
|
||||
|
||||
Reference in New Issue
Block a user