Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
101 commits
Select commit Hold shift + click to select a range
2be181f
Implement new release file notifications.yaml
pablomendezroyo Mar 18, 2025
326cb3d
Merge pull request #2100 from dappnode/pablo/add-notifications-file
pablomendezroyo Mar 18, 2025
5f94bff
Implement backend API calls
pablomendezroyo Mar 18, 2025
a093e08
Merge pull request #2102 from dappnode/pablo/implement-backend-api-calls
pablomendezroyo Mar 18, 2025
c54bd62
bump ajv and fix import issue
pablomendezroyo Mar 18, 2025
1ba25fc
Implement notifications file and json schema
pablomendezroyo Mar 18, 2025
22efe65
fix import
pablomendezroyo Mar 18, 2025
cea4212
fix import
pablomendezroyo Mar 18, 2025
27f57a4
Merge branch 'pablo-mateu/refactor-notifications' into pablo/implemen…
pablomendezroyo Mar 18, 2025
9765688
Root notifications tab (#2105)
mateumiralles Mar 18, 2025
c5bef6c
fix `gatusConfig` urls (#2107)
mateumiralles Mar 18, 2025
0f5a472
remove uri
pablomendezroyo Mar 19, 2025
3be2055
Merge pull request #2103 from dappnode/pablo/implement-notifications-…
pablomendezroyo Mar 19, 2025
752f247
remove temperature daemon
pablomendezroyo Mar 19, 2025
8894add
add type for custom endpoints
pablomendezroyo Mar 19, 2025
6cd0e61
add validation for custom endpoints
pablomendezroyo Mar 19, 2025
9a14ad8
Merge pull request #2108 from dappnode/pablo/remove-host-notifications
pablomendezroyo Mar 19, 2025
b1deaa4
Merge pull request #2109 from dappnode/pablo/add-custom-endpoint
pablomendezroyo Mar 19, 2025
8838c30
Implement notifications module (#2112)
pablomendezroyo Mar 19, 2025
0cd9c7b
Add `notifications.yaml` to dappmanager (#2113)
pablomendezroyo Mar 20, 2025
b487cbd
fix tests
pablomendezroyo Mar 20, 2025
2d2293b
Implement notifications `inbox` subpath (#2106)
mateumiralles Mar 20, 2025
dc62b89
remove unnecesary asyn-await
Mar 20, 2025
ded9ccf
Check is core package before updating notifications settings (#2114)
pablomendezroyo Mar 20, 2025
8dc351b
Return is core to in getEndpoints from notifications (#2115)
pablomendezroyo Mar 20, 2025
ffe0143
fix getEndpoints method (#2116)
mateumiralles Mar 20, 2025
0064a1f
improve code redability
Mar 21, 2025
33c4716
Add `notifications/settings` subpath (#2117)
mateumiralles Mar 21, 2025
b230971
Ensure notificationsEndpoints by package is initalized (#2118)
mateumiralles Mar 21, 2025
e3f80aa
Initialize in single line (#2119)
pablomendezroyo Mar 21, 2025
ff315a0
Return avatarUrl in `/package-manifest` (#2120)
pablomendezroyo Mar 25, 2025
ef58251
Ensure directory exists (#2124)
pablomendezroyo Mar 25, 2025
372e58f
Notifications step in installer (#2121)
mateumiralles Mar 26, 2025
22b05bf
Prevent endpoints autoupdate (#2125)
mateumiralles Mar 27, 2025
33ba769
Merge branch 'develop' into pablo-mateu/refactor-notifications
pablomendezroyo Mar 27, 2025
5c3b23a
Revert "Prevent endpoints autoupdate (#2125)"
mateumiralles Mar 27, 2025
095e9e1
Merge branch 'develop' into pablo-mateu/refactor-notifications
pablomendezroyo Mar 28, 2025
27507de
filter error notifications on `Inbox` (#2128)
mateumiralles Mar 28, 2025
2197daf
Retrieve notification icon from the notification itself (#2129)
mateumiralles Mar 31, 2025
a84ddab
ensure state update (#2132)
mateumiralles Apr 2, 2025
aced309
Persist user settings in notifications (#2134)
pablomendezroyo Apr 3, 2025
c720af5
fix merge conditions
pablomendezroyo Apr 3, 2025
f8cad7f
Add unit testing to nofiications merge userr settings (#2135)
pablomendezroyo Apr 3, 2025
f9def81
update not name
pablomendezroyo Apr 3, 2025
93f81f6
fix NotificationCategory enum
mateumiralles Apr 3, 2025
dcfe425
fix NotificationCategory ref
mateumiralles Apr 3, 2025
6ee17f0
Merge endpoints config on installer (#2136)
mateumiralles Apr 7, 2025
69f92e2
Merge branch 'develop' into pablo-mateu/refactor-notifications
pablomendezroyo Apr 8, 2025
ff3cb0f
Update "pkg updates" endpoint desc
mateumiralles Apr 8, 2025
407c029
Deprecate old push notifications (#2138)
mateumiralles Apr 9, 2025
462ac72
Request to mark notifications as seen (#2142)
mateumiralles Apr 10, 2025
ad891ed
Add priority field (#2149)
pablomendezroyo Apr 21, 2025
43f2f3e
Relocating Legacy Notifications Tab (#2143)
mateumiralles Apr 22, 2025
9e070b0
fix notifications unnecessary re-renders
mateumiralles Apr 23, 2025
b7de5dd
Add status field to notification (#2153)
pablomendezroyo Apr 24, 2025
57553c9
Notifications inbox UI fixes (#2156)
mateumiralles Apr 25, 2025
48ff6a4
fix resolved label
mateumiralles Apr 25, 2025
97bbfc3
Notifications general switch (#2155)
mateumiralles May 5, 2025
d5ad136
Welcome notifications modal (#2144)
mateumiralles May 14, 2025
63ab944
Add pagination to history (#2160)
mateumiralles May 14, 2025
6839ddf
Merge branch 'develop' into pablo-mateu/refactor-notifications
pablomendezroyo May 14, 2025
3c9d0a7
remove priority
pablomendezroyo May 15, 2025
6407b63
Display alerts component (#2164)
mateumiralles May 15, 2025
595a8d6
remove triggered filter in banner component
mateumiralles May 15, 2025
807f480
fix linter
mateumiralles May 15, 2025
5966e44
update eth repository not copy
May 15, 2025
76b16cc
pagination fix
mateumiralles May 20, 2025
7492116
fix types & schemas
mateumiralles May 20, 2025
98c6318
isBanner in custom shcemas
mateumiralles May 20, 2025
2e5be38
update avatar
mateumiralles May 20, 2025
d2df7d6
Update set seen by correlationId endpoint (#2167)
mateumiralles May 21, 2025
b02ecda
start-stop notifications services fix
mateumiralles May 21, 2025
8635641
scrollable categories mobile view
mateumiralles May 21, 2025
cbea9ee
update CTA urls
mateumiralles May 21, 2025
0f4dae4
resolved banner notifications fix
mateumiralles May 21, 2025
ee87d52
Merge branch 'develop' into pablo-mateu/refactor-notifications
mateumiralles May 21, 2025
14e2dd2
validate notifications and setupwizard schemas too (#2168)
pablomendezroyo May 21, 2025
a080882
condition operator fix
mateumiralles May 21, 2025
d5b5422
be more flexible in url notifications schema pattern
May 22, 2025
7469db9
Filter banner by correlationId
mateumiralles May 22, 2025
dcb758b
external cta urls
mateumiralles May 22, 2025
9f8afc4
more flexibility on regex url notifications
May 22, 2025
b70424b
fix lint
May 23, 2025
8def149
schemas url fix
mateumiralles May 23, 2025
f66ee89
add failure treeshold of 3
pablomendezroyo May 25, 2025
f5413a3
Implement notifications settings allDnps (#2170)
pablomendezroyo May 26, 2025
3cacca4
add correlationid prop
May 26, 2025
ef6586c
fix merging notifications settings
May 26, 2025
2847ce9
Allow MD in endpoint descr
mateumiralles May 26, 2025
6871c58
fix schema test
May 26, 2025
9527495
Notifications settings tooltip (#2169)
mateumiralles May 26, 2025
7b98826
use correlation id in dappmanager
May 26, 2025
a2bb59c
add /dnp to installer url
May 26, 2025
8cb61d6
onboarding link opens new tab
mateumiralles May 26, 2025
2edcbdd
internet notification fix (#2172)
mateumiralles May 26, 2025
7190d5a
Improve notifications onboarding (#2171)
mateumiralles May 27, 2025
c946fb7
notifications CTA button fix
mateumiralles May 27, 2025
d074ddd
toast feedback on settings update
mateumiralles May 28, 2025
48408bb
fix notifications update toast
mateumiralles May 28, 2025
a61fe2a
remove hardcoded ipfs
pablomendezroyo May 28, 2025
b21ff84
add missing patterns
pablomendezroyo May 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified avatar-dappmanager.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions notifications.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
customEndpoints:
- name: "Package updates notifications"
isBanner: false
correlationId: "dappmanager-update-pkg"
description: "This endpoint notifies users about available package updates."
enabled: true
1 change: 0 additions & 1 deletion packages/admin-ui/server-mock/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ startHttpApi({
env: () => {},
fileDownload: () => {},
globalEnvs: () => {},
notificationSend: () => {},
packageManifest: () => {},
metrics: () => {},
publicPackagesData: () => {},
Expand Down
19 changes: 13 additions & 6 deletions packages/admin-ui/src/__mock-backend__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,10 +183,6 @@ export const otherCalls: Omit<Routes, keyof typeof namedSpacedCalls> = {
newFeatureStatusSet: async () => {},
poweroffHost: async () => {},
rebootHost: async () => {},
rebootHostIsRequiredGet: async () => ({
rebootRequired: true,
pkgs: "docker"
}),
setStaticIp: async () => {},

systemInfoGet: async () => ({
Expand Down Expand Up @@ -385,8 +381,19 @@ export const otherCalls: Omit<Routes, keyof typeof namedSpacedCalls> = {
dockerHostVersion: "20.10.7",
dockerLatestVersion: "20.10.8"
}),
getIsConnectedToInternet: async () => false,
getCoreVersion: async () => "0.2.92"
getCoreVersion: async () => "0.2.92",
notificationsGetAllEndpoints: async () => {
return { "geth.dnp.dappnode.eth": { endpoints: [], customEndpoints: [], isCore: false } };
},
notificationsGetUnseenCount: async () => 2,
notificationsSetAllSeen: async () => {},
notificationSetSeenByCorrelationID: async () => {},
notificationsUpdateEndpoints: async () => {},
notificationsGetAll: async () => [],
notificationsGetBanner: async () => [],
notificationsApplyPreviousEndpoints: async () => {
return { endpoints: [], customEndpoints: [] };
}
};

export const calls: Routes = {
Expand Down
4 changes: 3 additions & 1 deletion packages/admin-ui/src/__mock-backend__/wifi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ export const wifi: Pick<Routes, "wifiCredentialsGet" | "wifiReportGet"> = {
report: {
lastLog: "[Error] any wifi error".replace(/\[.*?\]/g, ""),
exitCode: 57
}
},
isDefaultPassphrase: false,
isRunning: false
};
}
};
14 changes: 1 addition & 13 deletions packages/admin-ui/src/api/initialCalls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,12 @@
import { store } from "../store";
import { fetchDnpInstalled } from "services/dnpInstalled/actions";
import { fetchCoreUpdateData } from "services/coreUpdate/actions";
import {
fetchSystemInfo,
fetchVolumes,
fetchPasswordIsSecure,
fetchWifiCredentials,
fetchRebootIsRequired,
fetchShouldShowSmooth,
fetchIsConnectedToInternet
} from "services/dappnodeStatus/actions";
import { fetchSystemInfo, fetchVolumes, fetchShouldShowSmooth } from "services/dappnodeStatus/actions";

export function initialCallsOnOpen() {
store.dispatch<any>(fetchDnpInstalled());
store.dispatch<any>(fetchCoreUpdateData());
store.dispatch<any>(fetchSystemInfo());
store.dispatch<any>(fetchVolumes());
store.dispatch<any>(fetchPasswordIsSecure());
store.dispatch<any>(fetchWifiCredentials());
store.dispatch<any>(fetchRebootIsRequired());
store.dispatch<any>(fetchShouldShowSmooth());
store.dispatch<any>(fetchIsConnectedToInternet());
}
5 changes: 0 additions & 5 deletions packages/admin-ui/src/api/subscriptions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { store } from "../store";
// Actions to push received content
import { pushNotification } from "services/notifications/actions";
import { clearIsInstallingLog, updateIsInstallingLog } from "services/isInstallingLogs/actions";
import { updateVolumes, setSystemInfo } from "services/dappnodeStatus/actions";
import { setDnpInstalled } from "services/dnpInstalled/actions";
Expand All @@ -27,10 +26,6 @@ export function mapSubscriptionsToRedux(subscriptions: Subscriptions): void {
else store.dispatch(updateIsInstallingLog({ id, dnpName, log }));
});

subscriptions.pushNotification.on((notification) => {
store.dispatch(pushNotification(notification));
});

subscriptions.systemInfo.on((systemInfo) => {
store.dispatch(setSystemInfo(systemInfo));
});
Expand Down
228 changes: 125 additions & 103 deletions packages/admin-ui/src/components/NotificationsMain.tsx
Original file line number Diff line number Diff line change
@@ -1,116 +1,138 @@
import React from "react";
import { useSelector } from "react-redux";
import React, { useEffect, useMemo, useState } from "react";
import { NavLink } from "react-router-dom";
import { useApi } from "api";
import RenderMarkdown from "components/RenderMarkdown";
// Selectors
import { getCoreUpdateAvailable, getIsCoreUpdateTypePatch, getUpdatingCore } from "services/coreUpdate/selectors";
import {
getWifiStatus,
getPasswordIsSecure,
getRebootIsRequired,
getIsConnectedToInternet
} from "services/dappnodeStatus/selectors";
import { pathName as systemPathName, subPaths as systemSubPaths } from "pages/system/data";
import Button from "components/Button";
// Style
import Button, { ButtonVariant } from "components/Button";
import { api, useApi } from "api";
import { Notification, Priority } from "@dappnode/types";
import "./notificationsMain.scss";
import { autoUpdateIds } from "params";
import { AlertDismissible } from "./AlertDismissible";
import { MdClose } from "react-icons/md";
import { Accordion } from "react-bootstrap";
import { dappmanagerAliases, externalUrlProps } from "params";

/**
* Aggregate notification and display logic
* Displays banner notifications among all tabs
*/
export default function NotificationsView() {
const coreUpdateAvailable = useSelector(getCoreUpdateAvailable);
const updatingCore = useSelector(getUpdatingCore);
const isCoreUpdateTypePatch = useSelector(getIsCoreUpdateTypePatch);
const wifiStatus = useSelector(getWifiStatus);
const passwordIsSecure = useSelector(getPasswordIsSecure);
const rebootHostIsRequired = useSelector(getRebootIsRequired);
const isConnectedToInternet = useSelector(getIsConnectedToInternet);

// Check is auto updates are enabled for the core
const autoUpdateSettingsReq = useApi.autoUpdateDataGet();
const isCoreAutoUpdateActive = ((autoUpdateSettingsReq.data?.settings || {})[autoUpdateIds.SYSTEM_PACKAGES] || {})
.enabled;

const notifications = [
/**
* [HOST-CONNECTED-TO-INTERNET]
* Tell the user if is connected to internet
*/
{
id: "connectedToInternet",
linkText: "Navigate",
linkPath: "support/auto-diagnose",
body: `**Dappnode host is not connected to internet.** Click **Navigate** to autodiagnose and check the dappnode health.`,
active: isConnectedToInternet === false
},
/**
* [HOST-REBOOT]
* Tell the user to reboot the host
*/
{
id: "hostReboot",
linkText: "Reboot",
linkPath: systemPathName + "/" + systemSubPaths.power,
body: `**Dappnode host reboot required.** Click **Reboot** to reboot the host and apply the changes. The following packages will be updated: ${rebootHostIsRequired?.pkgs}`,
active: rebootHostIsRequired?.rebootRequired
},
/**
* [SYSTEM-UPDATE]
* Tell the user to update the core DNPs
*/
{
id: "systemUpdate",
linkText: "Update",
linkPath: systemPathName + "/" + systemSubPaths.update,
body: "**Dappnode system update available.** Click **Update** to review and approve it",
active:
coreUpdateAvailable &&
!updatingCore &&
// Show if NOT patch, or if patch is must not be active
(!isCoreUpdateTypePatch || !isCoreAutoUpdateActive)
},
/**
* [WIFI-PASSWORD]
* Tell the user to change the wifi credentials
*/
{
id: "wifiCredentials",
linkText: "Change",
linkPath: systemPathName + "/" + systemSubPaths.security,
body: "**Change the Dappnode WiFi credentials**, they are insecure default values.",
active: wifiStatus?.isDefaultPassphrase && wifiStatus?.isRunning
},
/**
* [HOST-USER-PASSWORD]
* Tell the user to change the host's "dappnode" user password
*/
{
id: "hostPasswordInsecure",
linkText: "Change",
linkPath: systemPathName + "/" + systemSubPaths.security,
body: "**Change the host 'dappnode' user password**, it's an insecure default.",
active: passwordIsSecure === false
const [notifications, setNotifications] = useState<Notification[]>([]);

const numOfBannersShown = 3; // Number of banners that will be shown in the UI

// gets the timestamp of one month ago in UNIX format
const oneMonthAgoTimestamp = useMemo(() => {
const now = new Date();
now.setMonth(now.getMonth() - 1);
return Math.floor(now.getTime() / 1000); // Convert to seconds
}, []);

const notificationsCall = useApi.notificationsGetBanner(oneMonthAgoTimestamp);

useEffect(() => {
if (notificationsCall.data) {
setNotifications(filterNotifications(notificationsCall.data));
}
];
}, [notificationsCall.data]);

/**
* filters notifications:
* 1. Filters out notifications that have errors
* 2. Filters out duplicate notifications by correlationId, keeping the most recent one
* 3. Filters out resolved notifications
* 4. Filters out seen notifications
* 5. Sorts notifications by priority
*/

function filterNotifications(notifications: Notification[]): Notification[] {
const priorityOrder = [Priority.critical, Priority.high, Priority.medium, Priority.low];

const map = new Map<string, Notification>();

notifications
.filter((n) => !n.errors) // Filter out notifications with errors
.forEach((notification) => {
const existing = map.get(notification.correlationId);

if (!existing || new Date(notification.timestamp) > new Date(existing.timestamp)) {
map.set(notification.correlationId, notification);
}
});

return Array.from(map.values())
.filter((n) => n.status === "triggered") // Filter out resolved notifications
.filter((n) => n.seen === false) // Filter out seen notifications
.sort((a, b) => priorityOrder.indexOf(a.priority) - priorityOrder.indexOf(b.priority));
}

return (
<div>
{notifications
.filter(({ active }) => active)
.map(({ id, linkText, linkPath, body }) => (
<AlertDismissible key={id} className="main-notification" variant="warning">
<RenderMarkdown source={body} />
{linkText && linkPath ? (
<NavLink to={linkPath}>
<Button variant="warning">{linkText}</Button>
</NavLink>
) : null}
</AlertDismissible>
notifications &&
notifications.length > 0 && (
<div className="banner-notifications-col">
{notifications.slice(0, numOfBannersShown).map((notification) => (
<CollapsableBannerNotification
notification={notification}
key={notification.id}
onClose={() => setNotifications((prev) => prev.filter((n) => n.id !== notification.id))}
/>
))}
</div>
</div>
)
);
}

const priorityBtnVariants: Record<Priority, ButtonVariant> = {
[Priority.low]: "dappnode",
[Priority.medium]: "dappnode",
[Priority.high]: "warning",
[Priority.critical]: "danger"
};

export function CollapsableBannerNotification({
notification,
onClose
}: {
notification: Notification;
onClose: () => void;
}) {
const [isOpen, setIsOpen] = useState(notification.priority === Priority.critical);
const [hasClosed, setHasClosed] = useState(false);

const handleClose = () => {
api.notificationSetSeenByCorrelationID(notification.correlationId);
setHasClosed(true);
onClose();
};

const isExternalUrl =
notification.callToAction && !dappmanagerAliases.some((alias) => notification.callToAction!.url.includes(alias));

return (
!hasClosed && (
<Accordion defaultActiveKey={isOpen ? "0" : "1"}>
<Accordion.Toggle
as={"div"}
eventKey="0"
onClick={() => setIsOpen(!isOpen)}
className={`banner-card ${notification.priority}-priority`}
>
<div className="banner-header">
<h5>{notification.title}</h5>
<button className="close-btn" onClick={handleClose}>
<MdClose />
</button>
</div>
<Accordion.Collapse eventKey="0">
<div className="banner-body">
<RenderMarkdown source={notification.body} />
{notification.callToAction && (
<NavLink to={notification.callToAction.url} {...(isExternalUrl ? externalUrlProps : {})}>
<Button variant={priorityBtnVariants[notification.priority]}>
<div>{notification.callToAction.title}</div>
</Button>
</NavLink>
)}
</div>
</Accordion.Collapse>
</Accordion.Toggle>
</Accordion>
)
);
}
27 changes: 27 additions & 0 deletions packages/admin-ui/src/components/Searchbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from "react";
import { BiSearch } from "react-icons/bi";

import "./searchbar.scss";

interface SearchbarProps {
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
classname?: string;
placeholder?: string;
}

export function Searchbar({ value, onChange, classname, placeholder = "Search..." }: SearchbarProps) {
return (
<div className="searchbar-wrapper">
<BiSearch className="search-icon" />

<input
type="text"
placeholder={placeholder}
value={value}
onChange={onChange}
className={`searchbar ${classname}`}
/>
</div>
);
}
Loading
Loading