diff --git a/.env b/.env index ab9f444..0cc6095 100644 --- a/.env +++ b/.env @@ -1 +1 @@ -VITE_GATEWAY_BASE_URL=http://localhost:18080 +VITE_GATEWAY_BASE_URL=http://146.59.230.190:18080 diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 401edc5..f3ce366 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -7,7 +7,8 @@ "chart-*", "maincharts-*", "climatecharts-*", - "meteocharts-*" + "meteocharts-*", + "workspace-*" ], "permissions": [ "core:default", @@ -20,6 +21,7 @@ "core:window:allow-show", "core:window:allow-hide", "core:window:allow-set-focus", + "core:window:allow-set-title", "core:window:allow-minimize", "core:window:allow-maximize", @@ -30,4 +32,4 @@ "core:event:allow-listen", "core:event:allow-emit" ] -} \ No newline at end of file +} diff --git a/src/app/App.tsx b/src/app/App.tsx index 3225c41..d635617 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -7,7 +7,7 @@ import { MeteoPage } from "../features/meteo/pages/MeteoPage"; import { ClimateChartsPage } from "../features/climate/pages/ClimateChartsPage"; import { ConsolePage } from "../features/console/pages/ConsolePage"; 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 SynopticPage from "../features/synoptic/pages/SynopticPage"; import MeteoChartsPage from "../features/meteo/pages/MeteoChartsPage"; @@ -38,13 +38,17 @@ function App() { const { authenticated } = useAuth(); const [activePage, setActivePage] = useState("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 theme = params.get("theme") === "light" ? "light" : "dark"; - return ; + return ( + + + + ); } if (!authenticated) { diff --git a/src/components/charts/WorkspaceChart.tsx b/src/components/charts/WorkspaceChart.tsx index 3fbf711..5df00cc 100644 --- a/src/components/charts/WorkspaceChart.tsx +++ b/src/components/charts/WorkspaceChart.tsx @@ -4,7 +4,6 @@ import { Loader2, ChevronDown, Download, - Maximize2, Save, Settings2, SlidersHorizontal, @@ -73,8 +72,6 @@ type Props = { onIntervalChange: (interval: WorkspaceChartInterval) => void; onSave?: () => void; onExport?: () => void; - onDetach?: () => void; - onAttach?: () => void; loading?: boolean; configuredVariableCount?: number; }; @@ -127,8 +124,6 @@ export function WorkspaceChart({ onIntervalChange, onSave, onExport, - onDetach, - onAttach, configuredVariableCount = chart.variables.length }: Props) { const isDark = theme === "dark"; @@ -360,16 +355,6 @@ export function WorkspaceChart({ )} - {onDetach && onAttach && ( - - )} @@ -1250,4 +1235,4 @@ function downsampleRows(rows: ChartRow[], maxRows: number) { return rows.filter((_, index) => index % step === 0); } -export default WorkspaceChart; \ No newline at end of file +export default WorkspaceChart; diff --git a/src/features/chartworkspace/api/chartWorkspaceApi.ts b/src/features/chartworkspace/api/chartWorkspaceApi.ts new file mode 100644 index 0000000..ce2b0e9 --- /dev/null +++ b/src/features/chartworkspace/api/chartWorkspaceApi.ts @@ -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( + 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( + 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( + 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( + 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}`); + } +} diff --git a/src/features/chartworkspace/components/ChartConfigModal.tsx b/src/features/chartworkspace/components/ChartConfigModal.tsx index d9cd61e..700da06 100644 --- a/src/features/chartworkspace/components/ChartConfigModal.tsx +++ b/src/features/chartworkspace/components/ChartConfigModal.tsx @@ -13,6 +13,8 @@ import type { WorkspaceChartInterval, } from "../../../components/charts/WorkspaceChart"; import type { ChartVariable } from "../../telemetry/types/telemetryCatalog"; +import { ModeButton } from "./ChartWorkspaceControls"; +import { getVariableColor } from "../domain/chartWorkspaceModel"; export type ChartConfigModalChart = { 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 ( - - ); -} -function getVariableColor(index: number) { - const colors = [ - "#4FD1C5", - "#3B82F6", - "#FACC15", - "#7DD3FC", - "#A5B4FC", - "#FB7185", - ]; - - return colors[index % colors.length]; -} \ No newline at end of file diff --git a/src/features/chartworkspace/components/ChartWorkspaceControls.tsx b/src/features/chartworkspace/components/ChartWorkspaceControls.tsx new file mode 100644 index 0000000..cd83f4b --- /dev/null +++ b/src/features/chartworkspace/components/ChartWorkspaceControls.tsx @@ -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 ( + + ); +} + +export function ModeButton({ + theme, + active, + icon, + label, + onClick, +}: { + theme: "dark" | "light"; + active: boolean; + icon?: ReactNode; + label: string; + onClick: () => void; +}) { + return ( + + ); +} + +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 ( +
+
+
+ +
+ +

