Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import type { Stack } from '$features/stacks/models';

import NumberFormatter from '$comp/formatters/number.svelte';
import TimeAgo from '$comp/formatters/time-ago.svelte';
import { Checkbox } from '$comp/ui/checkbox';
import { nameof } from '$lib/utils';
import { type ColumnDef, renderComponent, type StockFeatures } from '@tanstack/svelte-table';

import StackStatusCell from './stack-status-cell.svelte';
import StackTagsCell from './stack-tags-cell.svelte';

export function getColumns(): ColumnDef<StockFeatures, Stack, unknown>[] {
return [
{
cell: (props) =>
renderComponent(Checkbox, {
'aria-label': 'Select row',
checked: props.row.getIsSelected(),
class: 'translate-y-[2px]',
disabled: !props.row.getCanSelect(),
indeterminate: props.row.getIsSomeSelected(),
onCheckedChange: (checked: 'indeterminate' | boolean) => props.row.getToggleSelectedHandler()({ target: { checked } })
}),
enableHiding: false,
enableSorting: false,
header: ({ table }) =>
renderComponent(Checkbox, {
checked: table.getIsAllRowsSelected(),
indeterminate: table.getIsSomeRowsSelected(),
onCheckedChange: (checked: 'indeterminate' | boolean) => table.getToggleAllRowsSelectedHandler()({ target: { checked } })
}),
id: 'select',
meta: {
class: 'w-6'
}
},
{
accessorKey: nameof<Stack>('title'),
enableHiding: false,
header: 'Title',
id: 'title'
},
{
accessorKey: nameof<Stack>('status'),
cell: (prop) => renderComponent(StackStatusCell, { value: prop.getValue<string>() }),
enableSorting: false,
header: 'Status',
id: 'status',
meta: {
class: 'w-28'
}
},
{
accessorKey: nameof<Stack>('tags'),
cell: (prop) => renderComponent(StackTagsCell, { tags: prop.getValue<string[]>() }),
enableSorting: false,
header: 'Tags',
id: 'tags',
meta: {
class: 'w-40'
}
},
{
accessorKey: nameof<Stack>('total_occurrences'),
cell: (prop) => renderComponent(NumberFormatter, { value: prop.getValue<number>() }),
header: 'Events',
id: 'events',
meta: {
class: 'w-24 text-right'
}
},
{
accessorKey: nameof<Stack>('first_occurrence'),
cell: (prop) => renderComponent(TimeAgo, { value: prop.getValue<string>() }),
header: 'First',
id: 'first',
meta: {
class: 'w-36'
}
},
{
accessorKey: nameof<Stack>('last_occurrence'),
cell: (prop) => renderComponent(TimeAgo, { value: prop.getValue<string>() }),
header: 'Last',
id: 'last',
meta: {
class: 'w-36'
}
}
];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script lang="ts">
import { Badge } from '$comp/ui/badge';

interface Props {
value: string;
}

let { value }: Props = $props();

const statusLabels: Record<string, string> = {
discarded: 'Discarded',
fixed: 'Fixed',
ignored: 'Ignored',
open: 'Open',
regressed: 'Regressed',
snoozed: 'Snoozed'
};

const statusVariants: Record<string, 'default' | 'destructive' | 'outline' | 'secondary'> = {
discarded: 'outline',
fixed: 'secondary',
ignored: 'outline',
open: 'default',
regressed: 'destructive',
snoozed: 'secondary'
};
</script>

<Badge variant={statusVariants[value] ?? 'outline'}>{statusLabels[value] ?? value}</Badge>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script lang="ts">
import { Badge } from '$comp/ui/badge';

interface Props {
tags: string[] | undefined;
}

let { tags }: Props = $props();
</script>

{#if tags && tags.length > 0}
<div class="flex flex-wrap gap-1">
{#each tags.slice(0, 3) as tag (tag)}
<Badge variant="outline" class="text-xs">{tag}</Badge>
{/each}
{#if tags.length > 3}
<Badge variant="outline" class="text-xs">+{tags.length - 3}</Badge>
{/if}
</div>
{/if}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<script lang="ts">
import type { Stack } from '$features/stacks/models';
import type { Snippet } from 'svelte';

import * as DataTable from '$comp/data-table';
import DelayedRender from '$comp/delayed-render.svelte';
import { type StockFeatures, type Table } from '@tanstack/svelte-table';

interface Props {
footerChildren?: Snippet;
isLoading: boolean;
limit: number;
rowClick?: (row: Stack) => void;
rowHref?: (row: Stack) => string;
table: Table<StockFeatures, Stack>;
}

let { footerChildren, isLoading, limit = $bindable(), rowClick, rowHref, table }: Props = $props();
</script>

<DataTable.Root>
<DataTable.Body {rowClick} {rowHref} {table}>
{#if isLoading}
<DelayedRender>
<DataTable.Loading {table} />
</DelayedRender>
{:else}
<DataTable.Empty {table} />
{/if}
</DataTable.Body>
<DataTable.Footer {table} class="w-full">
{#if footerChildren}
{@render footerChildren()}
{:else}
<div class="grid w-full grid-cols-1 items-center gap-2 sm:grid-cols-3">
<div class="flex min-w-0 items-center gap-2">
<DataTable.Selection {table} />
</div>

<div class="flex min-w-0 items-center justify-center">
<DataTable.PageCount {table} />
</div>

<div class="flex min-w-0 items-center justify-end gap-4">
<DataTable.PageSize bind:value={limit} {table} />
<DataTable.Pagination {table} />
</div>
</div>
{/if}
</DataTable.Footer>
</DataTable.Root>
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@

let { gravatar, intercomUnreadCount = 0, isChatEnabled, isImpersonating = false, isLoading, openChat, organizations = [], user }: Props = $props();
const sidebar = useSidebar();
const currentOrganizationId = $derived(organizations.find((organizationItem) => organizationItem.id === organization.current)?.id);
let openImpersonateDialog = $state(false);

function getUnreadCountLabel(unreadCount: number): string {
Expand Down Expand Up @@ -165,12 +166,12 @@
<A variant="ghost" href={resolve('/(app)/account/notifications')} class="w-full" onclick={onMenuClick}>Notifications</A>
<DropdownMenu.Shortcut>⇧⌘gn</DropdownMenu.Shortcut>
</DropdownMenu.Item>
{#if organization.current}
{#if currentOrganizationId}
<DropdownMenu.Item>
<Settings />
<A
variant="ghost"
href={resolve('/(app)/organization/[organizationId]/manage', { organizationId: organization.current })}
href={resolve('/(app)/organization/[organizationId]/manage', { organizationId: currentOrganizationId })}
class="flex w-full items-center gap-2"
onclick={onMenuClick}
>
Expand All @@ -182,7 +183,7 @@
<CreditCard />
<A
variant="ghost"
href={resolve('/(app)/organization/[organizationId]/billing', { organizationId: organization.current })}
href={resolve('/(app)/organization/[organizationId]/billing', { organizationId: currentOrganizationId })}
class="flex w-full items-center gap-2"
onclick={onMenuClick}
>
Expand Down
13 changes: 9 additions & 4 deletions src/Exceptionless.Web/ClientApp/src/routes/(app)/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,10 @@
});
const intercomOrganization = $derived(shouldFetchIntercomOrganization ? currentOrganizationQuery.data : undefined);

// Simple organization selection - pick first available if none selected
// Keep selected organization synchronized with current memberships.
$effect(() => {
void page.url.pathname;

if (!organizationsQuery.isSuccess) {
return;
}
Expand All @@ -243,14 +245,17 @@

// Redirect non-admins to add organization page
if (!isGlobalAdmin && !organizationsQuery.isLoading) {
goto(resolve(`/(app)/organization/add`));
const addOrganizationPath = resolve('/(app)/organization/add');
if (page.url.pathname !== addOrganizationPath) {
goto(addOrganizationPath);
}
}

return;
}

// Select first organization if none selected
if (!organization.current) {
const hasSelectedOrganization = !!organization.current && organizations.some((organizationItem) => organizationItem.id === organization.current);
if (!hasSelectedOrganization && !impersonatingOrganizationId) {
organization.current = organizations[0]!.id;
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
return;
}

if (organizationQuery.isSuccess && organizationId !== organization.current) {
if (organizationQuery.isSuccess && organization.current && organizationId !== organization.current) {
goto(page.url.pathname.replace(`/organization/${organizationId}`, `/organization/${organization.current}`));
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Issues from '@lucide/svelte/icons/bug';
import EventStream from '@lucide/svelte/icons/calendar-arrow-down';
import Events from '@lucide/svelte/icons/calendar-days';
import Support from '@lucide/svelte/icons/circle-help';
import Stacks from '@lucide/svelte/icons/layers';
import Sessions from '@lucide/svelte/icons/timer';

import type { NavigationItem } from '../routes.svelte';
Expand Down Expand Up @@ -37,6 +38,12 @@ export function routes(): NavigationItem[] {
icon: EventStream,
title: 'Event Stream'
},
{
group: 'Reports',
href: resolve('/(app)/stacks'),
icon: Stacks,
title: 'Stacks'
},
{
group: 'Reports',
href: resolve('/(app)/sessions'),
Expand Down
Loading