mirror of
https://github.com/amir20/dozzle.git
synced 2026-08-07 10:54:44 +00:00
198 lines
5.7 KiB
Vue
198 lines
5.7 KiB
Vue
<template>
|
|
<div
|
|
class="dropdown dropdown-hover absolute -left-2 z-10 font-sans"
|
|
:class="shouldShowBelow ? 'dropdown-right' : 'dropdown-right dropdown-end'"
|
|
v-show="container"
|
|
ref="dropdownRef"
|
|
@mouseenter="checkDropdownPosition"
|
|
>
|
|
<router-link
|
|
v-if="isFiltered"
|
|
@click="resetSearch()"
|
|
tabindex="0"
|
|
class="btn btn-square btn-xs border-base-content/20 bg-base-100 pointer-events-auto! opacity-0 shadow-sm group-hover/entry:opacity-90"
|
|
:to="{
|
|
name: '/container/[id].time.[datetime]',
|
|
params: { id: container.id, datetime: logEntry.date.toISOString() },
|
|
query: { logId: logEntry.id },
|
|
}"
|
|
>
|
|
<material-symbols:eye-tracking />
|
|
</router-link>
|
|
<button
|
|
tabindex="0"
|
|
class="btn btn-square btn-xs border-base-content/20 bg-base-100 border opacity-0 shadow-sm group-hover/entry:opacity-90"
|
|
v-else
|
|
>
|
|
<ion:ellipsis-vertical />
|
|
</button>
|
|
<ul
|
|
tabindex="0"
|
|
class="menu dropdown-content rounded-box bg-base-200 border-base-content/20 z-50 w-52 border p-1 text-sm shadow-sm"
|
|
@click="hideMenu"
|
|
>
|
|
<li v-if="isFiltered">
|
|
<router-link
|
|
@click="resetSearch()"
|
|
:to="{
|
|
name: '/container/[id].time.[datetime]',
|
|
params: { id: container.id, datetime: logEntry.date.toISOString() },
|
|
query: { logId: logEntry.id },
|
|
}"
|
|
>
|
|
<material-symbols:eye-tracking />
|
|
{{ $t("action.see-in-context") }}
|
|
</router-link>
|
|
</li>
|
|
<li>
|
|
<a
|
|
@click="copyLogMessage()"
|
|
:disabled="!isSupported"
|
|
:title="!isSupported ? $t('error.copy-not-supported') : ''"
|
|
:class="{ 'cursor-not-allowed opacity-50': !isSupported }"
|
|
>
|
|
<material-symbols:content-copy />
|
|
{{ $t("action.copy-log") }}
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a
|
|
@click="copyPermalink()"
|
|
:disabled="!isSupported"
|
|
:title="!isSupported ? $t('error.copy-not-supported') : ''"
|
|
:class="{ 'cursor-not-allowed opacity-50': !isSupported }"
|
|
>
|
|
<material-symbols:link />
|
|
{{ $t("action.copy-link") }}
|
|
</a>
|
|
</li>
|
|
|
|
<li v-if="logEntry instanceof ComplexLogEntry">
|
|
<a @click="showDrawer(LogDetails, { entry: logEntry })">
|
|
<material-symbols:code-blocks-rounded />
|
|
{{ $t("action.show-details") }}
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a @click="createAlert()">
|
|
<mdi:bell />
|
|
{{ $t("action.create-alert") }}
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import stripAnsi from "strip-ansi";
|
|
import { Container } from "@/models/Container";
|
|
import { LogEntry, SimpleLogEntry, ComplexLogEntry, GroupedLogEntry, JSONObject } from "@/models/LogEntry";
|
|
import LogDetails from "./LogDetails.vue";
|
|
import AlertForm from "@/components/Notification/AlertForm.vue";
|
|
|
|
const { logEntry, container } = defineProps<{
|
|
logEntry: LogEntry<string | JSONObject>;
|
|
container: Container;
|
|
}>();
|
|
|
|
const { showToast } = useToast();
|
|
const showDrawer = useDrawer();
|
|
const router = useRouter();
|
|
const { isSearching, resetSearch } = useSearchFilter();
|
|
const { levels } = useLoggingContext();
|
|
|
|
// Show "see in context" whenever the stream is narrowed, either by a text search
|
|
// or by a log-level filter, so the entry can be inspected in the full log stream.
|
|
const isFiltered = computed(() => isSearching.value || allLevels.some((level) => !levels.value.has(level)));
|
|
|
|
const { copy, isSupported, copied } = useClipboard({ legacy: true });
|
|
const { t } = useI18n();
|
|
|
|
async function copyLogMessage() {
|
|
if (!isSupported.value) {
|
|
return;
|
|
}
|
|
|
|
if (logEntry instanceof ComplexLogEntry) {
|
|
await copy(stripAnsi(logEntry.rawMessage));
|
|
} else if (logEntry instanceof SimpleLogEntry) {
|
|
await copy(stripAnsi(logEntry.rawMessage));
|
|
} else if (logEntry instanceof GroupedLogEntry) {
|
|
await copy(stripAnsi(logEntry.message.join("\n")));
|
|
}
|
|
|
|
if (copied.value) {
|
|
showToast(
|
|
{
|
|
title: t("toasts.copied.title"),
|
|
message: t("toasts.copied.message"),
|
|
type: "info",
|
|
},
|
|
{ expire: 2000 },
|
|
);
|
|
}
|
|
}
|
|
|
|
async function copyPermalink() {
|
|
if (!isSupported.value) {
|
|
return;
|
|
}
|
|
const url = router.resolve({
|
|
name: "/container/[id].time.[datetime]",
|
|
params: { id: container.id, datetime: logEntry.date.toISOString() },
|
|
query: { logId: logEntry.id },
|
|
}).href;
|
|
|
|
const resolved = new URL(url, window.location.origin);
|
|
|
|
await copy(resolved.href);
|
|
|
|
if (copied.value) {
|
|
showToast(
|
|
{
|
|
title: t("toasts.copied.title"),
|
|
message: t("toasts.copied.message"),
|
|
type: "info",
|
|
},
|
|
{ expire: 2000 },
|
|
);
|
|
}
|
|
}
|
|
|
|
function createAlert() {
|
|
const containerExpr = `name contains "${container.name}"`;
|
|
let logExpr = "";
|
|
if (logEntry.level && logEntry.level !== "unknown") {
|
|
logExpr = `level == "${logEntry.level}"`;
|
|
}
|
|
|
|
const nameParts = [container.name];
|
|
if (logEntry.level && logEntry.level !== "unknown") {
|
|
nameParts.push(logEntry.level);
|
|
}
|
|
const name = nameParts.join(" ");
|
|
|
|
showDrawer(AlertForm, { prefill: { name, containerExpression: containerExpr, logExpression: logExpr } }, "lg");
|
|
}
|
|
|
|
function hideMenu(e: MouseEvent) {
|
|
if (e.target instanceof HTMLAnchorElement) {
|
|
setTimeout(() => {
|
|
if (document.activeElement instanceof HTMLElement) {
|
|
document.activeElement.blur();
|
|
}
|
|
}, 50);
|
|
}
|
|
}
|
|
|
|
const dropdownRef = useTemplateRef<HTMLDivElement>("dropdownRef");
|
|
const shouldShowBelow = ref(false);
|
|
|
|
function checkDropdownPosition() {
|
|
if (!dropdownRef.value) return;
|
|
|
|
const rect = dropdownRef.value.getBoundingClientRect();
|
|
shouldShowBelow.value = rect.top < 150;
|
|
}
|
|
</script>
|