Skip to content

Commit

Permalink
feat: adds automatic redirect when a new container is found (#2396)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
amir20 committed Sep 27, 2023
1 parent c3b5991 commit 58edb4f
Show file tree
Hide file tree
Showing 15 changed files with 239 additions and 22 deletions.
3 changes: 3 additions & 0 deletions assets/auto-imports.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down Expand Up @@ -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']>
Expand Down Expand Up @@ -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']>
Expand Down
2 changes: 2 additions & 0 deletions assets/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down Expand Up @@ -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']
}
Expand Down
30 changes: 25 additions & 5 deletions assets/components/LogViewer/DockerEventLogItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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;
Expand All @@ -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>
Expand Down
39 changes: 39 additions & 0 deletions assets/components/common/TimedButton.vue
Original file line number Diff line number Diff line change
@@ -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>
3 changes: 3 additions & 0 deletions assets/composables/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const DEFAULT_SETTINGS: {
hourStyle: "auto" | "24" | "12";
softWrap: boolean;
collapseNav: boolean;
automaticRedirect: boolean;
} = {
search: true,
size: "medium",
Expand All @@ -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);
Expand All @@ -42,4 +44,5 @@ export const {
menuWidth,
size,
search,
automaticRedirect,
} = toRefs(settings);
26 changes: 21 additions & 5 deletions assets/composables/toast.ts
Original file line number Diff line number Diff line change
@@ -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"]) => {
Expand Down
7 changes: 6 additions & 1 deletion assets/layouts/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
5 changes: 5 additions & 0 deletions assets/pages/settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -95,6 +99,7 @@ import {
showAllContainers,
size,
softWrap,
automaticRedirect,
} from "@/composables/settings";
const { t } = useI18n();
Expand Down
20 changes: 18 additions & 2 deletions assets/stores/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down Expand Up @@ -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 },
);
}
})();

Expand Down
21 changes: 21 additions & 0 deletions locales/de.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
33 changes: 24 additions & 9 deletions locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
18 changes: 18 additions & 0 deletions locales/es.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
18 changes: 18 additions & 0 deletions locales/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

0 comments on commit 58edb4f

Please sign in to comment.