+ {title} +

+ +

+ {description} +

+ +
+ + + +
+
+
+ ); +} + +export function WorkspaceSelector({ + theme, + workspaces, + activeWorkspaceId, + detachedWorkspaceIds, + loading, + creating, + canCreateWorkspace, + onSelectWorkspace, + onCreateWorkspace, + onRenameWorkspace, + onDeleteWorkspace, +}: { + theme: "dark" | "light"; + workspaces: ChartWorkspaceResponse[]; + activeWorkspaceId: number | null; + detachedWorkspaceIds: Set; + loading: boolean; + creating: boolean; + canCreateWorkspace: boolean; + onSelectWorkspace: (workspaceId: number) => void; + onCreateWorkspace: (name: string) => void; + onRenameWorkspace: (workspaceId: number, name: string) => void | Promise; + onDeleteWorkspace: (workspaceId: number) => void | Promise; +}) { + const isDark = theme === "dark"; + const [open, setOpen] = useState(false); + const [createOpen, setCreateOpen] = useState(false); + const [renameWorkspace, setRenameWorkspace] = + useState(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 ( +
+ + + {open && ( +
+
+
+

Workspaces

+

+ {workspaces.length}/10 workspaces neste modulo. +

+
+ +
+ + + +
+
+ +
+ + setSearch(event.target.value)} + placeholder="Pesquisar workspaces..." + className="w-full bg-transparent outline-none placeholder:text-inherit" + /> +
+ +
+ {filteredWorkspaces.map((workspace) => { + const active = workspace.id === activeWorkspaceId; + const detached = detachedWorkspaceIds.has(workspace.id); + const chartCount = getWorkspaceChartCount(workspace); + + return ( +
+
+

+ {workspace.name} +

+

+ {chartCount}/10 graficos + {detached + ? " · detached" + : workspace.defaultWorkspace + ? " · ultimo aberto" + : ""} +

+
+ +
+ {active ? ( + + Aberto + + ) : detached ? ( + + Detached + + ) : ( + + )} + + + + +
+
+ ); + })} + + {filteredWorkspaces.length === 0 && ( +
+ Nenhum workspace encontrado. +
+ )} +
+
+ )} + + {createOpen && ( + setCreateOpen(false)} + onCreate={createWorkspace} + /> + )} + + {renameWorkspace && ( + setRenameWorkspace(null)} + onSubmit={renameSelectedWorkspace} + /> + )} +
+ ); +} + +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 ( +
+
+
+

{title}

+ + +
+ +
+ + + 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]` + } + /> +
+ +
+ + + +
+
+
+ ); +} + +function NewWorkspaceModal({ + theme, + creating, + onClose, + onCreate, +}: { + theme: "dark" | "light"; + creating: boolean; + onClose: () => void; + onCreate: (name: string) => void; +}) { + return ( + + ); +} diff --git a/src/features/chartworkspace/domain/chartWorkspaceModel.ts b/src/features/chartworkspace/domain/chartWorkspaceModel.ts new file mode 100644 index 0000000..1350b1e --- /dev/null +++ b/src/features/chartworkspace/domain/chartWorkspaceModel.ts @@ -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]; +} diff --git a/src/features/chartworkspace/hooks/useChartWorkspacePersistence.ts b/src/features/chartworkspace/hooks/useChartWorkspacePersistence.ts index 3b3c402..1370891 100644 --- a/src/features/chartworkspace/hooks/useChartWorkspacePersistence.ts +++ b/src/features/chartworkspace/hooks/useChartWorkspacePersistence.ts @@ -4,9 +4,11 @@ import type { WorkspaceChartMode, WorkspaceChartTimeRange, } from "../../../components/charts/WorkspaceChart"; -import { authFetch } from "../../../lib/api/authFetch"; -import { getBackendApiUrl } from "../../../lib/api/gatewayConfig"; -import { readJsonResponse } from "../../../lib/api/readJsonResponse"; +import { + loadChartWorkspace, + saveChartWorkspace, +} from "../api/chartWorkspaceApi"; +import type { ChartWorkspaceScope } from "../types"; const SAVE_DEBOUNCE_MS = 800; export type ChartLayoutMode = @@ -25,75 +27,67 @@ export type PersistedChartWorkspaceItem = { 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 = { scope: ChartWorkspaceScope; + workspaceId?: number | null; layoutMode: ChartLayoutMode; charts: PersistedChartWorkspaceItem[]; onLoaded: (workspace: { + id: number; + name: string; + defaultWorkspace: boolean; layoutMode: ChartLayoutMode; charts: PersistedChartWorkspaceItem[]; }) => void; + saveEnabled?: boolean; }; export function useChartWorkspacePersistence({ scope, + workspaceId = null, layoutMode, charts, onLoaded, + saveEnabled = true, }: UseChartWorkspacePersistenceParams) { const [loaded, setLoaded] = useState(false); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const saveTimeoutRef = useRef(null); + const loadedWorkspaceKeyRef = useRef(null); + + const workspaceKey = + workspaceId === null ? `scope:${scope}` : `id:${workspaceId}`; useEffect(() => { let cancelled = false; async function loadWorkspace() { - try { - const response = await authFetch( - getBackendApiUrl(`/api/chart-workspaces/${scope}`), - ); + setLoaded(false); + setSaving(false); + setError(null); + loadedWorkspaceKeyRef.current = null; - if (response.status === 404 || response.status === 500) { + try { + const payload = + await loadChartWorkspace(scope, workspaceId); + + if (!payload) { return; } - if (!response.ok) { - throw new Error(`Failed to load workspace: ${response.status}`); - } - - const payload = await readJsonResponse( - response, - "Failed to load chart workspace", - ); - if (cancelled) return; onLoaded({ + id: payload.id, + name: payload.name, + defaultWorkspace: payload.defaultWorkspace, layoutMode: payload.layoutMode, charts: JSON.parse(payload.chartsJson) as PersistedChartWorkspaceItem[], }); + loadedWorkspaceKeyRef.current = workspaceKey; setError(null); } catch (error) { if (!cancelled) { @@ -112,10 +106,11 @@ export function useChartWorkspacePersistence({ return () => { cancelled = true; }; - }, [scope]); + }, [scope, workspaceId]); useEffect(() => { - if (!loaded) return; + if (!loaded || !saveEnabled) return; + if (loadedWorkspaceKeyRef.current !== workspaceKey) return; if (saveTimeoutRef.current !== null) { window.clearTimeout(saveTimeoutRef.current); @@ -126,24 +121,15 @@ export function useChartWorkspacePersistence({ try { setSaving(true); - const response = await authFetch( - getBackendApiUrl(`/api/chart-workspaces/${scope}`), + await saveChartWorkspace( + scope, { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - layoutMode, - chartsJson: JSON.stringify(charts), - }), + layoutMode, + chartsJson: JSON.stringify(charts), }, + workspaceId, ); - if (!response.ok) { - throw new Error(`Failed to save workspace: ${response.status}`); - } - setError(null); } catch (error) { console.error("Failed to save chart workspace", error); @@ -161,7 +147,7 @@ export function useChartWorkspacePersistence({ window.clearTimeout(saveTimeoutRef.current); } }; - }, [charts, layoutMode, loaded, scope]); + }, [charts, layoutMode, loaded, saveEnabled, scope, workspaceId, workspaceKey]); return { loaded, diff --git a/src/features/chartworkspace/hooks/useChartWorkspaceSelection.ts b/src/features/chartworkspace/hooks/useChartWorkspaceSelection.ts new file mode 100644 index 0000000..9a0d0ba --- /dev/null +++ b/src/features/chartworkspace/hooks/useChartWorkspaceSelection.ts @@ -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([]); + const [activeWorkspaceId, setActiveWorkspaceId] = useState(null); + const [loading, setLoading] = useState(true); + const [creating, setCreating] = useState(false); + const [error, setError] = useState(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, + }; +} diff --git a/src/features/chartworkspace/pages/ChartWindowPage.tsx b/src/features/chartworkspace/pages/ChartWindowPage.tsx deleted file mode 100644 index 209927b..0000000 --- a/src/features/chartworkspace/pages/ChartWindowPage.tsx +++ /dev/null @@ -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([]); - const [configOpen, setConfigOpen] = useState(false); - - const currentWindow = getCurrentWindow(); - const latestChartRef = useRef(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) => { - 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) => { - setCharts((current) => - current.map((item) => { - if (item.id !== chartId) return item; - - const next = { - ...item, - ...patch, - }; - - latestChartRef.current = next; - - return next; - }), - ); - }; - - const updateChart = (patch: Partial) => { - 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 ( -
- Gráfico não encontrado. -
- ); - } - - 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 ( -
-
-
{ - void currentWindow.startDragging(); - }} - > - {chart.title} -
- -
- - - - - - - - - -
-
- -
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" - } - > - updateChart({ mode })} - onVariableToggle={toggleVariable} - onTimeRangeChange={(timeRange) => updateChart({ timeRange })} - onIntervalChange={(interval) => updateChart({ interval })} - /> -
- - {configOpen && ( - 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); - }} - /> - )} -
- ); -} - -function getVariableColor(index: number) { - const colors = [ - "#4FD1C5", - "#3B82F6", - "#FACC15", - "#7DD3FC", - "#A5B4FC", - "#FB7185", - ]; - - return colors[index % colors.length]; -} - -export default ChartWindowPage; \ No newline at end of file diff --git a/src/features/chartworkspace/pages/WorkspaceWindowPage.tsx b/src/features/chartworkspace/pages/WorkspaceWindowPage.tsx new file mode 100644 index 0000000..87551c4 --- /dev/null +++ b/src/features/chartworkspace/pages/WorkspaceWindowPage.tsx @@ -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 ( + + ); + } + + if (scope === "CLIMATE") { + return ( + + ); + } + + return ( + + ); + })(); + + return ( +
+
+
+ {windowTitle} +
+ +
+ + + + + +
+
+ +
{workspaceContent}
+
+ ); +} + +export default WorkspaceWindowPage; diff --git a/src/features/chartworkspace/types.ts b/src/features/chartworkspace/types.ts new file mode 100644 index 0000000..ec6bb8c --- /dev/null +++ b/src/features/chartworkspace/types.ts @@ -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; +}; diff --git a/src/features/chartworkspace/utils/openChartWindow.ts b/src/features/chartworkspace/utils/openChartWindow.ts deleted file mode 100644 index a56f86a..0000000 --- a/src/features/chartworkspace/utils/openChartWindow.ts +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/src/features/chartworkspace/utils/openWorkspaceWindow.ts b/src/features/chartworkspace/utils/openWorkspaceWindow.ts new file mode 100644 index 0000000..b8f3e1f --- /dev/null +++ b/src/features/chartworkspace/utils/openWorkspaceWindow.ts @@ -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; +} diff --git a/src/features/climate/pages/ClimateChartsPage.tsx b/src/features/climate/pages/ClimateChartsPage.tsx index dd30baa..f8be426 100644 --- a/src/features/climate/pages/ClimateChartsPage.tsx +++ b/src/features/climate/pages/ClimateChartsPage.tsx @@ -1,11 +1,18 @@ 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 { 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 { Cog, - Copy, Maximize2, AreaChart, BarChart3, @@ -27,8 +34,6 @@ import { WorkspaceChart, type WorkspaceChartConfig, type WorkspaceChartMode, - type WorkspaceChartTimeRange, - type WorkspaceChartInterval, } from "../../../components/charts/WorkspaceChart"; import type { ChartVariable } from "../../telemetry/types/telemetryCatalog"; @@ -37,40 +42,32 @@ import { useClimateChartSeries } from "../hooks/useClimateChartSeries"; import { useChartWorkspacePersistence, - type PersistedChartWorkspaceItem, type ChartLayoutMode, } 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 = { theme: "dark" | "light"; -}; - -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; + workspaceId?: number | null; + workspaceWindow?: boolean; }; const RADIUS = "rounded-[6px]"; -const MAX_CHARTS = 10; -const MAX_VARIABLES_PER_CHART = 6; const INITIAL_CHARTS: ChartWorkspaceItem[] = []; -export function ClimateChartsPage({ theme }: ClimateChartsPageProps) { +export function ClimateChartsPage({ + theme, + workspaceId = null, + workspaceWindow = false, +}: ClimateChartsPageProps) { const isDark = theme === "dark"; const { chartableVariables, connected } = useClimateChartCatalog(); @@ -83,6 +80,120 @@ export function ClimateChartsPage({ theme }: ClimateChartsPageProps) { const [movingChartId, setMovingChartId] = useState(null); const [newChartOpen, setNewChartOpen] = useState(false); const [placingChartId, setPlacingChartId] = useState(null); + const [detachedWorkspaceIds, setDetachedWorkspaceIds] = useState>(() => 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 nextVisibleCount = getVisibleSlotCount(nextLayoutMode); @@ -111,6 +222,7 @@ export function ClimateChartsPage({ theme }: ClimateChartsPageProps) { const workspacePersistence = useChartWorkspacePersistence({ scope: "CLIMATE", + workspaceId: activeWorkspaceId, layoutMode, charts, onLoaded: (workspace) => { @@ -209,31 +321,6 @@ export function ClimateChartsPage({ theme }: ClimateChartsPageProps) { 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) => { setCharts((current) => current.map((chart) => @@ -278,8 +365,6 @@ export function ClimateChartsPage({ theme }: ClimateChartsPageProps) { }; const closeChart = async (chartId: string) => { - await closeDetachedChartWindow(chartId); - setCharts((current) => current.map((chart) => chart.id === chartId @@ -299,8 +384,6 @@ export function ClimateChartsPage({ theme }: ClimateChartsPageProps) { }; const deleteChart = async (chartId: string) => { - await closeDetachedChartWindow(chartId); - setCharts((current) => current.filter((chart) => chart.id !== chartId)); if (configChartId === chartId) setConfigChartId(null); @@ -365,72 +448,6 @@ export function ClimateChartsPage({ theme }: ClimateChartsPageProps) { 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(() => { 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` } > + {!workspaceWindow && ( + void workspaceSelection.createWorkspace(name)} + onRenameWorkspace={(workspaceId, name) => void renameWorkspace(workspaceId, name)} + onDeleteWorkspace={(workspaceId) => void deleteWorkspace(workspaceId)} + /> + )} +
+ {!workspaceWindow && ( + + + )} +
))} @@ -768,7 +817,6 @@ function WorkspaceChartContainer({ movingChartId, setMovingChartId, swapCharts, - duplicateChart, closeChart, setConfigChartId, setChartMode, @@ -776,9 +824,6 @@ function WorkspaceChartContainer({ setCharts, placingChartId, placeChartHere, - detachChart, - attachChart, - moveDetachedChart }: { theme: "dark" | "light"; layoutMode: ChartLayoutMode; @@ -788,7 +833,6 @@ function WorkspaceChartContainer({ movingChartId: string | null; setMovingChartId: React.Dispatch>; swapCharts: (sourceId: string, targetId: string) => void; - duplicateChart: (chartId: string) => void; closeChart: (chartId: string) => void | Promise; setConfigChartId: React.Dispatch>; setChartMode: (chartId: string, mode: WorkspaceChartMode) => void; @@ -796,9 +840,6 @@ function WorkspaceChartContainer({ setCharts: React.Dispatch>; placingChartId: string | null; placeChartHere: (targetChartId: string) => void; - detachChart: (chartId: string) => void; - attachChart: (chartId: string) => void | Promise; - moveDetachedChart: (chartId: string, x: number, y: number) => void; }) { const isMoving = movingChartId === chartItem.id; @@ -872,23 +913,6 @@ function WorkspaceChartContainer({ - - - -
); @@ -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 ( - - ); -} - -function ModeButton({ - theme, - active, - icon, - label, - onClick, -}: { - theme: "dark" | "light"; - active: boolean; - icon?: React.ReactNode; - label: string; - onClick: () => void; -}) { - return ( - - ); -} - -function EmptyWorkspace({ - theme, - canAddMoreCharts, - onAddChart, - onOpenSaved, -}: { - theme: "dark" | "light"; - canAddMoreCharts: boolean; - onAddChart: () => void; - onOpenSaved: () => void; -}) { - const isDark = theme === "dark"; - - return ( -
-
-
- -
- -

- Nenhum gráfico aberto -

- -

- Abra um gráfico guardado ou crie um novo gráfico para continuar. -

- -
- - - -
-
-
- ); -} - -function getVariableColor(index: number) { - const colors = [ - "#4FD1C5", - "#3B82F6", - "#FACC15", - "#7DD3FC", - "#A5B4FC", - "#FB7185", - ]; - - return colors[index % colors.length]; -} - -export default ClimateChartsPage; \ No newline at end of file +export default ClimateChartsPage; diff --git a/src/features/maincharts/pages/MainChartsPage.tsx b/src/features/maincharts/pages/MainChartsPage.tsx index 608f5bd..687743f 100644 --- a/src/features/maincharts/pages/MainChartsPage.tsx +++ b/src/features/maincharts/pages/MainChartsPage.tsx @@ -1,7 +1,15 @@ 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 { 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 { Cog, @@ -27,8 +35,6 @@ import { WorkspaceChart, type WorkspaceChartConfig, type WorkspaceChartMode, - type WorkspaceChartTimeRange, - type WorkspaceChartInterval, } from "../../../components/charts/WorkspaceChart"; import { useTelemetryCatalog } from "../../telemetry/hooks/useTelemetryCatalog"; import type { ChartVariable } from "../../telemetry/types/telemetryCatalog"; @@ -36,40 +42,32 @@ import { useTelemetryChartSeries } from "../../telemetry/hooks/useTelemetryChart import { useChartWorkspacePersistence, - type PersistedChartWorkspaceItem, type ChartLayoutMode, } 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 = { theme: "dark" | "light"; -}; - -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; + workspaceId?: number | null; + workspaceWindow?: boolean; }; const RADIUS = "rounded-[6px]"; -const MAX_CHARTS = 10; -const MAX_VARIABLES_PER_CHART = 6; const INITIAL_CHARTS: ChartWorkspaceItem[] = []; -export function MainChartsPage({ theme }: MainChartsPageProps) { +export function MainChartsPage({ + theme, + workspaceId = null, + workspaceWindow = false, +}: MainChartsPageProps) { const isDark = theme === "dark"; const { chartableVariables, connected } = useTelemetryCatalog(); @@ -82,6 +80,94 @@ export function MainChartsPage({ theme }: MainChartsPageProps) { const [movingChartId, setMovingChartId] = useState(null); const [newChartOpen, setNewChartOpen] = useState(false); const [placingChartId, setPlacingChartId] = useState(null); + const [detachedWorkspaceIds, setDetachedWorkspaceIds] = useState>(() => 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(() => typeof window !== "undefined" ? 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 nextVisibleCount = getVisibleSlotCount(nextLayoutMode); @@ -129,6 +244,7 @@ export function MainChartsPage({ theme }: MainChartsPageProps) { const workspacePersistence = useChartWorkspacePersistence({ scope: "GLOBAL", + workspaceId: activeWorkspaceId, layoutMode, charts, 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) => { setCharts((current) => 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` } > + {!workspaceWindow && ( + void workspaceSelection.createWorkspace(name)} + onRenameWorkspace={(workspaceId, name) => void renameWorkspace(workspaceId, name)} + onDeleteWorkspace={(workspaceId) => void deleteWorkspace(workspaceId)} + /> + )} +
+ {!workspaceWindow && ( + + + )} +
{ if (canAddMoreCharts) setNewChartOpen(true); }} @@ -696,8 +807,6 @@ export function MainChartsPage({ theme }: MainChartsPageProps) { setCharts={setCharts} placingChartId={placingChartId} placeChartHere={placeChartHere} - detachChart={detachChart} - attachChart={attachChart} moveDetachedChart={moveDetachedChart} viewportCompact={viewportCompact} layoutMode={layoutMode} @@ -794,8 +903,6 @@ function WorkspaceChartContainer({ setCharts, placingChartId, placeChartHere, - detachChart, - attachChart, moveDetachedChart, viewportCompact, layoutMode, @@ -815,8 +922,6 @@ function WorkspaceChartContainer({ setCharts: React.Dispatch>; placingChartId: string | null; placeChartHere: (targetChartId: string) => void; - detachChart: (chartId: string) => void; - attachChart: (chartId: string) => void | Promise; moveDetachedChart: (chartId: string, x: number, y: number) => void; viewportCompact: boolean; layoutMode: ChartLayoutMode; @@ -903,19 +1008,6 @@ function WorkspaceChartContainer({ - - - ); -} - -function ModeButton({ - theme, - active, - icon, - label, - onClick, -}: { - theme: "dark" | "light"; - active: boolean; - icon?: React.ReactNode; - label: string; - onClick: () => void; -}) { - return ( - - ); -} - -function EmptyWorkspace({ - theme, - canAddMoreCharts, - onAddChart, - onOpenSaved, -}: { - theme: "dark" | "light"; - canAddMoreCharts: boolean; - onAddChart: () => void; - onOpenSaved: () => void; -}) { - const isDark = theme === "dark"; - - return ( -
-
-
- -
- -

- Nenhum gráfico aberto -

- -

- Abra um gráfico guardado ou crie um novo gráfico para continuar. -

- -
- - - -
-
-
- ); -} - -function getVariableColor(index: number) { - const colors = [ - "#4FD1C5", - "#3B82F6", - "#FACC15", - "#7DD3FC", - "#A5B4FC", - "#FB7185", - ]; - - return colors[index % colors.length]; -} - -export default MainChartsPage; \ No newline at end of file +export default MainChartsPage; diff --git a/src/features/meteo/pages/MeteoChartsPage.tsx b/src/features/meteo/pages/MeteoChartsPage.tsx index 5d21116..abab648 100644 --- a/src/features/meteo/pages/MeteoChartsPage.tsx +++ b/src/features/meteo/pages/MeteoChartsPage.tsx @@ -1,5 +1,5 @@ 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 { AreaChart, @@ -25,12 +25,19 @@ import { getBackendApiUrl } from "../../../lib/api/gatewayConfig"; import { readOptionalJsonResponse } from "../../../lib/api/readJsonResponse"; 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 { WorkspaceChart, type WorkspaceChartConfig, - type WorkspaceChartInterval, type WorkspaceChartMode, type WorkspaceChartTimeRange, } from "../../../components/charts/WorkspaceChart"; @@ -41,11 +48,20 @@ import { useMeteoChartCatalog } from "../hooks/useMeteoChartCatalog"; import { useChartWorkspacePersistence, type ChartLayoutMode, - type PersistedChartWorkspaceItem, } 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 = { theme: "dark" | "light"; + workspaceId?: number | null; + workspaceWindow?: boolean; }; type HistorianPoint = { @@ -56,33 +72,14 @@ type HistorianPoint = { 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 MAX_CHARTS = 10; -const MAX_VARIABLES_PER_CHART = 6; const INITIAL_CHARTS: ChartWorkspaceItem[] = []; export function MeteoChartsPage({ theme, + workspaceId = null, + workspaceWindow = false, }: MeteoChartsPageProps) { const isDark = theme === "dark"; @@ -100,6 +97,123 @@ export function MeteoChartsPage({ const [movingChartId, setMovingChartId] = useState(null); const [newChartOpen, setNewChartOpen] = useState(false); const [placingChartId, setPlacingChartId] = useState(null); + const [detachedWorkspaceIds, setDetachedWorkspaceIds] = useState>(() => 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 nextVisibleCount = getVisibleSlotCount(nextLayoutMode); @@ -126,6 +240,7 @@ export function MeteoChartsPage({ const workspacePersistence = useChartWorkspacePersistence({ scope: "METEO", + workspaceId: activeWorkspaceId, layoutMode, charts, onLoaded: (workspace) => { @@ -386,50 +501,6 @@ export function MeteoChartsPage({ 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) => { setCharts((current) => 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` } > + {!workspaceWindow && ( + void workspaceSelection.createWorkspace(name)} + onRenameWorkspace={(workspaceId, name) => void renameWorkspace(workspaceId, name)} + onDeleteWorkspace={(workspaceId) => void deleteWorkspace(workspaceId)} + /> + )} +
+ {!workspaceWindow && ( + + + )} +
{ if (canAddMoreCharts) setNewChartOpen(true); }} @@ -683,8 +791,6 @@ export function MeteoChartsPage({ setCharts={setCharts} placingChartId={placingChartId} placeChartHere={placeChartHere} - detachChart={detachChart} - attachChart={attachChart} moveDetachedChart={moveDetachedChart} /> ))} @@ -791,8 +897,6 @@ function WorkspaceChartContainer({ setCharts, placingChartId, placeChartHere, - detachChart, - attachChart, moveDetachedChart, }: { theme: "dark" | "light"; @@ -811,8 +915,6 @@ function WorkspaceChartContainer({ setCharts: React.Dispatch>; placingChartId: string | null; placeChartHere: (targetChartId: string) => void; - detachChart: (chartId: string) => void; - attachChart: (chartId: string) => void | Promise; moveDetachedChart: (chartId: string, x: number, y: number) => void; }) { const isMoving = movingChartId === chartItem.id; @@ -899,19 +1001,6 @@ function WorkspaceChartContainer({ - - - ); -} - -function ModeButton({ - theme, - active, - icon, - label, - onClick, -}: { - theme: "dark" | "light"; - active: boolean; - icon?: React.ReactNode; - label: string; - onClick: () => void; -}) { - return ( - - ); -} - -function EmptyWorkspace({ - theme, - canAddMoreCharts, - onAddChart, - onOpenSaved, -}: { - theme: "dark" | "light"; - canAddMoreCharts: boolean; - onAddChart: () => void; - onOpenSaved: () => void; -}) { - const isDark = theme === "dark"; - - return ( -
-
-
- -
- -

- Nenhum gráfico aberto -

- -

- Abra um gráfico guardado ou crie um novo gráfico meteorológico - para continuar. -

- -
- - - -
-
-
- ); -} - -function getVariableColor(index: number) { - const colors = [ - "#4FD1C5", - "#3B82F6", - "#FACC15", - "#7DD3FC", - "#A5B4FC", - "#FB7185", - ]; - - return colors[index % colors.length]; -} export default MeteoChartsPage; diff --git a/src/features/meteo/pages/MeteoPage.tsx b/src/features/meteo/pages/MeteoPage.tsx index c2fd499..2568693 100644 --- a/src/features/meteo/pages/MeteoPage.tsx +++ b/src/features/meteo/pages/MeteoPage.tsx @@ -68,6 +68,7 @@ type Accent = "amber" | "blue" | "cyan" | "emerald" | "violet"; const HISTORY_HOURS = 6; const RADIUS = "rounded-[5px]"; +const OVERVIEW_PANEL_HEIGHT = "h-[488px] min-h-[488px]"; const HISTORY_KEYS = { temperature: "temperature", @@ -969,10 +970,10 @@ function WeatherSummaryPanel({ const isDark = theme === "dark"; return ( -
+

Resumo meteorológico

-
+
+

Direção do vento

-
-
+
+
@@ -1135,7 +1136,7 @@ function WindDirectionPanel({
-
+
+