@@ -78,7 +49,7 @@ function DashboardView(props: { dashboard: IDashboardModel, isEditing: boolean,
style={{
'--dashboard-cols': numberOfColumns,
'--dashboard-w': `${dashboardWidth}px`,
- '--widget-size': `${dynamicWidgetSize}`
+ '--widget-size': `${widgetSize}`
} as CSSProperties}
>
diff --git a/web/apps/app/components/dashboards/Dashboards.tsx b/web/apps/app/components/dashboards/Dashboards.tsx
index 7011755368..bec726c207 100644
--- a/web/apps/app/components/dashboards/Dashboards.tsx
+++ b/web/apps/app/components/dashboards/Dashboards.tsx
@@ -1,6 +1,7 @@
'use client';
import React, { useCallback, useEffect, useState } from 'react';
+import { notFound } from 'next/navigation';
import Image from 'next/image';
import dynamic from 'next/dynamic';
import { Typography } from '@signalco/ui-primitives/Typography';
@@ -19,7 +20,9 @@ import useDashboard from '../../src/hooks/dashboards/useDashboard';
import { WidgetModel } from '../../src/dashboards/DashboardsRepository';
import { SpacesEditingBackground } from './SpacesEditingBackground';
import DashboardView from './DashboardView';
+import { DashboardSkeleton } from './DashboardSkeleton';
import DashboardSettings from './DashboardSettings';
+import { DashboardPadding } from './DashboardPadding';
const WidgetStoreDynamic = dynamic(() => import('../widget-store/WidgetStore'));
@@ -71,20 +74,20 @@ function SpaceBackground({ background }: { background?: string }) {
export function Dashboards() {
const { t } = useLocale('App', 'Dashboards');
const [selectedId, setDashboardId] = useSearchParam('dashboard');
- const selectedDashboard = useDashboard(selectedId);
+ const { data: selectedDashboard, isLoading: selectedDashboardIsLoading, error: selectedDashboardError } = useDashboard(selectedId);
const saveDashboard = useSaveDashboard();
const [isEditingValue, setIsEditing] = useSearchParam('editing');
const isEditing = isEditingValue === 'true';
const handleEditDone = async () => {
- if (!selectedDashboard.data) {
+ if (!selectedDashboard) {
console.warn('Can not save - dashboard not selected');
return;
}
try {
- console.debug(`Saving dashboard ${selectedId}...`, selectedDashboard.data);
- await saveDashboard.mutateAsync(selectedDashboard.data);
+ console.debug(`Saving dashboard ${selectedId}...`, selectedDashboard);
+ await saveDashboard.mutateAsync(selectedDashboard);
} catch (err) {
console.error('Failed to save dashboards', err);
showNotification(t('SaveFailedNotification'), 'error');
@@ -111,9 +114,9 @@ export function Dashboards() {
const [showWidgetStore, setShowWidgetStore] = useState(false);
const handleAddWidget = useCallback((widgetType: widgetType) => {
- selectedDashboard.data?.widgets.push(new WidgetModel('new-widget', selectedDashboard.data.widgets.length, widgetType));
+ selectedDashboard?.widgets.push(new WidgetModel('new-widget', selectedDashboard.widgets.length, widgetType));
setShowWidgetStore(false);
- }, [selectedDashboard.data?.widgets]);
+ }, [selectedDashboard?.widgets]);
const handleAddWidgetPlaceholder = () => {
setIsEditing('true');
@@ -123,9 +126,16 @@ export function Dashboards() {
const [isDashboardSettingsOpenValue, setIsDashboardSettingsOpen] = useSearchParam('settings');
const isDashboardSettingsOpen = isDashboardSettingsOpenValue === 'true';
+ const isLoading = Boolean(selectedId) && selectedDashboardIsLoading;
+
+ // Handle 404
+ if (!selectedDashboard && !selectedDashboardIsLoading) {
+ return notFound();
+ }
+
return (
<>
-
+
{isEditing && (
<>
@@ -139,33 +149,35 @@ export function Dashboards() {
>
)}
-
-
-
- {selectedDashboard.data
- ? (
-
- ) : (
-
-
-
-
- {t('NoDashboardsPlaceholder')}
- {t('NoDashboardsHelpText')}
-
-
-
-
- )}
-
-
-
- {selectedDashboard.data && (
+ }
+ loadingLabel="Loading dashboard..."
+ error={selectedDashboardError}>
+
+ {selectedDashboard
+ ? (
+
+ ) : (
+
+
+
+
+ {t('NoDashboardsPlaceholder')}
+ {t('NoDashboardsHelpText')}
+
+
+
+
+ )}
+
+
+ {selectedDashboard && (
setIsDashboardSettingsOpen(undefined)} />
)}
diff --git a/web/apps/app/components/dashboards/DashboardsDefaultRedirect.tsx b/web/apps/app/components/dashboards/DashboardsDefaultRedirect.tsx
new file mode 100644
index 0000000000..8669ad6530
--- /dev/null
+++ b/web/apps/app/components/dashboards/DashboardsDefaultRedirect.tsx
@@ -0,0 +1,30 @@
+'use client';
+
+import { PropsWithChildren, useEffect } from 'react';
+import { Loadable } from '@signalco/ui/Loadable';
+import { useSearchParam } from '@signalco/hooks/useSearchParam';
+import useDashboards from '../../src/hooks/dashboards/useDashboards';
+import { DashboardSkeleton } from '../../components/dashboards/DashboardSkeleton';
+
+export function DashboardsDefaultRedirect({ children }: PropsWithChildren) {
+ const [selectedId, setDashboardId] = useSearchParam('dashboard');
+
+ const { data: dashboards, isLoading: dashboardsIsLoading, error } = useDashboards();
+ useEffect(() => {
+ if (!selectedId && dashboards?.length && !dashboardsIsLoading) {
+ console.debug('Selecting first available dashboard', dashboards[0]?.id);
+ setDashboardId(dashboards[0]?.id);
+ }
+ }, [selectedId, dashboards, dashboardsIsLoading, setDashboardId]);
+
+ const isLoading = !error && !selectedId && (dashboardsIsLoading || (dashboards?.length ?? 0) > 0);
+
+ return (
+ }
+ loadingLabel="Loading dashboards...">
+ {children}
+
+ );
+}
diff --git a/web/apps/app/components/dashboards/NoWidgetsPlaceholder.tsx b/web/apps/app/components/dashboards/NoWidgetsPlaceholder.tsx
new file mode 100644
index 0000000000..dc0a463542
--- /dev/null
+++ b/web/apps/app/components/dashboards/NoWidgetsPlaceholder.tsx
@@ -0,0 +1,23 @@
+import Image from 'next/image';
+import { Typography } from '@signalco/ui-primitives/Typography';
+import { Stack } from '@signalco/ui-primitives/Stack';
+import { Row } from '@signalco/ui-primitives/Row';
+import { Button } from '@signalco/ui-primitives/Button';
+import useLocale from '../../src/hooks/useLocale';
+
+export function NoWidgetsPlaceholder({ onAdd }: { onAdd: () => void }) {
+ const { t } = useLocale('App', 'Dashboards');
+
+ return (
+
+
+
+
+ {t('NoWidgets')}
+ {t('NoWidgetsHelpTextFirstLine')}
{t('NoWidgetsHelpTextSecondLine')}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/web/apps/app/components/dashboards/useDashboardSizes.ts b/web/apps/app/components/dashboards/useDashboardSizes.ts
new file mode 100644
index 0000000000..ce195b7388
--- /dev/null
+++ b/web/apps/app/components/dashboards/useDashboardSizes.ts
@@ -0,0 +1,20 @@
+import { useState } from 'react';
+import { useResizeObserver } from '@enterwell/react-hooks';
+
+export function useDashboardSizes() {
+ const defaultWidth = typeof window !== 'undefined' ? window.innerWidth : 0;
+ const [elementWidth, setElementWidth] = useState(defaultWidth);
+ const resizeObserverRef = useResizeObserver((_, entry) => {
+ setElementWidth(entry.contentRect.width);
+ });
+ const widgetSize = 1 + 64 + 1 + 4; // Default widget is 64x64 + 2px for border + 8 spacing between widgets (2x4px)
+ const dashboardWidth = elementWidth;
+ const numberOfColumns = Math.max(2, Math.floor(dashboardWidth / widgetSize));
+ const dynamicWidgetSize = `calc((${dashboardWidth}px - ${(numberOfColumns - 1) * 0.5}rem) / ${numberOfColumns})`;
+ return {
+ resizeObserverRef,
+ widgetSize: dynamicWidgetSize,
+ numberOfColumns,
+ dashboardWidth
+ };
+}
\ No newline at end of file
diff --git a/web/apps/app/components/settings/pages/DeveloperSettings.tsx b/web/apps/app/components/settings/pages/DeveloperSettings.tsx
index 8c26bea3d1..054eb19990 100644
--- a/web/apps/app/components/settings/pages/DeveloperSettings.tsx
+++ b/web/apps/app/components/settings/pages/DeveloperSettings.tsx
@@ -1,9 +1,59 @@
'use client';
-import React from 'react';
+
+import React, { useEffect, useState } from 'react';
+import { Typography } from '@signalco/ui-primitives/Typography';
+import { Stack } from '@signalco/ui-primitives/Stack';
+import { Button } from '@signalco/ui-primitives/Button';
+import { showNotification } from '@signalco/ui-notifications';
import { FormBuilder } from '@enterwell/react-form-builder';
import { useDeveloperForm } from '../hooks/useDeveloperForm';
export function DeveloperSettings() {
const form = useDeveloperForm();
- return ;
+
+ const [workerRegistration, setWorkerRegistration] = useState();
+ useEffect(() => {
+ (async () => {
+ try {
+ setWorkerRegistration(await navigator.serviceWorker.ready);
+ } catch (error) {
+ console.debug('Service worker not available', error);
+ }
+ })();
+ }, []);
+
+ const handleUnregisterWorker = async () => {
+ if (!workerRegistration) {
+ console.warn('No worker to unregister');
+ return;
+ }
+
+ try {
+ await workerRegistration.unregister();
+ setWorkerRegistration(undefined);
+ showNotification('Service worker unregistered successfully.', 'success');
+ } catch (error) {
+ console.error('Failed to unregister service worker', error);
+ showNotification('Failed to unregister service worker.', 'error');
+ }
+ }
+
+ return (
+
+
+
+ Service Worker
+ Service worker is {workerRegistration ? 'registered' : 'not registered'}
+ {Boolean(workerRegistration) && (
+ <>
+ Active state: {workerRegistration?.active?.state ?? '-'}
+ Installing state: {workerRegistration?.installing?.state ?? '-'}
+ Waiting state: {workerRegistration?.waiting?.state ?? '-'}
+ Scope: {workerRegistration?.scope}
+
+ >
+ )}
+
+
+ );
}
diff --git a/web/apps/app/components/widgets/parts/piece/PrimaryValueLabel.tsx b/web/apps/app/components/widgets/parts/piece/PrimaryValueLabel.tsx
index 431ee2ddf6..ef5067a669 100644
--- a/web/apps/app/components/widgets/parts/piece/PrimaryValueLabel.tsx
+++ b/web/apps/app/components/widgets/parts/piece/PrimaryValueLabel.tsx
@@ -23,11 +23,11 @@ export function PrimaryValueLabel({ value, unit, size = 'normal' }: { value: num
return (
- {valueWhole}
+ {Number.isNaN(valueWhole) ? 0 : valueWhole}
- {unit}
- .{degreesDecimal}
+ {unit ?? ''}
+ .{Number.isNaN(degreesDecimal) ? 0 : degreesDecimal}
);
diff --git a/web/apps/app/locales/en.json b/web/apps/app/locales/en.json
index f8b62d89a6..1fcbb21414 100644
--- a/web/apps/app/locales/en.json
+++ b/web/apps/app/locales/en.json
@@ -156,6 +156,7 @@
"NoWidgetsHelpTextFirstLine": "Dashboard is a bit empty.",
"NoWidgetsHelpTextSecondLine": "Start by adding a widget from widget store.",
"DashboardSettings": "Dashboard settings",
+ "DashboardsLoadingError": "There was an error while loading dashboards. Please refresh the page.",
"Cancel": "Cancel",
"SaveChanges": "Save changes",
"Advanced": "Advanced",
diff --git a/web/apps/app/src/hooks/dashboards/useDashboard.ts b/web/apps/app/src/hooks/dashboards/useDashboard.ts
index 85c24c9da3..ed95ce8b6b 100644
--- a/web/apps/app/src/hooks/dashboards/useDashboard.ts
+++ b/web/apps/app/src/hooks/dashboards/useDashboard.ts
@@ -1,11 +1,26 @@
import { useMemo } from 'react';
-import { IDashboardModel } from '../../dashboards/DashboardsRepository';
-import useDashboards from './useDashboards';
+import useEntity from '../signalco/entity/useEntity';
+import { dashboardModelFromEntity, IDashboardModel } from '../../dashboards/DashboardsRepository';
-export default function useDashboard(id?: string): Omit, 'data'> & { data: IDashboardModel | undefined } {
- const dashboards = useDashboards();
- return useMemo(() => ({
- ...dashboards,
- data: dashboards.data?.find(d => d.id === id)
- }), [dashboards, id]);
+export default function useDashboard(id?: string): Omit, 'data'> & { data: IDashboardModel | null | undefined } {
+ const dashboardEntity = useEntity(id);
+ const dashboard = useMemo(() =>
+ dashboardEntity.data
+ ? dashboardModelFromEntity(dashboardEntity.data, 0, [])
+ : null,
+ [dashboardEntity.data]);
+
+ if (!dashboardEntity.isLoading && Boolean(dashboardEntity.data) && dashboardEntity.data?.type !== 2) {
+ return {
+ isLoading: false,
+ error: new Error('Invalid dashboard entity'),
+ isPending: false,
+ isStale: true,
+ data: null
+ };
+ }
+ return {
+ ...dashboardEntity,
+ data: dashboard
+ };
}
From 7f62088276f5bd1fe95026f013a30c5b8c45410e Mon Sep 17 00:00:00 2001
From: Aleksandar Toplek
Date: Fri, 16 Aug 2024 09:34:58 +0200
Subject: [PATCH 24/28] chore(hooks): useSearchParam added missing parameter
name in ignore case debug log
---
web/packages/hooks/src/useSearchParam.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/web/packages/hooks/src/useSearchParam.ts b/web/packages/hooks/src/useSearchParam.ts
index 262146af84..ea28305cf8 100644
--- a/web/packages/hooks/src/useSearchParam.ts
+++ b/web/packages/hooks/src/useSearchParam.ts
@@ -12,11 +12,11 @@ export function useSetSearchParam(parameterName: string): (value: string | undef
const currentValue = searchParams.get(parameterName);
if (currentValue === value ||
currentValue == null && value == null) {
- console.debug('useSearchParam: Ignoring because value is the same as current value', parameterName, value, '===', currentValue);
+ console.debug('useSearchParam: Ignoring because value', parameterName, 'is the same as current value', parameterName, value, '===', currentValue);
return;
}
- console.debug('useSearchParam: Setting value', parameterName, 'from', currentValue, 'to', value);
+ console.debug('useSearchParam: Setting value', parameterName, ' from', currentValue, 'to', value);
const currentSearch = new URLSearchParams(Array.from(searchParams.entries()));
if (value)
From e47849a98ee96b73f52ed9a295c96ccaea63e56c Mon Sep 17 00:00:00 2001
From: Aleksandar Toplek
Date: Fri, 16 Aug 2024 09:35:19 +0200
Subject: [PATCH 25/28] feat(hooks): Removed unused useWindowWidth, replaced
with useWindowRect
---
web/packages/hooks/src/useWindowWidth.ts | 16 ----------------
1 file changed, 16 deletions(-)
delete mode 100644 web/packages/hooks/src/useWindowWidth.ts
diff --git a/web/packages/hooks/src/useWindowWidth.ts b/web/packages/hooks/src/useWindowWidth.ts
deleted file mode 100644
index 509e43f1de..0000000000
--- a/web/packages/hooks/src/useWindowWidth.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { useCallback, useState } from 'react';
-import { useWindowEvent } from './useWindowEvent';
-
-const browser = typeof window !== 'undefined';
-
-export function useWindowWidth() {
- const [width, setWidth] = useState(browser ? window.innerWidth : undefined);
-
- const updateNumberOfColumns = useCallback(() => {
- setWidth(window.innerWidth || 0);
- }, []);
-
- useWindowEvent('resize', updateNumberOfColumns);
-
- return width;
-}
From c50bcbf350e62d64499b442f19290d3f788ac814 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Fri, 16 Aug 2024 07:37:48 +0000
Subject: [PATCH 26/28] chore(deps): update dependency dotnet-sdk to v8.0.401
---
station/global.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/station/global.json b/station/global.json
index 269d698804..1ceee139e8 100644
--- a/station/global.json
+++ b/station/global.json
@@ -1,6 +1,6 @@
{
"sdk": {
- "version": "8.0.400",
+ "version": "8.0.401",
"rollForward": "latestMajor"
}
}
\ No newline at end of file
From 835797aa14713786814b7a99da4aa1a30d4a568d Mon Sep 17 00:00:00 2001
From: Aleksandar Toplek
Date: Fri, 16 Aug 2024 09:37:54 +0200
Subject: [PATCH 27/28] fix(web): Moved channels FAQ data to separate file to
fix invalid paga export build error
---
.../web/app/channels/[channelName]/page.tsx | 2 +-
web/apps/web/app/channels/data.ts | 17 +++++++++++++++++
web/apps/web/app/channels/page.tsx | 18 +-----------------
3 files changed, 19 insertions(+), 18 deletions(-)
create mode 100644 web/apps/web/app/channels/data.ts
diff --git a/web/apps/web/app/channels/[channelName]/page.tsx b/web/apps/web/app/channels/[channelName]/page.tsx
index 92dfbad9eb..a4f32e9de5 100644
--- a/web/apps/web/app/channels/[channelName]/page.tsx
+++ b/web/apps/web/app/channels/[channelName]/page.tsx
@@ -9,7 +9,7 @@ import { NavigatingButton } from '@signalco/ui/NavigatingButton';
import { Breadcrumbs } from '@signalco/ui/Breadcrumbs';
import { channelCategories as channelCategoriesData, channelsData } from '@signalco/data/data';
import { SectionsView } from '@signalco/cms-core/SectionsView';
-import { channelsFaq } from '../page';
+import { channelsFaq } from '../data';
import { sectionsComponentRegistry } from '../../page';
import { KnownPages } from '../../../src/knownPages';
import ShareSocial from '../../../components/pages/ShareSocial';
diff --git a/web/apps/web/app/channels/data.ts b/web/apps/web/app/channels/data.ts
new file mode 100644
index 0000000000..a42bbd2897
--- /dev/null
+++ b/web/apps/web/app/channels/data.ts
@@ -0,0 +1,17 @@
+import { KnownPages } from '../../src/knownPages';
+
+export const channelsFaq = [
+ {
+ component: 'Faq1',
+ header: 'FAQ',
+ description: 'Find answers to common questions about signalco channels.',
+ ctas: [
+ { label: 'Contact', href: KnownPages.Contact },
+ ],
+ features: [
+ { id: 'channel', header: 'What is Channel?', description: 'Channel is Entity that contains all information required for connected online service, application or device. Channels can execute actions directly or contain connected entities to manage.' },
+ { id: 'entities', header: 'What are Entities?', description: 'Entity is a thing you want to automate in signalco. This can be is online service connected to signalco, smart device, your custom space, automation process, etc.' },
+ { id: 'executions', header: 'What are Executions?', description: 'Execution is when one of your automation processes executes one action (e.g.: when automation sends an email when new subscribes is added to a list).' },
+ ]
+ }
+];
\ No newline at end of file
diff --git a/web/apps/web/app/channels/page.tsx b/web/apps/web/app/channels/page.tsx
index e3cb7ddc43..cb7c705a1c 100644
--- a/web/apps/web/app/channels/page.tsx
+++ b/web/apps/web/app/channels/page.tsx
@@ -5,23 +5,7 @@ import { sectionsComponentRegistry } from '../page';
import PageCenterHeader from '../../components/pages/PageCenterHeader';
import CtaSection from '../../components/pages/CtaSection';
import ChannelsGallery from '../../components/channels/ChannelsGallery';
-import { KnownPages } from '../../src/knownPages';
-
-export const channelsFaq = [
- {
- component: 'Faq1',
- header: 'FAQ',
- description: 'Find answers to common questions about signalco channels.',
- ctas: [
- { label: 'Contact', href: KnownPages.Contact },
- ],
- features: [
- { id: 'channel', header: 'What is Channel?', description: 'Channel is Entity that contains all information required for connected online service, application or device. Channels can execute actions directly or contain connected entities to manage.' },
- { id: 'entities', header: 'What are Entities?', description: 'Entity is a thing you want to automate in signalco. This can be is online service connected to signalco, smart device, your custom space, automation process, etc.' },
- { id: 'executions', header: 'What are Executions?', description: 'Execution is when one of your automation processes executes one action (e.g.: when automation sends an email when new subscribes is added to a list).' },
- ]
- }
-];
+import { channelsFaq } from './data';
export default function ChannelsPage() {
return (
From ad73a90bda8ad8aa361dc6a287a7fbb116227f7e Mon Sep 17 00:00:00 2001
From: Aleksandar Toplek
Date: Fri, 16 Aug 2024 09:40:27 +0200
Subject: [PATCH 28/28] fix(web): Invalid import of RoadmapItem in roadmap
section component
---
web/apps/web/app/roadmap/RoadmapSection.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/web/apps/web/app/roadmap/RoadmapSection.tsx b/web/apps/web/app/roadmap/RoadmapSection.tsx
index e90adf175a..006dd0f874 100644
--- a/web/apps/web/app/roadmap/RoadmapSection.tsx
+++ b/web/apps/web/app/roadmap/RoadmapSection.tsx
@@ -2,8 +2,8 @@
import { useCallback } from 'react';
import { usePromise } from '@enterwell/react-hooks';
-import { RoadmapItem } from '../../api/github/[owner]/[repo]/issues/route';
import Roadmap from './roadmap';
+import { RoadmapItem } from '../api/github/[owner]/[repo]/issues/route';
export function RoadmapSection() {
const fetchCallback = useCallback(() => fetch('/api/github/signalco-io/signalco/issues').then(res => res.json()), []);