Skip to content

Commit 368646a

Browse files
authored
feat: improves toasts for mobile (#3547)
1 parent 691a896 commit 368646a

File tree

11 files changed

+159
-130
lines changed

11 files changed

+159
-130
lines changed

assets/components/LogViewer/ContainerEventLogItem.vue

Lines changed: 0 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -2,80 +2,14 @@
22
<LogItem :logEntry>
33
<div class="whitespace-pre-wrap" :data-event="logEntry.event" v-html="logEntry.message"></div>
44
</LogItem>
5-
6-
<div
7-
class="alert alert-info my-4 w-auto flex-none font-sans text-[1rem] md:mx-auto md:w-1/2"
8-
v-if="followEligible && showCard"
9-
>
10-
<carbon:information class="size-6 shrink-0 stroke-current" />
11-
<div>
12-
<h3 class="text-lg font-bold">{{ $t("alert.similar-container-found.title") }}</h3>
13-
{{ $t("alert.similar-container-found.message", { containerId: nextContainer.id }) }}
14-
</div>
15-
<div>
16-
<TimedButton
17-
v-if="automaticRedirect"
18-
class="btn-primary btn-sm"
19-
@finished="redirectNow()"
20-
@click="showCard = false"
21-
>
22-
{{ $t("button.cancel") }}
23-
</TimedButton>
24-
<router-link
25-
:to="{ name: '/container/[id]', params: { id: nextContainer.id } }"
26-
class="btn btn-primary btn-sm"
27-
v-else
28-
>
29-
{{ $t("button.redirect") }}
30-
</router-link>
31-
</div>
32-
</div>
335
</template>
346
<script lang="ts" setup>
357
import { ContainerEventLogEntry } from "@/models/LogEntry";
36-
const router = useRouter();
37-
const { showToast } = useToast();
38-
const { t } = useI18n();
398
409
const { logEntry } = defineProps<{
4110
logEntry: ContainerEventLogEntry;
4211
showContainerName?: boolean;
4312
}>();
44-
45-
const showCard = ref(true);
46-
const { containers } = useLoggingContext();
47-
const store = useContainerStore();
48-
const { containers: allContainers } = storeToRefs(store);
49-
50-
const nextContainer = computed(
51-
() =>
52-
[
53-
...allContainers.value.filter(
54-
(c) => c.created > containers.value[0].created && c.name === containers.value[0].name,
55-
),
56-
].sort((a, b) => +a.created - +b.created)[0],
57-
);
58-
59-
const followEligible = computed(
60-
() =>
61-
router.currentRoute.value.name === "/container/[id]" && // we are on a container page
62-
logEntry.event === "container-stopped" && // container was stopped
63-
containers.value.length === 1 && // only one container
64-
Date.now() - +logEntry.date < 5 * 60 * 1000 && // was stopped in the last 5 minutes
65-
nextContainer.value !== undefined, // there is a next container
66-
);
67-
68-
function redirectNow() {
69-
showToast(
70-
{
71-
title: t("alert.redirected.title"),
72-
message: t("alert.redirected.message", { containerId: nextContainer.value?.id }),
73-
type: "info",
74-
},
75-
{ expire: 5000 },
76-
);
77-
router.push({ name: "/container/[id]", params: { id: nextContainer.value?.id } });
78-
}
7913
</script>
8014

8115
<style scoped>

assets/components/common/ToastModal.vue

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<div class="toast toast-end whitespace-normal max-md:m-0 max-md:w-full">
33
<div
44
class="alert max-w-xl max-md:rounded-none"
5-
v-for="toast in toasts"
5+
v-for="{ toast, options: { timed } } in toasts"
66
:key="toast.id"
77
:class="{
88
'alert-error': toast.type === 'error',
@@ -18,7 +18,21 @@
1818
<div v-html="toast.message" class="[&>a]:underline"></div>
1919
</div>
2020
<div>
21-
<button class="btn btn-circle btn-xs" @click="removeToast(toast.id)"><mdi:close /></button>
21+
<TimedButton
22+
v-if="timed"
23+
class="btn-primary btn-sm"
24+
:duration="timed"
25+
@finished="
26+
removeToast(toast.id);
27+
toast.action?.handler();
28+
"
29+
@cancelled="removeToast(toast.id)"
30+
>
31+
{{ toast.action?.label }}
32+
</TimedButton>
33+
<button class="btn btn-circle btn-xs" @click="removeToast(toast.id)" v-else>
34+
<mdi:close />
35+
</button>
2236
</div>
2337
</div>
2438
</div>

assets/composable/toast.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,55 @@ type Toast = {
44
title?: string;
55
message: string;
66
type: "success" | "error" | "warning" | "info";
7+
action?: {
8+
label: string;
9+
handler: () => void;
10+
};
711
};
812

913
type ToastOptions = {
1014
expire?: number;
1115
once?: boolean;
16+
timed?: number;
1217
};
1318

14-
const toasts = ref<Toast[]>([]);
19+
const toasts = ref<
20+
{
21+
toast: Toast;
22+
options: ToastOptions;
23+
}[]
24+
>([]);
1525

1626
const showToast = (
1727
toast: Omit<Toast, "id" | "createdAt"> & { id?: string },
18-
{ expire = -1, once = false }: ToastOptions = { expire: -1, once: false },
28+
{ expire = -1, once = false, timed }: ToastOptions = { expire: -1, once: false },
1929
) => {
20-
if (once && toasts.value.some((t) => t.id === toast.id)) {
30+
if (once && !toast.id) {
31+
throw new Error("Toast id is required when once is true");
32+
}
33+
if (once && toasts.value.some((t) => t.toast.id === toast.id)) {
2134
return;
2235
}
23-
toasts.value.push({
36+
37+
const toastWithId = {
2438
id: Date.now().toString(),
25-
createdAt: new Date(),
2639
...toast,
40+
createdAt: new Date(),
41+
};
42+
toasts.value.push({
43+
toast: toastWithId,
44+
options: { expire, once, timed },
2745
});
46+
2847
if (expire > 0) {
2948
setTimeout(() => {
30-
removeToast(toasts.value[0].id);
49+
removeToast(toastWithId.id);
3150
}, expire);
3251
}
3352
};
3453

3554
const removeToast = (id: Toast["id"]) => {
36-
toasts.value = toasts.value.filter((toast) => toast.id !== id);
55+
toasts.value = toasts.value.filter((instance) => instance.toast.id !== id);
3756
};
3857

3958
export const useToast = () => {

assets/pages/container/[id].vue

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,18 @@
1111
</template>
1212

1313
<script lang="ts" setup>
14+
import { type Container } from "@/models/Container";
1415
const route = useRoute("/container/[id]");
1516
const id = toRef(() => route.params.id);
16-
1717
const containerStore = useContainerStore();
1818
const currentContainer = containerStore.currentContainer(id);
1919
const { ready } = storeToRefs(containerStore);
20-
2120
const pinnedLogsStore = usePinnedLogsStore();
2221
const { pinnedLogs } = storeToRefs(pinnedLogsStore);
22+
const { containers: allContainers } = storeToRefs(containerStore) as unknown as { containers: Ref<Container[]> };
23+
const { showToast } = useToast();
24+
const { t } = useI18n();
25+
const router = useRouter();
2326
2427
watchEffect(() => {
2528
if (ready.value) {
@@ -30,6 +33,48 @@ watchEffect(() => {
3033
}
3134
}
3235
});
36+
37+
const redirectTrigger = ref(false);
38+
watch(currentContainer, () => (redirectTrigger.value = false));
39+
40+
watchEffect(() => {
41+
if (redirectTrigger.value) return;
42+
if (!currentContainer.value) return;
43+
if (currentContainer.value.state === "running") return;
44+
if (Date.now() - +currentContainer.value.finishedAt > 5 * 60 * 1000) return;
45+
46+
const nextContainer = allContainers.value
47+
.filter((c) => c.startedAt > currentContainer.value.startedAt && c.name === currentContainer.value.name)
48+
.sort((a, b) => +a.created - +b.created)[0];
49+
50+
if (!nextContainer) return;
51+
52+
if (automaticRedirect.value) {
53+
redirectTrigger.value = true;
54+
showToast(
55+
{
56+
title: t("alert.similar-container-found.title"),
57+
message: t("alert.similar-container-found.message", { containerId: nextContainer.id }),
58+
type: "info",
59+
action: {
60+
label: t("button.cancel"),
61+
handler: () => {
62+
showToast(
63+
{
64+
title: t("alert.redirected.title"),
65+
message: t("alert.redirected.message", { containerId: nextContainer.id }),
66+
type: "info",
67+
},
68+
{ expire: 5000 },
69+
);
70+
router.push({ name: "/container/[id]", params: { id: nextContainer.id } });
71+
},
72+
},
73+
},
74+
{ timed: 4000 },
75+
);
76+
}
77+
});
3378
</script>
3479
<route lang="yaml">
3580
meta:

internal/agent/client.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ func (c *Client) StreamEvents(ctx context.Context, events chan<- docker.Containe
236236
ActorID: resp.Event.ActorId,
237237
Name: resp.Event.Name,
238238
Host: resp.Event.Host,
239+
Time: resp.Event.Timestamp.AsTime(),
239240
}
240241
}
241242
}

internal/agent/pb/rpc.pb.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/agent/pb/rpc_grpc.pb.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)