1
0
mirror of https://github.com/amir20/dozzle.git synced 2025-12-25 23:03:47 +01:00

feat: implements a toast for alerting errors and other useful information (#2395)

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

* removes unused code
This commit is contained in:
Amir Raminfar
2023-09-27 13:29:33 -07:00
committed by GitHub
parent 9751ce3cf5
commit c3b5991dc7
9 changed files with 114 additions and 44 deletions

View File

@@ -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']
@@ -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']>
@@ -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']>

View File

@@ -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,
};
};

View File

@@ -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">
@@ -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);
@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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([]);
@@ -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[]),
@@ -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]);

View File

@@ -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
@@ -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

View File

@@ -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",

36
pnpm-lock.yaml generated
View File

@@ -66,8 +66,8 @@ dependencies:
specifier: ^3.0.1
version: 3.0.1(d3-selection@3.0.0)
daisyui:
specifier: ^3.7.7
version: 3.7.7(ts-node@10.9.1)
specifier: ^3.8.0
version: 3.8.0(ts-node@10.9.1)
date-fns:
specifier: ^2.30.0
version: 2.30.0
@@ -210,7 +210,7 @@ devDependencies:
version: 2.6.0(@vueuse/core@10.4.1)(vite@4.4.9)(vue@3.3.4)
vitepress:
specifier: 1.0.0-rc.17
version: 1.0.0-rc.17(@algolia/client-search@4.20.0)(@types/node@20.6.3)(fuse.js@6.6.2)(search-insights@2.8.2)
version: 1.0.0-rc.17(@algolia/client-search@4.20.0)(@types/node@20.6.3)(fuse.js@6.6.2)(search-insights@2.8.3)
vitest:
specifier: ^0.34.5
version: 0.34.5(jsdom@22.1.0)
@@ -220,10 +220,10 @@ devDependencies:
packages:
/@algolia/autocomplete-core@1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.8.2):
/@algolia/autocomplete-core@1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.8.3):
resolution: {integrity: sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw==}
dependencies:
'@algolia/autocomplete-plugin-algolia-insights': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.8.2)
'@algolia/autocomplete-plugin-algolia-insights': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.8.3)
'@algolia/autocomplete-shared': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)
transitivePeerDependencies:
- '@algolia/client-search'
@@ -231,13 +231,13 @@ packages:
- search-insights
dev: true
/@algolia/autocomplete-plugin-algolia-insights@1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.8.2):
/@algolia/autocomplete-plugin-algolia-insights@1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.8.3):
resolution: {integrity: sha512-a/yTUkcO/Vyy+JffmAnTWbr4/90cLzw+CC3bRbhnULr/EM0fGNvM13oQQ14f2moLMcVDyAx/leczLlAOovhSZg==}
peerDependencies:
search-insights: '>= 1 < 3'
dependencies:
'@algolia/autocomplete-shared': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)
search-insights: 2.8.2
search-insights: 2.8.3
transitivePeerDependencies:
- '@algolia/client-search'
- algoliasearch
@@ -444,10 +444,10 @@ packages:
resolution: {integrity: sha512-SPiDHaWKQZpwR2siD0KQUwlStvIAnEyK6tAE2h2Wuoq8ue9skzhlyVQ1ddzOxX6khULnAALDiR/isSF3bnuciA==}
dev: true
/@docsearch/js@3.5.2(@algolia/client-search@4.20.0)(search-insights@2.8.2):
/@docsearch/js@3.5.2(@algolia/client-search@4.20.0)(search-insights@2.8.3):
resolution: {integrity: sha512-p1YFTCDflk8ieHgFJYfmyHBki1D61+U9idwrLh+GQQMrBSP3DLGKpy0XUJtPjAOPltcVbqsTjiPFfH7JImjUNg==}
dependencies:
'@docsearch/react': 3.5.2(@algolia/client-search@4.20.0)(search-insights@2.8.2)
'@docsearch/react': 3.5.2(@algolia/client-search@4.20.0)(search-insights@2.8.3)
preact: 10.17.1
transitivePeerDependencies:
- '@algolia/client-search'
@@ -457,7 +457,7 @@ packages:
- search-insights
dev: true
/@docsearch/react@3.5.2(@algolia/client-search@4.20.0)(search-insights@2.8.2):
/@docsearch/react@3.5.2(@algolia/client-search@4.20.0)(search-insights@2.8.3):
resolution: {integrity: sha512-9Ahcrs5z2jq/DcAvYtvlqEBHImbm4YJI8M9y0x6Tqg598P40HTEkX7hsMcIuThI+hTFxRGZ9hll0Wygm2yEjng==}
peerDependencies:
'@types/react': '>= 16.8.0 < 19.0.0'
@@ -474,11 +474,11 @@ packages:
search-insights:
optional: true
dependencies:
'@algolia/autocomplete-core': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.8.2)
'@algolia/autocomplete-core': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.8.3)
'@algolia/autocomplete-preset-algolia': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)
'@docsearch/css': 3.5.2
algoliasearch: 4.20.0
search-insights: 2.8.2
search-insights: 2.8.3
transitivePeerDependencies:
- '@algolia/client-search'
dev: true
@@ -2274,8 +2274,8 @@ packages:
d3-timer: 3.0.1
dev: false
/daisyui@3.7.7(ts-node@10.9.1):
resolution: {integrity: sha512-2/nFdW/6R9MMnR8tTm07jPVyPaZwpUSkVsFAADb7Oq8N2Ynbls57laDdNqxTCUmn0QvcZi01TKl8zQbAwRfw1w==}
/daisyui@3.8.0(ts-node@10.9.1):
resolution: {integrity: sha512-VsWD//XlHkOBFSiRNTOZTxTJ/W8xI65erowErfbDWrPTkMYqf0ee/FTaqn4rquZoCcumENdFegAL8eELMnztxQ==}
engines: {node: '>=16.9.0'}
dependencies:
colord: 2.9.3
@@ -4095,8 +4095,8 @@ packages:
resolution: {integrity: sha512-4AsO/FrViE/iDNEPaAQlb77tf0csuq27EsVpy6ett584EcRTp6pTDLoGWVxCD77y5iU5FauOvhsI4o1APwPoSQ==}
dev: true
/search-insights@2.8.2:
resolution: {integrity: sha512-PxA9M5Q2bpBelVvJ3oDZR8nuY00Z6qwOxL53wNpgzV28M/D6u9WUbImDckjLSILBF8F1hn/mgyuUaOPtjow4Qw==}
/search-insights@2.8.3:
resolution: {integrity: sha512-W9rZfQ9XEfF0O6ntgQOTI7Txc8nkZrO4eJ/pTHK0Br6wWND2sPGPoWg+yGhdIW7wMbLqk8dc23IyEtLlNGpeNw==}
dev: true
/semver@5.7.2:
@@ -4845,7 +4845,7 @@ packages:
optionalDependencies:
fsevents: 2.3.3
/vitepress@1.0.0-rc.17(@algolia/client-search@4.20.0)(@types/node@20.6.3)(fuse.js@6.6.2)(search-insights@2.8.2):
/vitepress@1.0.0-rc.17(@algolia/client-search@4.20.0)(@types/node@20.6.3)(fuse.js@6.6.2)(search-insights@2.8.3):
resolution: {integrity: sha512-OPyP/VExNUk1aiErOgLE7Vv9Tgr9i9DZHDlDzqBcLfvDpXq33CDfn42JSzpsKGA020HOFOnV+t3/S1i/T5n7bg==}
hasBin: true
peerDependencies:
@@ -4855,7 +4855,7 @@ packages:
optional: true
dependencies:
'@docsearch/css': 3.5.2
'@docsearch/js': 3.5.2(@algolia/client-search@4.20.0)(search-insights@2.8.2)
'@docsearch/js': 3.5.2(@algolia/client-search@4.20.0)(search-insights@2.8.3)
'@vue/devtools-api': 6.5.0
'@vueuse/core': 10.4.1(vue@3.3.4)
'@vueuse/integrations': 10.4.1(focus-trap@7.5.2)(fuse.js@6.6.2)(vue@3.3.4)