Skip to content

Commit 5c13d0b

Browse files
WIP - add promo modal pages
1 parent e81b855 commit 5c13d0b

File tree

7 files changed

+541
-18
lines changed

7 files changed

+541
-18
lines changed

src/Frontend/src/components/audit/AuditList.vue

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,26 @@ import { useRoute, useRouter } from "vue-router";
55
import ResultsCount from "@/components/ResultsCount.vue";
66
import FiltersPanel from "@/components/audit/FiltersPanel.vue";
77
import AuditListItem from "@/components/audit/AuditListItem.vue";
8-
import { computed, onBeforeMount, ref, watch } from "vue";
8+
import { onBeforeMount, ref, watch } from "vue";
99
import RefreshConfig from "../RefreshConfig.vue";
1010
import LoadingSpinner from "@/components/LoadingSpinner.vue";
1111
import useFetchWithAutoRefresh from "@/composables/autoRefresh";
12-
import { MessageStatus } from "@/resources/Message";
1312
import FAIcon from "@/components/FAIcon.vue";
1413
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
14+
import WizardDialog from "@/components/platformcapabilities/WizardDialog.vue";
15+
import { getAuditingWizardPages } from "@/components/platformcapabilities/wizards/AuditingWizardPages";
16+
import { CapabilityStatus } from "@/components/platformcapabilities/types";
1517
1618
const store = useAuditStore();
19+
const { hasSuccessfulMessages } = storeToRefs(store);
1720
const { messages, totalCount, sortBy, messageFilterString, selectedEndpointName, itemsPerPage, dateRange } = storeToRefs(store);
1821
const route = useRoute();
1922
const router = useRouter();
2023
const autoRefreshValue = ref<number | null>(null);
2124
const { refreshNow, isRefreshing, updateInterval, start, stop } = useFetchWithAutoRefresh("audit-list", store.refresh, 3000);
2225
const firstLoad = ref(true);
23-
24-
// Check if there are no successful audit messages
25-
const hasNoSuccessfulMessages = computed(() => {
26-
if (firstLoad.value || messages.value.length === 0) {
27-
return false;
28-
}
29-
return !messages.value.some((msg) => msg.status === MessageStatus.Successful || msg.status === MessageStatus.ResolvedSuccessfully);
30-
});
26+
const showWizard = ref(false);
27+
const wizardPages = getAuditingWizardPages(CapabilityStatus.EndpointsNotConfigured);
3128
3229
onBeforeMount(() => {
3330
setQuery();
@@ -109,17 +106,18 @@ watch(autoRefreshValue, (newValue) => {
109106
<div class="row">
110107
<ResultsCount :displayed="messages.length" :total="totalCount" />
111108
</div>
112-
<div v-if="hasNoSuccessfulMessages" class="no-audit-banner">
109+
<div v-if="!hasSuccessfulMessages" class="no-audit-banner">
113110
<div class="banner-content">
114111
<FAIcon :icon="faInfoCircle" class="banner-icon" />
115112
<div class="banner-text">
116113
<strong>No successful audit messages found.</strong>
117-
<p>Auditing may not be enabled on your endpoints. Learn how to enable auditing to track all messages flowing through your system.</p>
114+
<p>Auditing may not be enabled on your endpoints. Click 'Get Started' to find out how to enable auditing.</p>
118115
</div>
119-
<a href="https://docs.particular.net/nservicebus/operations/auditing" target="_blank" class="banner-link">Learn More</a>
116+
<button class="banner-link" @click="showWizard = true">Get Started</button>
120117
</div>
121118
</div>
122119
</div>
120+
<WizardDialog v-if="showWizard" title="Getting Started with Auditing" :pages="wizardPages" @close="showWizard = false" />
123121
<div class="row results-table">
124122
<LoadingSpinner v-if="firstLoad" />
125123
<template v-for="message in messages" :key="message.id">
@@ -191,13 +189,15 @@ watch(autoRefreshValue, (newValue) => {
191189
padding: 8px 16px;
192190
background-color: #007bff;
193191
color: white;
192+
border: none;
194193
border-radius: 4px;
195194
text-decoration: none;
196195
font-size: 14px;
197196
font-weight: 500;
198197
white-space: nowrap;
199198
align-self: center;
200199
transition: background-color 0.2s ease;
200+
cursor: pointer;
201201
}
202202
203203
.banner-link:hover {

src/Frontend/src/components/platformcapabilities/CapabilityCard.vue

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
<script setup lang="ts">
2+
import { ref, computed } from "vue";
23
import FAIcon from "@/components/FAIcon.vue";
34
import { IconDefinition, faCircle } from "@fortawesome/free-solid-svg-icons";
45
import { CapabilityStatus, StatusIndicator, Capability } from "@/components/platformcapabilities/types";
6+
import WizardDialog, { type WizardPage } from "./WizardDialog.vue";
57
68
const props = defineProps<{
79
status: CapabilityStatus;
@@ -13,7 +15,22 @@ const props = defineProps<{
1315
description: string;
1416
indicators?: StatusIndicator[];
1517
isLoading?: boolean;
18+
wizardPages?: WizardPage[];
1619
}>();
20+
21+
const showWizard = ref(false);
22+
23+
const shouldShowWizard = computed(() => {
24+
return props.wizardPages && props.wizardPages.length > 0 && (props.status === CapabilityStatus.EndpointsNotConfigured || props.status === CapabilityStatus.InstanceNotConfigured || props.status === CapabilityStatus.Unavailable);
25+
});
26+
27+
function handleButtonClick() {
28+
if (shouldShowWizard.value) {
29+
showWizard.value = true;
30+
} else if (props.status === CapabilityStatus.Available) {
31+
window.location.href = props.helpButtonUrl;
32+
}
33+
}
1734
</script>
1835

1936
<template>
@@ -80,10 +97,12 @@ const props = defineProps<{
8097
<div class="capability-description">
8198
{{ props.description }}
8299
</div>
83-
<a :href="props.helpButtonUrl" :target="props.status === CapabilityStatus.Available ? '_self' : '_blank'" class="btn-primary learn-more-btn">
100+
<button class="btn-primary learn-more-btn" @click="handleButtonClick">
84101
{{ props.helpButtonText }}
85-
</a>
102+
</button>
86103
</div>
104+
105+
<WizardDialog v-if="showWizard && wizardPages" :title="`Getting Started with ${props.title}`" :pages="wizardPages" @close="showWizard = false" />
87106
</div>
88107
</template>
89108

src/Frontend/src/components/platformcapabilities/PlatformCapabilitiesDashboardItem.vue

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
<script setup lang="ts">
2+
import { computed } from "vue";
23
import CapabilityCard from "@/components/platformcapabilities/CapabilityCard.vue";
34
import { useAuditingCapability } from "@/components/platformcapabilities/capabilities/AuditingCapability";
45
import { useMonitoringCapability } from "@/components/platformcapabilities/capabilities/MonitoringCapability";
56
import { Capability } from "@/components/platformcapabilities/types";
7+
import { getAuditingWizardPages } from "@/components/platformcapabilities/wizards/AuditingWizardPages";
8+
import { getMonitoringWizardPages } from "@/components/platformcapabilities/wizards/MonitoringWizardPages";
69
710
const auditing = useAuditingCapability();
811
const monitoring = useMonitoringCapability();
12+
13+
const auditingWizardPages = computed(() => getAuditingWizardPages(auditing.status.value));
14+
const monitoringWizardPages = computed(() => getMonitoringWizardPages(monitoring.status.value));
915
</script>
1016

1117
<template>
@@ -25,6 +31,7 @@ const monitoring = useMonitoringCapability();
2531
:isLoading="auditing.isLoading.value"
2632
:help-button-text="auditing.helpButtonText.value"
2733
:help-button-url="auditing.helpButtonUrl.value"
34+
:wizard-pages="auditingWizardPages"
2835
></CapabilityCard>
2936
<CapabilityCard
3037
:title="Capability.Monitoring"
@@ -36,6 +43,7 @@ const monitoring = useMonitoringCapability();
3643
:isLoading="monitoring.isLoading.value"
3744
:help-button-text="monitoring.helpButtonText.value"
3845
:help-button-url="monitoring.helpButtonUrl.value"
46+
:wizard-pages="monitoringWizardPages"
3947
></CapabilityCard>
4048
</div>
4149
</div>
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
<script setup lang="ts">
2+
import { ref, computed, onMounted, onUnmounted } from "vue";
3+
import { Modal } from "bootstrap";
4+
import FAIcon from "@/components/FAIcon.vue";
5+
import { faChevronLeft, faChevronRight, faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons";
6+
7+
export interface WizardPage {
8+
title: string;
9+
content: string;
10+
image?: string;
11+
learnMoreUrl?: string;
12+
learnMoreText?: string;
13+
}
14+
15+
const props = defineProps<{
16+
title: string;
17+
pages: WizardPage[];
18+
}>();
19+
20+
const emit = defineEmits<{ close: [] }>();
21+
22+
const modalRef = ref<HTMLElement | null>(null);
23+
let modalInstance: Modal | null = null;
24+
25+
const currentPageIndex = ref(0);
26+
27+
const currentPage = computed(() => props.pages[currentPageIndex.value]);
28+
const isFirstPage = computed(() => currentPageIndex.value === 0);
29+
const isLastPage = computed(() => currentPageIndex.value === props.pages.length - 1);
30+
const totalPages = computed(() => props.pages.length);
31+
32+
function nextPage() {
33+
if (!isLastPage.value) {
34+
currentPageIndex.value++;
35+
}
36+
}
37+
38+
function previousPage() {
39+
if (!isFirstPage.value) {
40+
currentPageIndex.value--;
41+
}
42+
}
43+
44+
function goToPage(index: number) {
45+
if (index >= 0 && index < props.pages.length) {
46+
currentPageIndex.value = index;
47+
}
48+
}
49+
50+
function close() {
51+
modalInstance?.hide();
52+
}
53+
54+
function handleHidden() {
55+
emit("close");
56+
}
57+
58+
function handleKeydown(event: KeyboardEvent) {
59+
if (event.key === "ArrowRight" && !isLastPage.value) {
60+
nextPage();
61+
} else if (event.key === "ArrowLeft" && !isFirstPage.value) {
62+
previousPage();
63+
}
64+
}
65+
66+
onMounted(() => {
67+
if (modalRef.value) {
68+
modalInstance = new Modal(modalRef.value, {
69+
backdrop: true,
70+
keyboard: true,
71+
});
72+
modalRef.value.addEventListener("hidden.bs.modal", handleHidden);
73+
modalInstance.show();
74+
}
75+
window.addEventListener("keydown", handleKeydown);
76+
});
77+
78+
onUnmounted(() => {
79+
window.removeEventListener("keydown", handleKeydown);
80+
if (modalRef.value) {
81+
modalRef.value.removeEventListener("hidden.bs.modal", handleHidden);
82+
}
83+
modalInstance?.dispose();
84+
});
85+
</script>
86+
87+
<template>
88+
<div ref="modalRef" class="modal fade" tabindex="-1" role="dialog" :aria-label="title">
89+
<div class="modal-dialog modal-lg modal-dialog-centered">
90+
<div class="modal-content wizard-content">
91+
<div class="modal-header">
92+
<h5 class="modal-title">{{ title }}</h5>
93+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
94+
</div>
95+
96+
<div class="modal-body">
97+
<div v-if="currentPage.image" class="wizard-image text-center mb-4">
98+
<img :src="currentPage.image" :alt="currentPage.title" class="img-fluid rounded" />
99+
</div>
100+
101+
<div class="wizard-page-content">
102+
<h4 class="page-title mb-3">{{ currentPage.title }}</h4>
103+
<div class="page-content" v-html="currentPage.content"></div>
104+
105+
<a v-if="currentPage.learnMoreUrl" :href="currentPage.learnMoreUrl" target="_blank" rel="noopener noreferrer" class="learn-more-link mt-3 d-inline-flex align-items-center gap-2">
106+
{{ currentPage.learnMoreText || "Learn more in the documentation" }}
107+
<FAIcon :icon="faExternalLinkAlt" class="small" />
108+
</a>
109+
</div>
110+
</div>
111+
112+
<div class="modal-footer d-flex justify-content-between align-items-center">
113+
<div class="page-indicators d-flex gap-2">
114+
<button v-for="(_, index) in pages" :key="index" type="button" class="page-dot" :class="{ active: index === currentPageIndex, visited: index < currentPageIndex }" @click="goToPage(index)" :aria-label="`Go to page ${index + 1}`"></button>
115+
</div>
116+
117+
<div class="page-counter text-muted small">Step {{ currentPageIndex + 1 }} of {{ totalPages }}</div>
118+
119+
<div class="navigation-buttons d-flex gap-2">
120+
<button v-if="!isFirstPage" type="button" class="btn btn-outline-secondary" @click="previousPage">
121+
<FAIcon :icon="faChevronLeft" class="me-1" />
122+
Back
123+
</button>
124+
125+
<button v-if="!isLastPage" type="button" class="btn btn-primary" @click="nextPage">
126+
Next
127+
<FAIcon :icon="faChevronRight" class="ms-1" />
128+
</button>
129+
130+
<button v-if="isLastPage" type="button" class="btn btn-primary" @click="close">Got it!</button>
131+
</div>
132+
</div>
133+
</div>
134+
</div>
135+
</div>
136+
</template>
137+
138+
<style scoped>
139+
.wizard-content {
140+
border-radius: 12px;
141+
overflow: hidden;
142+
}
143+
144+
.wizard-image img {
145+
max-height: 200px;
146+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
147+
}
148+
149+
.page-title {
150+
font-size: 1.25rem;
151+
font-weight: 600;
152+
color: #333;
153+
}
154+
155+
.page-content {
156+
font-size: 0.95rem;
157+
line-height: 1.7;
158+
color: #555;
159+
}
160+
161+
.page-content :deep(p) {
162+
margin-bottom: 0.75rem;
163+
}
164+
165+
.page-content :deep(ul) {
166+
margin: 0.75rem 0;
167+
padding-left: 1.25rem;
168+
}
169+
170+
.page-content :deep(li) {
171+
margin-bottom: 0.5rem;
172+
}
173+
174+
.page-content :deep(code) {
175+
background-color: #f5f5f5;
176+
padding: 2px 6px;
177+
border-radius: 4px;
178+
font-size: 0.9em;
179+
color: #d63384;
180+
}
181+
182+
.page-content :deep(strong) {
183+
color: #333;
184+
}
185+
186+
.learn-more-link {
187+
color: #007bff;
188+
text-decoration: none;
189+
font-weight: 500;
190+
}
191+
192+
.learn-more-link:hover {
193+
color: #0056b3;
194+
text-decoration: underline;
195+
}
196+
197+
.page-dot {
198+
width: 10px;
199+
height: 10px;
200+
border-radius: 50%;
201+
border: none;
202+
background-color: #ddd;
203+
cursor: pointer;
204+
transition: all 0.2s ease;
205+
padding: 0;
206+
}
207+
208+
.page-dot:hover {
209+
background-color: #bbb;
210+
}
211+
212+
.page-dot.visited {
213+
background-color: #007bff;
214+
opacity: 0.5;
215+
}
216+
217+
.page-dot.active {
218+
background-color: #007bff;
219+
opacity: 1;
220+
transform: scale(1.2);
221+
}
222+
223+
.modal-footer {
224+
background-color: #f8f9fa;
225+
}
226+
</style>

0 commit comments

Comments
 (0)