Skip to content
Merged
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: 1 addition & 5 deletions src/Exceptionless.Web/ClientApp/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -152,11 +152,7 @@
}

html {
overflow-y: scroll;
}

html:has([data-slot='sheet-content']) {
overflow-y: hidden;
overflow: hidden;
}

body {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,17 @@
<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!' }}
preventScroll={false}
>
<Sheet.Header>
<Sheet.Title class="flex items-center gap-2">
<Sheet.Header class="pt-4.5 pb-0">
<Sheet.Title class="flex items-center gap-2 text-2xl font-semibold tracking-tight" level={3}>
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">
<div class="mt-0.5 px-4">
{#if eventId}
<EventsOverview {filterChanged} id={eventId} {handleError} />
{/if}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
<script module lang="ts">
import type { RowData } from '@tanstack/svelte-table';

type TData = RowData;
</script>

<script generics="TData extends RowData" lang="ts">
import type { StockFeatures, Table } from '@tanstack/svelte-table';

import { Muted } from '$comp/typography';
import { Button } from '$comp/ui/button';
import * as Dialog from '$comp/ui/dialog';
import ChevronDown from '@lucide/svelte/icons/chevron-down';
import ChevronUp from '@lucide/svelte/icons/chevron-up';
import GripVertical from '@lucide/svelte/icons/grip-vertical';
import Plus from '@lucide/svelte/icons/plus';
import X from '@lucide/svelte/icons/x';

interface Props {
open: boolean;
table: Table<StockFeatures, TData>;
}

let { open = $bindable(), table }: Props = $props();

let draggedColumnId = $state<null | string>(null);

const allColumns = $derived(table.getAllLeafColumns().filter((column) => column.id !== 'select'));
const visibleColumns = $derived(allColumns.filter((column) => column.getIsVisible()));
const availableColumns = $derived(allColumns.filter((column) => column.getCanHide() && !column.getIsVisible()));

function getColumnLabel(column: (typeof allColumns)[number]): string {
if (typeof column.columnDef.header === 'string') {
return column.columnDef.header;
}

return column.id.replace(/[_-]/g, ' ').replace(/\w\S*/g, (word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase());
}

function addColumn(column: (typeof allColumns)[number]): void {
column.toggleVisibility(true);
}

function removeColumn(column: (typeof allColumns)[number]): void {
if (visibleColumns.length > 1) {
column.toggleVisibility(false);
}
}

function canRemoveColumn(column: (typeof allColumns)[number]): boolean {
return column.getCanHide() && visibleColumns.length > 1;
}

function moveColumnUp(columnId: string): void {
const columnIds = visibleColumns.map((c) => c.id);
const index = columnIds.indexOf(columnId);
if (index <= 0) {
return;
}

const temp = columnIds[index]!;
columnIds[index] = columnIds[index - 1]!;
columnIds[index - 1] = temp;
applyColumnOrder(columnIds);
}

function moveColumnDown(columnId: string): void {
const columnIds = visibleColumns.map((c) => c.id);
const index = columnIds.indexOf(columnId);
if (index === -1 || index >= columnIds.length - 1) {
return;
}

const temp = columnIds[index]!;
columnIds[index] = columnIds[index + 1]!;
columnIds[index + 1] = temp;
applyColumnOrder(columnIds);
}

function applyColumnOrder(columnIds: string[]): void {
const hiddenIds = allColumns.filter((c) => !c.getIsVisible()).map((c) => c.id);
table.setColumnOrder(['select', ...columnIds, ...hiddenIds]);
}

function handleDragStart(event: DragEvent, columnId: string): void {
draggedColumnId = columnId;
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', columnId);
}
}

function handleDragOver(event: DragEvent, targetColumnId: string): void {
event.preventDefault();
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'move';
}

if (draggedColumnId && draggedColumnId !== targetColumnId) {
const columnIds = visibleColumns.map((c) => c.id);
const fromIndex = columnIds.indexOf(draggedColumnId);
const toIndex = columnIds.indexOf(targetColumnId);
if (fromIndex === -1 || toIndex === -1) {
return;
}

const [moved] = columnIds.splice(fromIndex, 1);
if (moved) {
columnIds.splice(toIndex, 0, moved);
applyColumnOrder(columnIds);
}
}
}

function handleDragEnd(): void {
draggedColumnId = null;
}
</script>

<Dialog.Root bind:open>
<Dialog.Content class="sm:max-w-2xl" preventScroll={false} overlayClass="bg-black/20 supports-backdrop-filter:backdrop-blur-none">
<Dialog.Header>
<Dialog.Title>Manage Columns</Dialog.Title>
<Dialog.Description>
<Muted class="text-xs">Add columns from the available list and reorder selected columns.</Muted>
</Dialog.Description>
</Dialog.Header>

<div class="grid grid-cols-2 gap-4">
<!-- Available columns -->
<div class="flex flex-col gap-2">
<h3 class="text-sm font-medium">Available</h3>
<div class="border-input rounded-md border">
<div class="max-h-64 overflow-y-auto p-2">
{#if availableColumns.length === 0}
<p class="text-muted-foreground py-4 text-center text-sm">All columns are visible</p>
{:else}
{#each availableColumns as column (column.id)}
<div class="hover:bg-accent flex items-center justify-between rounded-sm px-2 py-1.5 text-sm">
<span>{getColumnLabel(column)}</span>
<Button variant="ghost" size="icon-xs" onclick={() => addColumn(column)} title="Add column">
<Plus class="size-3.5" />
</Button>
</div>
{/each}
{/if}
</div>
</div>
</div>

<!-- Selected columns -->
<div class="flex flex-col gap-2">
<h3 class="text-sm font-medium">Selected</h3>
<div class="border-input rounded-md border">
<div class="max-h-64 overflow-y-auto p-2" role="list">
{#each visibleColumns as column, index (column.id)}
<div
class={[
'group/column flex cursor-grab items-center gap-1 rounded-sm px-2 py-1.5 text-sm active:cursor-grabbing',
draggedColumnId === column.id && 'bg-accent/70'
]}
draggable="true"
ondragstart={(event) => handleDragStart(event, column.id)}
ondragover={(event) => handleDragOver(event, column.id)}
ondragend={handleDragEnd}
role="listitem"
>
<GripVertical class="text-muted-foreground/60 size-3.5 shrink-0 opacity-0 transition-opacity group-hover/column:opacity-100" />
<span class="min-w-0 flex-1 truncate">{getColumnLabel(column)}</span>
<div class="flex shrink-0 items-center gap-0.5 opacity-0 transition-opacity group-hover/column:opacity-100">
<Button variant="ghost" size="icon-xs" onclick={() => moveColumnUp(column.id)} disabled={index === 0} title="Move up">
<ChevronUp class="size-3.5" />
</Button>
<Button
variant="ghost"
size="icon-xs"
onclick={() => moveColumnDown(column.id)}
disabled={index === visibleColumns.length - 1}
title="Move down"
>
<ChevronDown class="size-3.5" />
</Button>
{#if canRemoveColumn(column)}
<Button variant="ghost" size="icon-xs" onclick={() => removeColumn(column)} title="Remove column">
<X class="size-3.5" />
</Button>
{/if}
</div>
</div>
{/each}
</div>
</div>
</div>
</div>
</Dialog.Content>
</Dialog.Root>
Loading