Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/Exceptionless.Core/Models/SavedView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ public record SavedView : IOwnedByOrganizationWithIdentity, IHaveDates
/// <summary>Column display order per dashboard table, excluding utility columns.</summary>
public List<string>? ColumnOrder { get; set; }

/// <summary>Whether dashboard statistic cards are shown for this view. Null means use the default.</summary>
public bool? ShowStats { get; set; }

/// <summary>Whether the dashboard chart is shown for this view. Null means use the default.</summary>
public bool? ShowChart { get; set; }

/// <summary>Display name shown in the sidebar and picker.</summary>
[Required]
[MaxLength(100)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,17 @@
</script>

<Sheet.Root onOpenChange={handleOpenChange} open={!!eventId}>
<Sheet.Content class="w-full overflow-y-scroll sm:max-w-full! md:w-5/6!">
<Sheet.Content
class="top-15.25! bottom-0! h-auto! w-full transform-gpu overflow-y-auto rounded-l-lg border-l shadow-2xl duration-150 ease-out will-change-transform sm:max-w-full! md:w-5/6!"
overlayProps={{ class: 'top-15.25! bg-black/5 supports-backdrop-filter:backdrop-blur-none!' }}
>
<Sheet.Header>
<Sheet.Title>Event Details <Button href={resolvedHref} size="sm" title="Open in new window" variant="ghost"><ExternalLink /></Button></Sheet.Title>
<Sheet.Title class="flex items-center gap-2">
Event Details
<Button aria-label="Open event details in new window" href={resolvedHref} size="icon-sm" title="Open in new window" variant="ghost">
<ExternalLink aria-hidden="true" />
</Button>
</Sheet.Title>
</Sheet.Header>
<div class="px-4">
{#if eventId}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@
},
stacks: {
color: 'var(--chart-2)',
label: 'Unique Events (Stacks)'
label: 'Issues'
}
} satisfies Chart.ChartConfig;

