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:
3
assets/auto-imports.d.ts
vendored
3
assets/auto-imports.d.ts
vendored
@@ -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']>
|
||||
|
||||
29
assets/composables/toast.ts
Normal file
29
assets/composables/toast.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
36
pnpm-lock.yaml
generated
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user