Skip to content

Commit

Permalink
feat: implements a toast for alerting errors and other useful informa…
Browse files Browse the repository at this point in the history
…tion (#2395)

* feat: implements a toast for alerting errors and other useful information

* removes unused code
  • Loading branch information
amir20 committed Sep 27, 2023
1 parent 9751ce3 commit c3b5991
Show file tree
Hide file tree
Showing 9 changed files with 114 additions and 44 deletions.
3 changes: 3 additions & 0 deletions assets/auto-imports.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ declare global {
const useTitle: typeof import('@vueuse/core')['useTitle']
const useToNumber: typeof import('@vueuse/core')['useToNumber']
const useToString: typeof import('@vueuse/core')['useToString']
const useToast: typeof import('./composables/toast')['useToast']
const useToggle: typeof import('@vueuse/core')['useToggle']
const useTransition: typeof import('@vueuse/core')['useTransition']
const useTrunc: typeof import('@vueuse/math')['useTrunc']
Expand Down Expand Up @@ -662,6 +663,7 @@ declare module 'vue' {
readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
readonly useToast: UnwrapRef<typeof import('./composables/toast')['useToast']>
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
Expand Down Expand Up @@ -997,6 +999,7 @@ declare module '@vue/runtime-core' {
readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
readonly useToast: UnwrapRef<typeof import('./composables/toast')['useToast']>
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
Expand Down
29 changes: 29 additions & 0 deletions assets/composables/toast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
type Toast = {
id: number;
createdAt: Date;
message: string;
type: "success" | "error" | "warning" | "info";
};

const toasts = ref<Toast[]>([]);

const showToast = (message: string, type: Toast["type"]) => {
toasts.value.push({
id: Date.now(),
createdAt: new Date(),
message,
type,
});
};

const removeToast = (id: Toast["id"]) => {
toasts.value = toasts.value.filter((toast) => toast.id !== id);
};

export const useToast = () => {
return {
toasts,
showToast,
removeToast,
};
};
33 changes: 23 additions & 10 deletions assets/layouts/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@
</splitpanes>
</pane>
</splitpanes>
<button
@click="collapse"
class="btn btn-circle fixed bottom-8 left-4"
:class="{ '-left-3': collapseNav }"
<label
class="btn btn-circle swap swap-rotate fixed bottom-8 left-4"
:class="{ '!-left-3': collapseNav }"
v-if="!isMobile"
>
<mdi:light-chevron-right v-if="collapseNav" />
<mdi:light-chevron-left v-else />
</button>
<input type="checkbox" v-model="collapseNav" />
<mdi:light-chevron-right class="swap-on text-secondary" />
<mdi:light-chevron-left class="swap-off" />
</label>
</div>
<dialog ref="modal" class="modal items-start bg-white/20 backdrop:backdrop-blur-sm" @close="open = false">
<div class="modal-box max-w-2xl bg-transparent pt-20 shadow-none">
Expand All @@ -42,16 +42,32 @@
<button>close</button>
</form>
</dialog>
<div class="toast toast-end whitespace-normal">
<div
class="alert max-w-xl"
v-for="toast in toasts"
:key="toast.id"
:class="{ 'alert-error': toast.type === 'error', 'alert-info': toast.type === 'info' }"
>
<span>{{ toast.message }}</span>
<div>
<button class="btn btn-circle btn-xs" @click="removeToast(toast.id)"><mdi:close /></button>
</div>
</div>
</div>
</template>

<script lang="ts" setup>
// @ts-ignore - splitpanes types are not available
import { Splitpanes, Pane } from "splitpanes";
import { collapseNav } from "@/composables/settings";
const { authorizationNeeded } = config;
const containerStore = useContainerStore();
const { activeContainers } = storeToRefs(containerStore);
const { toasts, removeToast } = useToast();
const modal = ref<HTMLDialogElement>();
const open = ref(false);
Expand All @@ -74,9 +90,6 @@ function showFuzzySearch() {
open.value = true;
}
function collapse() {
collapseNav.value = !collapseNav.value;
}
function onResized(e: any) {
if (e.length == 2) {
menuWidth.value = e[0].size;
Expand Down
2 changes: 2 additions & 0 deletions assets/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
--bc: var(--base-content-color);
--in: 207 90% 54%;
--inc: 207 90% 94%;
--er: 4 90% 58%;
--erc: 4 90% 98%;
}
html[data-theme="dark"] {
@mixin dark;
Expand Down
28 changes: 14 additions & 14 deletions assets/modules/i18n.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { type App } from "vue";
import { createI18n } from "vue-i18n";

const messages = Object.fromEntries(
Object.entries(import.meta.glob<{ default: any }>("../../locales/*.y(a)?ml", { eager: true })).map(([key, value]) => {
const yaml = key.endsWith(".yaml");
return [key.slice(14, yaml ? -5 : -4), value.default];
}),
);
const i18n = createI18n({
legacy: false,
locale: navigator.language.slice(0, 2),
fallbackLocale: "en",
messages,
});
export const install = (app: App) => {
const messages = Object.fromEntries(
Object.entries(import.meta.glob<{ default: any }>("../../locales/*.y(a)?ml", { eager: true })).map(
([key, value]) => {
const yaml = key.endsWith(".yaml");
return [key.slice(14, yaml ? -5 : -4), value.default];
},
),
);
const i18n = createI18n({
legacy: false,
locale: navigator.language.slice(0, 2),
fallbackLocale: "en",
messages,
});
app.use(i18n);
};

export default i18n;
16 changes: 16 additions & 0 deletions assets/stores/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { acceptHMRUpdate, defineStore } from "pinia";
import { Ref, UnwrapNestedRefs } from "vue";
import type { ContainerHealth, ContainerJson, ContainerStat } from "@/types/Container";
import { Container } from "@/models/Container";
import i18n from "@/modules/i18n";

const { showToast } = useToast();
// @ts-ignore
const { t } = i18n.global;

export const useContainerStore = defineStore("container", () => {
const containers: Ref<Container[]> = ref([]);
Expand Down Expand Up @@ -30,6 +35,9 @@ export const useContainerStore = defineStore("container", () => {
es?.close();
ready.value = false;
es = new EventSource(`${config.base}/api/events/stream`);
es.addEventListener("error", (e) => {
showToast(t("error.events-stream"), "error");
});

es.addEventListener("containers-changed", (e: Event) =>
updateContainers(JSON.parse((e as MessageEvent).data) as ContainerJson[]),
Expand Down Expand Up @@ -63,6 +71,14 @@ export const useContainerStore = defineStore("container", () => {

connect();

(async function () {
try {
await until(ready).toBe(true, { timeout: 8000, throwOnTimeout: true });
} catch (e) {
showToast(t("error.events-timeout"), "error");
}
})();

const updateContainers = (containersPayload: ContainerJson[]) => {
const existingContainers = containersPayload.filter((c) => allContainersById.value[c.id]);
const newContainers = containersPayload.filter((c) => !allContainersById.value[c.id]);
Expand Down
9 changes: 8 additions & 1 deletion locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ error:
invalid-auth: Username and 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.
title:
page-not-found: Page not found
login: Authentication Required
Expand Down Expand Up @@ -56,5 +62,6 @@ settings:
search: Enable searching with Dozzle using
using-version: You are using Dozzle {version}.
update-available: >-
New version is available! Update to <a href="{href}" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
New version is available! Update to <a href="{href}" target="_blank"
rel="noreferrer noopener">{nextVersion}</a>.
show-std: Show stdout and stderr labels
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"d3-selection": "^3.0.0",
"d3-shape": "^3.2.0",
"d3-transition": "^3.0.1",
"daisyui": "^3.7.7",
"daisyui": "^3.8.0",
"date-fns": "^2.30.0",
"entities": "^4.5.0",
"fuse.js": "^6.6.2",
Expand Down
36 changes: 18 additions & 18 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit c3b5991

Please sign in to comment.