add legend explaining sds concepts

This commit is contained in:
Arseniy Klempner 2025-03-27 19:05:57 -07:00
parent 45909aee7d
commit 054760e752
No known key found for this signature in database
GPG Key ID: 51653F18863BD24B
11 changed files with 998 additions and 175 deletions

View File

@ -5,6 +5,8 @@
import { getIdenticon } from '$lib/identicon.svelte';
import { getMessageId } from '$lib/sds/message';
import type { MessageChannelEventObject } from '$lib/sds/stream';
import HistoryItem from './HistoryItem.svelte';
import LegendModal from './LegendModal.svelte';
// Map event types to colors using index signature
const eventColors: { [key in string]: string } = {
@ -31,6 +33,7 @@
let identicon: any = $state(null);
let currentFilter: string = $state('all');
let currentIdFilter: string | null = $state(null);
let showLegend: boolean = $state(false);
// Map of filter values to event types
const filterMap: { [key: string]: string | null } = {
@ -127,6 +130,11 @@
currentIdFilter = null;
}
// Toggle legend display
function toggleLegend() {
showLegend = !showLegend;
}
onMount(async () => {
identicon = await getIdenticon();
// Subscribe to the event stream and collect events
@ -137,6 +145,7 @@
}
history = [event, ...history];
});
(window as any).saveHistory = saveHistory;
});
onDestroy(() => {
@ -145,16 +154,32 @@
unsubscribe();
}
});
const saveHistory = () => {
const sampleHistory = history.map((event) => {
if((event.payload as any).bloomFilter) {
(event.payload as any).bloomFilter = new Uint8Array([0, 0, 0, 0]);
}
return {
type: event.type,
payload: event.payload
};
});
localStorage.setItem('history', JSON.stringify(sampleHistory));
};
</script>
<div class="history-container">
<select class="item-filter" onchange={handleFilterChange} value={currentFilter}>
<option value="all">All ({eventCounts.all})</option>
<option value="sent">Sent ({eventCounts.sent})</option>
<option value="received">Received ({eventCounts.received})</option>
<option value="delivered">Delivered ({eventCounts.delivered})</option>
<option value="acknowledged">Acknowledged ({eventCounts.acknowledged})</option>
</select>
<div class="header">
<button class="help-button" onclick={toggleLegend}>?</button>
<select class="item-filter" onchange={handleFilterChange} value={currentFilter}>
<option value="all">All ({eventCounts.all})</option>
<option value="sent">Sent ({eventCounts.sent})</option>
<option value="received">Received ({eventCounts.received})</option>
<option value="delivered">Delivered ({eventCounts.delivered})</option>
<option value="acknowledged">Acknowledged ({eventCounts.acknowledged})</option>
</select>
</div>
{#if currentIdFilter}
<div class="id-filter-badge">
@ -164,54 +189,16 @@
{/if}
{#each filteredHistory as event, index}
{@const color = eventColors[event.type] || '#888'}
{@const name = eventNames[event.type] || event.type}
{@const id = getMessageId(event)}
{@const matchesFilter = currentIdFilter && id === currentIdFilter}
<div class="history-item" onclick={() => handleEventClick(id)}>
<div class="item-container">
<div
class="event-box {matchesFilter ? 'highlight' : ''}"
style="background-color: {color};"
>
<div class="identicon">
<img src="data:image/svg+xml;base64,{identicons[index]}" alt="Identicon" />
</div>
<div class="event-info">
<div class="event-type">
{name}
</div>
<div class="event-id">
{id}
</div>
</div>
{#if event.type === MessageChannelEvent.MessageDelivered}
<div class="lamport-timestamp">
{event.payload.sentOrReceived}
</div>
{/if}
{#if event.type === MessageChannelEvent.MessageSent || event.type === MessageChannelEvent.MessageReceived}
<div class="lamport-timestamp">
{event.payload.lamportTimestamp}
</div>
{/if}
</div>
{#if event.type === MessageChannelEvent.MessageSent || event.type === MessageChannelEvent.MessageReceived}
{#each event.payload.causalHistory as dependency}
{@const dependencyMatchesFilter =
currentIdFilter && dependency.messageId === currentIdFilter}
<div
class="dependency-box {dependencyMatchesFilter ? 'highlight' : ''}"
style="background-color: {color};"
onclick={(event) => handleDependencyClick(dependency.messageId, event)}
>
{dependency.messageId}
</div>
{/each}
{/if}
</div>
</div>
<HistoryItem
{event}
identicon={identicons[index]}
currentIdFilter={currentIdFilter}
onEventClick={handleEventClick}
onDependencyClick={handleDependencyClick}
/>
{/each}
<LegendModal bind:isOpen={showLegend} />
</div>
<style>
@ -228,135 +215,39 @@
padding: 10px;
}
.history-item {
padding: 8px;
width: 100%;
cursor: pointer;
}
.item-container {
.header {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 6px;
width: 100%;
}
.event-box {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
border-radius: 8px;
width: 100%;
min-height: 70px;
color: white;
padding: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
position: relative;
transition: box-shadow 0.3s ease;
}
.dependency-box {
.help-button {
width: 28px;
height: 28px;
border-radius: 50%;
background-color: #f3f4f6;
border: 1px solid #d1d5db;
color: #4b5563;
font-weight: bold;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
margin-left: auto;
width: auto;
max-width: 80%;
min-height: 40px;
font-size: 11px;
font-family: monospace;
opacity: 0.85;
padding: 6px 12px;
border-radius: 8px;
color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.3s ease;
cursor: pointer;
margin-right: 8px;
transition: all 0.2s;
}
.highlight {
border-left: 4px solid white;
border-right: 4px solid white;
position: relative;
background-image: linear-gradient(rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.1));
.help-button:hover {
background-color: #e5e7eb;
color: #1f2937;
}
.highlight .event-type {
font-size: 16px;
color: white;
font-weight: bold;
font-style: italic;
letter-spacing: 0.5px;
}
.highlight .event-id,
.dependency-box.highlight {
/* color: white; */
font-weight: bold;
font-style: italic;
letter-spacing: 0.5px;
}
.dependency-box.highlight {
font-size: 12px;
/* background-color: rgba(255, 255, 255, 0.2) !important; */
}
.identicon {
width: 40px;
height: 40px;
.item-filter {
flex: 1;
padding: 8px;
border-radius: 4px;
overflow: hidden;
margin-right: 12px;
}
.identicon img {
width: 100%;
height: 100%;
object-fit: contain;
}
.identicon-placeholder {
width: 40px;
height: 40px;
border-radius: 4px;
background-color: rgba(255, 255, 255, 0.2);
margin-right: 12px;
}
.event-info {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.event-type {
font-size: 14px;
font-weight: bold;
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.event-id {
font-family: monospace;
font-size: 10px;
color: rgba(255, 255, 255, 0.7);
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.lamport-timestamp {
position: absolute;
top: 8px;
right: 12px;
font-size: 12px;
color: rgba(255, 255, 255, 0.9);
font-weight: 500;
border: 1px solid #ddd;
}
.id-filter-badge {
@ -365,7 +256,7 @@
background-color: #f3f4f6;
border-radius: 16px;
padding: 4px 12px;
margin: 8px 0;
margin: 8px;
max-width: fit-content;
font-size: 12px;
}

View File

@ -0,0 +1,222 @@
<script lang="ts">
import { MessageChannelEvent } from '@waku/sds';
import type { MessageChannelEventObject } from '$lib/sds/stream';
import { getMessageId } from '$lib/sds/message';
export let event: MessageChannelEventObject;
export let identicon: string;
export let currentIdFilter: string | null = null;
export let onEventClick: (id: string | null) => void;
export let onDependencyClick: (messageId: string, event: Event) => void;
// Map event types to colors using index signature
const eventColors: { [key in string]: string } = {
[MessageChannelEvent.MessageSent]: '#3B82F6', // blue
[MessageChannelEvent.MessageDelivered]: '#10B981', // green
[MessageChannelEvent.MessageReceived]: '#8B5CF6', // purple
[MessageChannelEvent.MessageAcknowledged]: '#059669', // dark green
[MessageChannelEvent.PartialAcknowledgement]: '#6D28D9', // dark purple
[MessageChannelEvent.MissedMessages]: '#EF4444' // red
};
// Event type to display name using index signature
const eventNames: { [key in string]: string } = {
[MessageChannelEvent.MessageSent]: 'Sent',
[MessageChannelEvent.MessageDelivered]: 'Delivered',
[MessageChannelEvent.MessageReceived]: 'Received',
[MessageChannelEvent.MessageAcknowledged]: 'Acknowledged',
[MessageChannelEvent.PartialAcknowledgement]: 'Partially Acknowledged',
[MessageChannelEvent.MissedMessages]: 'Missed'
};
$: id = getMessageId(event);
$: color = eventColors[event.type] || '#888';
$: name = eventNames[event.type] || event.type;
$: matchesFilter = currentIdFilter && id === currentIdFilter;
function handleEventClick() {
onEventClick(id);
}
function handleDependencyClick(messageId: string, e: Event) {
onDependencyClick(messageId, e);
}
</script>
<div class="history-item" on:click={handleEventClick}>
<div class="item-container">
<div
class="event-box {matchesFilter ? 'highlight' : ''}"
style="background-color: {color};"
>
<div class="identicon">
<img src="data:image/svg+xml;base64,{identicon}" alt="Identicon" />
</div>
<div class="event-info">
<div class="event-type">
{name}
</div>
<div class="event-id">
{id}
</div>
</div>
{#if event.type === MessageChannelEvent.MessageDelivered}
<div class="sent-or-received">
{event.payload.sentOrReceived}
</div>
{/if}
{#if event.type === MessageChannelEvent.MessageSent || event.type === MessageChannelEvent.MessageReceived}
<div class="lamport-timestamp">
{event.payload.lamportTimestamp}
</div>
{/if}
</div>
{#if event.type === MessageChannelEvent.MessageSent || event.type === MessageChannelEvent.MessageReceived}
<!-- <section class="dependency-container"> -->
{#each event.payload.causalHistory as dependency}
{@const dependencyMatchesFilter =
currentIdFilter && dependency.messageId === currentIdFilter}
<div
class="dependency-box {dependencyMatchesFilter ? 'highlight' : ''}"
style="background-color: {color};"
on:click={(e) => handleDependencyClick(dependency.messageId, e)}
>
{dependency.messageId}
</div>
{/each}
<!-- </section> -->
{/if}
</div>
</div>
<style>
.history-item {
padding: 8px;
width: 100%;
cursor: pointer;
}
.item-container {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 6px;
width: 100%;
}
.event-box {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
border-radius: 8px;
width: 100%;
min-height: 70px;
color: white;
padding: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
position: relative;
transition: box-shadow 0.3s ease;
}
.dependency-box {
display: flex;
align-items: center;
justify-content: center;
margin-left: auto;
width: auto;
max-width: 80%;
min-height: 40px;
font-size: 11px;
font-family: monospace;
opacity: 0.85;
padding: 6px 12px;
border-radius: 8px;
color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.3s ease;
}
.highlight {
border-left: 4px solid white;
border-right: 4px solid white;
position: relative;
background-image: linear-gradient(rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.1));
}
.highlight .event-type {
font-size: 16px;
color: white;
font-weight: bold;
font-style: italic;
letter-spacing: 0.5px;
}
.highlight .event-id,
.dependency-box.highlight {
font-weight: bold;
font-style: italic;
letter-spacing: 0.5px;
}
.dependency-box.highlight {
font-size: 12px;
}
.identicon {
width: 40px;
height: 40px;
border-radius: 4px;
overflow: hidden;
margin-right: 12px;
}
.identicon img {
width: 100%;
height: 100%;
object-fit: contain;
}
.event-info {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.event-type {
font-size: 14px;
font-weight: bold;
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.event-id {
font-family: monospace;
font-size: 10px;
color: rgba(255, 255, 255, 0.7);
max-width: 220px;
/* overflow: hidden; */
text-overflow: ellipsis;
white-space: nowrap;
}
.lamport-timestamp {
position: absolute;
top: 8px;
right: 12px;
font-size: 12px;
color: rgba(255, 255, 255, 0.9);
font-weight: 500;
}
.sent-or-received {
position: absolute;
top: 8px;
right: 12px;
font-size: 12px;
color: rgba(255, 255, 255, 0.9);
font-weight: 500;
}
</style>

View File

@ -0,0 +1,64 @@
<script lang="ts">
import LegendModal from './LegendModal.svelte';
let showLegend = $state(false);
function toggleLegend() {
showLegend = !showLegend;
}
</script>
<div class="legend-demo">
<h2>Message Events Visualization</h2>
<div class="actions">
<button class="info-button" on:click={toggleLegend}>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="16" x2="12" y2="12"></line>
<line x1="12" y1="8" x2="12.01" y2="8"></line>
</svg>
Show Event Types Legend
</button>
</div>
<LegendModal bind:isOpen={showLegend} />
</div>
<style>
.legend-demo {
padding: 20px;
}
h2 {
margin-bottom: 20px;
}
.actions {
margin-bottom: 20px;
}
.info-button {
display: flex;
align-items: center;
gap: 8px;
background-color: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 8px 16px;
cursor: pointer;
font-size: 14px;
color: #374151;
transition: all 0.2s;
}
.info-button:hover {
background-color: #f3f4f6;
border-color: #d1d5db;
}
.info-button svg {
width: 16px;
height: 16px;
}
</style>

View File

@ -0,0 +1,371 @@
<script lang="ts">
import { historyJson } from "$lib/data/history_sample";
import type { MessageChannelEventObject } from "$lib/sds/stream";
import { MessageChannelEvent } from "@waku/sds";
import HistoryItem from "$lib/components/HistoryItem.svelte";
import { onMount, onDestroy } from 'svelte';
import { createTooltip } from '$lib/utils/tooltipUtils';
// Parse history data
const history: MessageChannelEventObject[] = JSON.parse(historyJson);
// Create sample identicons (using a placeholder)
const placeholderIdenticon = "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgdmlld0JveD0iMCAwIDQwIDQwIj48cmVjdCB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIGZpbGw9IiNkZGQiLz48L3N2Zz4=";
// Sample handler functions - they don't need to do anything in the legend
function handleItemClick(id: string | null) {
// No action needed for the legend
}
function handleDependencyClick(messageId: string, event: Event) {
// Prevent event bubbling
event.stopPropagation();
// No action needed for the legend
}
// Get one sample of each event type for the legend
const eventTypes = [
MessageChannelEvent.MessageSent,
MessageChannelEvent.MessageDelivered,
MessageChannelEvent.MessageReceived,
MessageChannelEvent.MessageAcknowledged,
MessageChannelEvent.PartialAcknowledgement,
MessageChannelEvent.MissedMessages
];
// Find one event of each type to display in the legend
let legendItems = history.slice(2, 6);
// Simple tooltip text
const leftTooltipText = "Messages are sent between peers in the network. Each message has a unique ID and can depend on other messages. Events like Sent, Received, Delivered and Acknowledged show the status of messages.";
const rightTooltipText = "Dependencies represent messages that a current message depends on. They appear as smaller boxes below the main message.";
const lamportTooltipText = "<b>Lamport timestamps</b> provide a way to order events in a distributed system. They increment with each message, ensuring a consistent ordering across all peers in the network, even without synchronized clocks.";
const eventIdTooltipText = "Unique <b>Message ID</b> assigned to each message.";
const dependencyContainerTooltipText = "Each message comes with <b>causal history</b> attached, containing the last two message IDs from the local history of the sender.";
// Custom highlight classes
const tooltipHighlightClass = "tooltip-highlight";
console.log(legendItems);
// Make isOpen bindable so parent can track when modal is closed
export let isOpen = false;
// Reference elements for tooltips
let modalElement: HTMLElement;
let leftAnchorElement: HTMLElement;
let rightAnchorElement: HTMLElement;
let lamportTimestampElement: HTMLElement | null = null;
let eventIdElement: HTMLElement | null = null;
let dependencyContainerElement: HTMLElement | null = null;
// let leftTooltipRef: ReturnType<typeof createTooltip>;
// let rightTooltipRef: ReturnType<typeof createTooltip>;
let lamportTooltipRef: ReturnType<typeof createTooltip>;
let eventIdTooltipRef: ReturnType<typeof createTooltip>;
let dependencyContainerTooltipRef: ReturnType<typeof createTooltip>;
// Handles closing the modal and updates the parent via bindable prop
function closeModal() {
isOpen = false;
}
// Cleanup tooltips when component is destroyed
function cleanupTooltips() {
// leftTooltipRef?.destroy();
// rightTooltipRef?.destroy();
lamportTooltipRef?.destroy();
eventIdTooltipRef?.destroy();
dependencyContainerTooltipRef?.destroy();
}
// Create tooltips once elements are mounted and modal is open
function setupTooltips() {
// Clean up any existing tooltips first
cleanupTooltips();
if (!isOpen || !leftAnchorElement || !rightAnchorElement) return;
// Create left tooltip
// leftTooltipRef = createTooltip(leftAnchorElement, {
// position: 'left',
// content: leftTooltipText,
// width: 200,
// showOnHover: false,
// visible: true,
// offset: 20
// });
// Create right tooltip
// rightTooltipRef = createTooltip(rightAnchorElement, {
// position: 'right',
// content: rightTooltipText,
// width: 200,
// showOnHover: false,
// visible: true,
// offset: 20
// });
// Wait a moment to find and add tooltip to the specific elements
setTimeout(() => {
// Find the first lamport timestamp element
lamportTimestampElement = document.querySelector('.legend-content .lamport-timestamp');
if (lamportTimestampElement) {
// Create lamport timestamp tooltip
lamportTooltipRef = createTooltip(lamportTimestampElement, {
position: 'right',
content: lamportTooltipText,
width: 220,
showOnHover: false,
visible: true,
offset: 10,
verticalOffset: -100,
highlightTarget: true,
highlightClass: tooltipHighlightClass
});
}
// Find the first event-id element
eventIdElement = document.querySelector('.legend-content .event-id');
if (eventIdElement) {
// Create event-id tooltip
eventIdTooltipRef = createTooltip(eventIdElement, {
position: 'left',
content: eventIdTooltipText,
width: 180,
showOnHover: false,
visible: true,
offset: 10,
highlightTarget: true,
highlightClass: tooltipHighlightClass
});
}
// Find the first dependency-container element
dependencyContainerElement = document.querySelector('.legend-content .dependency-box');
if (dependencyContainerElement) {
// Create dependency-container tooltip
dependencyContainerTooltipRef = createTooltip(dependencyContainerElement, {
position: 'left',
content: dependencyContainerTooltipText,
width: 250,
showOnHover: false,
visible: true,
offset: 20,
verticalOffset: 40,
highlightTarget: true,
highlightClass: tooltipHighlightClass
});
}
}, 100);
}
onMount(() => {
if (isOpen) {
// Need to wait for the DOM to update
setTimeout(setupTooltips, 0);
}
});
onDestroy(() => {
cleanupTooltips();
});
// Watch for changes to isOpen state
$: if (isOpen) {
// Need to wait for the DOM to update
setTimeout(setupTooltips, 0);
} else {
cleanupTooltips();
}
</script>
{#if isOpen}
<div class="legend-modal-backdrop" on:click={closeModal}>
<div class="legend-container">
<!-- Main Modal (Center) -->
<div class="legend-modal" bind:this={modalElement} on:click|stopPropagation>
<!-- Tooltip anchor elements -->
<div class="tooltip-anchor left-anchor" bind:this={leftAnchorElement}></div>
<div class="tooltip-anchor right-anchor" bind:this={rightAnchorElement}></div>
<div class="legend-header">
<h2>Legend</h2>
<button class="close-button" on:click={closeModal}>×</button>
</div>
<div class="legend-content">
{#each legendItems as event}
<div class="legend-item">
<HistoryItem
{event}
identicon={placeholderIdenticon}
onEventClick={handleItemClick}
onDependencyClick={handleDependencyClick}
/>
</div>
{/each}
</div>
</div>
</div>
</div>
{/if}
<style>
.legend-modal-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.legend-container {
position: relative;
display: flex;
justify-content: center;
align-items: flex-start;
}
.legend-modal {
position: relative;
background-color: white;
border-radius: 8px;
width: 600px;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
z-index: 2;
}
.tooltip-anchor {
position: absolute;
width: 1px;
height: 40%;
top: 30%;
}
.left-anchor {
left: 0;
}
.right-anchor {
right: 0;
}
.legend-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #eee;
}
.legend-header h2 {
margin: 0;
font-size: 18px;
}
.close-button {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
}
.close-button:hover {
color: #333;
}
.legend-content {
padding: 16px;
}
.legend-item {
margin-bottom: 12px;
}
/* Custom highlight for lamport timestamp */
:global(.tooltip-highlight) {
background-color: rgba(253, 230, 138, 0.9) !important;
border-radius: 4px !important;
box-shadow: 0 0 0 2px #f59e0b !important;
color: #000 !important;
font-weight: bold !important;
animation: pulse 1.5s infinite !important;
max-width: none !important;
}
/* Custom highlight for event ID */
:global(.event-id-highlight) {
background-color: rgba(191, 219, 254, 0.9) !important;
border-radius: 4px !important;
box-shadow: 0 0 0 2px #3b82f6 !important;
color: #000 !important;
font-weight: bold !important;
animation: pulseCyan 1.5s infinite !important;
}
/* Custom highlight for dependency container */
:global(.dependency-container-highlight) {
background-color: rgba(220, 252, 231, 0.7) !important;
border-radius: 4px !important;
box-shadow: 0 0 0 2px #10b981 !important;
padding: 4px !important;
animation: pulseGreen 1.5s infinite !important;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.7);
}
70% {
box-shadow: 0 0 0 6px rgba(245, 158, 11, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0);
}
}
@keyframes pulseCyan {
0% {
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7);
}
70% {
box-shadow: 0 0 0 6px rgba(59, 130, 246, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
}
}
@keyframes pulseGreen {
0% {
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7);
}
70% {
box-shadow: 0 0 0 6px rgba(16, 185, 129, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0);
}
}
/* Responsive adjustments */
@media (max-width: 1200px) {
.tooltip-anchor {
display: none;
}
}
</style>

View File

@ -160,8 +160,8 @@
font-weight: bold;
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
/* overflow: hidden; */
/* text-overflow: ellipsis; */
}
.event-id {
@ -169,8 +169,8 @@
font-size: 10px;
color: rgba(255, 255, 255, 0.7);
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
/* overflow: hidden; */
/* text-overflow: ellipsis; */
white-space: nowrap;
}

View File

@ -0,0 +1,129 @@
<script lang="ts">
import { onMount } from 'svelte';
export let targetElement: HTMLElement | null = null;
export let position: 'left' | 'right' = 'right';
export let content: string = '';
export let offset: number = 20;
export let verticalOffset: number = 0;
export let width: number = 200;
export let showOnHover: boolean = true;
export let visible: boolean = false;
export let highlightTarget: boolean = false;
export let highlightClass: string = 'tooltip-target-highlight';
let tooltipElement: HTMLElement;
let originalTargetClasses: string = '';
// Position the tooltip relative to the target element
function positionTooltip() {
if (!targetElement || !tooltipElement) return;
const targetRect = targetElement.getBoundingClientRect();
// Calculate vertical center alignment with optional vertical offset
const top = targetRect.top + (targetRect.height / 2) + verticalOffset;
// Position horizontally based on the selected position
if (position === 'left') {
tooltipElement.style.right = `${window.innerWidth - targetRect.left + offset}px`;
} else {
tooltipElement.style.left = `${targetRect.right + offset}px`;
}
tooltipElement.style.top = `${top}px`;
tooltipElement.style.transform = 'translateY(-50%)';
}
// Reposition on window resize
function handleResize() {
if (visible) {
positionTooltip();
}
}
function showTooltip() {
visible = true;
// Position after becoming visible
setTimeout(positionTooltip, 0);
}
function hideTooltip() {
visible = false;
}
function applyHighlight() {
if (targetElement && highlightTarget) {
// Store original classes before adding highlight
originalTargetClasses = targetElement.className;
targetElement.classList.add(highlightClass);
}
}
function removeHighlight() {
if (targetElement && highlightTarget) {
// If we stored original classes, restore them
if (originalTargetClasses) {
// Remove highlight class
targetElement.classList.remove(highlightClass);
}
}
}
onMount(() => {
applyHighlight();
if (visible) {
positionTooltip();
}
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
// Ensure we remove highlight when component is destroyed
removeHighlight();
};
});
</script>
{#if visible}
<div
class="tooltip {position}-tooltip"
bind:this={tooltipElement}
style="width: {width}px;"
>
{@html content}
</div>
{/if}
<style>
.tooltip {
position: fixed;
background-color: white;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
padding: 16px;
font-size: 14px;
line-height: 1.5;
color: #4b5563;
z-index: 1000;
max-width: 300px;
opacity: 0;
animation: fadeIn 0.2s forwards;
}
@keyframes fadeIn {
to {
opacity: 1;
}
}
:global(.tooltip-target-highlight) {
outline: 2px solid rgba(252, 211, 77, 0.8) !important;
outline-offset: 2px !important;
transition: outline-color 0.2s ease !important;
z-index: 2 !important;
position: relative !important;
}
</style>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,51 @@
import { MessageChannelEvent } from '@waku/sds';
// Sample history with different event types for the legend
export const historyJson = JSON.stringify([
{
type: MessageChannelEvent.MessageSent,
payload: {
messageId: "db7ce7bff8734cc868da5bd8d880b58765ed9e0481f0c2f6b0ec86258322a3fa",
channelId: "channel-id",
lamportTimestamp: 6,
causalHistory: [],
bloomFilter: { 0: 0, 1: 0, 2: 0, 3: 0 },
content: { 0: 131, 1: 244 }
}
},
{
type: MessageChannelEvent.MessageDelivered,
payload: {
messageId: "bc8701dd8eacca44f01a177a2d7e2ac879dd189b1f8ea2b57b10bbdb82042bc0",
sentOrReceived: "received"
}
},
{
type: MessageChannelEvent.MessageReceived,
payload: {
messageId: "bc8701dd8eacca44f01a177a2d7e2ac879dd189b1f8ea2b57b10bbdb82042bc0",
channelId: "channel-id",
causalHistory: [],
lamportTimestamp: 4,
bloomFilter: { 0: 0, 1: 0, 2: 0, 3: 0 },
content: { 0: 226, 1: 83 }
}
},
{
type: MessageChannelEvent.MessageAcknowledged,
payload: "217e647921f9a6fc8ecfc480e207db828e18c5868d229cf5c5bf59be89dc70ff"
},
{
type: MessageChannelEvent.PartialAcknowledgement,
payload: {
messageId: "d81b2617e63162843413eea8c62dada058ac7e6c8f8463fd5ef1171939cd9415",
acknowledgementBitmask: new Uint8Array([1, 0, 1])
}
},
{
type: MessageChannelEvent.MissedMessages,
payload: {
messageIds: ["58cf44867e529152f4095aa6a951500a6a45571a062dc0bddb06aef48c97f85b"]
}
}
]);

View File

@ -0,0 +1,91 @@
import { mount, unmount } from 'svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
interface TooltipOptions {
position?: 'left' | 'right';
content: string;
offset?: number;
verticalOffset?: number;
width?: number;
showOnHover?: boolean;
visible?: boolean;
highlightTarget?: boolean;
highlightClass?: string;
}
/**
* Creates a tooltip positioned relative to a target element
* @param targetElement - The element to attach the tooltip to
* @param options - Configuration options for the tooltip
* @returns An object with methods to control the tooltip
*/
export function createTooltip(targetElement: HTMLElement, options: TooltipOptions) {
const {
position = 'right',
content,
offset = 20,
width = 200,
showOnHover = true,
visible = false,
highlightTarget = false,
highlightClass = 'tooltip-target-highlight',
verticalOffset = 0
} = options;
// Store current visibility state
let isVisible = visible;
// Create a container for the tooltip
const container = document.createElement('div');
document.body.appendChild(container);
// Initialize the tooltip component using Svelte's mount API
const tooltipInstance = mount(Tooltip, {
target: container,
props: {
targetElement,
position,
content,
offset,
width,
verticalOffset,
showOnHover,
visible: isVisible,
highlightTarget,
highlightClass
}
});
// Return methods to control the tooltip
return {
destroy: () => {
// In Svelte 5, we use the unmount function instead of $destroy
unmount(tooltipInstance);
container.remove();
},
updatePosition: () => {
tooltipInstance.$set({ targetElement });
},
updateContent: (newContent: string) => {
tooltipInstance.$set({ content: newContent });
},
show: () => {
isVisible = true;
tooltipInstance.$set({ visible: true });
},
hide: () => {
isVisible = false;
tooltipInstance.$set({ visible: false });
},
toggle: () => {
isVisible = !isVisible;
tooltipInstance.$set({ visible: isVisible });
},
updateOptions: (newOptions: Partial<TooltipOptions>) => {
if (newOptions.visible !== undefined) {
isVisible = newOptions.visible;
}
tooltipInstance.$set(newOptions);
}
};
}

View File

@ -23,12 +23,14 @@
<div class="flex flex-col sm:flex-row gap-4 sm:gap-6 px-4">
<div class="flex-1 w-full">
<PageLayout title="History" maxWidth="2xl" padding="sm:p-6 px-4" margin="sm:my-6 my-4">
<p class="text-gray-600 text-sm mb-4 leading-relaxed">Log of all events as they are emitted by the message channel.</p>
<History />
</PageLayout>
</div>
<div class="flex-1 w-full">
<PageLayout title="Missing" maxWidth="2xl" padding="sm:p-6 px-4" margin="sm:my-6 my-4">
<p class="text-gray-600 text-sm mb-4 leading-relaxed">List of messages that are currently known to be missing from the channel.</p>
<Missing />
</PageLayout>
</div>