mirror of
https://github.com/amir20/dozzle.git
synced 2025-12-24 14:31:44 +01:00
feat: adds automatic redirect when a new container is found (#2396)
* feat: implements a toast for alerting errors and other useful information * removes unused code * feat: adds automatic redirect when a new container is found * complete the flow with alerts and settings page * adds more langs and option for once * removes files
This commit is contained in:
3
assets/auto-imports.d.ts
vendored
3
assets/auto-imports.d.ts
vendored
@@ -18,6 +18,7 @@ declare global {
|
||||
const arrayEquals: typeof import('./utils/index')['arrayEquals']
|
||||
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
||||
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
|
||||
const automaticRedirect: typeof import('./composables/settings')['automaticRedirect']
|
||||
const collapseNav: typeof import('./composables/settings')['collapseNav']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const computedAsync: typeof import('@vueuse/core')['computedAsync']
|
||||
@@ -378,6 +379,7 @@ declare module 'vue' {
|
||||
readonly arrayEquals: UnwrapRef<typeof import('./utils/index')['arrayEquals']>
|
||||
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
|
||||
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
|
||||
readonly automaticRedirect: UnwrapRef<typeof import('./composables/settings')['automaticRedirect']>
|
||||
readonly collapseNav: UnwrapRef<typeof import('./composables/settings')['collapseNav']>
|
||||
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
|
||||
@@ -714,6 +716,7 @@ declare module '@vue/runtime-core' {
|
||||
readonly arrayEquals: UnwrapRef<typeof import('./utils/index')['arrayEquals']>
|
||||
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
|
||||
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
|
||||
readonly automaticRedirect: UnwrapRef<typeof import('./composables/settings')['automaticRedirect']>
|
||||
readonly collapseNav: UnwrapRef<typeof import('./composables/settings')['collapseNav']>
|
||||
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
|
||||
|
||||
2
assets/components.d.ts
vendored
2
assets/components.d.ts
vendored
@@ -14,6 +14,7 @@ declare module 'vue' {
|
||||
'Carbon:macShift': typeof import('~icons/carbon/mac-shift')['default']
|
||||
'Carbon:star': typeof import('~icons/carbon/star')['default']
|
||||
'Carbon:starFilled': typeof import('~icons/carbon/star-filled')['default']
|
||||
'Carbon:warning': typeof import('~icons/carbon/warning')['default']
|
||||
'Cil:checkCircle': typeof import('~icons/cil/check-circle')['default']
|
||||
'Cil:circle': typeof import('~icons/cil/circle')['default']
|
||||
'Cil:columns': typeof import('~icons/cil/columns')['default']
|
||||
@@ -73,6 +74,7 @@ declare module 'vue' {
|
||||
StatMonitor: typeof import('./components/LogViewer/StatMonitor.vue')['default']
|
||||
StatSparkline: typeof import('./components/LogViewer/StatSparkline.vue')['default']
|
||||
Tag: typeof import('./components/common/Tag.vue')['default']
|
||||
TimedButton: typeof import('./components/common/TimedButton.vue')['default']
|
||||
Toggle: typeof import('./components/common/Toggle.vue')['default']
|
||||
ZigZag: typeof import('./components/LogViewer/ZigZag.vue')['default']
|
||||
}
|
||||
|
||||
@@ -6,12 +6,17 @@
|
||||
v-if="nextContainer && logEntry.event === 'container-stopped'"
|
||||
>
|
||||
<carbon:information class="h-6 w-6 shrink-0 stroke-current" />
|
||||
<span>
|
||||
Another container instance with the same name was created <distance-time :date="nextContainer.created" />. Do
|
||||
you want to redirect to the new one?
|
||||
</span>
|
||||
<div>
|
||||
<router-link :to="{ name: 'container-id', params: { id: nextContainer.id } }" class="btn btn-primary btn-sm">
|
||||
<h3 class="text-lg font-bold">{{ $t("alert.similar-container-found.title") }}</h3>
|
||||
{{ $t("alert.similar-container-found.message", { containerId: nextContainer.id }) }}
|
||||
</div>
|
||||
<div>
|
||||
<TimedButton v-if="automaticRedirect" class="btn-primary btn-sm" @finished="redirectNow()">Cancel</TimedButton>
|
||||
<router-link
|
||||
:to="{ name: 'container-id', params: { id: nextContainer.id } }"
|
||||
class="btn btn-primary btn-sm"
|
||||
v-else
|
||||
>
|
||||
Redirect
|
||||
</router-link>
|
||||
</div>
|
||||
@@ -20,6 +25,9 @@
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { DockerEventLogEntry } from "@/models/LogEntry";
|
||||
const router = useRouter();
|
||||
const { showToast } = useToast();
|
||||
const { t } = useI18n();
|
||||
|
||||
const { logEntry } = defineProps<{
|
||||
logEntry: DockerEventLogEntry;
|
||||
@@ -41,6 +49,18 @@ const nextContainer = computed(
|
||||
)
|
||||
.toSorted((a, b) => +a.created - +b.created)[0],
|
||||
);
|
||||
|
||||
function redirectNow() {
|
||||
showToast(
|
||||
{
|
||||
title: t("alert.redirected.title"),
|
||||
message: t("alert.redirected.message", { containerId: nextContainer.value.id }),
|
||||
type: "info",
|
||||
},
|
||||
{ expire: 5000 },
|
||||
);
|
||||
router.push({ name: "container-id", params: { id: nextContainer.value.id } });
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
|
||||
39
assets/components/common/TimedButton.vue
Normal file
39
assets/components/common/TimedButton.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<button class="btn relative overflow-hidden" @click="cancel()">
|
||||
<div class="absolute inset-0 origin-left bg-white/30" ref="progress"></div>
|
||||
<div class="z-10">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const progress = ref<HTMLElement>();
|
||||
const finished = defineEmit();
|
||||
const cancelled = defineEmit();
|
||||
let animation: Animation | undefined;
|
||||
|
||||
onMounted(async () => {
|
||||
animation = progress.value?.animate([{ transform: "scaleX(0)" }, { transform: "scaleX(1)" }], {
|
||||
duration: 4000,
|
||||
easing: "linear",
|
||||
fill: "forwards",
|
||||
});
|
||||
try {
|
||||
await animation?.finished;
|
||||
finished();
|
||||
} catch (e) {
|
||||
progress.value?.animate([{ transform: "scaleX(1)" }, { transform: "scaleX(0)" }], {
|
||||
duration: 0,
|
||||
fill: "forwards",
|
||||
});
|
||||
cancelled();
|
||||
}
|
||||
});
|
||||
|
||||
const cancel = () => {
|
||||
animation?.cancel();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss"></style>
|
||||
@@ -13,6 +13,7 @@ export const DEFAULT_SETTINGS: {
|
||||
hourStyle: "auto" | "24" | "12";
|
||||
softWrap: boolean;
|
||||
collapseNav: boolean;
|
||||
automaticRedirect: boolean;
|
||||
} = {
|
||||
search: true,
|
||||
size: "medium",
|
||||
@@ -25,6 +26,7 @@ export const DEFAULT_SETTINGS: {
|
||||
hourStyle: "auto",
|
||||
softWrap: true,
|
||||
collapseNav: false,
|
||||
automaticRedirect: true,
|
||||
};
|
||||
|
||||
export const settings = useStorage(DOZZLE_SETTINGS_KEY, DEFAULT_SETTINGS);
|
||||
@@ -42,4 +44,5 @@ export const {
|
||||
menuWidth,
|
||||
size,
|
||||
search,
|
||||
automaticRedirect,
|
||||
} = toRefs(settings);
|
||||
|
||||
@@ -1,19 +1,35 @@
|
||||
type Toast = {
|
||||
id: number;
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
title?: string;
|
||||
message: string;
|
||||
type: "success" | "error" | "warning" | "info";
|
||||
};
|
||||
|
||||
type ToastOptions = {
|
||||
expire?: number;
|
||||
once?: boolean;
|
||||
};
|
||||
|
||||
const toasts = ref<Toast[]>([]);
|
||||
|
||||
const showToast = (message: string, type: Toast["type"]) => {
|
||||
const showToast = (
|
||||
toast: Omit<Toast, "id" | "createdAt"> & { id?: string },
|
||||
{ expire = -1, once = false }: ToastOptions = { expire: -1, once: false },
|
||||
) => {
|
||||
if (once && toasts.value.some((t) => t.id === toast.id)) {
|
||||
return;
|
||||
}
|
||||
toasts.value.push({
|
||||
id: Date.now(),
|
||||
id: Date.now().toString(),
|
||||
createdAt: new Date(),
|
||||
message,
|
||||
type,
|
||||
...toast,
|
||||
});
|
||||
if (expire > 0) {
|
||||
setTimeout(() => {
|
||||
removeToast(toasts.value[0].id);
|
||||
}, expire);
|
||||
}
|
||||
};
|
||||
|
||||
const removeToast = (id: Toast["id"]) => {
|
||||
|
||||
@@ -49,7 +49,12 @@
|
||||
:key="toast.id"
|
||||
:class="{ 'alert-error': toast.type === 'error', 'alert-info': toast.type === 'info' }"
|
||||
>
|
||||
<span>{{ toast.message }}</span>
|
||||
<carbon:information class="h-6 w-6 shrink-0 stroke-current" v-if="toast.type === 'info'" />
|
||||
<carbon:warning class="h-6 w-6 shrink-0 stroke-current" v-else-if="toast.type === 'error'" />
|
||||
<div>
|
||||
<h3 class="text-lg font-bold" v-if="toast.title">{{ toast.title }}</h3>
|
||||
{{ toast.message }}
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-circle btn-xs" @click="removeToast(toast.id)"><mdi:close /></button>
|
||||
</div>
|
||||
|
||||
@@ -80,6 +80,10 @@
|
||||
<div>
|
||||
<toggle v-model="showAllContainers">{{ $t("settings.show-stopped-containers") }}</toggle>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<toggle v-model="automaticRedirect">{{ $t("settings.automatic-redirect") }}</toggle>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
@@ -95,6 +99,7 @@ import {
|
||||
showAllContainers,
|
||||
size,
|
||||
softWrap,
|
||||
automaticRedirect,
|
||||
} from "@/composables/settings";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -36,7 +36,15 @@ export const useContainerStore = defineStore("container", () => {
|
||||
ready.value = false;
|
||||
es = new EventSource(`${config.base}/api/events/stream`);
|
||||
es.addEventListener("error", (e) => {
|
||||
showToast(t("error.events-stream"), "error");
|
||||
showToast(
|
||||
{
|
||||
id: "events-stream",
|
||||
message: t("error.events-stream.message"),
|
||||
title: t("error.events-stream.title"),
|
||||
type: "error",
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
});
|
||||
|
||||
es.addEventListener("containers-changed", (e: Event) =>
|
||||
@@ -75,7 +83,15 @@ export const useContainerStore = defineStore("container", () => {
|
||||
try {
|
||||
await until(ready).toBe(true, { timeout: 8000, throwOnTimeout: true });
|
||||
} catch (e) {
|
||||
showToast(t("error.events-timeout"), "error");
|
||||
showToast(
|
||||
{
|
||||
id: "events-timeout",
|
||||
message: t("error.events-timeout.message"),
|
||||
title: t("error.events-timeout.title"),
|
||||
type: "error",
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
@@ -28,6 +28,26 @@ error:
|
||||
invalid-auth: Benutzername und Passwort sind ungültig.
|
||||
logs-skipped: \{total} Einträge übersprungen
|
||||
container-not-found: Container nicht gefunden.
|
||||
events-stream:
|
||||
title: Unerwarteter Fehler
|
||||
message: >-
|
||||
Dozzle UI konnte keine Verbindung zur API herstellen. Bitte überprüfen Sie
|
||||
Ihre Netzwerkeinstellungen. Wenn Sie einen Reverse-Proxy verwenden,
|
||||
stellen Sie bitte sicher, dass er ordnungsgemäß konfiguriert ist.
|
||||
events-timeout:
|
||||
title: Etwas stimmt nicht
|
||||
message: >-
|
||||
Dozzle UI hat beim Verbinden mit der API ein Timeout. Bitte überprüfen Sie
|
||||
die Netzwerkverbindung und versuchen Sie es erneut.
|
||||
alert:
|
||||
redirected:
|
||||
title: Zu neuem Container umgeleitet
|
||||
message: Dozzle hat Sie automatisch zu neuem Container {containerId} umgeleitet.
|
||||
similar-container-found:
|
||||
title: Ähnlicher Container gefunden
|
||||
message: >-
|
||||
Dozzle hat einen ähnlichen Container {containerId} gefunden, der auf dem
|
||||
gleichen Host ausgeführt wird. Möchten Sie zu ihm wechseln?
|
||||
title:
|
||||
page-not-found: Seite nicht gefunden
|
||||
login: Authentifizierung erforderlich
|
||||
@@ -57,3 +77,4 @@ settings:
|
||||
update-available: >-
|
||||
Eine neue Version ist verfügbar! Aktualisiere auf <a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
|
||||
show-std: Zeige stdout und stderr Labels
|
||||
automatic-redirect: Automatische Weiterleitung
|
||||
|
||||
@@ -25,16 +25,30 @@ tooltip:
|
||||
search: Search containers (⌘ + k, ⌃k)
|
||||
pin-column: Pin as column
|
||||
error:
|
||||
page-not-found: This page does not exist.
|
||||
invalid-auth: Username and password are not valid.
|
||||
page-not-found: This page does not exist
|
||||
invalid-auth: Username or password are not valid
|
||||
logs-skipped: Skipped {total} entries
|
||||
container-not-found: Container not found.
|
||||
events-stream: >-
|
||||
Unexpected error received from Dozzle API. Please check Dozzle logs and make
|
||||
sure proper installation.
|
||||
events-timeout: >-
|
||||
Something is not right. Dozzle still hasn't received any data over 8
|
||||
seconds. Please check network settings.
|
||||
container-not-found: Container not found
|
||||
events-stream:
|
||||
title: Unexpected Error
|
||||
message: >-
|
||||
Dozzle UI wasn't able to connect API. Please check your network settings.
|
||||
If you are using a reverse proxy, please make sure it is configured
|
||||
properly.
|
||||
events-timeout:
|
||||
title: Something is not right
|
||||
message: >-
|
||||
Dozzle UI timeed out while connecting to API. Please check network
|
||||
connection and try again.
|
||||
alert:
|
||||
redirected:
|
||||
title: Redirected to new container
|
||||
message: Dozzle automatically redirected you to new container {containerId}.
|
||||
similar-container-found:
|
||||
title: Similar container found
|
||||
message: >-
|
||||
Dozzle found a similar container {containerId} that is running on the same
|
||||
host. Do you want to switch to it?
|
||||
title:
|
||||
page-not-found: Page not found
|
||||
login: Authentication Required
|
||||
@@ -65,3 +79,4 @@ settings:
|
||||
New version is available! Update to <a href="{href}" target="_blank"
|
||||
rel="noreferrer noopener">{nextVersion}</a>.
|
||||
show-std: Show stdout and stderr labels
|
||||
automatic-redirect: Automatische Weiterleitung zu neuen Containern mit demselben Namen
|
||||
|
||||
@@ -28,6 +28,23 @@ error:
|
||||
invalid-auth: El nombre de usuario y la contraseña no son válidos.
|
||||
logs-skipped: Omitidas {total} entrada/s
|
||||
container-not-found: Contenedor no encontrado.
|
||||
events-stream:
|
||||
title: Error inesperado
|
||||
message: >-
|
||||
Dozzle UI no pudo conectar con la API. Por favor, compruebe la configuración de su red.
|
||||
Si está utilizando un proxy inverso, por favor, asegúrese de que está configurado correctamente.
|
||||
events-timeout:
|
||||
title: Algo no está bien
|
||||
message: >-
|
||||
Dozzle UI se agotó el tiempo de espera al conectarse a la API. Por favor, compruebe la conexión de red y vuelva a intentarlo.
|
||||
alert:
|
||||
redirected:
|
||||
title: Redirigido a nuevo contenedor
|
||||
message: Dozzle le redirigió automáticamente al nuevo contenedor {containerId}.
|
||||
similar-container-found:
|
||||
title: Contenedor similar encontrado
|
||||
message: >-
|
||||
Dozzle encontró un contenedor similar {containerId} que se está ejecutando en el mismo host. ¿Quieres cambiar a él?
|
||||
title:
|
||||
page-not-found: Página no encontrada
|
||||
login: Autenticación requerida
|
||||
@@ -58,3 +75,4 @@ settings:
|
||||
¡La nueva versión está disponible! Actualizar a la
|
||||
<a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
|
||||
show-std: Mostrar etiquetas de salida estándar y salida de error estándar
|
||||
automatic-redirect: Redireccionar automáticamente a nuevos contenedores con el mismo nombre
|
||||
|
||||
@@ -28,6 +28,23 @@ error:
|
||||
invalid-auth: O nome de usuário e a senha não são válidos.
|
||||
logs-skipped: Saltado {total} entradas
|
||||
container-not-found: Contentor não encontrado.
|
||||
events-stream:
|
||||
title: Erro inesperado
|
||||
message: >-
|
||||
Dozzle UI não conseguiu ligar à API. Por favor, verifique as suas definições de rede.
|
||||
Se estiver a utilizar um proxy reverso, certifique-se de que está configurado correctamente.
|
||||
events-timeout:
|
||||
title: Algo não está bem
|
||||
message: >-
|
||||
Dozzle UI esgotou o tempo limite ao ligar à API. Por favor, verifique a ligação de rede e tente novamente.
|
||||
alert:
|
||||
redirected:
|
||||
title: Redirecionado para novo contentor
|
||||
message: Dozzle redirecionou-o automaticamente para o novo contentor {containerId}.
|
||||
similar-container-found:
|
||||
title: Contentor semelhante encontrado
|
||||
message: >-
|
||||
Dozzle encontrou um contentor semelhante {containerId} que está a ser executado no mesmo anfitrião. Quer mudar para ele?
|
||||
title:
|
||||
page-not-found: Página não encontrada
|
||||
login: Autenticação Requerida
|
||||
@@ -58,3 +75,4 @@ settings:
|
||||
Está disponível uma nova versão! Actualização para
|
||||
<a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
|
||||
show-std: Mostrar etiquetas de saída padrão e saída de erro padrão
|
||||
automatic-redirect: Redirecionar automaticamente para novos contentores com o mesmo nome
|
||||
|
||||
@@ -28,6 +28,23 @@ error:
|
||||
invalid-auth: Имя пользователя или пароль неверны.
|
||||
logs-skipped: Пропущено {total} записей
|
||||
container-not-found: Контейнер не найден.
|
||||
events-stream:
|
||||
title: Неожиданная ошибка
|
||||
message: >-
|
||||
Dozzle UI не смог подключиться к API. Пожалуйста, проверьте настройки сети.
|
||||
Если вы используете обратный прокси, убедитесь, что он настроен правильно.
|
||||
events-timeout:
|
||||
title: Что-то не так
|
||||
message: >-
|
||||
Dozzle UI превысил время ожидания при подключении к API. Пожалуйста, проверьте сетевое подключение и повторите попытку.
|
||||
alert:
|
||||
redirected:
|
||||
title: Перенаправлен на новый контейнер
|
||||
message: Dozzle автоматически перенаправил вас на новый контейнер {containerId}.
|
||||
similar-container-found:
|
||||
title: Найден похожий контейнер
|
||||
message: >-
|
||||
Dozzle нашел похожий контейнер {containerId}, который работает на том же хосте. Хотите переключиться на него?
|
||||
title:
|
||||
page-not-found: Страница не найдена
|
||||
login: Требуется авторизация
|
||||
@@ -56,3 +73,4 @@ settings:
|
||||
update-available: >-
|
||||
Доступна новая версия! Обновить до <a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
|
||||
show-std: Показывать метки stdout и stderr
|
||||
automatic-redirect: Автоматическое перенаправление на новые контейнеры с тем же именем.
|
||||
|
||||
@@ -28,6 +28,23 @@ error:
|
||||
invalid-auth: 用户名和密码无效。
|
||||
logs-skipped: 跳过 {total} 条记录
|
||||
container-not-found: 容器未找到。
|
||||
events-stream:
|
||||
title: 意外错误
|
||||
message: >-
|
||||
Dozzle UI无法连接到API。请检查您的网络设置。
|
||||
如果您使用反向代理,请确保其正确配置。
|
||||
events-timeout:
|
||||
title: 出了点问题
|
||||
message: >-
|
||||
Dozzle UI在连接到API时超时。请检查网络连接并重试。
|
||||
alert:
|
||||
redirected:
|
||||
title: 重定向到新容器
|
||||
message: Dozzle自动将您重定向到新容器 {containerId}。
|
||||
similar-container-found:
|
||||
title: 找到相似的容器
|
||||
message: >-
|
||||
Dozzle发现了一个相似的容器 {containerId},它在同一主机上运行。您想切换到它吗?d
|
||||
title:
|
||||
page-not-found: 页面未找到
|
||||
login: 需要身份验证
|
||||
@@ -56,3 +73,4 @@ settings:
|
||||
update-available: >-
|
||||
新版本可用!更新到 <a href="{href}" rel="noreferrer noopener">{nextVersion}</a>。
|
||||
show-std: 显示stdout和stderr标签
|
||||
automatic-redirect: 自动重定向到同名的新容器
|
||||
|
||||
Reference in New Issue
Block a user