Changes full workspace config. Still some work to do ui + logic
This commit is contained in:
@@ -1 +1 @@
|
|||||||
VITE_GATEWAY_BASE_URL=http://localhost:18080
|
VITE_GATEWAY_BASE_URL=http://146.59.230.190:18080
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
"chart-*",
|
"chart-*",
|
||||||
"maincharts-*",
|
"maincharts-*",
|
||||||
"climatecharts-*",
|
"climatecharts-*",
|
||||||
"meteocharts-*"
|
"meteocharts-*",
|
||||||
|
"workspace-*"
|
||||||
],
|
],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
"core:window:allow-show",
|
"core:window:allow-show",
|
||||||
"core:window:allow-hide",
|
"core:window:allow-hide",
|
||||||
"core:window:allow-set-focus",
|
"core:window:allow-set-focus",
|
||||||
|
"core:window:allow-set-title",
|
||||||
|
|
||||||
"core:window:allow-minimize",
|
"core:window:allow-minimize",
|
||||||
"core:window:allow-maximize",
|
"core:window:allow-maximize",
|
||||||
|
|||||||
+8
-4
@@ -7,7 +7,7 @@ import { MeteoPage } from "../features/meteo/pages/MeteoPage";
|
|||||||
import { ClimateChartsPage } from "../features/climate/pages/ClimateChartsPage";
|
import { ClimateChartsPage } from "../features/climate/pages/ClimateChartsPage";
|
||||||
import { ConsolePage } from "../features/console/pages/ConsolePage";
|
import { ConsolePage } from "../features/console/pages/ConsolePage";
|
||||||
import { MainChartsPage } from "../features/maincharts/pages/MainChartsPage";
|
import { MainChartsPage } from "../features/maincharts/pages/MainChartsPage";
|
||||||
import { ChartWindowPage } from "../features/chartworkspace/pages/ChartWindowPage";
|
import { WorkspaceWindowPage } from "../features/chartworkspace/pages/WorkspaceWindowPage";
|
||||||
import { SettingsPage } from "../features/settings/pages/SettingsPage";
|
import { SettingsPage } from "../features/settings/pages/SettingsPage";
|
||||||
import SynopticPage from "../features/synoptic/pages/SynopticPage";
|
import SynopticPage from "../features/synoptic/pages/SynopticPage";
|
||||||
import MeteoChartsPage from "../features/meteo/pages/MeteoChartsPage";
|
import MeteoChartsPage from "../features/meteo/pages/MeteoChartsPage";
|
||||||
@@ -38,13 +38,17 @@ function App() {
|
|||||||
const { authenticated } = useAuth();
|
const { authenticated } = useAuth();
|
||||||
const [activePage, setActivePage] = useState<AppPage>("dashboard");
|
const [activePage, setActivePage] = useState<AppPage>("dashboard");
|
||||||
|
|
||||||
const isChartWindow = window.location.pathname.startsWith("/chart-window/");
|
const isWorkspaceWindow = window.location.pathname.startsWith("/workspace-window/");
|
||||||
|
|
||||||
if (isChartWindow) {
|
if (isWorkspaceWindow) {
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const theme = params.get("theme") === "light" ? "light" : "dark";
|
const theme = params.get("theme") === "light" ? "light" : "dark";
|
||||||
|
|
||||||
return <ChartWindowPage theme={theme} />;
|
return (
|
||||||
|
<RuntimeConfigProvider>
|
||||||
|
<WorkspaceWindowPage theme={theme} />
|
||||||
|
</RuntimeConfigProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!authenticated) {
|
if (!authenticated) {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
Download,
|
Download,
|
||||||
Maximize2,
|
|
||||||
Save,
|
Save,
|
||||||
Settings2,
|
Settings2,
|
||||||
SlidersHorizontal,
|
SlidersHorizontal,
|
||||||
@@ -73,8 +72,6 @@ type Props = {
|
|||||||
onIntervalChange: (interval: WorkspaceChartInterval) => void;
|
onIntervalChange: (interval: WorkspaceChartInterval) => void;
|
||||||
onSave?: () => void;
|
onSave?: () => void;
|
||||||
onExport?: () => void;
|
onExport?: () => void;
|
||||||
onDetach?: () => void;
|
|
||||||
onAttach?: () => void;
|
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
configuredVariableCount?: number;
|
configuredVariableCount?: number;
|
||||||
};
|
};
|
||||||
@@ -127,8 +124,6 @@ export function WorkspaceChart({
|
|||||||
onIntervalChange,
|
onIntervalChange,
|
||||||
onSave,
|
onSave,
|
||||||
onExport,
|
onExport,
|
||||||
onDetach,
|
|
||||||
onAttach,
|
|
||||||
configuredVariableCount = chart.variables.length
|
configuredVariableCount = chart.variables.length
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const isDark = theme === "dark";
|
const isDark = theme === "dark";
|
||||||
@@ -360,16 +355,6 @@ export function WorkspaceChart({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{onDetach && onAttach && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={detached ? onAttach : onDetach}
|
|
||||||
title={detached ? "Repor janela" : "Destacar janela"}
|
|
||||||
className={windowButtonClass(isDark)}
|
|
||||||
>
|
|
||||||
<Maximize2 className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { authFetch } from "../../../lib/api/authFetch";
|
||||||
|
import { getBackendApiUrl } from "../../../lib/api/gatewayConfig";
|
||||||
|
import { readJsonResponse } from "../../../lib/api/readJsonResponse";
|
||||||
|
import type { ChartLayoutMode } from "../hooks/useChartWorkspacePersistence";
|
||||||
|
import type { ChartWorkspaceScope } from "../types";
|
||||||
|
|
||||||
|
export type ChartWorkspaceResponse = {
|
||||||
|
id: number;
|
||||||
|
scope: ChartWorkspaceScope;
|
||||||
|
name: string;
|
||||||
|
sortOrder: number;
|
||||||
|
defaultWorkspace: boolean;
|
||||||
|
layoutMode: ChartLayoutMode;
|
||||||
|
chartsJson: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ChartWorkspaceSaveRequest = {
|
||||||
|
name?: string;
|
||||||
|
sortOrder?: number;
|
||||||
|
defaultWorkspace?: boolean;
|
||||||
|
layoutMode: ChartLayoutMode;
|
||||||
|
chartsJson: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function workspaceUrl(workspaceId: number | null, scope: ChartWorkspaceScope) {
|
||||||
|
if (workspaceId !== null) {
|
||||||
|
return getBackendApiUrl(`/api/chart-workspaces/id/${workspaceId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getBackendApiUrl(`/api/chart-workspaces/${scope}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listChartWorkspaces(scope: ChartWorkspaceScope) {
|
||||||
|
const response = await authFetch(
|
||||||
|
getBackendApiUrl(`/api/chart-workspaces?scope=${scope}`),
|
||||||
|
);
|
||||||
|
|
||||||
|
return readJsonResponse<ChartWorkspaceResponse[]>(
|
||||||
|
response,
|
||||||
|
"Failed to list chart workspaces",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadChartWorkspace(
|
||||||
|
scope: ChartWorkspaceScope,
|
||||||
|
workspaceId: number | null = null,
|
||||||
|
) {
|
||||||
|
const response = await authFetch(workspaceUrl(workspaceId, scope));
|
||||||
|
|
||||||
|
if (response.status === 404 || response.status === 500) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return readJsonResponse<ChartWorkspaceResponse>(
|
||||||
|
response,
|
||||||
|
"Failed to load chart workspace",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createChartWorkspace(
|
||||||
|
scope: ChartWorkspaceScope,
|
||||||
|
request: ChartWorkspaceSaveRequest,
|
||||||
|
) {
|
||||||
|
const response = await authFetch(
|
||||||
|
getBackendApiUrl(`/api/chart-workspaces?scope=${scope}`),
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(request),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return readJsonResponse<ChartWorkspaceResponse>(
|
||||||
|
response,
|
||||||
|
"Failed to create chart workspace",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveChartWorkspace(
|
||||||
|
scope: ChartWorkspaceScope,
|
||||||
|
request: ChartWorkspaceSaveRequest,
|
||||||
|
workspaceId: number | null = null,
|
||||||
|
) {
|
||||||
|
const response = await authFetch(workspaceUrl(workspaceId, scope), {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(request),
|
||||||
|
});
|
||||||
|
|
||||||
|
return readJsonResponse<ChartWorkspaceResponse>(
|
||||||
|
response,
|
||||||
|
"Failed to save chart workspace",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteChartWorkspace(workspaceId: number) {
|
||||||
|
const response = await authFetch(
|
||||||
|
getBackendApiUrl(`/api/chart-workspaces/id/${workspaceId}`),
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to delete chart workspace: ${response.status}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,8 @@ import type {
|
|||||||
WorkspaceChartInterval,
|
WorkspaceChartInterval,
|
||||||
} from "../../../components/charts/WorkspaceChart";
|
} from "../../../components/charts/WorkspaceChart";
|
||||||
import type { ChartVariable } from "../../telemetry/types/telemetryCatalog";
|
import type { ChartVariable } from "../../telemetry/types/telemetryCatalog";
|
||||||
|
import { ModeButton } from "./ChartWorkspaceControls";
|
||||||
|
import { getVariableColor } from "../domain/chartWorkspaceModel";
|
||||||
|
|
||||||
export type ChartConfigModalChart = {
|
export type ChartConfigModalChart = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -369,48 +371,4 @@ function EmptyVariableList({ theme }: { theme: "dark" | "light" }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ModeButton({
|
|
||||||
theme,
|
|
||||||
active,
|
|
||||||
icon,
|
|
||||||
label,
|
|
||||||
onClick,
|
|
||||||
}: {
|
|
||||||
theme: "dark" | "light";
|
|
||||||
active: boolean;
|
|
||||||
icon?: React.ReactNode;
|
|
||||||
label: string;
|
|
||||||
onClick: () => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onClick}
|
|
||||||
className={
|
|
||||||
active
|
|
||||||
? theme === "dark"
|
|
||||||
? `${RADIUS} flex items-center justify-center gap-2 bg-[#4FD1C5] px-3 py-2 text-xs font-black text-[#07101B]`
|
|
||||||
: `${RADIUS} flex items-center justify-center gap-2 bg-[#0F766E] px-3 py-2 text-xs font-black text-white`
|
|
||||||
: theme === "dark"
|
|
||||||
? `${RADIUS} flex items-center justify-center gap-2 border border-[#263247] bg-[#111A2B] px-3 py-2 text-xs font-bold text-[#A8B3C7]`
|
|
||||||
: `${RADIUS} flex items-center justify-center gap-2 border border-[#D7DEE8] bg-[#F8FAFC] px-3 py-2 text-xs font-bold text-slate-600`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
{label}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getVariableColor(index: number) {
|
|
||||||
const colors = [
|
|
||||||
"#4FD1C5",
|
|
||||||
"#3B82F6",
|
|
||||||
"#FACC15",
|
|
||||||
"#7DD3FC",
|
|
||||||
"#A5B4FC",
|
|
||||||
"#FB7185",
|
|
||||||
];
|
|
||||||
|
|
||||||
return colors[index % colors.length];
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,538 @@
|
|||||||
|
import { useState, type ReactNode } from "react";
|
||||||
|
import { BarChart3, ChevronDown, Pencil, Play, Plus, Search, Trash2, X } from "lucide-react";
|
||||||
|
import type { ChartWorkspaceResponse } from "../api/chartWorkspaceApi";
|
||||||
|
|
||||||
|
const RADIUS = "rounded-[6px]";
|
||||||
|
|
||||||
|
export function LayoutButton({
|
||||||
|
theme,
|
||||||
|
active,
|
||||||
|
icon,
|
||||||
|
title,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
theme: "dark" | "light";
|
||||||
|
active: boolean;
|
||||||
|
icon: ReactNode;
|
||||||
|
title: string;
|
||||||
|
onClick: () => void;
|
||||||
|
}) {
|
||||||
|
const isDark = theme === "dark";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title={title}
|
||||||
|
aria-label={title}
|
||||||
|
onClick={onClick}
|
||||||
|
className={
|
||||||
|
active
|
||||||
|
? isDark
|
||||||
|
? `${RADIUS} grid h-10 w-10 place-items-center bg-[#4FD1C5] text-[#07101B]`
|
||||||
|
: `${RADIUS} grid h-10 w-10 place-items-center bg-[#0F766E] text-white`
|
||||||
|
: isDark
|
||||||
|
? `${RADIUS} grid h-10 w-10 place-items-center border border-[#263247] bg-[#111A2B] text-[#A8B3C7] transition hover:border-[#36506D] hover:text-white`
|
||||||
|
: `${RADIUS} grid h-10 w-10 place-items-center border border-[#D7DEE8] bg-[#F8FAFC] text-slate-600 transition hover:bg-white hover:text-[#0F172A]`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ModeButton({
|
||||||
|
theme,
|
||||||
|
active,
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
theme: "dark" | "light";
|
||||||
|
active: boolean;
|
||||||
|
icon?: ReactNode;
|
||||||
|
label: string;
|
||||||
|
onClick: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
|
className={
|
||||||
|
active
|
||||||
|
? theme === "dark"
|
||||||
|
? `${RADIUS} flex items-center justify-center gap-2 bg-[#4FD1C5] px-3 py-2 text-xs font-black text-[#07101B]`
|
||||||
|
: `${RADIUS} flex items-center justify-center gap-2 bg-[#0F766E] px-3 py-2 text-xs font-black text-white`
|
||||||
|
: theme === "dark"
|
||||||
|
? `${RADIUS} flex items-center justify-center gap-2 border border-[#263247] bg-[#111A2B] px-3 py-2 text-xs font-bold text-[#A8B3C7]`
|
||||||
|
: `${RADIUS} flex items-center justify-center gap-2 border border-[#D7DEE8] bg-[#F8FAFC] px-3 py-2 text-xs font-bold text-slate-600`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmptyWorkspace({
|
||||||
|
theme,
|
||||||
|
canAddMoreCharts,
|
||||||
|
title = "Nenhum gráfico aberto",
|
||||||
|
description = "Abra um gráfico guardado ou crie um novo gráfico para continuar.",
|
||||||
|
addLabel = "Novo Gráfico",
|
||||||
|
savedLabel = "Abrir Guardado",
|
||||||
|
minHeightClass = "min-h-0 flex-1",
|
||||||
|
onAddChart,
|
||||||
|
onOpenSaved,
|
||||||
|
}: {
|
||||||
|
theme: "dark" | "light";
|
||||||
|
canAddMoreCharts: boolean;
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
addLabel?: string;
|
||||||
|
savedLabel?: string;
|
||||||
|
minHeightClass?: string;
|
||||||
|
onAddChart: () => void;
|
||||||
|
onOpenSaved: () => void;
|
||||||
|
}) {
|
||||||
|
const isDark = theme === "dark";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className={
|
||||||
|
isDark
|
||||||
|
? `${RADIUS} flex ${minHeightClass} items-center justify-center border border-dashed border-[#33445F] bg-[#0E1726]/70`
|
||||||
|
: `${RADIUS} flex ${minHeightClass} items-center justify-center border border-dashed border-[#CBD5E1] bg-white`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="max-w-[420px] text-center">
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
isDark
|
||||||
|
? "mx-auto grid h-16 w-16 place-items-center rounded-2xl border border-[#263247] bg-[#111A2B] text-[#4FD1C5]"
|
||||||
|
: "mx-auto grid h-16 w-16 place-items-center rounded-2xl border border-[#D7DEE8] bg-[#F8FAFC] text-[#0F766E]"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<BarChart3 className="h-8 w-8" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2
|
||||||
|
className={
|
||||||
|
isDark
|
||||||
|
? "mt-5 text-xl font-black text-white"
|
||||||
|
: "mt-5 text-xl font-black text-[#0F172A]"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p
|
||||||
|
className={
|
||||||
|
isDark
|
||||||
|
? "mt-3 text-sm leading-6 text-[#A8B3C7]"
|
||||||
|
: "mt-3 text-sm leading-6 text-slate-600"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-6 flex items-center justify-center gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={!canAddMoreCharts}
|
||||||
|
onClick={onAddChart}
|
||||||
|
className={
|
||||||
|
canAddMoreCharts
|
||||||
|
? `inline-flex h-11 items-center gap-3 ${RADIUS} bg-[#18B8A6] px-5 text-sm font-black text-white transition hover:bg-[#21C7B5]`
|
||||||
|
: `inline-flex h-11 cursor-not-allowed items-center gap-3 ${RADIUS} bg-[#18B8A6]/40 px-5 text-sm font-black text-white`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Plus className="h-5 w-5" />
|
||||||
|
{addLabel}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onOpenSaved}
|
||||||
|
className={
|
||||||
|
isDark
|
||||||
|
? `inline-flex h-11 items-center gap-3 ${RADIUS} border border-[#263247] bg-[#111A2B] px-5 text-sm font-black text-white`
|
||||||
|
: `inline-flex h-11 items-center gap-3 ${RADIUS} border border-[#D7DEE8] bg-[#F8FAFC] px-5 text-sm font-black text-[#0F172A]`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Search className="h-5 w-5" />
|
||||||
|
{savedLabel}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WorkspaceSelector({
|
||||||
|
theme,
|
||||||
|
workspaces,
|
||||||
|
activeWorkspaceId,
|
||||||
|
detachedWorkspaceIds,
|
||||||
|
loading,
|
||||||
|
creating,
|
||||||
|
canCreateWorkspace,
|
||||||
|
onSelectWorkspace,
|
||||||
|
onCreateWorkspace,
|
||||||
|
onRenameWorkspace,
|
||||||
|
onDeleteWorkspace,
|
||||||
|
}: {
|
||||||
|
theme: "dark" | "light";
|
||||||
|
workspaces: ChartWorkspaceResponse[];
|
||||||
|
activeWorkspaceId: number | null;
|
||||||
|
detachedWorkspaceIds: Set<number>;
|
||||||
|
loading: boolean;
|
||||||
|
creating: boolean;
|
||||||
|
canCreateWorkspace: boolean;
|
||||||
|
onSelectWorkspace: (workspaceId: number) => void;
|
||||||
|
onCreateWorkspace: (name: string) => void;
|
||||||
|
onRenameWorkspace: (workspaceId: number, name: string) => void | Promise<void>;
|
||||||
|
onDeleteWorkspace: (workspaceId: number) => void | Promise<void>;
|
||||||
|
}) {
|
||||||
|
const isDark = theme === "dark";
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
|
const [renameWorkspace, setRenameWorkspace] =
|
||||||
|
useState<ChartWorkspaceResponse | null>(null);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const activeWorkspace =
|
||||||
|
workspaces.find((workspace) => workspace.id === activeWorkspaceId) ??
|
||||||
|
null;
|
||||||
|
|
||||||
|
const filteredWorkspaces = workspaces.filter((workspace) =>
|
||||||
|
workspace.name.toLowerCase().includes(search.toLowerCase()),
|
||||||
|
);
|
||||||
|
|
||||||
|
const createWorkspace = (name: string) => {
|
||||||
|
onCreateWorkspace(name);
|
||||||
|
setCreateOpen(false);
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renameSelectedWorkspace = (name: string) => {
|
||||||
|
if (!renameWorkspace) return;
|
||||||
|
|
||||||
|
void onRenameWorkspace(renameWorkspace.id, name);
|
||||||
|
setRenameWorkspace(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={loading}
|
||||||
|
onClick={() => setOpen((value) => !value)}
|
||||||
|
className={
|
||||||
|
isDark
|
||||||
|
? `flex h-10 min-w-[260px] items-center justify-between gap-3 ${RADIUS} border border-[#263247] bg-[#111A2B] px-4 text-sm font-black text-white transition hover:border-[#36506D] disabled:cursor-not-allowed disabled:text-[#7F8CA3]`
|
||||||
|
: `flex h-10 min-w-[260px] items-center justify-between gap-3 ${RADIUS} border border-[#D7DEE8] bg-[#F8FAFC] px-4 text-sm font-black text-[#0F172A] transition hover:bg-white disabled:cursor-not-allowed disabled:text-slate-400`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="min-w-0 truncate">
|
||||||
|
{loading
|
||||||
|
? "A carregar workspaces..."
|
||||||
|
: activeWorkspace?.name ?? "Workspaces"}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<ChevronDown className="h-4 w-4 shrink-0" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{open && (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
isDark
|
||||||
|
? `${RADIUS} absolute left-0 top-[calc(100%+8px)] z-40 w-[460px] border border-[#263247] bg-[#0E1726] p-3 shadow-2xl`
|
||||||
|
: `${RADIUS} absolute left-0 top-[calc(100%+8px)] z-40 w-[460px] border border-[#D7DEE8] bg-white p-3 shadow-2xl`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="mb-3 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-sm font-black">Workspaces</h2>
|
||||||
|
<p className={isDark ? "mt-1 text-xs text-[#7F8CA3]" : "mt-1 text-xs text-slate-500"}>
|
||||||
|
{workspaces.length}/10 workspaces neste modulo.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={!canCreateWorkspace || creating}
|
||||||
|
onClick={() => setCreateOpen(true)}
|
||||||
|
className={
|
||||||
|
canCreateWorkspace && !creating
|
||||||
|
? `inline-flex h-9 items-center gap-2 ${RADIUS} bg-[#18B8A6] px-3 text-xs font-black text-white transition hover:bg-[#21C7B5]`
|
||||||
|
: `inline-flex h-9 cursor-not-allowed items-center gap-2 ${RADIUS} bg-[#18B8A6]/40 px-3 text-xs font-black text-white`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Novo
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" onClick={() => setOpen(false)}>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
isDark
|
||||||
|
? `${RADIUS} mb-3 flex h-10 items-center gap-2 border border-[#263247] bg-[#07101B] px-3 text-sm text-[#7F8CA3]`
|
||||||
|
: `${RADIUS} mb-3 flex h-10 items-center gap-2 border border-[#D7DEE8] bg-[#F8FAFC] px-3 text-sm text-slate-500`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Search className="h-4 w-4" />
|
||||||
|
<input
|
||||||
|
value={search}
|
||||||
|
onChange={(event) => setSearch(event.target.value)}
|
||||||
|
placeholder="Pesquisar workspaces..."
|
||||||
|
className="w-full bg-transparent outline-none placeholder:text-inherit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-h-[300px] space-y-2 overflow-y-auto pr-1">
|
||||||
|
{filteredWorkspaces.map((workspace) => {
|
||||||
|
const active = workspace.id === activeWorkspaceId;
|
||||||
|
const detached = detachedWorkspaceIds.has(workspace.id);
|
||||||
|
const chartCount = getWorkspaceChartCount(workspace);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={workspace.id}
|
||||||
|
className={
|
||||||
|
isDark
|
||||||
|
? `${RADIUS} flex w-full items-center justify-between gap-3 border border-[#263247] bg-[#111A2B] px-3 py-3 transition hover:border-[#36506D]`
|
||||||
|
: `${RADIUS} flex w-full items-center justify-between gap-3 border border-[#D7DEE8] bg-[#F8FAFC] px-3 py-3 transition hover:bg-white`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="truncate text-sm font-black">
|
||||||
|
{workspace.name}
|
||||||
|
</p>
|
||||||
|
<p className={isDark ? "mt-1 truncate text-xs text-[#7F8CA3]" : "mt-1 truncate text-xs text-slate-500"}>
|
||||||
|
{chartCount}/10 graficos
|
||||||
|
{detached
|
||||||
|
? " · detached"
|
||||||
|
: workspace.defaultWorkspace
|
||||||
|
? " · ultimo aberto"
|
||||||
|
: ""}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex shrink-0 items-center gap-3">
|
||||||
|
{active ? (
|
||||||
|
<span className="text-xs font-black text-[#4FD1C5]">
|
||||||
|
Aberto
|
||||||
|
</span>
|
||||||
|
) : detached ? (
|
||||||
|
<span className="text-xs font-black text-[#FACC15]">
|
||||||
|
Detached
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
onSelectWorkspace(workspace.id);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
title="Abrir workspace"
|
||||||
|
className="text-[#7F8CA3] transition hover:text-[#4FD1C5]"
|
||||||
|
>
|
||||||
|
<Play className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setRenameWorkspace(workspace)}
|
||||||
|
title="Renomear workspace"
|
||||||
|
className="text-[#7F8CA3] transition hover:text-[#4FD1C5]"
|
||||||
|
>
|
||||||
|
<Pencil className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => void onDeleteWorkspace(workspace.id)}
|
||||||
|
title="Eliminar workspace"
|
||||||
|
className="text-[#7F8CA3] transition hover:text-red-400"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{filteredWorkspaces.length === 0 && (
|
||||||
|
<div className={isDark ? `${RADIUS} border border-dashed border-[#263247] px-4 py-6 text-center text-sm font-bold text-[#7F8CA3]` : `${RADIUS} border border-dashed border-[#D7DEE8] px-4 py-6 text-center text-sm font-bold text-slate-500`}>
|
||||||
|
Nenhum workspace encontrado.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{createOpen && (
|
||||||
|
<NewWorkspaceModal
|
||||||
|
theme={theme}
|
||||||
|
creating={creating}
|
||||||
|
onClose={() => setCreateOpen(false)}
|
||||||
|
onCreate={createWorkspace}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{renameWorkspace && (
|
||||||
|
<WorkspaceNameModal
|
||||||
|
theme={theme}
|
||||||
|
title="Renomear workspace"
|
||||||
|
actionLabel="Guardar"
|
||||||
|
initialName={renameWorkspace.name}
|
||||||
|
creating={false}
|
||||||
|
onClose={() => setRenameWorkspace(null)}
|
||||||
|
onSubmit={renameSelectedWorkspace}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWorkspaceChartCount(workspace: ChartWorkspaceResponse) {
|
||||||
|
try {
|
||||||
|
const charts = JSON.parse(workspace.chartsJson) as unknown[];
|
||||||
|
return Array.isArray(charts) ? charts.length : 0;
|
||||||
|
} catch {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function WorkspaceNameModal({
|
||||||
|
theme,
|
||||||
|
title,
|
||||||
|
actionLabel,
|
||||||
|
initialName = "",
|
||||||
|
creating,
|
||||||
|
onClose,
|
||||||
|
onSubmit,
|
||||||
|
}: {
|
||||||
|
theme: "dark" | "light";
|
||||||
|
title: string;
|
||||||
|
actionLabel: string;
|
||||||
|
initialName?: string;
|
||||||
|
creating: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSubmit: (name: string) => void;
|
||||||
|
}) {
|
||||||
|
const isDark = theme === "dark";
|
||||||
|
const [name, setName] = useState(initialName);
|
||||||
|
const canCreate = name.trim().length > 0 && !creating;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/55 p-6">
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
isDark
|
||||||
|
? `${RADIUS} w-full max-w-[420px] border border-[#263247] bg-[#0E1726] shadow-2xl`
|
||||||
|
: `${RADIUS} w-full max-w-[420px] border border-[#D7DEE8] bg-white shadow-2xl`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<header
|
||||||
|
className={
|
||||||
|
isDark
|
||||||
|
? "flex items-center justify-between border-b border-[#263247] px-5 py-4"
|
||||||
|
: "flex items-center justify-between border-b border-[#D7DEE8] px-5 py-4"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<h2 className="text-sm font-black">{title}</h2>
|
||||||
|
|
||||||
|
<button type="button" onClick={onClose}>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="p-5">
|
||||||
|
<label className="mb-2 block text-sm font-black">
|
||||||
|
Nome
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
autoFocus
|
||||||
|
value={name}
|
||||||
|
onChange={(event) => setName(event.target.value)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Enter" && canCreate) {
|
||||||
|
onSubmit(name);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="Ex: Estufa norte"
|
||||||
|
className={
|
||||||
|
isDark
|
||||||
|
? `${RADIUS} h-11 w-full border border-[#263247] bg-[#07101B] px-3 text-sm font-bold text-white outline-none focus:border-[#4FD1C5]`
|
||||||
|
: `${RADIUS} h-11 w-full border border-[#D7DEE8] bg-[#F8FAFC] px-3 text-sm font-bold text-[#0F172A] outline-none focus:border-[#0F766E]`
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer
|
||||||
|
className={
|
||||||
|
isDark
|
||||||
|
? "flex justify-end gap-3 border-t border-[#263247] px-5 py-4"
|
||||||
|
: "flex justify-end gap-3 border-t border-[#D7DEE8] px-5 py-4"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className={
|
||||||
|
isDark
|
||||||
|
? `${RADIUS} border border-[#263247] bg-[#111A2B] px-4 py-2 text-sm font-bold text-white`
|
||||||
|
: `${RADIUS} border border-[#D7DEE8] bg-[#F8FAFC] px-4 py-2 text-sm font-bold text-[#0F172A]`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={!canCreate}
|
||||||
|
onClick={() => onSubmit(name)}
|
||||||
|
className={
|
||||||
|
canCreate
|
||||||
|
? `${RADIUS} bg-[#18B8A6] px-4 py-2 text-sm font-black text-white transition hover:bg-[#21C7B5]`
|
||||||
|
: `${RADIUS} cursor-not-allowed bg-[#18B8A6]/40 px-4 py-2 text-sm font-black text-white`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{actionLabel}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NewWorkspaceModal({
|
||||||
|
theme,
|
||||||
|
creating,
|
||||||
|
onClose,
|
||||||
|
onCreate,
|
||||||
|
}: {
|
||||||
|
theme: "dark" | "light";
|
||||||
|
creating: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onCreate: (name: string) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<WorkspaceNameModal
|
||||||
|
theme={theme}
|
||||||
|
title="Novo workspace"
|
||||||
|
actionLabel="Criar"
|
||||||
|
creating={creating}
|
||||||
|
onClose={onClose}
|
||||||
|
onSubmit={onCreate}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import type { ChartLayoutMode } from "../hooks/useChartWorkspacePersistence";
|
||||||
|
|
||||||
|
export const MAX_CHARTS = 10;
|
||||||
|
export const MAX_VARIABLES_PER_CHART = 6;
|
||||||
|
|
||||||
|
export const CHART_VARIABLE_COLORS = [
|
||||||
|
"#4FD1C5",
|
||||||
|
"#3B82F6",
|
||||||
|
"#FACC15",
|
||||||
|
"#7DD3FC",
|
||||||
|
"#A5B4FC",
|
||||||
|
"#FB7185",
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getVisibleSlotCount(layoutMode: ChartLayoutMode) {
|
||||||
|
if (layoutMode === "single") return 1;
|
||||||
|
if (layoutMode === "twoColumns") return 2;
|
||||||
|
if (layoutMode === "twoRows") return 2;
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function layoutGridClass(layoutMode: ChartLayoutMode) {
|
||||||
|
if (layoutMode === "fourGrid") {
|
||||||
|
return "grid min-h-0 flex-1 grid-cols-2 grid-rows-2 gap-3 overflow-hidden";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layoutMode === "twoColumns") {
|
||||||
|
return "grid min-h-0 flex-1 grid-cols-2 gap-3 overflow-hidden";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layoutMode === "twoRows") {
|
||||||
|
return "grid min-h-0 flex-1 grid-rows-2 gap-3 overflow-hidden";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "grid min-h-0 flex-1 gap-3 overflow-hidden";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getVariableColor(index: number) {
|
||||||
|
return CHART_VARIABLE_COLORS[index % CHART_VARIABLE_COLORS.length];
|
||||||
|
}
|
||||||
@@ -4,9 +4,11 @@ import type {
|
|||||||
WorkspaceChartMode,
|
WorkspaceChartMode,
|
||||||
WorkspaceChartTimeRange,
|
WorkspaceChartTimeRange,
|
||||||
} from "../../../components/charts/WorkspaceChart";
|
} from "../../../components/charts/WorkspaceChart";
|
||||||
import { authFetch } from "../../../lib/api/authFetch";
|
import {
|
||||||
import { getBackendApiUrl } from "../../../lib/api/gatewayConfig";
|
loadChartWorkspace,
|
||||||
import { readJsonResponse } from "../../../lib/api/readJsonResponse";
|
saveChartWorkspace,
|
||||||
|
} from "../api/chartWorkspaceApi";
|
||||||
|
import type { ChartWorkspaceScope } from "../types";
|
||||||
const SAVE_DEBOUNCE_MS = 800;
|
const SAVE_DEBOUNCE_MS = 800;
|
||||||
|
|
||||||
export type ChartLayoutMode =
|
export type ChartLayoutMode =
|
||||||
@@ -25,75 +27,67 @@ export type PersistedChartWorkspaceItem = {
|
|||||||
interval: WorkspaceChartInterval;
|
interval: WorkspaceChartInterval;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ChartWorkspaceScope =
|
|
||||||
| "GLOBAL"
|
|
||||||
| "CLIMATE"
|
|
||||||
| "IRRIGATION"
|
|
||||||
| "METEO"
|
|
||||||
| "LIGHTING"
|
|
||||||
| "HYDRO"
|
|
||||||
| "AEROPONICS";
|
|
||||||
|
|
||||||
type ChartWorkspaceResponse = {
|
|
||||||
id: number;
|
|
||||||
scope: ChartWorkspaceScope;
|
|
||||||
layoutMode: ChartLayoutMode;
|
|
||||||
chartsJson: string;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type UseChartWorkspacePersistenceParams = {
|
type UseChartWorkspacePersistenceParams = {
|
||||||
scope: ChartWorkspaceScope;
|
scope: ChartWorkspaceScope;
|
||||||
|
workspaceId?: number | null;
|
||||||
layoutMode: ChartLayoutMode;
|
layoutMode: ChartLayoutMode;
|
||||||
charts: PersistedChartWorkspaceItem[];
|
charts: PersistedChartWorkspaceItem[];
|
||||||
onLoaded: (workspace: {
|
onLoaded: (workspace: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
defaultWorkspace: boolean;
|
||||||
layoutMode: ChartLayoutMode;
|
layoutMode: ChartLayoutMode;
|
||||||
charts: PersistedChartWorkspaceItem[];
|
charts: PersistedChartWorkspaceItem[];
|
||||||
}) => void;
|
}) => void;
|
||||||
|
saveEnabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useChartWorkspacePersistence({
|
export function useChartWorkspacePersistence({
|
||||||
scope,
|
scope,
|
||||||
|
workspaceId = null,
|
||||||
layoutMode,
|
layoutMode,
|
||||||
charts,
|
charts,
|
||||||
onLoaded,
|
onLoaded,
|
||||||
|
saveEnabled = true,
|
||||||
}: UseChartWorkspacePersistenceParams) {
|
}: UseChartWorkspacePersistenceParams) {
|
||||||
const [loaded, setLoaded] = useState(false);
|
const [loaded, setLoaded] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const saveTimeoutRef = useRef<number | null>(null);
|
const saveTimeoutRef = useRef<number | null>(null);
|
||||||
|
const loadedWorkspaceKeyRef = useRef<string | null>(null);
|
||||||
|
|
||||||
|
const workspaceKey =
|
||||||
|
workspaceId === null ? `scope:${scope}` : `id:${workspaceId}`;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
async function loadWorkspace() {
|
async function loadWorkspace() {
|
||||||
try {
|
setLoaded(false);
|
||||||
const response = await authFetch(
|
setSaving(false);
|
||||||
getBackendApiUrl(`/api/chart-workspaces/${scope}`),
|
setError(null);
|
||||||
);
|
loadedWorkspaceKeyRef.current = null;
|
||||||
|
|
||||||
if (response.status === 404 || response.status === 500) {
|
try {
|
||||||
|
const payload =
|
||||||
|
await loadChartWorkspace(scope, workspaceId);
|
||||||
|
|
||||||
|
if (!payload) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to load workspace: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = await readJsonResponse<ChartWorkspaceResponse>(
|
|
||||||
response,
|
|
||||||
"Failed to load chart workspace",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
|
|
||||||
onLoaded({
|
onLoaded({
|
||||||
|
id: payload.id,
|
||||||
|
name: payload.name,
|
||||||
|
defaultWorkspace: payload.defaultWorkspace,
|
||||||
layoutMode: payload.layoutMode,
|
layoutMode: payload.layoutMode,
|
||||||
charts: JSON.parse(payload.chartsJson) as PersistedChartWorkspaceItem[],
|
charts: JSON.parse(payload.chartsJson) as PersistedChartWorkspaceItem[],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
loadedWorkspaceKeyRef.current = workspaceKey;
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
@@ -112,10 +106,11 @@ export function useChartWorkspacePersistence({
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [scope]);
|
}, [scope, workspaceId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loaded) return;
|
if (!loaded || !saveEnabled) return;
|
||||||
|
if (loadedWorkspaceKeyRef.current !== workspaceKey) return;
|
||||||
|
|
||||||
if (saveTimeoutRef.current !== null) {
|
if (saveTimeoutRef.current !== null) {
|
||||||
window.clearTimeout(saveTimeoutRef.current);
|
window.clearTimeout(saveTimeoutRef.current);
|
||||||
@@ -126,24 +121,15 @@ export function useChartWorkspacePersistence({
|
|||||||
try {
|
try {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
|
|
||||||
const response = await authFetch(
|
await saveChartWorkspace(
|
||||||
getBackendApiUrl(`/api/chart-workspaces/${scope}`),
|
scope,
|
||||||
{
|
{
|
||||||
method: "PUT",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
layoutMode,
|
layoutMode,
|
||||||
chartsJson: JSON.stringify(charts),
|
chartsJson: JSON.stringify(charts),
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
|
workspaceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to save workspace: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to save chart workspace", error);
|
console.error("Failed to save chart workspace", error);
|
||||||
@@ -161,7 +147,7 @@ export function useChartWorkspacePersistence({
|
|||||||
window.clearTimeout(saveTimeoutRef.current);
|
window.clearTimeout(saveTimeoutRef.current);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [charts, layoutMode, loaded, scope]);
|
}, [charts, layoutMode, loaded, saveEnabled, scope, workspaceId, workspaceKey]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loaded,
|
loaded,
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import {
|
||||||
|
createChartWorkspace,
|
||||||
|
deleteChartWorkspace,
|
||||||
|
listChartWorkspaces,
|
||||||
|
saveChartWorkspace,
|
||||||
|
type ChartWorkspaceResponse,
|
||||||
|
} from "../api/chartWorkspaceApi";
|
||||||
|
import type { ChartWorkspaceScope } from "../types";
|
||||||
|
|
||||||
|
type UseChartWorkspaceSelectionParams = {
|
||||||
|
scope: ChartWorkspaceScope;
|
||||||
|
defaultName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useChartWorkspaceSelection({
|
||||||
|
scope,
|
||||||
|
defaultName,
|
||||||
|
}: UseChartWorkspaceSelectionParams) {
|
||||||
|
const [workspaces, setWorkspaces] = useState<ChartWorkspaceResponse[]>([]);
|
||||||
|
const [activeWorkspaceId, setActiveWorkspaceId] = useState<number | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [creating, setCreating] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const activeWorkspace = useMemo(
|
||||||
|
() =>
|
||||||
|
workspaces.find((workspace) => workspace.id === activeWorkspaceId) ??
|
||||||
|
null,
|
||||||
|
[activeWorkspaceId, workspaces],
|
||||||
|
);
|
||||||
|
|
||||||
|
const loadWorkspaces = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const loadedWorkspaces =
|
||||||
|
await listChartWorkspaces(scope);
|
||||||
|
|
||||||
|
setWorkspaces(loadedWorkspaces);
|
||||||
|
setActiveWorkspaceId((current) => {
|
||||||
|
if (current && loadedWorkspaces.some((item) => item.id === current)) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
loadedWorkspaces.find((item) => item.defaultWorkspace)?.id ??
|
||||||
|
loadedWorkspaces[0]?.id ??
|
||||||
|
null
|
||||||
|
);
|
||||||
|
});
|
||||||
|
setError(null);
|
||||||
|
} catch (exception) {
|
||||||
|
console.error("Failed to load chart workspaces", exception);
|
||||||
|
setError("Nao foi possivel carregar os workspaces.");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [scope]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadWorkspaces();
|
||||||
|
}, [loadWorkspaces]);
|
||||||
|
|
||||||
|
const selectWorkspace = useCallback(async (workspaceId: number) => {
|
||||||
|
const workspace =
|
||||||
|
workspaces.find((item) => item.id === workspaceId);
|
||||||
|
|
||||||
|
setActiveWorkspaceId(workspaceId);
|
||||||
|
|
||||||
|
if (!workspace) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await saveChartWorkspace(
|
||||||
|
scope,
|
||||||
|
{
|
||||||
|
name: workspace.name,
|
||||||
|
sortOrder: workspace.sortOrder,
|
||||||
|
defaultWorkspace: true,
|
||||||
|
layoutMode: workspace.layoutMode,
|
||||||
|
chartsJson: workspace.chartsJson,
|
||||||
|
},
|
||||||
|
workspace.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
setWorkspaces((current) =>
|
||||||
|
current.map((item) => ({
|
||||||
|
...item,
|
||||||
|
defaultWorkspace: item.id === workspaceId,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
setError(null);
|
||||||
|
} catch (exception) {
|
||||||
|
console.error("Failed to set default chart workspace", exception);
|
||||||
|
setError("Nao foi possivel guardar o workspace selecionado.");
|
||||||
|
}
|
||||||
|
}, [scope, workspaces]);
|
||||||
|
|
||||||
|
const createWorkspace = useCallback(async (name: string) => {
|
||||||
|
if (workspaces.length >= 10 || creating) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setCreating(true);
|
||||||
|
|
||||||
|
const workspaceName = name.trim();
|
||||||
|
const createdWorkspace = await createChartWorkspace(scope, {
|
||||||
|
name: workspaceName || defaultName,
|
||||||
|
sortOrder: workspaces.length,
|
||||||
|
defaultWorkspace: true,
|
||||||
|
layoutMode: "fourGrid",
|
||||||
|
chartsJson: "[]",
|
||||||
|
});
|
||||||
|
|
||||||
|
setWorkspaces((current) => [
|
||||||
|
...current.map((workspace) => ({
|
||||||
|
...workspace,
|
||||||
|
defaultWorkspace: false,
|
||||||
|
})),
|
||||||
|
createdWorkspace,
|
||||||
|
]);
|
||||||
|
setActiveWorkspaceId(createdWorkspace.id);
|
||||||
|
setError(null);
|
||||||
|
} catch (exception) {
|
||||||
|
console.error("Failed to create chart workspace", exception);
|
||||||
|
setError("Nao foi possivel criar o workspace.");
|
||||||
|
} finally {
|
||||||
|
setCreating(false);
|
||||||
|
}
|
||||||
|
}, [creating, defaultName, scope, workspaces.length]);
|
||||||
|
|
||||||
|
const deleteWorkspace = useCallback(async (workspaceId: number) => {
|
||||||
|
const workspace =
|
||||||
|
workspaces.find((item) => item.id === workspaceId);
|
||||||
|
|
||||||
|
if (!workspace) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteChartWorkspace(workspaceId);
|
||||||
|
|
||||||
|
const remaining =
|
||||||
|
workspaces.filter((item) => item.id !== workspaceId);
|
||||||
|
setWorkspaces(remaining);
|
||||||
|
|
||||||
|
if (activeWorkspaceId === workspaceId) {
|
||||||
|
const nextWorkspace =
|
||||||
|
remaining.find((item) => item.defaultWorkspace) ??
|
||||||
|
remaining[0] ??
|
||||||
|
null;
|
||||||
|
|
||||||
|
setActiveWorkspaceId(nextWorkspace?.id ?? null);
|
||||||
|
|
||||||
|
if (nextWorkspace) {
|
||||||
|
await saveChartWorkspace(
|
||||||
|
scope,
|
||||||
|
{
|
||||||
|
name: nextWorkspace.name,
|
||||||
|
sortOrder: nextWorkspace.sortOrder,
|
||||||
|
defaultWorkspace: true,
|
||||||
|
layoutMode: nextWorkspace.layoutMode,
|
||||||
|
chartsJson: nextWorkspace.chartsJson,
|
||||||
|
},
|
||||||
|
nextWorkspace.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(null);
|
||||||
|
} catch (exception) {
|
||||||
|
console.error("Failed to delete chart workspace", exception);
|
||||||
|
setError("Nao foi possivel eliminar o workspace.");
|
||||||
|
}
|
||||||
|
}, [activeWorkspaceId, scope, workspaces]);
|
||||||
|
|
||||||
|
const updateWorkspaceName = useCallback(
|
||||||
|
(workspaceId: number, name: string) => {
|
||||||
|
setWorkspaces((current) =>
|
||||||
|
current.map((workspace) =>
|
||||||
|
workspace.id === workspaceId
|
||||||
|
? {
|
||||||
|
...workspace,
|
||||||
|
name,
|
||||||
|
}
|
||||||
|
: workspace,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const setActiveWorkspace = useCallback(
|
||||||
|
(workspaceId: number | null) => {
|
||||||
|
setActiveWorkspaceId(workspaceId);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
workspaces,
|
||||||
|
activeWorkspace,
|
||||||
|
activeWorkspaceId,
|
||||||
|
loading,
|
||||||
|
creating,
|
||||||
|
error,
|
||||||
|
canCreateWorkspace: workspaces.length < 10,
|
||||||
|
createWorkspace,
|
||||||
|
deleteWorkspace,
|
||||||
|
updateWorkspaceName,
|
||||||
|
setActiveWorkspace,
|
||||||
|
selectWorkspace,
|
||||||
|
reloadWorkspaces: loadWorkspaces,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,363 +0,0 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
|
||||||
import { emit } from "@tauri-apps/api/event";
|
|
||||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
|
||||||
import { X, Minus, Maximize2, PanelTop, Cog, Square } from "lucide-react";
|
|
||||||
import { ChartConfigModal } from "../components/ChartConfigModal";
|
|
||||||
|
|
||||||
import {
|
|
||||||
WorkspaceChart,
|
|
||||||
type WorkspaceChartConfig,
|
|
||||||
type WorkspaceChartMode,
|
|
||||||
type WorkspaceChartTimeRange,
|
|
||||||
type WorkspaceChartInterval,
|
|
||||||
} from "../../../components/charts/WorkspaceChart";
|
|
||||||
|
|
||||||
import { useTelemetryCatalog } from "../../telemetry/hooks/useTelemetryCatalog";
|
|
||||||
import { useTelemetryChartSeries } from "../../telemetry/hooks/useTelemetryChartSeries";
|
|
||||||
|
|
||||||
import {
|
|
||||||
useChartWorkspacePersistence,
|
|
||||||
type PersistedChartWorkspaceItem,
|
|
||||||
} from "../hooks/useChartWorkspacePersistence";
|
|
||||||
|
|
||||||
type ChartWorkspaceItem = PersistedChartWorkspaceItem & {
|
|
||||||
hidden?: boolean;
|
|
||||||
collapsed?: boolean;
|
|
||||||
detached?: boolean;
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
subtitle: string;
|
|
||||||
mode: WorkspaceChartMode;
|
|
||||||
selectedSensorKeys: string[];
|
|
||||||
hiddenSensorKeys?: string[];
|
|
||||||
timeRange: WorkspaceChartTimeRange;
|
|
||||||
interval: WorkspaceChartInterval;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ChartWindowPageProps = {
|
|
||||||
theme: "dark" | "light";
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ChartWindowPage({ theme }: ChartWindowPageProps) {
|
|
||||||
const isDark = theme === "dark";
|
|
||||||
const parts = window.location.pathname.split("/").filter(Boolean);
|
|
||||||
const chartId = parts[parts.length - 1] ?? "";
|
|
||||||
|
|
||||||
const params = new URLSearchParams(window.location.search);
|
|
||||||
|
|
||||||
const scope =
|
|
||||||
(params.get("scope") as "GLOBAL" | "CLIMATE") ?? "GLOBAL";
|
|
||||||
|
|
||||||
const channel =
|
|
||||||
(params.get("channel") as "maincharts" | "climatecharts") ??
|
|
||||||
"maincharts";
|
|
||||||
|
|
||||||
const { chartableVariables, connected } = useTelemetryCatalog();
|
|
||||||
|
|
||||||
const [charts, setCharts] = useState<ChartWorkspaceItem[]>([]);
|
|
||||||
const [configOpen, setConfigOpen] = useState(false);
|
|
||||||
|
|
||||||
const currentWindow = getCurrentWindow();
|
|
||||||
const latestChartRef = useRef<ChartWorkspaceItem | null>(null);
|
|
||||||
|
|
||||||
const titleBarClass = isDark
|
|
||||||
? "flex h-10 shrink-0 items-center border-b border-white/10 bg-[#071421] text-slate-100"
|
|
||||||
: "flex h-10 shrink-0 items-center border-b border-slate-200 bg-white text-slate-950";
|
|
||||||
|
|
||||||
const titleButtonClass = isDark
|
|
||||||
? "grid h-10 w-11 place-items-center text-slate-400 transition hover:bg-white/10 hover:text-white"
|
|
||||||
: "grid h-10 w-11 place-items-center text-slate-500 transition hover:bg-slate-100 hover:text-slate-950";
|
|
||||||
|
|
||||||
const closeButtonClass = isDark
|
|
||||||
? "grid h-10 w-11 place-items-center text-slate-400 transition hover:bg-red-500 hover:text-white"
|
|
||||||
: "grid h-10 w-11 place-items-center text-slate-500 transition hover:bg-red-500 hover:text-white";
|
|
||||||
|
|
||||||
useChartWorkspacePersistence({
|
|
||||||
scope,
|
|
||||||
layoutMode: "fourGrid",
|
|
||||||
charts,
|
|
||||||
onLoaded: (workspace) => {
|
|
||||||
const loadedCharts = workspace.charts as ChartWorkspaceItem[];
|
|
||||||
setCharts(loadedCharts);
|
|
||||||
|
|
||||||
latestChartRef.current =
|
|
||||||
loadedCharts.find((item) => item.id === chartId) ?? null;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const chart = useMemo(
|
|
||||||
() => charts.find((item) => item.id === chartId) ?? null,
|
|
||||||
[charts, chartId],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
latestChartRef.current = chart;
|
|
||||||
}, [chart]);
|
|
||||||
|
|
||||||
const emitMainUpdate = async (patch: Partial<ChartWorkspaceItem>) => {
|
|
||||||
await emit(`${channel}://update-chart`, {
|
|
||||||
chartId,
|
|
||||||
patch,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const emitMainHidden = async () => {
|
|
||||||
await emit(`${channel}://hide-chart`, {
|
|
||||||
chartId,
|
|
||||||
chart: latestChartRef.current,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const emitMainAttached = async () => {
|
|
||||||
await emit(`${channel}://attach-chart`, {
|
|
||||||
chartId,
|
|
||||||
chart: latestChartRef.current,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const unlistenPromise = currentWindow.onCloseRequested(async (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await emitMainHidden();
|
|
||||||
} finally {
|
|
||||||
await currentWindow.destroy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
void unlistenPromise.then((unlisten) => unlisten());
|
|
||||||
};
|
|
||||||
}, [chartId]);
|
|
||||||
|
|
||||||
const applyLocalPatch = (patch: Partial<ChartWorkspaceItem>) => {
|
|
||||||
setCharts((current) =>
|
|
||||||
current.map((item) => {
|
|
||||||
if (item.id !== chartId) return item;
|
|
||||||
|
|
||||||
const next = {
|
|
||||||
...item,
|
|
||||||
...patch,
|
|
||||||
};
|
|
||||||
|
|
||||||
latestChartRef.current = next;
|
|
||||||
|
|
||||||
return next;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateChart = (patch: Partial<ChartWorkspaceItem>) => {
|
|
||||||
applyLocalPatch(patch);
|
|
||||||
|
|
||||||
void emitMainUpdate(patch).catch((error) => {
|
|
||||||
console.error("Failed to sync chart update to main window", error);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleVariable = (variableKey: string) => {
|
|
||||||
const currentChart = latestChartRef.current;
|
|
||||||
if (!currentChart) return;
|
|
||||||
|
|
||||||
const hiddenSensorKeys = currentChart.hiddenSensorKeys ?? [];
|
|
||||||
const isHidden = hiddenSensorKeys.includes(variableKey);
|
|
||||||
|
|
||||||
const nextHiddenSensorKeys = isHidden
|
|
||||||
? hiddenSensorKeys.filter((key) => key !== variableKey)
|
|
||||||
: [...hiddenSensorKeys, variableKey];
|
|
||||||
|
|
||||||
updateChart({
|
|
||||||
hiddenSensorKeys: nextHiddenSensorKeys,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const { seriesByKey, loading } = useTelemetryChartSeries(
|
|
||||||
chart?.selectedSensorKeys ?? [],
|
|
||||||
chart?.timeRange ?? "24h",
|
|
||||||
chart?.interval ?? "5m",
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectedVariables = useMemo(
|
|
||||||
() =>
|
|
||||||
chartableVariables.filter((variable) =>
|
|
||||||
chart?.selectedSensorKeys.includes(variable.key),
|
|
||||||
),
|
|
||||||
[chart?.selectedSensorKeys, chartableVariables],
|
|
||||||
);
|
|
||||||
|
|
||||||
const attachAndClose = async () => {
|
|
||||||
try {
|
|
||||||
await emitMainAttached();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to attach chart", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
await currentWindow.destroy();
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeAndHide = async () => {
|
|
||||||
try {
|
|
||||||
await emitMainHidden();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to hide chart", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
await currentWindow.destroy();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!chart && charts.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!chart) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
isDark
|
|
||||||
? "flex h-screen items-center justify-center bg-[#071421] text-sm font-bold text-slate-500"
|
|
||||||
: "flex h-screen items-center justify-center bg-white text-sm font-bold text-slate-500"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Gráfico não encontrado.
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const variablesStillResolving =
|
|
||||||
chart.selectedSensorKeys.length > 0 &&
|
|
||||||
selectedVariables.length === 0;
|
|
||||||
|
|
||||||
const chartConfig: WorkspaceChartConfig = {
|
|
||||||
id: chart.id,
|
|
||||||
title: "",
|
|
||||||
subtitle: "",
|
|
||||||
icon: Maximize2,
|
|
||||||
status: connected ? "online" : "offline",
|
|
||||||
sourceLabel: connected ? "Telemetry" : "Offline",
|
|
||||||
mode: chart.mode,
|
|
||||||
timeRange: chart.timeRange,
|
|
||||||
interval: chart.interval,
|
|
||||||
variables: chartableVariables
|
|
||||||
.filter((variable) =>
|
|
||||||
chart.selectedSensorKeys.includes(variable.key),
|
|
||||||
)
|
|
||||||
.map((variable, index) => ({
|
|
||||||
key: variable.key,
|
|
||||||
label: variable.label,
|
|
||||||
unit: variable.unit,
|
|
||||||
color: getVariableColor(index),
|
|
||||||
data: seriesByKey[variable.key] ?? [],
|
|
||||||
visible: !(chart.hiddenSensorKeys ?? []).includes(variable.key),
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
isDark
|
|
||||||
? "flex h-screen flex-col overflow-hidden bg-[#071421] text-slate-100"
|
|
||||||
: "flex h-screen flex-col overflow-hidden bg-white text-slate-950"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<header className={titleBarClass}>
|
|
||||||
<div
|
|
||||||
className="flex h-full flex-1 items-center px-3 text-xs font-black"
|
|
||||||
onPointerDown={() => {
|
|
||||||
void currentWindow.startDragging();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{chart.title}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex h-full items-center">
|
|
||||||
<button type="button" title="Configurar" onClick={() => setConfigOpen(true)} className={titleButtonClass}>
|
|
||||||
<Cog className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button type="button" title="Repor no grid" onClick={attachAndClose} className={titleButtonClass}>
|
|
||||||
<PanelTop className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button type="button" title="Minimizar" onClick={() => currentWindow.minimize()} className={titleButtonClass}>
|
|
||||||
<Minus className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button type="button" title="Maximizar" onClick={() => currentWindow.toggleMaximize()} className={titleButtonClass}>
|
|
||||||
<Square className="h-3.5 w-3.5" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button type="button" title="Fechar" onClick={closeAndHide} className={closeButtonClass}>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main
|
|
||||||
className={
|
|
||||||
isDark
|
|
||||||
? "min-h-0 flex-1 bg-[#071421] [&>section]:h-full [&>section]:rounded-none [&>section]:border-0 [&>section]:bg-[#071421]"
|
|
||||||
: "min-h-0 flex-1 bg-white [&>section]:h-full [&>section]:rounded-none [&>section]:border-0 [&>section]:bg-white [&>section]:shadow-none"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<WorkspaceChart
|
|
||||||
theme={theme}
|
|
||||||
chart={chartConfig}
|
|
||||||
detached
|
|
||||||
loading={loading || variablesStillResolving}
|
|
||||||
configuredVariableCount={chart.selectedSensorKeys.length}
|
|
||||||
onModeChange={(mode) => updateChart({ mode })}
|
|
||||||
onVariableToggle={toggleVariable}
|
|
||||||
onTimeRangeChange={(timeRange) => updateChart({ timeRange })}
|
|
||||||
onIntervalChange={(interval) => updateChart({ interval })}
|
|
||||||
/>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
{configOpen && (
|
|
||||||
<ChartConfigModal
|
|
||||||
theme={theme}
|
|
||||||
chart={chart}
|
|
||||||
variables={chartableVariables}
|
|
||||||
onClose={() => setConfigOpen(false)}
|
|
||||||
onSave={async (updatedChart) => {
|
|
||||||
setCharts((current) =>
|
|
||||||
current.map((item) =>
|
|
||||||
item.id === updatedChart.id
|
|
||||||
? { ...item, ...updatedChart }
|
|
||||||
: item,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
latestChartRef.current = {
|
|
||||||
...chart,
|
|
||||||
...updatedChart,
|
|
||||||
};
|
|
||||||
|
|
||||||
await emit(`${channel}://replace-chart`, {
|
|
||||||
chart: updatedChart,
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error("Failed to replace chart in main window", error);
|
|
||||||
});
|
|
||||||
|
|
||||||
setConfigOpen(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getVariableColor(index: number) {
|
|
||||||
const colors = [
|
|
||||||
"#4FD1C5",
|
|
||||||
"#3B82F6",
|
|
||||||
"#FACC15",
|
|
||||||
"#7DD3FC",
|
|
||||||
"#A5B4FC",
|
|
||||||
"#FB7185",
|
|
||||||
];
|
|
||||||
|
|
||||||
return colors[index % colors.length];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ChartWindowPage;
|
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { emit, listen } from "@tauri-apps/api/event";
|
||||||
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
|
import { Minus, Square, X } from "lucide-react";
|
||||||
|
import { MainChartsPage } from "../../maincharts/pages/MainChartsPage";
|
||||||
|
import MeteoChartsPage from "../../meteo/pages/MeteoChartsPage";
|
||||||
|
import { ClimateChartsPage } from "../../climate/pages/ClimateChartsPage";
|
||||||
|
import type { ChartWorkspaceScope } from "../types";
|
||||||
|
|
||||||
|
type WorkspaceWindowPageProps = {
|
||||||
|
theme: "dark" | "light";
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseWorkspaceRoute() {
|
||||||
|
const parts = window.location.pathname.split("/").filter(Boolean);
|
||||||
|
const scope = (parts[1] ?? "GLOBAL") as ChartWorkspaceScope;
|
||||||
|
const workspaceId = Number(parts[2] ?? "");
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const title = params.get("title") ?? `Workspace ${scope}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
scope,
|
||||||
|
workspaceId: Number.isFinite(workspaceId) ? workspaceId : null,
|
||||||
|
title,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WorkspaceWindowPage({ theme }: WorkspaceWindowPageProps) {
|
||||||
|
const isDark = theme === "dark";
|
||||||
|
const currentWindow = getCurrentWindow();
|
||||||
|
const { scope, workspaceId, title } = parseWorkspaceRoute();
|
||||||
|
const [windowTitle, setWindowTitle] = useState(title);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void currentWindow.setTitle(windowTitle);
|
||||||
|
}, [currentWindow, windowTitle]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unlistenPromise = listen<{
|
||||||
|
scope: ChartWorkspaceScope;
|
||||||
|
workspaceId: number;
|
||||||
|
name: string;
|
||||||
|
}>("workspace-window://renamed", (event) => {
|
||||||
|
if (
|
||||||
|
event.payload.scope !== scope ||
|
||||||
|
event.payload.workspaceId !== workspaceId
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setWindowTitle(event.payload.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
void unlistenPromise.then((unlisten) => unlisten());
|
||||||
|
};
|
||||||
|
}, [scope, workspaceId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unlistenPromise = currentWindow.onCloseRequested(() => {
|
||||||
|
if (!workspaceId) return;
|
||||||
|
|
||||||
|
void emit("workspace-window://closed", {
|
||||||
|
scope,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
void unlistenPromise.then((unlisten) => unlisten());
|
||||||
|
};
|
||||||
|
}, [currentWindow, scope, workspaceId]);
|
||||||
|
|
||||||
|
const titleBarClass = isDark
|
||||||
|
? "flex h-10 shrink-0 items-center border-b border-white/10 bg-[#071421] text-slate-100"
|
||||||
|
: "flex h-10 shrink-0 items-center border-b border-slate-200 bg-white text-slate-950";
|
||||||
|
|
||||||
|
const titleButtonClass = isDark
|
||||||
|
? "grid h-10 w-11 place-items-center text-slate-400 transition hover:bg-white/10 hover:text-white"
|
||||||
|
: "grid h-10 w-11 place-items-center text-slate-500 transition hover:bg-slate-100 hover:text-slate-950";
|
||||||
|
|
||||||
|
const closeButtonClass = isDark
|
||||||
|
? "grid h-10 w-11 place-items-center text-slate-400 transition hover:bg-red-500 hover:text-white"
|
||||||
|
: "grid h-10 w-11 place-items-center text-slate-500 transition hover:bg-red-500 hover:text-white";
|
||||||
|
|
||||||
|
const workspaceContent = (() => {
|
||||||
|
if (scope === "METEO") {
|
||||||
|
return (
|
||||||
|
<MeteoChartsPage
|
||||||
|
theme={theme}
|
||||||
|
workspaceId={workspaceId}
|
||||||
|
workspaceWindow
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scope === "CLIMATE") {
|
||||||
|
return (
|
||||||
|
<ClimateChartsPage
|
||||||
|
theme={theme}
|
||||||
|
workspaceId={workspaceId}
|
||||||
|
workspaceWindow
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MainChartsPage
|
||||||
|
theme={theme}
|
||||||
|
workspaceId={workspaceId}
|
||||||
|
workspaceWindow
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
isDark
|
||||||
|
? "flex h-screen flex-col overflow-hidden bg-[#071421] text-slate-100"
|
||||||
|
: "flex h-screen flex-col overflow-hidden bg-[#F8FAFC] text-slate-950"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<header data-tauri-drag-region className={titleBarClass}>
|
||||||
|
<div
|
||||||
|
data-tauri-drag-region
|
||||||
|
className="flex h-full flex-1 items-center px-3 text-xs font-black"
|
||||||
|
>
|
||||||
|
{windowTitle}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex h-full items-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title="Minimizar"
|
||||||
|
onClick={() => currentWindow.minimize()}
|
||||||
|
className={titleButtonClass}
|
||||||
|
>
|
||||||
|
<Minus className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title="Maximizar"
|
||||||
|
onClick={() => currentWindow.toggleMaximize()}
|
||||||
|
className={titleButtonClass}
|
||||||
|
>
|
||||||
|
<Square className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title="Fechar"
|
||||||
|
onClick={() => currentWindow.close()}
|
||||||
|
className={closeButtonClass}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="min-h-0 flex-1 p-3">{workspaceContent}</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WorkspaceWindowPage;
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import type {
|
||||||
|
WorkspaceChartInterval,
|
||||||
|
WorkspaceChartMode,
|
||||||
|
WorkspaceChartTimeRange,
|
||||||
|
} from "../../components/charts/WorkspaceChart";
|
||||||
|
import type { PersistedChartWorkspaceItem } from "./hooks/useChartWorkspacePersistence";
|
||||||
|
|
||||||
|
export type ChartWorkspaceScope =
|
||||||
|
| "GLOBAL"
|
||||||
|
| "CLIMATE"
|
||||||
|
| "IRRIGATION"
|
||||||
|
| "METEO"
|
||||||
|
| "LIGHTING"
|
||||||
|
| "HYDRO"
|
||||||
|
| "AEROPONICS";
|
||||||
|
|
||||||
|
export type ChartWorkspaceChannel =
|
||||||
|
| "maincharts"
|
||||||
|
| "climatecharts"
|
||||||
|
| "meteocharts";
|
||||||
|
|
||||||
|
export type ChartWorkspaceItem = PersistedChartWorkspaceItem & {
|
||||||
|
hidden?: boolean;
|
||||||
|
collapsed?: boolean;
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
mode: WorkspaceChartMode;
|
||||||
|
selectedSensorKeys: string[];
|
||||||
|
hiddenSensorKeys?: string[];
|
||||||
|
timeRange: WorkspaceChartTimeRange;
|
||||||
|
interval: WorkspaceChartInterval;
|
||||||
|
detached?: boolean;
|
||||||
|
windowX?: number;
|
||||||
|
windowY?: number;
|
||||||
|
windowWidth?: number;
|
||||||
|
windowHeight?: number;
|
||||||
|
windowZIndex?: number;
|
||||||
|
};
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
|
|
||||||
|
|
||||||
export async function openChartWindow(
|
|
||||||
chartId: string,
|
|
||||||
theme: "dark" | "light",
|
|
||||||
title: string,
|
|
||||||
scope: "GLOBAL" | "CLIMATE" | "METEO" = "GLOBAL",
|
|
||||||
channel: "maincharts" | "climatecharts" | "meteocharts" = "maincharts",
|
|
||||||
) {
|
|
||||||
const label = `${channel}-${chartId}`;
|
|
||||||
|
|
||||||
const existing = await WebviewWindow.getByLabel(label);
|
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
await existing.show();
|
|
||||||
await existing.setFocus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chartWindow = new WebviewWindow(label, {
|
|
||||||
url: `/chart-window/${chartId}?theme=${theme}&scope=${scope}&channel=${channel}`,
|
|
||||||
|
|
||||||
title,
|
|
||||||
|
|
||||||
width: 920,
|
|
||||||
height: 680,
|
|
||||||
minWidth: 720,
|
|
||||||
minHeight: 480,
|
|
||||||
|
|
||||||
decorations: false,
|
|
||||||
transparent: false,
|
|
||||||
|
|
||||||
resizable: true,
|
|
||||||
maximizable: true,
|
|
||||||
minimizable: true,
|
|
||||||
closable: true,
|
|
||||||
|
|
||||||
visible: true,
|
|
||||||
focus: true,
|
|
||||||
center: true,
|
|
||||||
|
|
||||||
skipTaskbar: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
chartWindow.once("tauri://created", () => {
|
|
||||||
console.log(`Chart window created: ${label}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
chartWindow.once("tauri://error", (error) => {
|
|
||||||
console.error(`Failed to create chart window: ${label}`, error);
|
|
||||||
});
|
|
||||||
|
|
||||||
return chartWindow;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||||
|
import type { ChartWorkspaceScope } from "../types";
|
||||||
|
|
||||||
|
export async function openWorkspaceWindow(
|
||||||
|
workspaceId: number,
|
||||||
|
scope: ChartWorkspaceScope,
|
||||||
|
theme: "dark" | "light",
|
||||||
|
title: string,
|
||||||
|
) {
|
||||||
|
const label = `workspace-${scope.toLowerCase()}-${workspaceId}`;
|
||||||
|
const encodedTitle = encodeURIComponent(title);
|
||||||
|
const existing = await WebviewWindow.getByLabel(label);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
await existing.setTitle(title);
|
||||||
|
await existing.show();
|
||||||
|
await existing.setFocus();
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceWindow = new WebviewWindow(label, {
|
||||||
|
url: `/workspace-window/${scope}/${workspaceId}?theme=${theme}&title=${encodedTitle}`,
|
||||||
|
title,
|
||||||
|
|
||||||
|
width: 1280,
|
||||||
|
height: 820,
|
||||||
|
minWidth: 960,
|
||||||
|
minHeight: 640,
|
||||||
|
|
||||||
|
decorations: false,
|
||||||
|
transparent: false,
|
||||||
|
|
||||||
|
resizable: true,
|
||||||
|
maximizable: true,
|
||||||
|
minimizable: true,
|
||||||
|
closable: true,
|
||||||
|
|
||||||
|
visible: true,
|
||||||
|
focus: true,
|
||||||
|
center: true,
|
||||||
|
|
||||||
|
skipTaskbar: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
workspaceWindow.once("tauri://created", () => {
|
||||||
|
console.log(`Workspace window created: ${label}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
workspaceWindow.once("tauri://error", (error) => {
|
||||||
|
console.error(`Failed to create workspace window: ${label}`, error);
|
||||||
|
});
|
||||||
|
|
||||||
|
return workspaceWindow;
|
||||||
|
}
|
||||||
@@ -1,11 +1,18 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { emit, listen } from "@tauri-apps/api/event";
|
||||||
import { ChartConfigModal } from "../../chartworkspace/components/ChartConfigModal";
|
import { ChartConfigModal } from "../../chartworkspace/components/ChartConfigModal";
|
||||||
import { openChartWindow } from "../../chartworkspace/utils/openChartWindow";
|
import {
|
||||||
|
EmptyWorkspace,
|
||||||
|
LayoutButton,
|
||||||
|
ModeButton,
|
||||||
|
WorkspaceSelector,
|
||||||
|
} from "../../chartworkspace/components/ChartWorkspaceControls";
|
||||||
|
import { openWorkspaceWindow } from "../../chartworkspace/utils/openWorkspaceWindow";
|
||||||
|
import { useChartWorkspaceSelection } from "../../chartworkspace/hooks/useChartWorkspaceSelection";
|
||||||
|
import { saveChartWorkspace } from "../../chartworkspace/api/chartWorkspaceApi";
|
||||||
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
|
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||||
import {
|
import {
|
||||||
Cog,
|
Cog,
|
||||||
Copy,
|
|
||||||
Maximize2,
|
Maximize2,
|
||||||
AreaChart,
|
AreaChart,
|
||||||
BarChart3,
|
BarChart3,
|
||||||
@@ -27,8 +34,6 @@ import {
|
|||||||
WorkspaceChart,
|
WorkspaceChart,
|
||||||
type WorkspaceChartConfig,
|
type WorkspaceChartConfig,
|
||||||
type WorkspaceChartMode,
|
type WorkspaceChartMode,
|
||||||
type WorkspaceChartTimeRange,
|
|
||||||
type WorkspaceChartInterval,
|
|
||||||
} from "../../../components/charts/WorkspaceChart";
|
} from "../../../components/charts/WorkspaceChart";
|
||||||
|
|
||||||
import type { ChartVariable } from "../../telemetry/types/telemetryCatalog";
|
import type { ChartVariable } from "../../telemetry/types/telemetryCatalog";
|
||||||
@@ -37,40 +42,32 @@ import { useClimateChartSeries } from "../hooks/useClimateChartSeries";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
useChartWorkspacePersistence,
|
useChartWorkspacePersistence,
|
||||||
type PersistedChartWorkspaceItem,
|
|
||||||
type ChartLayoutMode,
|
type ChartLayoutMode,
|
||||||
} from "../../chartworkspace/hooks/useChartWorkspacePersistence";
|
} from "../../chartworkspace/hooks/useChartWorkspacePersistence";
|
||||||
|
import type { ChartWorkspaceItem } from "../../chartworkspace/types";
|
||||||
|
import {
|
||||||
|
getVariableColor,
|
||||||
|
getVisibleSlotCount,
|
||||||
|
layoutGridClass,
|
||||||
|
MAX_CHARTS,
|
||||||
|
MAX_VARIABLES_PER_CHART,
|
||||||
|
} from "../../chartworkspace/domain/chartWorkspaceModel";
|
||||||
|
|
||||||
type ClimateChartsPageProps = {
|
type ClimateChartsPageProps = {
|
||||||
theme: "dark" | "light";
|
theme: "dark" | "light";
|
||||||
};
|
workspaceId?: number | null;
|
||||||
|
workspaceWindow?: boolean;
|
||||||
type ChartWorkspaceItem = PersistedChartWorkspaceItem & {
|
|
||||||
hidden?: boolean;
|
|
||||||
collapsed?: boolean;
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
subtitle: string;
|
|
||||||
mode: WorkspaceChartMode;
|
|
||||||
selectedSensorKeys: string[];
|
|
||||||
hiddenSensorKeys?: string[];
|
|
||||||
timeRange: WorkspaceChartTimeRange;
|
|
||||||
interval: WorkspaceChartInterval;
|
|
||||||
detached?: boolean;
|
|
||||||
windowX?: number;
|
|
||||||
windowY?: number;
|
|
||||||
windowWidth?: number;
|
|
||||||
windowHeight?: number;
|
|
||||||
windowZIndex?: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const RADIUS = "rounded-[6px]";
|
const RADIUS = "rounded-[6px]";
|
||||||
const MAX_CHARTS = 10;
|
|
||||||
const MAX_VARIABLES_PER_CHART = 6;
|
|
||||||
|
|
||||||
const INITIAL_CHARTS: ChartWorkspaceItem[] = [];
|
const INITIAL_CHARTS: ChartWorkspaceItem[] = [];
|
||||||
|
|
||||||
export function ClimateChartsPage({ theme }: ClimateChartsPageProps) {
|
export function ClimateChartsPage({
|
||||||
|
theme,
|
||||||
|
workspaceId = null,
|
||||||
|
workspaceWindow = false,
|
||||||
|
}: ClimateChartsPageProps) {
|
||||||
const isDark = theme === "dark";
|
const isDark = theme === "dark";
|
||||||
|
|
||||||
const { chartableVariables, connected } = useClimateChartCatalog();
|
const { chartableVariables, connected } = useClimateChartCatalog();
|
||||||
@@ -83,6 +80,120 @@ export function ClimateChartsPage({ theme }: ClimateChartsPageProps) {
|
|||||||
const [movingChartId, setMovingChartId] = useState<string | null>(null);
|
const [movingChartId, setMovingChartId] = useState<string | null>(null);
|
||||||
const [newChartOpen, setNewChartOpen] = useState(false);
|
const [newChartOpen, setNewChartOpen] = useState(false);
|
||||||
const [placingChartId, setPlacingChartId] = useState<string | null>(null);
|
const [placingChartId, setPlacingChartId] = useState<string | null>(null);
|
||||||
|
const [detachedWorkspaceIds, setDetachedWorkspaceIds] = useState<Set<number>>(() => new Set());
|
||||||
|
|
||||||
|
const workspaceSelection = useChartWorkspaceSelection({
|
||||||
|
scope: "CLIMATE",
|
||||||
|
defaultName: "Workspace Clima",
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeWorkspaceId = workspaceId ?? workspaceSelection.activeWorkspaceId;
|
||||||
|
const activeWorkspace =
|
||||||
|
workspaceSelection.workspaces.find((workspace) => workspace.id === activeWorkspaceId) ??
|
||||||
|
workspaceSelection.activeWorkspace;
|
||||||
|
const activeWorkspaceName = activeWorkspace?.name ?? "Workspace Clima";
|
||||||
|
|
||||||
|
const workspaceWindowLabel = (workspaceId: number) =>
|
||||||
|
`workspace-climate-${workspaceId}`;
|
||||||
|
|
||||||
|
const renameWorkspace = async (workspaceId: number, name: string) => {
|
||||||
|
const workspace = workspaceSelection.workspaces.find(
|
||||||
|
(item) => item.id === workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!workspace) return;
|
||||||
|
|
||||||
|
await saveChartWorkspace(
|
||||||
|
"CLIMATE",
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
sortOrder: workspace.sortOrder,
|
||||||
|
defaultWorkspace: workspace.defaultWorkspace,
|
||||||
|
layoutMode: workspace.layoutMode,
|
||||||
|
chartsJson: workspace.chartsJson,
|
||||||
|
},
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
workspaceSelection.updateWorkspaceName(workspaceId, name);
|
||||||
|
|
||||||
|
const existing = await WebviewWindow.getByLabel(
|
||||||
|
workspaceWindowLabel(workspaceId),
|
||||||
|
);
|
||||||
|
await existing?.setTitle(name);
|
||||||
|
await emit("workspace-window://renamed", {
|
||||||
|
scope: "CLIMATE",
|
||||||
|
workspaceId,
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteWorkspace = async (workspaceId: number) => {
|
||||||
|
const existing = await WebviewWindow.getByLabel(
|
||||||
|
workspaceWindowLabel(workspaceId),
|
||||||
|
);
|
||||||
|
await existing?.close();
|
||||||
|
|
||||||
|
setDetachedWorkspaceIds((current) => {
|
||||||
|
const next = new Set(current);
|
||||||
|
next.delete(workspaceId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
|
||||||
|
await workspaceSelection.deleteWorkspace(workspaceId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const detachWorkspace = () => {
|
||||||
|
if (!activeWorkspaceId) return;
|
||||||
|
|
||||||
|
setDetachedWorkspaceIds((current) => new Set(current).add(activeWorkspaceId));
|
||||||
|
|
||||||
|
const nextWorkspace = workspaceSelection.workspaces.find(
|
||||||
|
(workspace) =>
|
||||||
|
workspace.id !== activeWorkspaceId &&
|
||||||
|
!detachedWorkspaceIds.has(workspace.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (nextWorkspace) {
|
||||||
|
void workspaceSelection.selectWorkspace(nextWorkspace.id);
|
||||||
|
} else {
|
||||||
|
workspaceSelection.setActiveWorkspace(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
void openWorkspaceWindow(
|
||||||
|
activeWorkspaceId,
|
||||||
|
"CLIMATE",
|
||||||
|
theme,
|
||||||
|
activeWorkspaceName,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unlistenPromise = listen<{
|
||||||
|
scope: string;
|
||||||
|
workspaceId: number;
|
||||||
|
}>("workspace-window://closed", (event) => {
|
||||||
|
if (event.payload.scope !== "CLIMATE") return;
|
||||||
|
|
||||||
|
setDetachedWorkspaceIds((current) => {
|
||||||
|
const next = new Set(current);
|
||||||
|
next.delete(event.payload.workspaceId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
void unlistenPromise.then((unlisten) => unlisten());
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCharts([]);
|
||||||
|
setConfigChartId(null);
|
||||||
|
setMovingChartId(null);
|
||||||
|
setPlacingChartId(null);
|
||||||
|
setSavedOpen(false);
|
||||||
|
}, [activeWorkspaceId]);
|
||||||
|
|
||||||
const changeLayoutMode = (nextLayoutMode: ChartLayoutMode) => {
|
const changeLayoutMode = (nextLayoutMode: ChartLayoutMode) => {
|
||||||
const nextVisibleCount = getVisibleSlotCount(nextLayoutMode);
|
const nextVisibleCount = getVisibleSlotCount(nextLayoutMode);
|
||||||
@@ -111,6 +222,7 @@ export function ClimateChartsPage({ theme }: ClimateChartsPageProps) {
|
|||||||
|
|
||||||
const workspacePersistence = useChartWorkspacePersistence({
|
const workspacePersistence = useChartWorkspacePersistence({
|
||||||
scope: "CLIMATE",
|
scope: "CLIMATE",
|
||||||
|
workspaceId: activeWorkspaceId,
|
||||||
layoutMode,
|
layoutMode,
|
||||||
charts,
|
charts,
|
||||||
onLoaded: (workspace) => {
|
onLoaded: (workspace) => {
|
||||||
@@ -209,31 +321,6 @@ export function ClimateChartsPage({ theme }: ClimateChartsPageProps) {
|
|||||||
setNewChartOpen(false);
|
setNewChartOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const duplicateChart = (chartId: string) => {
|
|
||||||
if (!canAddMoreCharts) return;
|
|
||||||
|
|
||||||
const chart = charts.find((item) => item.id === chartId);
|
|
||||||
if (!chart) return;
|
|
||||||
|
|
||||||
const copy: ChartWorkspaceItem = {
|
|
||||||
...chart,
|
|
||||||
id: `chart-${Date.now()}`,
|
|
||||||
title: `${chart.title} cópia`,
|
|
||||||
};
|
|
||||||
|
|
||||||
setCharts((current) => {
|
|
||||||
const next = [...current, copy];
|
|
||||||
|
|
||||||
if (next.length >= 3) {
|
|
||||||
setLayoutMode("fourGrid");
|
|
||||||
} else if (next.length === 2) {
|
|
||||||
setLayoutMode("twoColumns");
|
|
||||||
}
|
|
||||||
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const setChartMode = (chartId: string, mode: WorkspaceChartMode) => {
|
const setChartMode = (chartId: string, mode: WorkspaceChartMode) => {
|
||||||
setCharts((current) =>
|
setCharts((current) =>
|
||||||
current.map((chart) =>
|
current.map((chart) =>
|
||||||
@@ -278,8 +365,6 @@ export function ClimateChartsPage({ theme }: ClimateChartsPageProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const closeChart = async (chartId: string) => {
|
const closeChart = async (chartId: string) => {
|
||||||
await closeDetachedChartWindow(chartId);
|
|
||||||
|
|
||||||
setCharts((current) =>
|
setCharts((current) =>
|
||||||
current.map((chart) =>
|
current.map((chart) =>
|
||||||
chart.id === chartId
|
chart.id === chartId
|
||||||
@@ -299,8 +384,6 @@ export function ClimateChartsPage({ theme }: ClimateChartsPageProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const deleteChart = async (chartId: string) => {
|
const deleteChart = async (chartId: string) => {
|
||||||
await closeDetachedChartWindow(chartId);
|
|
||||||
|
|
||||||
setCharts((current) => current.filter((chart) => chart.id !== chartId));
|
setCharts((current) => current.filter((chart) => chart.id !== chartId));
|
||||||
|
|
||||||
if (configChartId === chartId) setConfigChartId(null);
|
if (configChartId === chartId) setConfigChartId(null);
|
||||||
@@ -365,72 +448,6 @@ export function ClimateChartsPage({ theme }: ClimateChartsPageProps) {
|
|||||||
setSavedOpen(false);
|
setSavedOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeDetachedChartWindow = async (chartId: string) => {
|
|
||||||
const label = `climatecharts-${chartId}`;
|
|
||||||
const existing = await WebviewWindow.getByLabel(label);
|
|
||||||
|
|
||||||
if (!existing) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await existing.destroy();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to destroy detached chart window: ${label}`, error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const detachChart = (chartId: string) => {
|
|
||||||
setCharts((current) =>
|
|
||||||
current.map((chart) =>
|
|
||||||
chart.id === chartId
|
|
||||||
? {
|
|
||||||
...chart,
|
|
||||||
detached: true,
|
|
||||||
hidden: false,
|
|
||||||
collapsed: false,
|
|
||||||
}
|
|
||||||
: chart,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
window.setTimeout(() => {
|
|
||||||
const chart = charts.find((item) => item.id === chartId);
|
|
||||||
|
|
||||||
void openChartWindow(
|
|
||||||
chartId,
|
|
||||||
theme,
|
|
||||||
chart?.title ?? "Chart",
|
|
||||||
"CLIMATE",
|
|
||||||
"climatecharts",
|
|
||||||
);
|
|
||||||
}, 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
const attachChart = async (chartId: string) => {
|
|
||||||
setCharts((current) =>
|
|
||||||
current.map((chart) =>
|
|
||||||
chart.id === chartId
|
|
||||||
? {
|
|
||||||
...chart,
|
|
||||||
detached: false,
|
|
||||||
hidden: false,
|
|
||||||
collapsed: false,
|
|
||||||
}
|
|
||||||
: chart,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await closeDetachedChartWindow(chartId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const moveDetachedChart = (chartId: string, x: number, y: number) => {
|
|
||||||
setCharts((current) =>
|
|
||||||
current.map((chart) =>
|
|
||||||
chart.id === chartId
|
|
||||||
? { ...chart, windowX: x, windowY: y }
|
|
||||||
: chart,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unlistenAttachPromise = listen<{
|
const unlistenAttachPromise = listen<{
|
||||||
@@ -545,6 +562,22 @@ export function ClimateChartsPage({ theme }: ClimateChartsPageProps) {
|
|||||||
: `${RADIUS} relative flex flex-wrap items-center gap-3 border border-[#D7DEE8] bg-white p-2`
|
: `${RADIUS} relative flex flex-wrap items-center gap-3 border border-[#D7DEE8] bg-white p-2`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
{!workspaceWindow && (
|
||||||
|
<WorkspaceSelector
|
||||||
|
theme={theme}
|
||||||
|
workspaces={workspaceSelection.workspaces}
|
||||||
|
activeWorkspaceId={workspaceSelection.activeWorkspaceId}
|
||||||
|
detachedWorkspaceIds={detachedWorkspaceIds}
|
||||||
|
loading={workspaceSelection.loading}
|
||||||
|
creating={workspaceSelection.creating}
|
||||||
|
canCreateWorkspace={workspaceSelection.canCreateWorkspace}
|
||||||
|
onSelectWorkspace={workspaceSelection.selectWorkspace}
|
||||||
|
onCreateWorkspace={(name) => void workspaceSelection.createWorkspace(name)}
|
||||||
|
onRenameWorkspace={(workspaceId, name) => void renameWorkspace(workspaceId, name)}
|
||||||
|
onDeleteWorkspace={(workspaceId) => void deleteWorkspace(workspaceId)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<LayoutButton
|
<LayoutButton
|
||||||
theme={theme}
|
theme={theme}
|
||||||
@@ -579,6 +612,26 @@ export function ClimateChartsPage({ theme }: ClimateChartsPageProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{!workspaceWindow && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title="Destacar workspace"
|
||||||
|
aria-label="Destacar workspace"
|
||||||
|
disabled={!activeWorkspaceId || workspaceWindow}
|
||||||
|
onClick={detachWorkspace}
|
||||||
|
className={
|
||||||
|
activeWorkspaceId && !workspaceWindow
|
||||||
|
? isDark
|
||||||
|
? `${RADIUS} grid h-10 w-10 place-items-center border border-[#263247] bg-[#111A2B] text-[#A8B3C7] transition hover:border-[#36506D] hover:text-white`
|
||||||
|
: `${RADIUS} grid h-10 w-10 place-items-center border border-[#D7DEE8] bg-[#F8FAFC] text-slate-600 transition hover:bg-white hover:text-[#0F172A]`
|
||||||
|
: `${RADIUS} grid h-10 w-10 cursor-not-allowed place-items-center border border-[#263247] bg-[#111A2B]/50 text-[#7F8CA3]/50`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Maximize2 className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="ml-auto flex items-center gap-4">
|
<div className="ml-auto flex items-center gap-4">
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
@@ -671,7 +724,6 @@ export function ClimateChartsPage({ theme }: ClimateChartsPageProps) {
|
|||||||
movingChartId={movingChartId}
|
movingChartId={movingChartId}
|
||||||
setMovingChartId={setMovingChartId}
|
setMovingChartId={setMovingChartId}
|
||||||
swapCharts={swapCharts}
|
swapCharts={swapCharts}
|
||||||
duplicateChart={duplicateChart}
|
|
||||||
closeChart={closeChart}
|
closeChart={closeChart}
|
||||||
setConfigChartId={setConfigChartId}
|
setConfigChartId={setConfigChartId}
|
||||||
setChartMode={setChartMode}
|
setChartMode={setChartMode}
|
||||||
@@ -679,9 +731,6 @@ export function ClimateChartsPage({ theme }: ClimateChartsPageProps) {
|
|||||||
setCharts={setCharts}
|
setCharts={setCharts}
|
||||||
placingChartId={placingChartId}
|
placingChartId={placingChartId}
|
||||||
placeChartHere={placeChartHere}
|
placeChartHere={placeChartHere}
|
||||||
detachChart={detachChart}
|
|
||||||
attachChart={attachChart}
|
|
||||||
moveDetachedChart={moveDetachedChart}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@@ -768,7 +817,6 @@ function WorkspaceChartContainer({
|
|||||||
movingChartId,
|
movingChartId,
|
||||||
setMovingChartId,
|
setMovingChartId,
|
||||||
swapCharts,
|
swapCharts,
|
||||||
duplicateChart,
|
|
||||||
closeChart,
|
closeChart,
|
||||||
setConfigChartId,
|
setConfigChartId,
|
||||||
setChartMode,
|
setChartMode,
|
||||||
@@ -776,9 +824,6 @@ function WorkspaceChartContainer({
|
|||||||
setCharts,
|
setCharts,
|
||||||
placingChartId,
|
placingChartId,
|
||||||
placeChartHere,
|
placeChartHere,
|
||||||
detachChart,
|
|
||||||
attachChart,
|
|
||||||
moveDetachedChart
|
|
||||||
}: {
|
}: {
|
||||||
theme: "dark" | "light";
|
theme: "dark" | "light";
|
||||||
layoutMode: ChartLayoutMode;
|
layoutMode: ChartLayoutMode;
|
||||||
@@ -788,7 +833,6 @@ function WorkspaceChartContainer({
|
|||||||
movingChartId: string | null;
|
movingChartId: string | null;
|
||||||
setMovingChartId: React.Dispatch<React.SetStateAction<string | null>>;
|
setMovingChartId: React.Dispatch<React.SetStateAction<string | null>>;
|
||||||
swapCharts: (sourceId: string, targetId: string) => void;
|
swapCharts: (sourceId: string, targetId: string) => void;
|
||||||
duplicateChart: (chartId: string) => void;
|
|
||||||
closeChart: (chartId: string) => void | Promise<void>;
|
closeChart: (chartId: string) => void | Promise<void>;
|
||||||
setConfigChartId: React.Dispatch<React.SetStateAction<string | null>>;
|
setConfigChartId: React.Dispatch<React.SetStateAction<string | null>>;
|
||||||
setChartMode: (chartId: string, mode: WorkspaceChartMode) => void;
|
setChartMode: (chartId: string, mode: WorkspaceChartMode) => void;
|
||||||
@@ -796,9 +840,6 @@ function WorkspaceChartContainer({
|
|||||||
setCharts: React.Dispatch<React.SetStateAction<ChartWorkspaceItem[]>>;
|
setCharts: React.Dispatch<React.SetStateAction<ChartWorkspaceItem[]>>;
|
||||||
placingChartId: string | null;
|
placingChartId: string | null;
|
||||||
placeChartHere: (targetChartId: string) => void;
|
placeChartHere: (targetChartId: string) => void;
|
||||||
detachChart: (chartId: string) => void;
|
|
||||||
attachChart: (chartId: string) => void | Promise<void>;
|
|
||||||
moveDetachedChart: (chartId: string, x: number, y: number) => void;
|
|
||||||
}) {
|
}) {
|
||||||
const isMoving = movingChartId === chartItem.id;
|
const isMoving = movingChartId === chartItem.id;
|
||||||
|
|
||||||
@@ -872,23 +913,6 @@ function WorkspaceChartContainer({
|
|||||||
<Cog className="h-4 w-4" />
|
<Cog className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button type="button" title="Duplicar" className={floatingIconClass(theme)} onClick={() => duplicateChart(chartItem.id)}>
|
|
||||||
<Copy className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
title={chartItem.detached ? "Repor janela" : "Destacar janela"}
|
|
||||||
className={floatingIconClass(theme)}
|
|
||||||
onClick={() =>
|
|
||||||
chartItem.detached
|
|
||||||
? attachChart(chartItem.id)
|
|
||||||
: detachChart(chartItem.id)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Maximize2 className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
title="Fechar"
|
title="Fechar"
|
||||||
@@ -929,8 +953,6 @@ function WorkspaceChartContainer({
|
|||||||
configuredVariableCount={chartItem.selectedSensorKeys.length}
|
configuredVariableCount={chartItem.selectedSensorKeys.length}
|
||||||
loading={loading || variablesStillResolving}
|
loading={loading || variablesStillResolving}
|
||||||
detached={chartItem.detached}
|
detached={chartItem.detached}
|
||||||
onDetach={() => detachChart(chartItem.id)}
|
|
||||||
onAttach={() => attachChart(chartItem.id)}
|
|
||||||
onTimeRangeChange={(range) =>
|
onTimeRangeChange={(range) =>
|
||||||
setCharts((current) =>
|
setCharts((current) =>
|
||||||
current.map((chart) =>
|
current.map((chart) =>
|
||||||
@@ -983,32 +1005,6 @@ function WorkspaceChartContainer({
|
|||||||
onVariableToggle={(variableKey) =>
|
onVariableToggle={(variableKey) =>
|
||||||
toggleVariable(chartItem.id, variableKey)
|
toggleVariable(chartItem.id, variableKey)
|
||||||
}
|
}
|
||||||
onHeaderPointerDown={
|
|
||||||
chartItem.detached
|
|
||||||
? (event) => {
|
|
||||||
const startX = event.clientX;
|
|
||||||
const startY = event.clientY;
|
|
||||||
const initialX = chartItem.windowX ?? 120;
|
|
||||||
const initialY = chartItem.windowY ?? 120;
|
|
||||||
|
|
||||||
const handlePointerMove = (moveEvent: PointerEvent) => {
|
|
||||||
moveDetachedChart(
|
|
||||||
chartItem.id,
|
|
||||||
initialX + moveEvent.clientX - startX,
|
|
||||||
initialY + moveEvent.clientY - startY,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePointerUp = () => {
|
|
||||||
window.removeEventListener("pointermove", handlePointerMove);
|
|
||||||
window.removeEventListener("pointerup", handlePointerUp);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("pointermove", handlePointerMove);
|
|
||||||
window.addEventListener("pointerup", handlePointerUp);
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -1465,194 +1461,5 @@ function EmptyVariableList({ theme }: { theme: "dark" | "light" }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function layoutGridClass(layoutMode: ChartLayoutMode) {
|
|
||||||
if (layoutMode === "fourGrid") {
|
|
||||||
return "grid min-h-0 flex-1 grid-cols-2 grid-rows-2 gap-3 overflow-hidden";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (layoutMode === "twoColumns") {
|
|
||||||
return "grid min-h-0 flex-1 grid-cols-2 gap-3 overflow-hidden";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (layoutMode === "twoRows") {
|
|
||||||
return "grid min-h-0 flex-1 grid-rows-2 gap-3 overflow-hidden";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "grid min-h-0 flex-1 gap-3 overflow-hidden";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getVisibleSlotCount(layoutMode: ChartLayoutMode) {
|
|
||||||
if (layoutMode === "single") return 1;
|
|
||||||
if (layoutMode === "twoColumns") return 2;
|
|
||||||
if (layoutMode === "twoRows") return 2;
|
|
||||||
return 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
function LayoutButton({
|
|
||||||
theme,
|
|
||||||
active,
|
|
||||||
icon,
|
|
||||||
title,
|
|
||||||
onClick,
|
|
||||||
}: {
|
|
||||||
theme: "dark" | "light";
|
|
||||||
active: boolean;
|
|
||||||
icon: React.ReactNode;
|
|
||||||
title: string;
|
|
||||||
onClick: () => void;
|
|
||||||
}) {
|
|
||||||
const isDark = theme === "dark";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
title={title}
|
|
||||||
aria-label={title}
|
|
||||||
onClick={onClick}
|
|
||||||
className={
|
|
||||||
active
|
|
||||||
? isDark
|
|
||||||
? `${RADIUS} grid h-10 w-10 place-items-center bg-[#4FD1C5] text-[#07101B]`
|
|
||||||
: `${RADIUS} grid h-10 w-10 place-items-center bg-[#0F766E] text-white`
|
|
||||||
: isDark
|
|
||||||
? `${RADIUS} grid h-10 w-10 place-items-center border border-[#263247] bg-[#111A2B] text-[#A8B3C7] transition hover:border-[#36506D] hover:text-white`
|
|
||||||
: `${RADIUS} grid h-10 w-10 place-items-center border border-[#D7DEE8] bg-[#F8FAFC] text-slate-600 transition hover:bg-white hover:text-[#0F172A]`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ModeButton({
|
|
||||||
theme,
|
|
||||||
active,
|
|
||||||
icon,
|
|
||||||
label,
|
|
||||||
onClick,
|
|
||||||
}: {
|
|
||||||
theme: "dark" | "light";
|
|
||||||
active: boolean;
|
|
||||||
icon?: React.ReactNode;
|
|
||||||
label: string;
|
|
||||||
onClick: () => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onClick}
|
|
||||||
className={
|
|
||||||
active
|
|
||||||
? theme === "dark"
|
|
||||||
? `${RADIUS} flex items-center justify-center gap-2 bg-[#4FD1C5] px-3 py-2 text-xs font-black text-[#07101B]`
|
|
||||||
: `${RADIUS} flex items-center justify-center gap-2 bg-[#0F766E] px-3 py-2 text-xs font-black text-white`
|
|
||||||
: theme === "dark"
|
|
||||||
? `${RADIUS} flex items-center justify-center gap-2 border border-[#263247] bg-[#111A2B] px-3 py-2 text-xs font-bold text-[#A8B3C7]`
|
|
||||||
: `${RADIUS} flex items-center justify-center gap-2 border border-[#D7DEE8] bg-[#F8FAFC] px-3 py-2 text-xs font-bold text-slate-600`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
{label}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function EmptyWorkspace({
|
|
||||||
theme,
|
|
||||||
canAddMoreCharts,
|
|
||||||
onAddChart,
|
|
||||||
onOpenSaved,
|
|
||||||
}: {
|
|
||||||
theme: "dark" | "light";
|
|
||||||
canAddMoreCharts: boolean;
|
|
||||||
onAddChart: () => void;
|
|
||||||
onOpenSaved: () => void;
|
|
||||||
}) {
|
|
||||||
const isDark = theme === "dark";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section
|
|
||||||
className={
|
|
||||||
isDark
|
|
||||||
? `${RADIUS} flex min-h-0 flex-1 items-center justify-center border border-dashed border-[#33445F] bg-[#0E1726]/70`
|
|
||||||
: `${RADIUS} flex min-h-0 flex-1 items-center justify-center border border-dashed border-[#CBD5E1] bg-white`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="max-w-[420px] text-center">
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
isDark
|
|
||||||
? "mx-auto grid h-16 w-16 place-items-center rounded-2xl border border-[#263247] bg-[#111A2B] text-[#4FD1C5]"
|
|
||||||
: "mx-auto grid h-16 w-16 place-items-center rounded-2xl border border-[#D7DEE8] bg-[#F8FAFC] text-[#0F766E]"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<BarChart3 className="h-8 w-8" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2
|
|
||||||
className={
|
|
||||||
isDark
|
|
||||||
? "mt-5 text-xl font-black text-white"
|
|
||||||
: "mt-5 text-xl font-black text-[#0F172A]"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Nenhum gráfico aberto
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<p
|
|
||||||
className={
|
|
||||||
isDark
|
|
||||||
? "mt-3 text-sm leading-6 text-[#A8B3C7]"
|
|
||||||
: "mt-3 text-sm leading-6 text-slate-600"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Abra um gráfico guardado ou crie um novo gráfico para continuar.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="mt-6 flex items-center justify-center gap-3">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled={!canAddMoreCharts}
|
|
||||||
onClick={onAddChart}
|
|
||||||
className={
|
|
||||||
canAddMoreCharts
|
|
||||||
? `inline-flex h-11 items-center gap-3 ${RADIUS} bg-[#18B8A6] px-5 text-sm font-black text-white transition hover:bg-[#21C7B5]`
|
|
||||||
: `inline-flex h-11 cursor-not-allowed items-center gap-3 ${RADIUS} bg-[#18B8A6]/40 px-5 text-sm font-black text-white`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Plus className="h-5 w-5" />
|
|
||||||
Novo Gráfico
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onOpenSaved}
|
|
||||||
className={
|
|
||||||
isDark
|
|
||||||
? `inline-flex h-11 items-center gap-3 ${RADIUS} border border-[#263247] bg-[#111A2B] px-5 text-sm font-black text-white`
|
|
||||||
: `inline-flex h-11 items-center gap-3 ${RADIUS} border border-[#D7DEE8] bg-[#F8FAFC] px-5 text-sm font-black text-[#0F172A]`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Search className="h-5 w-5" />
|
|
||||||
Abrir Guardado
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getVariableColor(index: number) {
|
|
||||||
const colors = [
|
|
||||||
"#4FD1C5",
|
|
||||||
"#3B82F6",
|
|
||||||
"#FACC15",
|
|
||||||
"#7DD3FC",
|
|
||||||
"#A5B4FC",
|
|
||||||
"#FB7185",
|
|
||||||
];
|
|
||||||
|
|
||||||
return colors[index % colors.length];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ClimateChartsPage;
|
export default ClimateChartsPage;
|
||||||
@@ -1,7 +1,15 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { emit, listen } from "@tauri-apps/api/event";
|
||||||
import { ChartConfigModal } from "../../chartworkspace/components/ChartConfigModal";
|
import { ChartConfigModal } from "../../chartworkspace/components/ChartConfigModal";
|
||||||
import { openChartWindow } from "../../chartworkspace/utils/openChartWindow";
|
import {
|
||||||
|
EmptyWorkspace,
|
||||||
|
LayoutButton,
|
||||||
|
ModeButton,
|
||||||
|
WorkspaceSelector,
|
||||||
|
} from "../../chartworkspace/components/ChartWorkspaceControls";
|
||||||
|
import { openWorkspaceWindow } from "../../chartworkspace/utils/openWorkspaceWindow";
|
||||||
|
import { useChartWorkspaceSelection } from "../../chartworkspace/hooks/useChartWorkspaceSelection";
|
||||||
|
import { saveChartWorkspace } from "../../chartworkspace/api/chartWorkspaceApi";
|
||||||
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
|
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||||
import {
|
import {
|
||||||
Cog,
|
Cog,
|
||||||
@@ -27,8 +35,6 @@ import {
|
|||||||
WorkspaceChart,
|
WorkspaceChart,
|
||||||
type WorkspaceChartConfig,
|
type WorkspaceChartConfig,
|
||||||
type WorkspaceChartMode,
|
type WorkspaceChartMode,
|
||||||
type WorkspaceChartTimeRange,
|
|
||||||
type WorkspaceChartInterval,
|
|
||||||
} from "../../../components/charts/WorkspaceChart";
|
} from "../../../components/charts/WorkspaceChart";
|
||||||
import { useTelemetryCatalog } from "../../telemetry/hooks/useTelemetryCatalog";
|
import { useTelemetryCatalog } from "../../telemetry/hooks/useTelemetryCatalog";
|
||||||
import type { ChartVariable } from "../../telemetry/types/telemetryCatalog";
|
import type { ChartVariable } from "../../telemetry/types/telemetryCatalog";
|
||||||
@@ -36,40 +42,32 @@ import { useTelemetryChartSeries } from "../../telemetry/hooks/useTelemetryChart
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
useChartWorkspacePersistence,
|
useChartWorkspacePersistence,
|
||||||
type PersistedChartWorkspaceItem,
|
|
||||||
type ChartLayoutMode,
|
type ChartLayoutMode,
|
||||||
} from "../../chartworkspace/hooks/useChartWorkspacePersistence";
|
} from "../../chartworkspace/hooks/useChartWorkspacePersistence";
|
||||||
|
import type { ChartWorkspaceItem } from "../../chartworkspace/types";
|
||||||
|
import {
|
||||||
|
getVariableColor,
|
||||||
|
getVisibleSlotCount,
|
||||||
|
layoutGridClass,
|
||||||
|
MAX_CHARTS,
|
||||||
|
MAX_VARIABLES_PER_CHART,
|
||||||
|
} from "../../chartworkspace/domain/chartWorkspaceModel";
|
||||||
|
|
||||||
type MainChartsPageProps = {
|
type MainChartsPageProps = {
|
||||||
theme: "dark" | "light";
|
theme: "dark" | "light";
|
||||||
};
|
workspaceId?: number | null;
|
||||||
|
workspaceWindow?: boolean;
|
||||||
type ChartWorkspaceItem = PersistedChartWorkspaceItem & {
|
|
||||||
hidden?: boolean;
|
|
||||||
collapsed?: boolean;
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
subtitle: string;
|
|
||||||
mode: WorkspaceChartMode;
|
|
||||||
selectedSensorKeys: string[];
|
|
||||||
hiddenSensorKeys?: string[];
|
|
||||||
timeRange: WorkspaceChartTimeRange;
|
|
||||||
interval: WorkspaceChartInterval;
|
|
||||||
detached?: boolean;
|
|
||||||
windowX?: number;
|
|
||||||
windowY?: number;
|
|
||||||
windowWidth?: number;
|
|
||||||
windowHeight?: number;
|
|
||||||
windowZIndex?: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const RADIUS = "rounded-[6px]";
|
const RADIUS = "rounded-[6px]";
|
||||||
const MAX_CHARTS = 10;
|
|
||||||
const MAX_VARIABLES_PER_CHART = 6;
|
|
||||||
|
|
||||||
const INITIAL_CHARTS: ChartWorkspaceItem[] = [];
|
const INITIAL_CHARTS: ChartWorkspaceItem[] = [];
|
||||||
|
|
||||||
export function MainChartsPage({ theme }: MainChartsPageProps) {
|
export function MainChartsPage({
|
||||||
|
theme,
|
||||||
|
workspaceId = null,
|
||||||
|
workspaceWindow = false,
|
||||||
|
}: MainChartsPageProps) {
|
||||||
const isDark = theme === "dark";
|
const isDark = theme === "dark";
|
||||||
|
|
||||||
const { chartableVariables, connected } = useTelemetryCatalog();
|
const { chartableVariables, connected } = useTelemetryCatalog();
|
||||||
@@ -82,6 +80,94 @@ export function MainChartsPage({ theme }: MainChartsPageProps) {
|
|||||||
const [movingChartId, setMovingChartId] = useState<string | null>(null);
|
const [movingChartId, setMovingChartId] = useState<string | null>(null);
|
||||||
const [newChartOpen, setNewChartOpen] = useState(false);
|
const [newChartOpen, setNewChartOpen] = useState(false);
|
||||||
const [placingChartId, setPlacingChartId] = useState<string | null>(null);
|
const [placingChartId, setPlacingChartId] = useState<string | null>(null);
|
||||||
|
const [detachedWorkspaceIds, setDetachedWorkspaceIds] = useState<Set<number>>(() => new Set());
|
||||||
|
|
||||||
|
const workspaceSelection = useChartWorkspaceSelection({
|
||||||
|
scope: "GLOBAL",
|
||||||
|
defaultName: "Workspace Geral",
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeWorkspaceId = workspaceId ?? workspaceSelection.activeWorkspaceId;
|
||||||
|
const activeWorkspace =
|
||||||
|
workspaceSelection.workspaces.find((workspace) => workspace.id === activeWorkspaceId) ??
|
||||||
|
workspaceSelection.activeWorkspace;
|
||||||
|
const activeWorkspaceName = activeWorkspace?.name ?? "Workspace Geral";
|
||||||
|
|
||||||
|
const workspaceWindowLabel = (workspaceId: number) =>
|
||||||
|
`workspace-global-${workspaceId}`;
|
||||||
|
|
||||||
|
const renameWorkspace = async (workspaceId: number, name: string) => {
|
||||||
|
const workspace = workspaceSelection.workspaces.find(
|
||||||
|
(item) => item.id === workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!workspace) return;
|
||||||
|
|
||||||
|
await saveChartWorkspace(
|
||||||
|
"GLOBAL",
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
sortOrder: workspace.sortOrder,
|
||||||
|
defaultWorkspace: workspace.defaultWorkspace,
|
||||||
|
layoutMode: workspace.layoutMode,
|
||||||
|
chartsJson: workspace.chartsJson,
|
||||||
|
},
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
workspaceSelection.updateWorkspaceName(workspaceId, name);
|
||||||
|
|
||||||
|
const existing = await WebviewWindow.getByLabel(
|
||||||
|
workspaceWindowLabel(workspaceId),
|
||||||
|
);
|
||||||
|
await existing?.setTitle(name);
|
||||||
|
await emit("workspace-window://renamed", {
|
||||||
|
scope: "GLOBAL",
|
||||||
|
workspaceId,
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteWorkspace = async (workspaceId: number) => {
|
||||||
|
const existing = await WebviewWindow.getByLabel(
|
||||||
|
workspaceWindowLabel(workspaceId),
|
||||||
|
);
|
||||||
|
await existing?.close();
|
||||||
|
|
||||||
|
setDetachedWorkspaceIds((current) => {
|
||||||
|
const next = new Set(current);
|
||||||
|
next.delete(workspaceId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
|
||||||
|
await workspaceSelection.deleteWorkspace(workspaceId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const detachWorkspace = () => {
|
||||||
|
if (!activeWorkspaceId) return;
|
||||||
|
|
||||||
|
setDetachedWorkspaceIds((current) => new Set(current).add(activeWorkspaceId));
|
||||||
|
|
||||||
|
const nextWorkspace = workspaceSelection.workspaces.find(
|
||||||
|
(workspace) =>
|
||||||
|
workspace.id !== activeWorkspaceId &&
|
||||||
|
!detachedWorkspaceIds.has(workspace.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (nextWorkspace) {
|
||||||
|
void workspaceSelection.selectWorkspace(nextWorkspace.id);
|
||||||
|
} else {
|
||||||
|
workspaceSelection.setActiveWorkspace(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
void openWorkspaceWindow(
|
||||||
|
activeWorkspaceId,
|
||||||
|
"GLOBAL",
|
||||||
|
theme,
|
||||||
|
activeWorkspaceName,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const [viewportCompact, setViewportCompact] = useState(() =>
|
const [viewportCompact, setViewportCompact] = useState(() =>
|
||||||
typeof window !== "undefined"
|
typeof window !== "undefined"
|
||||||
? window.innerWidth <= 1280 || window.innerHeight <= 720
|
? window.innerWidth <= 1280 || window.innerHeight <= 720
|
||||||
@@ -102,6 +188,35 @@ export function MainChartsPage({ theme }: MainChartsPageProps) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unlistenPromise = listen<{
|
||||||
|
scope: string;
|
||||||
|
workspaceId: number;
|
||||||
|
}>("workspace-window://closed", (event) => {
|
||||||
|
if (event.payload.scope !== "GLOBAL") return;
|
||||||
|
|
||||||
|
setDetachedWorkspaceIds((current) => {
|
||||||
|
const next = new Set(current);
|
||||||
|
next.delete(event.payload.workspaceId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
void unlistenPromise.then((unlisten) => unlisten());
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCharts([]);
|
||||||
|
setConfigChartId(null);
|
||||||
|
setMovingChartId(null);
|
||||||
|
setPlacingChartId(null);
|
||||||
|
setSavedOpen(false);
|
||||||
|
}, [activeWorkspaceId]);
|
||||||
|
|
||||||
const changeLayoutMode = (nextLayoutMode: ChartLayoutMode) => {
|
const changeLayoutMode = (nextLayoutMode: ChartLayoutMode) => {
|
||||||
const nextVisibleCount = getVisibleSlotCount(nextLayoutMode);
|
const nextVisibleCount = getVisibleSlotCount(nextLayoutMode);
|
||||||
|
|
||||||
@@ -129,6 +244,7 @@ export function MainChartsPage({ theme }: MainChartsPageProps) {
|
|||||||
|
|
||||||
const workspacePersistence = useChartWorkspacePersistence({
|
const workspacePersistence = useChartWorkspacePersistence({
|
||||||
scope: "GLOBAL",
|
scope: "GLOBAL",
|
||||||
|
workspaceId: activeWorkspaceId,
|
||||||
layoutMode,
|
layoutMode,
|
||||||
charts,
|
charts,
|
||||||
onLoaded: (workspace) => {
|
onLoaded: (workspace) => {
|
||||||
@@ -396,48 +512,6 @@ export function MainChartsPage({ theme }: MainChartsPageProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const detachChart = (chartId: string) => {
|
|
||||||
setCharts((current) =>
|
|
||||||
current.map((chart) =>
|
|
||||||
chart.id === chartId
|
|
||||||
? {
|
|
||||||
...chart,
|
|
||||||
detached: true,
|
|
||||||
hidden: false,
|
|
||||||
collapsed: false,
|
|
||||||
}
|
|
||||||
: chart,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
window.setTimeout(() => {
|
|
||||||
const chart = charts.find((item) => item.id === chartId);
|
|
||||||
|
|
||||||
void openChartWindow(
|
|
||||||
chartId,
|
|
||||||
theme,
|
|
||||||
chart?.title ?? "Chart",
|
|
||||||
);
|
|
||||||
}, 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
const attachChart = async (chartId: string) => {
|
|
||||||
setCharts((current) =>
|
|
||||||
current.map((chart) =>
|
|
||||||
chart.id === chartId
|
|
||||||
? {
|
|
||||||
...chart,
|
|
||||||
detached: false,
|
|
||||||
hidden: false,
|
|
||||||
collapsed: false,
|
|
||||||
}
|
|
||||||
: chart,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await closeDetachedChartWindow(chartId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const moveDetachedChart = (chartId: string, x: number, y: number) => {
|
const moveDetachedChart = (chartId: string, x: number, y: number) => {
|
||||||
setCharts((current) =>
|
setCharts((current) =>
|
||||||
current.map((chart) =>
|
current.map((chart) =>
|
||||||
@@ -563,6 +637,22 @@ export function MainChartsPage({ theme }: MainChartsPageProps) {
|
|||||||
: `${RADIUS} relative flex flex-wrap items-center gap-3 border border-[#D7DEE8] bg-white p-2`
|
: `${RADIUS} relative flex flex-wrap items-center gap-3 border border-[#D7DEE8] bg-white p-2`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
{!workspaceWindow && (
|
||||||
|
<WorkspaceSelector
|
||||||
|
theme={theme}
|
||||||
|
workspaces={workspaceSelection.workspaces}
|
||||||
|
activeWorkspaceId={workspaceSelection.activeWorkspaceId}
|
||||||
|
detachedWorkspaceIds={detachedWorkspaceIds}
|
||||||
|
loading={workspaceSelection.loading}
|
||||||
|
creating={workspaceSelection.creating}
|
||||||
|
canCreateWorkspace={workspaceSelection.canCreateWorkspace}
|
||||||
|
onSelectWorkspace={workspaceSelection.selectWorkspace}
|
||||||
|
onCreateWorkspace={(name) => void workspaceSelection.createWorkspace(name)}
|
||||||
|
onRenameWorkspace={(workspaceId, name) => void renameWorkspace(workspaceId, name)}
|
||||||
|
onDeleteWorkspace={(workspaceId) => void deleteWorkspace(workspaceId)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<LayoutButton
|
<LayoutButton
|
||||||
theme={theme}
|
theme={theme}
|
||||||
@@ -597,6 +687,26 @@ export function MainChartsPage({ theme }: MainChartsPageProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{!workspaceWindow && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title="Destacar workspace"
|
||||||
|
aria-label="Destacar workspace"
|
||||||
|
disabled={!activeWorkspaceId || workspaceWindow}
|
||||||
|
onClick={detachWorkspace}
|
||||||
|
className={
|
||||||
|
activeWorkspaceId && !workspaceWindow
|
||||||
|
? isDark
|
||||||
|
? `${RADIUS} grid h-10 w-10 place-items-center border border-[#263247] bg-[#111A2B] text-[#A8B3C7] transition hover:border-[#36506D] hover:text-white`
|
||||||
|
: `${RADIUS} grid h-10 w-10 place-items-center border border-[#D7DEE8] bg-[#F8FAFC] text-slate-600 transition hover:bg-white hover:text-[#0F172A]`
|
||||||
|
: `${RADIUS} grid h-10 w-10 cursor-not-allowed place-items-center border border-[#263247] bg-[#111A2B]/50 text-[#7F8CA3]/50`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Maximize2 className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="ml-auto flex items-center gap-4">
|
<div className="ml-auto flex items-center gap-4">
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
@@ -671,6 +781,7 @@ export function MainChartsPage({ theme }: MainChartsPageProps) {
|
|||||||
<EmptyWorkspace
|
<EmptyWorkspace
|
||||||
theme={theme}
|
theme={theme}
|
||||||
canAddMoreCharts={canAddMoreCharts}
|
canAddMoreCharts={canAddMoreCharts}
|
||||||
|
minHeightClass="min-h-[520px]"
|
||||||
onAddChart={() => {
|
onAddChart={() => {
|
||||||
if (canAddMoreCharts) setNewChartOpen(true);
|
if (canAddMoreCharts) setNewChartOpen(true);
|
||||||
}}
|
}}
|
||||||
@@ -696,8 +807,6 @@ export function MainChartsPage({ theme }: MainChartsPageProps) {
|
|||||||
setCharts={setCharts}
|
setCharts={setCharts}
|
||||||
placingChartId={placingChartId}
|
placingChartId={placingChartId}
|
||||||
placeChartHere={placeChartHere}
|
placeChartHere={placeChartHere}
|
||||||
detachChart={detachChart}
|
|
||||||
attachChart={attachChart}
|
|
||||||
moveDetachedChart={moveDetachedChart}
|
moveDetachedChart={moveDetachedChart}
|
||||||
viewportCompact={viewportCompact}
|
viewportCompact={viewportCompact}
|
||||||
layoutMode={layoutMode}
|
layoutMode={layoutMode}
|
||||||
@@ -794,8 +903,6 @@ function WorkspaceChartContainer({
|
|||||||
setCharts,
|
setCharts,
|
||||||
placingChartId,
|
placingChartId,
|
||||||
placeChartHere,
|
placeChartHere,
|
||||||
detachChart,
|
|
||||||
attachChart,
|
|
||||||
moveDetachedChart,
|
moveDetachedChart,
|
||||||
viewportCompact,
|
viewportCompact,
|
||||||
layoutMode,
|
layoutMode,
|
||||||
@@ -815,8 +922,6 @@ function WorkspaceChartContainer({
|
|||||||
setCharts: React.Dispatch<React.SetStateAction<ChartWorkspaceItem[]>>;
|
setCharts: React.Dispatch<React.SetStateAction<ChartWorkspaceItem[]>>;
|
||||||
placingChartId: string | null;
|
placingChartId: string | null;
|
||||||
placeChartHere: (targetChartId: string) => void;
|
placeChartHere: (targetChartId: string) => void;
|
||||||
detachChart: (chartId: string) => void;
|
|
||||||
attachChart: (chartId: string) => void | Promise<void>;
|
|
||||||
moveDetachedChart: (chartId: string, x: number, y: number) => void;
|
moveDetachedChart: (chartId: string, x: number, y: number) => void;
|
||||||
viewportCompact: boolean;
|
viewportCompact: boolean;
|
||||||
layoutMode: ChartLayoutMode;
|
layoutMode: ChartLayoutMode;
|
||||||
@@ -903,19 +1008,6 @@ function WorkspaceChartContainer({
|
|||||||
<Copy className="h-4 w-4" />
|
<Copy className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
title={chartItem.detached ? "Repor janela" : "Destacar janela"}
|
|
||||||
className={floatingIconClass(theme)}
|
|
||||||
onClick={() =>
|
|
||||||
chartItem.detached
|
|
||||||
? attachChart(chartItem.id)
|
|
||||||
: detachChart(chartItem.id)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Maximize2 className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
title="Fechar"
|
title="Fechar"
|
||||||
@@ -956,8 +1048,6 @@ function WorkspaceChartContainer({
|
|||||||
loading={loading || variablesStillResolving}
|
loading={loading || variablesStillResolving}
|
||||||
detached={chartItem.detached}
|
detached={chartItem.detached}
|
||||||
compact={shouldUseCompactMode}
|
compact={shouldUseCompactMode}
|
||||||
onDetach={() => detachChart(chartItem.id)}
|
|
||||||
onAttach={() => attachChart(chartItem.id)}
|
|
||||||
onTimeRangeChange={(range) =>
|
onTimeRangeChange={(range) =>
|
||||||
setCharts((current) =>
|
setCharts((current) =>
|
||||||
current.map((chart) =>
|
current.map((chart) =>
|
||||||
@@ -1492,193 +1582,5 @@ function EmptyVariableList({ theme }: { theme: "dark" | "light" }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function layoutGridClass(layoutMode: ChartLayoutMode) {
|
|
||||||
if (layoutMode === "fourGrid") {
|
|
||||||
return "grid min-h-0 flex-1 grid-cols-2 grid-rows-2 gap-3 overflow-hidden";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (layoutMode === "twoColumns") {
|
|
||||||
return "grid min-h-0 flex-1 grid-cols-2 gap-3 overflow-hidden";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (layoutMode === "twoRows") {
|
|
||||||
return "grid min-h-0 flex-1 grid-rows-2 gap-3 overflow-hidden";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "grid min-h-0 flex-1 gap-3 overflow-hidden";
|
|
||||||
}
|
|
||||||
function getVisibleSlotCount(layoutMode: ChartLayoutMode) {
|
|
||||||
if (layoutMode === "single") return 1;
|
|
||||||
if (layoutMode === "twoColumns") return 2;
|
|
||||||
if (layoutMode === "twoRows") return 2;
|
|
||||||
return 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
function LayoutButton({
|
|
||||||
theme,
|
|
||||||
active,
|
|
||||||
icon,
|
|
||||||
title,
|
|
||||||
onClick,
|
|
||||||
}: {
|
|
||||||
theme: "dark" | "light";
|
|
||||||
active: boolean;
|
|
||||||
icon: React.ReactNode;
|
|
||||||
title: string;
|
|
||||||
onClick: () => void;
|
|
||||||
}) {
|
|
||||||
const isDark = theme === "dark";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
title={title}
|
|
||||||
aria-label={title}
|
|
||||||
onClick={onClick}
|
|
||||||
className={
|
|
||||||
active
|
|
||||||
? isDark
|
|
||||||
? `${RADIUS} grid h-10 w-10 place-items-center bg-[#4FD1C5] text-[#07101B]`
|
|
||||||
: `${RADIUS} grid h-10 w-10 place-items-center bg-[#0F766E] text-white`
|
|
||||||
: isDark
|
|
||||||
? `${RADIUS} grid h-10 w-10 place-items-center border border-[#263247] bg-[#111A2B] text-[#A8B3C7] transition hover:border-[#36506D] hover:text-white`
|
|
||||||
: `${RADIUS} grid h-10 w-10 place-items-center border border-[#D7DEE8] bg-[#F8FAFC] text-slate-600 transition hover:bg-white hover:text-[#0F172A]`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ModeButton({
|
|
||||||
theme,
|
|
||||||
active,
|
|
||||||
icon,
|
|
||||||
label,
|
|
||||||
onClick,
|
|
||||||
}: {
|
|
||||||
theme: "dark" | "light";
|
|
||||||
active: boolean;
|
|
||||||
icon?: React.ReactNode;
|
|
||||||
label: string;
|
|
||||||
onClick: () => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onClick}
|
|
||||||
className={
|
|
||||||
active
|
|
||||||
? theme === "dark"
|
|
||||||
? `${RADIUS} flex items-center justify-center gap-2 bg-[#4FD1C5] px-3 py-2 text-xs font-black text-[#07101B]`
|
|
||||||
: `${RADIUS} flex items-center justify-center gap-2 bg-[#0F766E] px-3 py-2 text-xs font-black text-white`
|
|
||||||
: theme === "dark"
|
|
||||||
? `${RADIUS} flex items-center justify-center gap-2 border border-[#263247] bg-[#111A2B] px-3 py-2 text-xs font-bold text-[#A8B3C7]`
|
|
||||||
: `${RADIUS} flex items-center justify-center gap-2 border border-[#D7DEE8] bg-[#F8FAFC] px-3 py-2 text-xs font-bold text-slate-600`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
{label}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function EmptyWorkspace({
|
|
||||||
theme,
|
|
||||||
canAddMoreCharts,
|
|
||||||
onAddChart,
|
|
||||||
onOpenSaved,
|
|
||||||
}: {
|
|
||||||
theme: "dark" | "light";
|
|
||||||
canAddMoreCharts: boolean;
|
|
||||||
onAddChart: () => void;
|
|
||||||
onOpenSaved: () => void;
|
|
||||||
}) {
|
|
||||||
const isDark = theme === "dark";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section
|
|
||||||
className={
|
|
||||||
isDark
|
|
||||||
? `${RADIUS} flex min-h-[520px] items-center justify-center border border-dashed border-[#33445F] bg-[#0E1726]/70`
|
|
||||||
: `${RADIUS} flex min-h-[520px] items-center justify-center border border-dashed border-[#CBD5E1] bg-white`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="max-w-[420px] text-center">
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
isDark
|
|
||||||
? "mx-auto grid h-16 w-16 place-items-center rounded-2xl border border-[#263247] bg-[#111A2B] text-[#4FD1C5]"
|
|
||||||
: "mx-auto grid h-16 w-16 place-items-center rounded-2xl border border-[#D7DEE8] bg-[#F8FAFC] text-[#0F766E]"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<BarChart3 className="h-8 w-8" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2
|
|
||||||
className={
|
|
||||||
isDark
|
|
||||||
? "mt-5 text-xl font-black text-white"
|
|
||||||
: "mt-5 text-xl font-black text-[#0F172A]"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Nenhum gráfico aberto
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<p
|
|
||||||
className={
|
|
||||||
isDark
|
|
||||||
? "mt-3 text-sm leading-6 text-[#A8B3C7]"
|
|
||||||
: "mt-3 text-sm leading-6 text-slate-600"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Abra um gráfico guardado ou crie um novo gráfico para continuar.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="mt-6 flex items-center justify-center gap-3">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled={!canAddMoreCharts}
|
|
||||||
onClick={onAddChart}
|
|
||||||
className={
|
|
||||||
canAddMoreCharts
|
|
||||||
? `inline-flex h-11 items-center gap-3 ${RADIUS} bg-[#18B8A6] px-5 text-sm font-black text-white transition hover:bg-[#21C7B5]`
|
|
||||||
: `inline-flex h-11 cursor-not-allowed items-center gap-3 ${RADIUS} bg-[#18B8A6]/40 px-5 text-sm font-black text-white`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Plus className="h-5 w-5" />
|
|
||||||
Novo Gráfico
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onOpenSaved}
|
|
||||||
className={
|
|
||||||
isDark
|
|
||||||
? `inline-flex h-11 items-center gap-3 ${RADIUS} border border-[#263247] bg-[#111A2B] px-5 text-sm font-black text-white`
|
|
||||||
: `inline-flex h-11 items-center gap-3 ${RADIUS} border border-[#D7DEE8] bg-[#F8FAFC] px-5 text-sm font-black text-[#0F172A]`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Search className="h-5 w-5" />
|
|
||||||
Abrir Guardado
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getVariableColor(index: number) {
|
|
||||||
const colors = [
|
|
||||||
"#4FD1C5",
|
|
||||||
"#3B82F6",
|
|
||||||
"#FACC15",
|
|
||||||
"#7DD3FC",
|
|
||||||
"#A5B4FC",
|
|
||||||
"#FB7185",
|
|
||||||
];
|
|
||||||
|
|
||||||
return colors[index % colors.length];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MainChartsPage;
|
export default MainChartsPage;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { emit, listen } from "@tauri-apps/api/event";
|
||||||
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
|
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||||
import {
|
import {
|
||||||
AreaChart,
|
AreaChart,
|
||||||
@@ -25,12 +25,19 @@ import { getBackendApiUrl } from "../../../lib/api/gatewayConfig";
|
|||||||
import { readOptionalJsonResponse } from "../../../lib/api/readJsonResponse";
|
import { readOptionalJsonResponse } from "../../../lib/api/readJsonResponse";
|
||||||
|
|
||||||
import { ChartConfigModal } from "../../chartworkspace/components/ChartConfigModal";
|
import { ChartConfigModal } from "../../chartworkspace/components/ChartConfigModal";
|
||||||
import { openChartWindow } from "../../chartworkspace/utils/openChartWindow";
|
import {
|
||||||
|
EmptyWorkspace,
|
||||||
|
LayoutButton,
|
||||||
|
ModeButton,
|
||||||
|
WorkspaceSelector,
|
||||||
|
} from "../../chartworkspace/components/ChartWorkspaceControls";
|
||||||
|
import { openWorkspaceWindow } from "../../chartworkspace/utils/openWorkspaceWindow";
|
||||||
|
import { useChartWorkspaceSelection } from "../../chartworkspace/hooks/useChartWorkspaceSelection";
|
||||||
|
import { saveChartWorkspace } from "../../chartworkspace/api/chartWorkspaceApi";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
WorkspaceChart,
|
WorkspaceChart,
|
||||||
type WorkspaceChartConfig,
|
type WorkspaceChartConfig,
|
||||||
type WorkspaceChartInterval,
|
|
||||||
type WorkspaceChartMode,
|
type WorkspaceChartMode,
|
||||||
type WorkspaceChartTimeRange,
|
type WorkspaceChartTimeRange,
|
||||||
} from "../../../components/charts/WorkspaceChart";
|
} from "../../../components/charts/WorkspaceChart";
|
||||||
@@ -41,11 +48,20 @@ import { useMeteoChartCatalog } from "../hooks/useMeteoChartCatalog";
|
|||||||
import {
|
import {
|
||||||
useChartWorkspacePersistence,
|
useChartWorkspacePersistence,
|
||||||
type ChartLayoutMode,
|
type ChartLayoutMode,
|
||||||
type PersistedChartWorkspaceItem,
|
|
||||||
} from "../../chartworkspace/hooks/useChartWorkspacePersistence";
|
} from "../../chartworkspace/hooks/useChartWorkspacePersistence";
|
||||||
|
import type { ChartWorkspaceItem } from "../../chartworkspace/types";
|
||||||
|
import {
|
||||||
|
getVariableColor,
|
||||||
|
getVisibleSlotCount,
|
||||||
|
layoutGridClass,
|
||||||
|
MAX_CHARTS,
|
||||||
|
MAX_VARIABLES_PER_CHART,
|
||||||
|
} from "../../chartworkspace/domain/chartWorkspaceModel";
|
||||||
|
|
||||||
type MeteoChartsPageProps = {
|
type MeteoChartsPageProps = {
|
||||||
theme: "dark" | "light";
|
theme: "dark" | "light";
|
||||||
|
workspaceId?: number | null;
|
||||||
|
workspaceWindow?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type HistorianPoint = {
|
type HistorianPoint = {
|
||||||
@@ -56,33 +72,14 @@ type HistorianPoint = {
|
|||||||
total?: number | string;
|
total?: number | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ChartWorkspaceItem = PersistedChartWorkspaceItem & {
|
|
||||||
hidden?: boolean;
|
|
||||||
collapsed?: boolean;
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
subtitle: string;
|
|
||||||
mode: WorkspaceChartMode;
|
|
||||||
selectedSensorKeys: string[];
|
|
||||||
hiddenSensorKeys?: string[];
|
|
||||||
timeRange: WorkspaceChartTimeRange;
|
|
||||||
interval: WorkspaceChartInterval;
|
|
||||||
detached?: boolean;
|
|
||||||
windowX?: number;
|
|
||||||
windowY?: number;
|
|
||||||
windowWidth?: number;
|
|
||||||
windowHeight?: number;
|
|
||||||
windowZIndex?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const RADIUS = "rounded-[6px]";
|
const RADIUS = "rounded-[6px]";
|
||||||
const MAX_CHARTS = 10;
|
|
||||||
const MAX_VARIABLES_PER_CHART = 6;
|
|
||||||
|
|
||||||
const INITIAL_CHARTS: ChartWorkspaceItem[] = [];
|
const INITIAL_CHARTS: ChartWorkspaceItem[] = [];
|
||||||
|
|
||||||
export function MeteoChartsPage({
|
export function MeteoChartsPage({
|
||||||
theme,
|
theme,
|
||||||
|
workspaceId = null,
|
||||||
|
workspaceWindow = false,
|
||||||
}: MeteoChartsPageProps) {
|
}: MeteoChartsPageProps) {
|
||||||
|
|
||||||
const isDark = theme === "dark";
|
const isDark = theme === "dark";
|
||||||
@@ -100,6 +97,123 @@ export function MeteoChartsPage({
|
|||||||
const [movingChartId, setMovingChartId] = useState<string | null>(null);
|
const [movingChartId, setMovingChartId] = useState<string | null>(null);
|
||||||
const [newChartOpen, setNewChartOpen] = useState(false);
|
const [newChartOpen, setNewChartOpen] = useState(false);
|
||||||
const [placingChartId, setPlacingChartId] = useState<string | null>(null);
|
const [placingChartId, setPlacingChartId] = useState<string | null>(null);
|
||||||
|
const [detachedWorkspaceIds, setDetachedWorkspaceIds] = useState<Set<number>>(() => new Set());
|
||||||
|
|
||||||
|
const workspaceSelection = useChartWorkspaceSelection({
|
||||||
|
scope: "METEO",
|
||||||
|
defaultName: "Workspace Meteo",
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeWorkspaceId = workspaceId ?? workspaceSelection.activeWorkspaceId;
|
||||||
|
const activeWorkspace =
|
||||||
|
workspaceSelection.workspaces.find((workspace) => workspace.id === activeWorkspaceId) ??
|
||||||
|
workspaceSelection.activeWorkspace;
|
||||||
|
const activeWorkspaceName = activeWorkspace?.name ?? "Workspace Meteo";
|
||||||
|
|
||||||
|
const workspaceWindowLabel = (workspaceId: number) =>
|
||||||
|
`workspace-meteo-${workspaceId}`;
|
||||||
|
|
||||||
|
const renameWorkspace = async (workspaceId: number, name: string) => {
|
||||||
|
const workspace = workspaceSelection.workspaces.find(
|
||||||
|
(item) => item.id === workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!workspace) return;
|
||||||
|
|
||||||
|
await saveChartWorkspace(
|
||||||
|
"METEO",
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
sortOrder: workspace.sortOrder,
|
||||||
|
defaultWorkspace: workspace.defaultWorkspace,
|
||||||
|
layoutMode: workspace.layoutMode,
|
||||||
|
chartsJson: workspace.chartsJson,
|
||||||
|
},
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
workspaceSelection.updateWorkspaceName(workspaceId, name);
|
||||||
|
|
||||||
|
const existing = await WebviewWindow.getByLabel(
|
||||||
|
workspaceWindowLabel(workspaceId),
|
||||||
|
);
|
||||||
|
await existing?.setTitle(name);
|
||||||
|
await emit("workspace-window://renamed", {
|
||||||
|
scope: "METEO",
|
||||||
|
workspaceId,
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteWorkspace = async (workspaceId: number) => {
|
||||||
|
const existing = await WebviewWindow.getByLabel(
|
||||||
|
workspaceWindowLabel(workspaceId),
|
||||||
|
);
|
||||||
|
await existing?.close();
|
||||||
|
|
||||||
|
setDetachedWorkspaceIds((current) => {
|
||||||
|
const next = new Set(current);
|
||||||
|
next.delete(workspaceId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
|
||||||
|
await workspaceSelection.deleteWorkspace(workspaceId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const detachWorkspace = () => {
|
||||||
|
if (!activeWorkspaceId) return;
|
||||||
|
|
||||||
|
setDetachedWorkspaceIds((current) => new Set(current).add(activeWorkspaceId));
|
||||||
|
|
||||||
|
const nextWorkspace = workspaceSelection.workspaces.find(
|
||||||
|
(workspace) =>
|
||||||
|
workspace.id !== activeWorkspaceId &&
|
||||||
|
!detachedWorkspaceIds.has(workspace.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (nextWorkspace) {
|
||||||
|
void workspaceSelection.selectWorkspace(nextWorkspace.id);
|
||||||
|
} else {
|
||||||
|
workspaceSelection.setActiveWorkspace(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
void openWorkspaceWindow(
|
||||||
|
activeWorkspaceId,
|
||||||
|
"METEO",
|
||||||
|
theme,
|
||||||
|
activeWorkspaceName,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unlistenPromise = listen<{
|
||||||
|
scope: string;
|
||||||
|
workspaceId: number;
|
||||||
|
}>("workspace-window://closed", (event) => {
|
||||||
|
if (event.payload.scope !== "METEO") return;
|
||||||
|
|
||||||
|
setDetachedWorkspaceIds((current) => {
|
||||||
|
const next = new Set(current);
|
||||||
|
next.delete(event.payload.workspaceId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
void unlistenPromise.then((unlisten) => unlisten());
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCharts([]);
|
||||||
|
setConfigChartId(null);
|
||||||
|
setMovingChartId(null);
|
||||||
|
setPlacingChartId(null);
|
||||||
|
setSavedOpen(false);
|
||||||
|
}, [activeWorkspaceId]);
|
||||||
|
|
||||||
const changeLayoutMode = (nextLayoutMode: ChartLayoutMode) => {
|
const changeLayoutMode = (nextLayoutMode: ChartLayoutMode) => {
|
||||||
const nextVisibleCount = getVisibleSlotCount(nextLayoutMode);
|
const nextVisibleCount = getVisibleSlotCount(nextLayoutMode);
|
||||||
@@ -126,6 +240,7 @@ export function MeteoChartsPage({
|
|||||||
|
|
||||||
const workspacePersistence = useChartWorkspacePersistence({
|
const workspacePersistence = useChartWorkspacePersistence({
|
||||||
scope: "METEO",
|
scope: "METEO",
|
||||||
|
workspaceId: activeWorkspaceId,
|
||||||
layoutMode,
|
layoutMode,
|
||||||
charts,
|
charts,
|
||||||
onLoaded: (workspace) => {
|
onLoaded: (workspace) => {
|
||||||
@@ -386,50 +501,6 @@ export function MeteoChartsPage({
|
|||||||
setSavedOpen(false);
|
setSavedOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const detachChart = (chartId: string) => {
|
|
||||||
setCharts((current) =>
|
|
||||||
current.map((chart) =>
|
|
||||||
chart.id === chartId
|
|
||||||
? {
|
|
||||||
...chart,
|
|
||||||
detached: true,
|
|
||||||
hidden: false,
|
|
||||||
collapsed: false,
|
|
||||||
}
|
|
||||||
: chart,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
window.setTimeout(() => {
|
|
||||||
const chart = charts.find((item) => item.id === chartId);
|
|
||||||
|
|
||||||
openChartWindow(
|
|
||||||
chartId,
|
|
||||||
theme,
|
|
||||||
chart?.title ?? "Meteo Chart",
|
|
||||||
"METEO",
|
|
||||||
"meteocharts",
|
|
||||||
);
|
|
||||||
}, 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
const attachChart = async (chartId: string) => {
|
|
||||||
setCharts((current) =>
|
|
||||||
current.map((chart) =>
|
|
||||||
chart.id === chartId
|
|
||||||
? {
|
|
||||||
...chart,
|
|
||||||
detached: false,
|
|
||||||
hidden: false,
|
|
||||||
collapsed: false,
|
|
||||||
}
|
|
||||||
: chart,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await closeDetachedChartWindow(chartId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const moveDetachedChart = (chartId: string, x: number, y: number) => {
|
const moveDetachedChart = (chartId: string, x: number, y: number) => {
|
||||||
setCharts((current) =>
|
setCharts((current) =>
|
||||||
current.map((chart) =>
|
current.map((chart) =>
|
||||||
@@ -549,6 +620,22 @@ export function MeteoChartsPage({
|
|||||||
: `${RADIUS} relative flex flex-wrap items-center gap-3 border border-[#D7DEE8] bg-white p-2`
|
: `${RADIUS} relative flex flex-wrap items-center gap-3 border border-[#D7DEE8] bg-white p-2`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
{!workspaceWindow && (
|
||||||
|
<WorkspaceSelector
|
||||||
|
theme={theme}
|
||||||
|
workspaces={workspaceSelection.workspaces}
|
||||||
|
activeWorkspaceId={workspaceSelection.activeWorkspaceId}
|
||||||
|
detachedWorkspaceIds={detachedWorkspaceIds}
|
||||||
|
loading={workspaceSelection.loading}
|
||||||
|
creating={workspaceSelection.creating}
|
||||||
|
canCreateWorkspace={workspaceSelection.canCreateWorkspace}
|
||||||
|
onSelectWorkspace={workspaceSelection.selectWorkspace}
|
||||||
|
onCreateWorkspace={(name) => void workspaceSelection.createWorkspace(name)}
|
||||||
|
onRenameWorkspace={(workspaceId, name) => void renameWorkspace(workspaceId, name)}
|
||||||
|
onDeleteWorkspace={(workspaceId) => void deleteWorkspace(workspaceId)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<LayoutButton
|
<LayoutButton
|
||||||
theme={theme}
|
theme={theme}
|
||||||
@@ -583,6 +670,26 @@ export function MeteoChartsPage({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{!workspaceWindow && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title="Destacar workspace"
|
||||||
|
aria-label="Destacar workspace"
|
||||||
|
disabled={!activeWorkspaceId || workspaceWindow}
|
||||||
|
onClick={detachWorkspace}
|
||||||
|
className={
|
||||||
|
activeWorkspaceId && !workspaceWindow
|
||||||
|
? isDark
|
||||||
|
? `${RADIUS} grid h-10 w-10 place-items-center border border-[#263247] bg-[#111A2B] text-[#A8B3C7] transition hover:border-[#36506D] hover:text-white`
|
||||||
|
: `${RADIUS} grid h-10 w-10 place-items-center border border-[#D7DEE8] bg-[#F8FAFC] text-slate-600 transition hover:bg-white hover:text-[#0F172A]`
|
||||||
|
: `${RADIUS} grid h-10 w-10 cursor-not-allowed place-items-center border border-[#263247] bg-[#111A2B]/50 text-[#7F8CA3]/50`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Maximize2 className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="ml-auto flex items-center gap-4">
|
<div className="ml-auto flex items-center gap-4">
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
@@ -657,6 +764,7 @@ export function MeteoChartsPage({
|
|||||||
<EmptyWorkspace
|
<EmptyWorkspace
|
||||||
theme={theme}
|
theme={theme}
|
||||||
canAddMoreCharts={canAddMoreCharts}
|
canAddMoreCharts={canAddMoreCharts}
|
||||||
|
description="Abra um gráfico guardado ou crie um novo gráfico meteorológico para continuar."
|
||||||
onAddChart={() => {
|
onAddChart={() => {
|
||||||
if (canAddMoreCharts) setNewChartOpen(true);
|
if (canAddMoreCharts) setNewChartOpen(true);
|
||||||
}}
|
}}
|
||||||
@@ -683,8 +791,6 @@ export function MeteoChartsPage({
|
|||||||
setCharts={setCharts}
|
setCharts={setCharts}
|
||||||
placingChartId={placingChartId}
|
placingChartId={placingChartId}
|
||||||
placeChartHere={placeChartHere}
|
placeChartHere={placeChartHere}
|
||||||
detachChart={detachChart}
|
|
||||||
attachChart={attachChart}
|
|
||||||
moveDetachedChart={moveDetachedChart}
|
moveDetachedChart={moveDetachedChart}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -791,8 +897,6 @@ function WorkspaceChartContainer({
|
|||||||
setCharts,
|
setCharts,
|
||||||
placingChartId,
|
placingChartId,
|
||||||
placeChartHere,
|
placeChartHere,
|
||||||
detachChart,
|
|
||||||
attachChart,
|
|
||||||
moveDetachedChart,
|
moveDetachedChart,
|
||||||
}: {
|
}: {
|
||||||
theme: "dark" | "light";
|
theme: "dark" | "light";
|
||||||
@@ -811,8 +915,6 @@ function WorkspaceChartContainer({
|
|||||||
setCharts: React.Dispatch<React.SetStateAction<ChartWorkspaceItem[]>>;
|
setCharts: React.Dispatch<React.SetStateAction<ChartWorkspaceItem[]>>;
|
||||||
placingChartId: string | null;
|
placingChartId: string | null;
|
||||||
placeChartHere: (targetChartId: string) => void;
|
placeChartHere: (targetChartId: string) => void;
|
||||||
detachChart: (chartId: string) => void;
|
|
||||||
attachChart: (chartId: string) => void | Promise<void>;
|
|
||||||
moveDetachedChart: (chartId: string, x: number, y: number) => void;
|
moveDetachedChart: (chartId: string, x: number, y: number) => void;
|
||||||
}) {
|
}) {
|
||||||
const isMoving = movingChartId === chartItem.id;
|
const isMoving = movingChartId === chartItem.id;
|
||||||
@@ -899,19 +1001,6 @@ function WorkspaceChartContainer({
|
|||||||
<Copy className="h-4 w-4" />
|
<Copy className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
title={chartItem.detached ? "Repor janela" : "Destacar janela"}
|
|
||||||
className={floatingIconClass(theme)}
|
|
||||||
onClick={() =>
|
|
||||||
chartItem.detached
|
|
||||||
? attachChart(chartItem.id)
|
|
||||||
: detachChart(chartItem.id)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Maximize2 className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
title="Fechar"
|
title="Fechar"
|
||||||
@@ -952,8 +1041,6 @@ function WorkspaceChartContainer({
|
|||||||
configuredVariableCount={chartItem.selectedSensorKeys.length}
|
configuredVariableCount={chartItem.selectedSensorKeys.length}
|
||||||
loading={loading || variablesStillResolving}
|
loading={loading || variablesStillResolving}
|
||||||
detached={chartItem.detached}
|
detached={chartItem.detached}
|
||||||
onDetach={() => detachChart(chartItem.id)}
|
|
||||||
onAttach={() => attachChart(chartItem.id)}
|
|
||||||
onTimeRangeChange={(range) =>
|
onTimeRangeChange={(range) =>
|
||||||
setCharts((current) =>
|
setCharts((current) =>
|
||||||
current.map((chart) =>
|
current.map((chart) =>
|
||||||
@@ -1631,195 +1718,5 @@ function EmptyVariableList({ theme }: { theme: "dark" | "light" }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function layoutGridClass(layoutMode: ChartLayoutMode) {
|
|
||||||
if (layoutMode === "fourGrid") {
|
|
||||||
return "grid min-h-0 flex-1 grid-cols-2 grid-rows-2 gap-3 overflow-hidden";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (layoutMode === "twoColumns") {
|
|
||||||
return "grid min-h-0 flex-1 grid-cols-2 gap-3 overflow-hidden";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (layoutMode === "twoRows") {
|
|
||||||
return "grid min-h-0 flex-1 grid-rows-2 gap-3 overflow-hidden";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "grid min-h-0 flex-1 gap-3 overflow-hidden";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getVisibleSlotCount(layoutMode: ChartLayoutMode) {
|
|
||||||
if (layoutMode === "single") return 1;
|
|
||||||
if (layoutMode === "twoColumns") return 2;
|
|
||||||
if (layoutMode === "twoRows") return 2;
|
|
||||||
return 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
function LayoutButton({
|
|
||||||
theme,
|
|
||||||
active,
|
|
||||||
icon,
|
|
||||||
title,
|
|
||||||
onClick,
|
|
||||||
}: {
|
|
||||||
theme: "dark" | "light";
|
|
||||||
active: boolean;
|
|
||||||
icon: React.ReactNode;
|
|
||||||
title: string;
|
|
||||||
onClick: () => void;
|
|
||||||
}) {
|
|
||||||
const isDark = theme === "dark";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
title={title}
|
|
||||||
aria-label={title}
|
|
||||||
onClick={onClick}
|
|
||||||
className={
|
|
||||||
active
|
|
||||||
? isDark
|
|
||||||
? `${RADIUS} grid h-10 w-10 place-items-center bg-[#4FD1C5] text-[#07101B]`
|
|
||||||
: `${RADIUS} grid h-10 w-10 place-items-center bg-[#0F766E] text-white`
|
|
||||||
: isDark
|
|
||||||
? `${RADIUS} grid h-10 w-10 place-items-center border border-[#263247] bg-[#111A2B] text-[#A8B3C7] transition hover:border-[#36506D] hover:text-white`
|
|
||||||
: `${RADIUS} grid h-10 w-10 place-items-center border border-[#D7DEE8] bg-[#F8FAFC] text-slate-600 transition hover:bg-white hover:text-[#0F172A]`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ModeButton({
|
|
||||||
theme,
|
|
||||||
active,
|
|
||||||
icon,
|
|
||||||
label,
|
|
||||||
onClick,
|
|
||||||
}: {
|
|
||||||
theme: "dark" | "light";
|
|
||||||
active: boolean;
|
|
||||||
icon?: React.ReactNode;
|
|
||||||
label: string;
|
|
||||||
onClick: () => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onClick}
|
|
||||||
className={
|
|
||||||
active
|
|
||||||
? theme === "dark"
|
|
||||||
? `${RADIUS} flex items-center justify-center gap-2 bg-[#4FD1C5] px-3 py-2 text-xs font-black text-[#07101B]`
|
|
||||||
: `${RADIUS} flex items-center justify-center gap-2 bg-[#0F766E] px-3 py-2 text-xs font-black text-white`
|
|
||||||
: theme === "dark"
|
|
||||||
? `${RADIUS} flex items-center justify-center gap-2 border border-[#263247] bg-[#111A2B] px-3 py-2 text-xs font-bold text-[#A8B3C7]`
|
|
||||||
: `${RADIUS} flex items-center justify-center gap-2 border border-[#D7DEE8] bg-[#F8FAFC] px-3 py-2 text-xs font-bold text-slate-600`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
{label}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function EmptyWorkspace({
|
|
||||||
theme,
|
|
||||||
canAddMoreCharts,
|
|
||||||
onAddChart,
|
|
||||||
onOpenSaved,
|
|
||||||
}: {
|
|
||||||
theme: "dark" | "light";
|
|
||||||
canAddMoreCharts: boolean;
|
|
||||||
onAddChart: () => void;
|
|
||||||
onOpenSaved: () => void;
|
|
||||||
}) {
|
|
||||||
const isDark = theme === "dark";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section
|
|
||||||
className={
|
|
||||||
isDark
|
|
||||||
? `${RADIUS} flex min-h-0 flex-1 items-center justify-center border border-dashed border-[#33445F] bg-[#0E1726]/70`
|
|
||||||
: `${RADIUS} flex min-h-0 flex-1 items-center justify-center border border-dashed border-[#CBD5E1] bg-white`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="max-w-[420px] text-center">
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
isDark
|
|
||||||
? "mx-auto grid h-16 w-16 place-items-center rounded-2xl border border-[#263247] bg-[#111A2B] text-[#4FD1C5]"
|
|
||||||
: "mx-auto grid h-16 w-16 place-items-center rounded-2xl border border-[#D7DEE8] bg-[#F8FAFC] text-[#0F766E]"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<BarChart3 className="h-8 w-8" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2
|
|
||||||
className={
|
|
||||||
isDark
|
|
||||||
? "mt-5 text-xl font-black text-white"
|
|
||||||
: "mt-5 text-xl font-black text-[#0F172A]"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Nenhum gráfico aberto
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<p
|
|
||||||
className={
|
|
||||||
isDark
|
|
||||||
? "mt-3 text-sm leading-6 text-[#A8B3C7]"
|
|
||||||
: "mt-3 text-sm leading-6 text-slate-600"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Abra um gráfico guardado ou crie um novo gráfico meteorológico
|
|
||||||
para continuar.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="mt-6 flex items-center justify-center gap-3">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled={!canAddMoreCharts}
|
|
||||||
onClick={onAddChart}
|
|
||||||
className={
|
|
||||||
canAddMoreCharts
|
|
||||||
? `inline-flex h-11 items-center gap-3 ${RADIUS} bg-[#18B8A6] px-5 text-sm font-black text-white transition hover:bg-[#21C7B5]`
|
|
||||||
: `inline-flex h-11 cursor-not-allowed items-center gap-3 ${RADIUS} bg-[#18B8A6]/40 px-5 text-sm font-black text-white`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Plus className="h-5 w-5" />
|
|
||||||
Novo Gráfico
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onOpenSaved}
|
|
||||||
className={
|
|
||||||
isDark
|
|
||||||
? `inline-flex h-11 items-center gap-3 ${RADIUS} border border-[#263247] bg-[#111A2B] px-5 text-sm font-black text-white`
|
|
||||||
: `inline-flex h-11 items-center gap-3 ${RADIUS} border border-[#D7DEE8] bg-[#F8FAFC] px-5 text-sm font-black text-[#0F172A]`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Search className="h-5 w-5" />
|
|
||||||
Abrir Guardado
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getVariableColor(index: number) {
|
|
||||||
const colors = [
|
|
||||||
"#4FD1C5",
|
|
||||||
"#3B82F6",
|
|
||||||
"#FACC15",
|
|
||||||
"#7DD3FC",
|
|
||||||
"#A5B4FC",
|
|
||||||
"#FB7185",
|
|
||||||
];
|
|
||||||
|
|
||||||
return colors[index % colors.length];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MeteoChartsPage;
|
export default MeteoChartsPage;
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ type Accent = "amber" | "blue" | "cyan" | "emerald" | "violet";
|
|||||||
|
|
||||||
const HISTORY_HOURS = 6;
|
const HISTORY_HOURS = 6;
|
||||||
const RADIUS = "rounded-[5px]";
|
const RADIUS = "rounded-[5px]";
|
||||||
|
const OVERVIEW_PANEL_HEIGHT = "h-[488px] min-h-[488px]";
|
||||||
|
|
||||||
const HISTORY_KEYS = {
|
const HISTORY_KEYS = {
|
||||||
temperature: "temperature",
|
temperature: "temperature",
|
||||||
@@ -969,10 +970,10 @@ function WeatherSummaryPanel({
|
|||||||
const isDark = theme === "dark";
|
const isDark = theme === "dark";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={panelClass(isDark)}>
|
<section className={`${panelClass(isDark)} ${OVERVIEW_PANEL_HEIGHT}`}>
|
||||||
<h2 className={panelTitleClass(isDark)}>Resumo meteorológico</h2>
|
<h2 className={panelTitleClass(isDark)}>Resumo meteorológico</h2>
|
||||||
|
|
||||||
<div className="mt-3 grid flex-1 content-start gap-2">
|
<div className="mt-3 grid content-start gap-2">
|
||||||
<SummaryTile
|
<SummaryTile
|
||||||
theme={theme}
|
theme={theme}
|
||||||
icon={Thermometer}
|
icon={Thermometer}
|
||||||
@@ -1065,11 +1066,11 @@ function WindDirectionPanel({
|
|||||||
const consistency = windConsistency(history);
|
const consistency = windConsistency(history);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={panelClass(isDark)}>
|
<section className={`${panelClass(isDark)} ${OVERVIEW_PANEL_HEIGHT}`}>
|
||||||
<h2 className={panelTitleClass(isDark)}>Direção do vento</h2>
|
<h2 className={panelTitleClass(isDark)}>Direção do vento</h2>
|
||||||
|
|
||||||
<div className="mt-2 flex flex-1 items-center justify-center">
|
<div className="flex min-h-0 flex-1 items-center justify-center py-4">
|
||||||
<div className="relative h-[250px] w-[250px]">
|
<div className="relative h-[240px] w-[240px]">
|
||||||
<div className={isDark ? "absolute inset-0 rounded-full border border-white/10 bg-[#0b1220]" : "absolute inset-0 rounded-full border border-slate-200 bg-slate-50"} />
|
<div className={isDark ? "absolute inset-0 rounded-full border border-white/10 bg-[#0b1220]" : "absolute inset-0 rounded-full border border-slate-200 bg-slate-50"} />
|
||||||
<div className={isDark ? "absolute inset-6 rounded-full border border-white/10" : "absolute inset-6 rounded-full border border-slate-200"} />
|
<div className={isDark ? "absolute inset-6 rounded-full border border-white/10" : "absolute inset-6 rounded-full border border-slate-200"} />
|
||||||
<div className={isDark ? "absolute inset-[74px] rounded-full border border-white/10 bg-white/[0.02]" : "absolute inset-[74px] rounded-full border border-slate-200 bg-white"} />
|
<div className={isDark ? "absolute inset-[74px] rounded-full border border-white/10 bg-white/[0.02]" : "absolute inset-[74px] rounded-full border border-slate-200 bg-white"} />
|
||||||
@@ -1135,7 +1136,7 @@ function WindDirectionPanel({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-2 grid grid-cols-3 gap-2">
|
<div className="mt-auto grid grid-cols-3 gap-2 pt-4">
|
||||||
<SmallStat
|
<SmallStat
|
||||||
theme={theme}
|
theme={theme}
|
||||||
label="Rajada máx."
|
label="Rajada máx."
|
||||||
@@ -1249,7 +1250,7 @@ function RealtimeChartPanel({
|
|||||||
const isDark = theme === "dark";
|
const isDark = theme === "dark";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={`${panelClass(isDark)} relative min-h-0 p-0`}>
|
<section className={`${panelClass(isDark)} ${OVERVIEW_PANEL_HEIGHT} relative min-h-0 overflow-hidden p-0`}>
|
||||||
<div className="absolute right-3 top-3 z-20">
|
<div className="absolute right-3 top-3 z-20">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1270,6 +1271,7 @@ function RealtimeChartPanel({
|
|||||||
chart={chart}
|
chart={chart}
|
||||||
loading={historyLoading && !hasAnyChartData}
|
loading={historyLoading && !hasAnyChartData}
|
||||||
configuredVariableCount={series.length}
|
configuredVariableCount={series.length}
|
||||||
|
compact
|
||||||
onModeChange={setMode}
|
onModeChange={setMode}
|
||||||
onVariableToggle={handleVariableToggle}
|
onVariableToggle={handleVariableToggle}
|
||||||
onTimeRangeChange={setTimeRange}
|
onTimeRangeChange={setTimeRange}
|
||||||
@@ -1297,7 +1299,7 @@ function chartColor(accent: Accent) {
|
|||||||
|
|
||||||
function CompactMeteoChart({ children }: { children: React.ReactNode }) {
|
function CompactMeteoChart({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className="h-full [&>section]:h-full [&>section]:border-0 [&>section]:bg-transparent [&>section]:shadow-none [&>section>header]:px-4 [&>section>header]:py-3 [&>section>main]:px-3 [&>section>main]:pb-3 [&_main_section>div.relative]:h-[235px] [&_main_section>div.relative]:min-h-[235px]">
|
<div className="h-full min-h-0 overflow-hidden [&>section]:h-full [&>section]:min-h-0 [&>section]:overflow-hidden [&>section]:border-0 [&>section]:bg-transparent [&>section]:shadow-none [&>section>header]:px-4 [&>section>header]:py-3 [&>section>main]:min-h-0 [&>section>main]:px-3 [&>section>main]:pb-3 [&>section>main>section]:min-h-0 [&>section>main>section]:overflow-hidden">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -1480,8 +1482,8 @@ function HeroState({ children }: { children: string }) {
|
|||||||
|
|
||||||
function panelClass(isDark: boolean) {
|
function panelClass(isDark: boolean) {
|
||||||
return isDark
|
return isDark
|
||||||
? `${RADIUS} flex h-full flex-col border border-white/10 bg-[#071421] p-3 shadow-[0_14px_34px_rgba(0,0,0,0.22)]`
|
? `${RADIUS} flex flex-col border border-white/10 bg-[#071421] p-3 shadow-[0_14px_34px_rgba(0,0,0,0.22)]`
|
||||||
: `${RADIUS} flex h-full flex-col border border-slate-200 bg-white p-3 shadow-[0_10px_26px_rgba(15,23,42,0.06)]`;
|
: `${RADIUS} flex flex-col border border-slate-200 bg-white p-3 shadow-[0_10px_26px_rgba(15,23,42,0.06)]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function panelTitleClass(isDark: boolean) {
|
function panelTitleClass(isDark: boolean) {
|
||||||
|
|||||||
Reference in New Issue
Block a user