Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
35 changes: 19 additions & 16 deletions app/src/app/(dashboard)/[id]/edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,9 @@ export default function DashboardEditorPage({
initialTemplate={
pendingTemplateId ? templateMap[pendingTemplateId] : undefined
}
initialPreviewData={editorMode === "edit" ? cachedPreviewData : undefined}
initialPreviewData={
editorMode === "edit" ? cachedPreviewData : undefined
}
/>

{templateWidget &&
Expand Down Expand Up @@ -545,23 +547,24 @@ export default function DashboardEditorPage({
<DashboardContainer
page={page}
editable
onRemoveWidget={removeWidget}
onEditWidget={openEditWidget}
onDuplicateWidget={duplicateWidget}
onLayoutChange={isActive ? updateGridLayout : undefined}
onWidgetSettingsChange={(widgetId, settings) => {
const target = page.widgets.find(
(w) => w.id === widgetId,
);
if (target) {
updateWidget(widgetId, { ...target, settings });
}
actions={{
onRemoveWidget: removeWidget,
onEditWidget: openEditWidget,
onDuplicateWidget: duplicateWidget,
onLayoutChange: isActive ? updateGridLayout : undefined,
onWidgetSettingsChange: (widgetId, settings) => {
const target = page.widgets.find(
(w) => w.id === widgetId,
);
if (target)
updateWidget(widgetId, { ...target, settings });
},
onNavigateToPage: handleNavigateToPage,
onSaveAsTemplate: setTemplateWidget,
onSyncWidget: handleSyncWidget,
onDetachWidget: handleDetachWidget,
}}
onNavigateToPage={handleNavigateToPage}
onSaveAsTemplate={setTemplateWidget}
templateMap={templateMap}
onSyncWidget={handleSyncWidget}
onDetachWidget={handleDetachWidget}
showParameterBar={showParameterBar}
/>
</div>
Expand Down
136 changes: 99 additions & 37 deletions app/src/app/(dashboard)/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
"use client";

import React, { use, useCallback, useEffect, useMemo, useRef, useState, useTransition } from "react";
import React, {
use,
useCallback,
useEffect,
useMemo,
useRef,
useState,
useTransition,
} from "react";
import { useRouter } from "next/navigation";
import { ArrowLeft, Filter, Pencil, LayoutDashboard, RefreshCw } from "lucide-react";
import {
ArrowLeft,
Filter,
Pencil,
LayoutDashboard,
RefreshCw,
} from "lucide-react";
import { useDashboard, useUpdateDashboard } from "@/hooks/use-dashboards";
import { useParameterStore } from "@/stores/parameter-store";
import { filterParentParams } from "@/lib/format-parameter-value";
Expand Down Expand Up @@ -79,7 +93,7 @@
const [showParameterBar, setShowParameterBar] = useState(true);
const [activePageIndex, setActivePageIndex] = useState(0);
const [visitedPages, setVisitedPages] = useState<Set<number>>(
() => new Set([0])
() => new Set([0]),
);

function markVisited(index: number) {
Expand All @@ -98,13 +112,17 @@
const [isPending, startTransition] = useTransition();
const layout = useMemo(
() => (dashboard ? migrateLayout(dashboard.layoutJson) : null),
[dashboard]
[dashboard],
);

// Auto-refresh: local override (null = use persisted settings from layout).
// Keyed by dashboard id so navigating to a different dashboard resets the override.
const [localSettings, setLocalSettings] = useState<{ dashboardId: string; settings: DashboardSettings } | null>(null);
const activeLocalSettings = localSettings?.dashboardId === id ? localSettings.settings : null;
const [localSettings, setLocalSettings] = useState<{
dashboardId: string;
settings: DashboardSettings;
} | null>(null);
const activeLocalSettings =
localSettings?.dashboardId === id ? localSettings.settings : null;
const autoRefreshSettings = activeLocalSettings ?? layout?.settings ?? {};
const refetchInterval = getRefetchInterval(autoRefreshSettings);

Expand All @@ -126,12 +144,18 @@
: { autoRefresh: true, refreshIntervalSeconds: seconds };
setLocalSettings({ dashboardId: id, settings: newSettings });
if (layout) {
const payload = { id, layoutJson: { ...layout, settings: newSettings } };
const payload = {
id,
layoutJson: { ...layout, settings: newSettings },
};
persistQueueRef.current = persistQueueRef.current
.catch(() => undefined)
.then(() => updateDashboard.mutateAsync(payload))
.catch((err: unknown) => {
console.error("[auto-save] Failed to persist dashboard settings:", err);
console.error(
"[auto-save] Failed to persist dashboard settings:",
err,
);
});
}
},
Expand All @@ -154,15 +178,19 @@
}, [customSeconds, applyInterval]);

// Derive display values from the effective (normalized) interval
const effectiveSeconds = typeof refetchInterval === "number" ? refetchInterval / 1000 : null;
const intervalLabel = effectiveSeconds !== null
? formatInterval(effectiveSeconds)
: "Auto-refresh";
const dropdownValue = effectiveSeconds !== null ? String(effectiveSeconds) : "off";
const effectiveSeconds =
typeof refetchInterval === "number" ? refetchInterval / 1000 : null;
const intervalLabel =
effectiveSeconds !== null

Check warning on line 184 in app/src/app/(dashboard)/[id]/page.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected negated condition.

See more on https://sonarcloud.io/project/issues?id=alfredo1996_neoboard&issues=AZ0roazPNWet5u0PgWDj&open=AZ0roazPNWet5u0PgWDj&pullRequest=203
? formatInterval(effectiveSeconds)
: "Auto-refresh";
const dropdownValue =
effectiveSeconds !== null ? String(effectiveSeconds) : "off";

Check warning on line 188 in app/src/app/(dashboard)/[id]/page.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected negated condition.

See more on https://sonarcloud.io/project/issues?id=alfredo1996_neoboard&issues=AZ0roazPNWet5u0PgWDk&open=AZ0roazPNWet5u0PgWDk&pullRequest=203
// Toolbar button label: show interval + live countdown when active
const buttonLabel = countdown !== null
? `${intervalLabel} · ${formatCountdown(countdown)}`
: intervalLabel;
const buttonLabel =
countdown !== null

Check warning on line 191 in app/src/app/(dashboard)/[id]/page.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected negated condition.

See more on https://sonarcloud.io/project/issues?id=alfredo1996_neoboard&issues=AZ0roazPNWet5u0PgWDl&open=AZ0roazPNWet5u0PgWDl&pullRequest=203
? `${intervalLabel} · ${formatCountdown(countdown)}`
: intervalLabel;

const handleNavigateToPage = useCallback(
(pageId: string) => {
Expand All @@ -173,7 +201,7 @@
setActivePageIndex(index);
}
},
[layout]
[layout],
);

if (isLoading) {
Expand Down Expand Up @@ -211,17 +239,16 @@
// layout is non-null here because dashboard is defined (guarded above)
const resolvedLayout = layout!;
const safeIndex = Math.min(activePageIndex, resolvedLayout.pages.length - 1);
const canEdit = dashboard.role === "owner" || dashboard.role === "editor" || dashboard.role === "admin";
const canEdit =
dashboard.role === "owner" ||
dashboard.role === "editor" ||
dashboard.role === "admin";

return (
<div className="flex flex-col h-full">
<Toolbar>
<ToolbarSection>
<Button
variant="ghost"
size="sm"
onClick={() => router.push("/")}
>
<Button variant="ghost" size="sm" onClick={() => router.push("/")}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back
</Button>
Expand All @@ -231,7 +258,9 @@
<Badge variant="secondary">{dashboard.role}</Badge>
<span className="text-xs text-muted-foreground">
· updated <TimeAgo date={dashboard.updatedAt} showTooltip={false} />
{dashboard.updatedByName ? <> by {dashboard.updatedByName}</> : null}
{dashboard.updatedByName ? (
<> by {dashboard.updatedByName}</>
) : null}
</span>
</ToolbarSection>
<ToolbarSection>
Expand All @@ -241,16 +270,24 @@
size="sm"
disabled={!hasParameters}
onClick={() => setShowParameterBar((prev) => !prev)}
aria-label={showParameterBar ? "Hide parameters" : "Show parameters"}
aria-label={
showParameterBar ? "Hide parameters" : "Show parameters"
}
>
<Filter className="mr-2 h-4 w-4" />
{!hasParameters || showParameterBar ? "Filters" : `Filters (${parameterCount})`}
{!hasParameters || showParameterBar
? "Filters"
: `Filters (${parameterCount})`}
</Button>
{canEdit && (
<>
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" data-testid="auto-refresh-trigger">
<Button
variant="ghost"
size="sm"
data-testid="auto-refresh-trigger"
>
<RefreshCw
className={`mr-2 h-4 w-4${isFetching ? " animate-spin" : ""}`}
/>
Expand All @@ -264,23 +301,39 @@
value={dropdownValue}
onValueChange={handleIntervalChange}
>
<DropdownMenuRadioItem value="off">Off</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="30">30 seconds</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="60">1 minute</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="300">5 minutes</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="600">10 minutes</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="off">
Off
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="30">
30 seconds
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="60">
1 minute
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="300">
5 minutes
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="600">
10 minutes
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
<DropdownMenuSeparator />
<div className="px-2 py-1.5 space-y-1.5">
<p className="text-xs text-muted-foreground">Custom (seconds)</p>
<p className="text-xs text-muted-foreground">
Custom (seconds)
</p>
<div className="flex gap-1.5">
<Input
type="number"
min={5}
placeholder="e.g. 5"
value={customSeconds}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setCustomSeconds(e.target.value)}
onKeyDown={(e: React.KeyboardEvent) => { if (e.key === "Enter") handleCustomApply(); }}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setCustomSeconds(e.target.value)
}
onKeyDown={(e: React.KeyboardEvent) => {
if (e.key === "Enter") handleCustomApply();
}}
className="h-7 text-xs"
data-testid="custom-interval-input"
/>
Expand All @@ -302,7 +355,11 @@
size="sm"
loading={isPending}
loadingText="Opening editor..."
onClick={() => startTransition(() => router.push(`/${id}/edit?page=${safeIndex}`))}
onClick={() =>
startTransition(() =>
router.push(`/${id}/edit?page=${safeIndex}`),
)
}
>
<Pencil className="mr-2 h-4 w-4" />
Edit
Expand Down Expand Up @@ -350,7 +407,12 @@
className={isActive ? undefined : "hidden"}
aria-hidden={!isActive}
>
<DashboardContainer page={page} refetchInterval={refetchInterval} onNavigateToPage={handleNavigateToPage} showParameterBar={showParameterBar} />
<DashboardContainer
page={page}
refetchInterval={refetchInterval}
actions={{ onNavigateToPage: handleNavigateToPage }}
showParameterBar={showParameterBar}
/>
</div>
);
})}
Expand Down
Loading
Loading