const series = [
{
color: chartConfig.stacks.color,
key: 'stacks',
label: 'Unique Events (Stacks)'
label: 'Issues'
},
{
color: chartConfig.events.color,
Expand All @@ -49,7 +49,7 @@
];
</script>

<div class="bg-card text-card-foreground rounded-lg border {className}">
<Chart.Shell class={className}>
{#if isLoading}
<Skeleton class="h-16 w-full rounded" />
{:else}
Expand Down Expand Up @@ -98,4 +98,4 @@
</AreaChart>
</Chart.Container>
{/if}
</div>
</Chart.Shell>
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
];
</script>

<div class="bg-card text-card-foreground rounded-lg border {className}">
<Chart.Shell class={className}>
{#if isLoading}
<Skeleton class="h-full w-full rounded-md" />
{:else}
Expand Down Expand Up @@ -68,4 +68,4 @@
</AreaChart>
</Chart.Container>
{/if}
</div>
</Chart.Shell>
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<script lang="ts">
import Number from '$comp/formatters/number.svelte';
import * as Card from '$comp/ui/card';
import { Skeleton } from '$comp/ui/skeleton';
import * as Tooltip from '$comp/ui/tooltip';
import CalendarDays from '@lucide/svelte/icons/calendar-days';
import Info from '@lucide/svelte/icons/info';
import Layers from '@lucide/svelte/icons/layers';
import Sparkles from '@lucide/svelte/icons/sparkles';
import TrendingUp from '@lucide/svelte/icons/trending-up';

interface Props {
eventsPerHour?: number;
isLoading?: boolean;
newIssues?: number;
totalEvents?: number;
totalIssues?: number;
}

let { eventsPerHour = 0, isLoading = false, newIssues = 0, totalEvents = 0, totalIssues = 0 }: Props = $props();

const metricCardClass =
"relative h-[66px]! justify-between gap-1! overflow-hidden bg-card py-2! ring-[color-mix(in_oklab,var(--chart-1)_42%,transparent)] before:absolute before:inset-x-0 before:top-0 before:h-1 before:bg-[linear-gradient(90deg,var(--chart-1),var(--chart-2))] before:content-['']";
const metricHeaderClass = 'flex flex-row items-center justify-between gap-1.5 px-3 pb-0';
const metricTitleClass = 'min-w-0 truncate text-xs font-semibold text-[color-mix(in_oklab,var(--chart-2)_82%,var(--foreground))]';
const metricIconClass = 'size-3.5 shrink-0 text-[var(--chart-2)]';
const metricValueClass = 'truncate text-lg leading-none font-bold text-[var(--chart-2)] tabular-nums sm:text-xl';
</script>

<div class="grid grid-cols-2 gap-2 sm:grid-cols-4">
<Card.Root size="sm" class={metricCardClass}>
<Card.Header class={metricHeaderClass}>
<div class="flex min-w-0 items-center gap-1.5">
<CalendarDays aria-hidden="true" class={metricIconClass} />
<Card.Title class={metricTitleClass}>Events</Card.Title>
</div>
<Tooltip.Root>
<Tooltip.Trigger
class="text-muted-foreground hover:text-foreground focus-visible:ring-ring rounded-sm outline-none focus-visible:ring-2"
aria-label="About events"
>
<Info aria-hidden="true" class="size-3.5" />
</Tooltip.Trigger>
<Tooltip.Content sideOffset={6}>Total event occurrences matching the current filters.</Tooltip.Content>
</Tooltip.Root>
</Card.Header>
<Card.Content class="px-3">
{#if isLoading}
<Skeleton class="h-5 w-16" />
{:else}
<div class={metricValueClass}>
<Number value={totalEvents} />
</div>
{/if}
</Card.Content>
</Card.Root>

<Card.Root size="sm" class={metricCardClass}>
<Card.Header class={metricHeaderClass}>
<div class="flex min-w-0 items-center gap-1.5">
<Layers aria-hidden="true" class={metricIconClass} />
<Card.Title class={metricTitleClass}>Issues</Card.Title>
</div>
<Tooltip.Root>
<Tooltip.Trigger
class="text-muted-foreground hover:text-foreground focus-visible:ring-ring rounded-sm outline-none focus-visible:ring-2"
aria-label="About issues"
>
<Info aria-hidden="true" class="size-3.5" />
</Tooltip.Trigger>
<Tooltip.Content sideOffset={6}>Unique issues matching the current filters.</Tooltip.Content>
</Tooltip.Root>
</Card.Header>
<Card.Content class="px-3">
{#if isLoading}
<Skeleton class="h-5 w-16" />
{:else}
<div class={metricValueClass}>
<Number value={totalIssues} />
</div>
{/if}
</Card.Content>
</Card.Root>

<Card.Root size="sm" class={metricCardClass}>
<Card.Header class={metricHeaderClass}>
<div class="flex min-w-0 items-center gap-1.5">
<Sparkles aria-hidden="true" class={metricIconClass} />
<Card.Title class={metricTitleClass}>New Issues</Card.Title>
</div>
<Tooltip.Root>
<Tooltip.Trigger
class="text-muted-foreground hover:text-foreground focus-visible:ring-ring rounded-sm outline-none focus-visible:ring-2"
aria-label="About new issues"
>
<Info aria-hidden="true" class="size-3.5" />
</Tooltip.Trigger>
<Tooltip.Content sideOffset={6}>Issues with their first occurrence in the selected time range.</Tooltip.Content>
</Tooltip.Root>
</Card.Header>
<Card.Content class="px-3">
{#if isLoading}
<Skeleton class="h-5 w-16" />
{:else}
<div class={metricValueClass}>
<Number value={newIssues} />
</div>
{/if}
</Card.Content>
</Card.Root>

<Card.Root size="sm" class={metricCardClass}>
<Card.Header class={metricHeaderClass}>
<div class="flex min-w-0 items-center gap-1.5">
<TrendingUp aria-hidden="true" class={metricIconClass} />
<Card.Title class={metricTitleClass}>Events/hr</Card.Title>
</div>
<Tooltip.Root>
<Tooltip.Trigger
class="text-muted-foreground hover:text-foreground focus-visible:ring-ring rounded-sm outline-none focus-visible:ring-2"
aria-label="About events per hour"
>
<Info aria-hidden="true" class="size-3.5" />
</Tooltip.Trigger>
<Tooltip.Content sideOffset={6}>Average event occurrences per hour across the selected time range.</Tooltip.Content>
</Tooltip.Root>
</Card.Header>
<Card.Content class="px-3">
{#if isLoading}
<Skeleton class="h-5 w-16" />
{:else}
<div class={metricValueClass}>
<Number value={eventsPerHour} formatOptions={{ maximumFractionDigits: 1 }} />
</div>
{/if}
</Card.Content>
</Card.Root>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@
onLoadView: (id: string) => void;
onResetToSaved: () => void;
savedViews: SavedView[];
setShowChart?: (show: boolean) => void;
setShowStats?: (show: boolean) => void;
showChart?: boolean;
showStats?: boolean;
sort?: string;
table: Table<StockFeatures, TData>;
time?: string;
Expand All @@ -67,6 +71,10 @@
onLoadView,
onResetToSaved,
savedViews,
setShowChart,
setShowStats,
showChart = true,
showStats = true,
sort,
table,
time,
Expand Down Expand Up @@ -234,6 +242,8 @@
is_private: isPrivate || undefined,
name,
organization_id: organizationId,
show_chart: showChart,
show_stats: showStats,
sort: sort || undefined,
time: time || undefined,
view_type: view
Expand Down Expand Up @@ -273,6 +283,8 @@
columns: columnVisibility,
filter: currentFilterString || null,
filter_definitions: serializeFilters(filters),
show_chart: showChart,
show_stats: showStats,
sort: sort || null,
time: time || null
};
Expand Down Expand Up @@ -346,6 +358,36 @@
</DropdownMenu.Item>
{/if}
</DropdownMenu.Group>
{#if setShowStats || setShowChart}
<DropdownMenu.Separator />
<DropdownMenu.Group>
<DropdownMenu.Label>Display</DropdownMenu.Label>
{#if setShowStats}
<DropdownMenu.CheckboxItem
checked={showStats}
onclick={(event) => {
event.preventDefault();
setShowStats(!showStats);
}}
onSelect={(event) => event.preventDefault()}
>
Stat boxes
</DropdownMenu.CheckboxItem>
{/if}
{#if setShowChart}
<DropdownMenu.CheckboxItem
checked={showChart}
onclick={(event) => {
event.preventDefault();
setShowChart(!showChart);
}}
onSelect={(event) => event.preventDefault()}
>
Chart
</DropdownMenu.CheckboxItem>
{/if}
</DropdownMenu.Group>
{/if}
{#if reorderableColumns.length > 0}
<DropdownMenu.Separator />
<DropdownMenu.Group>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,13 @@ export interface UseSavedViewsOptions {
getColumnOrder?: () => ColumnOrderState;
getColumnVisibility?: () => ColumnVisibilityState;
getFilterDefinitions?: () => string;
getShowChart?: () => boolean;
getShowStats?: () => boolean;
queryParams: SavedViewQueryParams;
setColumnOrder?: (order: ColumnOrderState) => void;
setColumnVisibility?: (visibility: ColumnVisibilityState) => void;
setShowChart?: (show: boolean) => void;
setShowStats?: (show: boolean) => void;
updateFilterCache: (key: string, filters: IFilter[]) => void;
view: string;
}
Expand Down Expand Up @@ -90,6 +94,11 @@ export function useSavedViews(options: UseSavedViewsOptions): UseSavedViewsRetur
}
}

function applyDisplayState(view: Pick<SavedView, 'show_chart' | 'show_stats'> | undefined): void {
options.setShowStats?.(view?.show_stats ?? true);
options.setShowChart?.(view?.show_chart ?? true);
}

// Hydrate filters/columns when a saved view loads, or clear params if the view is no longer found.
// lastLoadedViewId prevents re-hydration on background refetches (which would stomp user edits).
let lastLoadedViewId = '';
Expand All @@ -103,6 +112,7 @@ export function useSavedViews(options: UseSavedViewsOptions): UseSavedViewsRetur
if (!savedId) {
if (lastLoadedViewId !== '') {
applyColumnState(undefined);
applyDisplayState(undefined);
}

lastLoadedViewId = '';
Expand Down Expand Up @@ -148,6 +158,7 @@ export function useSavedViews(options: UseSavedViewsOptions): UseSavedViewsRetur
setSortQueryParam(options.queryParams, view.sort ?? null);
setTimeQueryParam(options.queryParams, view.time ?? null);
applyColumnState(view);
applyDisplayState(view);
});

// Detect if current filters or columns differ from the active saved view
Expand Down Expand Up @@ -181,6 +192,14 @@ export function useSavedViews(options: UseSavedViewsOptions): UseSavedViewsRetur
return true;
}

if (options.getShowStats && options.getShowStats() !== (view.show_stats ?? true)) {
return true;
}

if (options.getShowChart && options.getShowChart() !== (view.show_chart ?? true)) {
return true;
}

return false;
});

Expand All @@ -207,6 +226,7 @@ export function useSavedViews(options: UseSavedViewsOptions): UseSavedViewsRetur
setSortQueryParam(options.queryParams, view.sort ?? null);
setTimeQueryParam(options.queryParams, view.time ?? null);
applyColumnState(view);
applyDisplayState(view);
}

function handleClearSavedView() {
Expand All @@ -215,6 +235,7 @@ export function useSavedViews(options: UseSavedViewsOptions): UseSavedViewsRetur
setSortQueryParam(options.queryParams, null);
setTimeQueryParam(options.queryParams, null);
applyColumnState(undefined);
applyDisplayState(undefined);
}

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
];
</script>

<div class="bg-card text-card-foreground rounded-lg border shadow-sm {className}">
<Chart.Shell class={className}>
{#if isLoading}
<Skeleton class="h-16 w-full rounded" />
{:else}
Expand Down Expand Up @@ -98,4 +98,4 @@
</AreaChart>
</Chart.Container>
{/if}
</div>
</Chart.Shell>
Loading