diff --git a/packages/admin-ui/src/components/NotificationsMain.tsx b/packages/admin-ui/src/components/NotificationsMain.tsx
index f0d7b30a0a..39b072dd3e 100644
--- a/packages/admin-ui/src/components/NotificationsMain.tsx
+++ b/packages/admin-ui/src/components/NotificationsMain.tsx
@@ -1,11 +1,11 @@
-import React, { useEffect, useMemo, useState } from "react";
+import React, { useContext, useEffect, useMemo, useState } from "react";
import { NavLink } from "react-router-dom";
import RenderMarkdown from "components/RenderMarkdown";
import Button, { ButtonVariant } from "components/Button";
import { api, useApi } from "api";
import { Notification, Priority } from "@dappnode/types";
import { MdClose } from "react-icons/md";
-import { Accordion, useAccordionButton } from "react-bootstrap";
+import { Accordion, AccordionContext, useAccordionButton } from "react-bootstrap";
import { dappmanagerAliases, externalUrlProps } from "params";
import { resolveDappnodeUrl } from "utils/resolveDappnodeUrl";
import { IoIosArrowUp, IoIosArrowDown } from "react-icons/io";
@@ -103,7 +103,6 @@ export function CollapsableBannerNotification({
onClose: () => void;
}) {
const [hasClosed, setHasClosed] = useState(false);
- const [isOpen, setIsOpen] = useState(notification.priority === Priority.critical);
const handleClose = () => {
api.notificationSetSeenByCorrelationID({ correlationId: notification.correlationId });
@@ -114,12 +113,17 @@ export function CollapsableBannerNotification({
const isExternalUrl =
notification.callToAction && !dappmanagerAliases.some((alias) => notification.callToAction!.url.includes(alias));
+ // open by default if critical
+ const defaultKey = notification.priority === Priority.critical ? "0" : undefined;
const BannerToggle: React.FC<{
eventKey: string;
className?: string;
children: React.ReactNode;
- }> = ({ eventKey, className, children }) => {
- const onClick = useAccordionButton(eventKey, () => setIsOpen((prev) => !prev));
+ title: string;
+ }> = ({ eventKey, className, children, title }) => {
+ const onClick = useAccordionButton(eventKey);
+ const { activeEventKey } = useContext(AccordionContext);
+ const isOpen = activeEventKey === eventKey || (Array.isArray(activeEventKey) && activeEventKey.includes(eventKey));
return (
+
+
+ {title}
+ {isOpen ? : }
+
+
+
{children}
);
@@ -140,29 +159,14 @@ export function CollapsableBannerNotification({
if (hasClosed) return null;
- // open by default if critical
- const defaultKey = notification.priority === Priority.critical ? "0" : undefined;
-
return (
-
+
-
-
-
- {notification.title}
- {isOpen ? : }
-
-
-
-
+
{notification.callToAction && (
@@ -181,4 +185,3 @@ export function CollapsableBannerNotification({
);
}
-// ...existing code...
diff --git a/packages/admin-ui/src/components/Title.tsx b/packages/admin-ui/src/components/Title.tsx
index ea168050dd..757f15c7a2 100644
--- a/packages/admin-ui/src/components/Title.tsx
+++ b/packages/admin-ui/src/components/Title.tsx
@@ -6,7 +6,7 @@ interface TitleProps {
}
const Title: React.FC = ({ title, children }) => {
- return {children ? children.toUpperCase() : title.toLocaleUpperCase()}
;
+ return {children ? children : title}
;
};
export default Title;
diff --git a/packages/admin-ui/src/components/notificationsMain.scss b/packages/admin-ui/src/components/notificationsMain.scss
index 2b8fedf6ee..9aa3137ae9 100644
--- a/packages/admin-ui/src/components/notificationsMain.scss
+++ b/packages/admin-ui/src/components/notificationsMain.scss
@@ -47,6 +47,7 @@
flex-direction: row;
align-items: center;
gap: 2px;
+ font-weight: normal;
}
.close-btn {
diff --git a/packages/admin-ui/src/components/sidebar/sidebar.scss b/packages/admin-ui/src/components/sidebar/sidebar.scss
index aed24b41c5..35b0c7278b 100644
--- a/packages/admin-ui/src/components/sidebar/sidebar.scss
+++ b/packages/admin-ui/src/components/sidebar/sidebar.scss
@@ -53,7 +53,7 @@
&.selectable:hover,
&.selectable.active {
color: black;
- font-weight: 800;
+ font-weight: 600;
text-decoration: none;
}
&.selectable:hover {
diff --git a/packages/admin-ui/src/img/ai_stars.png b/packages/admin-ui/src/img/ai_stars.png
new file mode 100644
index 0000000000..d5d8234a3c
Binary files /dev/null and b/packages/admin-ui/src/img/ai_stars.png differ
diff --git a/packages/admin-ui/src/pages/dashboard/components/PackageUpdates.tsx b/packages/admin-ui/src/pages/dashboard/components/PackageUpdates.tsx
index 861e89f5ef..3ceea2047e 100644
--- a/packages/admin-ui/src/pages/dashboard/components/PackageUpdates.tsx
+++ b/packages/admin-ui/src/pages/dashboard/components/PackageUpdates.tsx
@@ -77,7 +77,7 @@ function UpdateCard({ update }: { update: UpdatesInterface }) {
}}
>
- {prettyDnpName(update.dnpName)} v{update.newVersion}{" "}
+ {prettyDnpName(update.dnpName)} v{update.newVersion}{" "}
{isOpen ? : }
diff --git a/packages/admin-ui/src/pages/dashboard/components/dashboard.scss b/packages/admin-ui/src/pages/dashboard/components/dashboard.scss
index 63d1acaaba..9db4b32468 100644
--- a/packages/admin-ui/src/pages/dashboard/components/dashboard.scss
+++ b/packages/admin-ui/src/pages/dashboard/components/dashboard.scss
@@ -28,6 +28,9 @@
}
.package-updates {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
@media screen and (min-width: 65rem) {
// Give the single card a max width that matches the other cards
grid-column: 1 / 4;
@@ -51,7 +54,7 @@
.chain-card .name,
.stats-card .id {
text-transform: capitalize;
- font-weight: 800;
+ font-weight: 600;
display: flex;
.text {
@@ -95,6 +98,10 @@
align-items: stretch;
height: 100%;
+ .dnp-name {
+ font-weight: 600;
+ }
+
.package-update-accordion {
min-width: max-content;
display: flex;
diff --git a/packages/admin-ui/src/pages/installer/components/InstallCardComponents/details.scss b/packages/admin-ui/src/pages/installer/components/InstallCardComponents/details.scss
index e8d0ffa8bf..136fd56f59 100644
--- a/packages/admin-ui/src/pages/installer/components/InstallCardComponents/details.scss
+++ b/packages/admin-ui/src/pages/installer/components/InstallCardComponents/details.scss
@@ -31,6 +31,6 @@
header,
.data > span:nth-child(2n-1) {
- font-weight: 800;
+ font-weight: 600;
}
}
diff --git a/packages/admin-ui/src/pages/installer/components/aiDappstore/InstallerAiBanner.tsx b/packages/admin-ui/src/pages/installer/components/aiDappstore/InstallerAiBanner.tsx
new file mode 100644
index 0000000000..aa13f67dc6
--- /dev/null
+++ b/packages/admin-ui/src/pages/installer/components/aiDappstore/InstallerAiBanner.tsx
@@ -0,0 +1,27 @@
+import Card from "components/Card";
+import React from "react";
+import aiStars from "img/ai_stars.png";
+import "./installerAiBanner.scss";
+
+export function InstallerAIBanner({ onCategoryChange }: { onCategoryChange: (category: string) => void }) {
+ return (
+ {
+ onCategoryChange("AI");
+ }}
+ >
+
+
+

+
+
AI Toolkit
+
+ Explore the new AI-powered Dappnode packages, running locally, privately, and securely on your node.
+
+
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages/installer/components/aiDappstore/installerAiBanner.scss b/packages/admin-ui/src/pages/installer/components/aiDappstore/installerAiBanner.scss
new file mode 100644
index 0000000000..2c86cf737a
--- /dev/null
+++ b/packages/admin-ui/src/pages/installer/components/aiDappstore/installerAiBanner.scss
@@ -0,0 +1,36 @@
+.installer-ai-banner {
+ margin-bottom: var(--default-spacing);
+ cursor: pointer;
+
+ &.group:hover h2 {
+ color: var(--dappnode-strong-main-color);
+ }
+ .ai-banner-row {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 10px;
+
+ img {
+ height: 75px;
+ width: 75px;
+ }
+
+ h2 {
+ margin: 0;
+ }
+
+ .description {
+ line-height: normal;
+
+ @media (max-width: 25rem) {
+ display: none;
+ }
+ }
+ }
+
+ // Adding margin-top in mobile view since category filter is hidden
+ @media (max-width: 40rem) {
+ margin-top: var(--default-spacing);
+ }
+}
diff --git a/packages/admin-ui/src/pages/installer/components/dappnodeDappstore/InstallerDnp.tsx b/packages/admin-ui/src/pages/installer/components/dappnodeDappstore/InstallerDnp.tsx
index 50a74e0597..7cd655620b 100644
--- a/packages/admin-ui/src/pages/installer/components/dappnodeDappstore/InstallerDnp.tsx
+++ b/packages/admin-ui/src/pages/installer/components/dappnodeDappstore/InstallerDnp.tsx
@@ -21,6 +21,7 @@ import { getDnpDirectory, getDirectoryRequestStatus } from "services/dnpDirector
import { fetchDnpDirectory } from "services/dnpDirectory/actions";
import { confirmPromise } from "components/ConfirmDialog";
import { stakehouseLsdUrl } from "params";
+import { InstallerAIBanner } from "../aiDappstore/InstallerAiBanner";
export const InstallerDnp: React.FC = () => {
const navigate = useNavigate();
@@ -92,8 +93,19 @@ export const InstallerDnp: React.FC = () => {
}
}
+ // Toggle category: sets only one category to true at a time
function onCategoryChange(category: string) {
- setSelectedCategories((x) => ({ ...x, [category]: !x[category] }));
+ setSelectedCategories((x) => {
+ const newCategories = { ...x, [category]: !x[category] };
+
+ // If the category has been set to true, set all others to false
+ if (newCategories[category]) {
+ Object.keys(newCategories).forEach((key) => {
+ if (key !== category) newCategories[key] = false;
+ });
+ }
+ return newCategories;
+ });
}
const directoryFiltered = filterDirectory({
@@ -122,7 +134,7 @@ export const InstallerDnp: React.FC = () => {
};
const dnpsNoError = directoryFiltered.filter((dnp) => dnp.status !== "error");
- const dnpsFeatured = dnpsNoError.filter((dnp) => dnp.isFeatured);
+ // const dnpsFeatured = dnpsNoError.filter((dnp) => dnp.isFeatured);
const dnpsNormal = dnpsNoError.filter((dnp) => !dnp.isFeatured);
const dnpsError = directoryFiltered.filter((dnp) => dnp.status === "error");
@@ -146,17 +158,20 @@ export const InstallerDnp: React.FC = () => {
!directoryFiltered.length ? (
) : (
-
-
-
- {dnpsError.length ? (
- showErrorDnps ? (
-
- ) : (
-
- )
- ) : null}
-
+ <>
+ {!selectedCategories.AI && }
+
+ {/* */}
+
+ {dnpsError.length ? (
+ showErrorDnps ? (
+
+ ) : (
+
+ )
+ ) : null}
+
+ >
)
) : requestStatus.error ? (
diff --git a/packages/admin-ui/src/pages/installer/components/installer.scss b/packages/admin-ui/src/pages/installer/components/installer.scss
index 89c74d4ae9..266815e8c8 100644
--- a/packages/admin-ui/src/pages/installer/components/installer.scss
+++ b/packages/admin-ui/src/pages/installer/components/installer.scss
@@ -85,7 +85,7 @@
.dnp-cards.featured {
/* Must be auto-fill so when there's one card on big screens, it appears normal */
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
-
+
> .card {
margin-top: 0;
}
@@ -147,6 +147,7 @@
/* Type filter */
.type-filter {
+ margin-top: 10px;
// Hide categories in mobile view
@media (max-width: 40rem) {
display: none;
diff --git a/packages/admin-ui/src/pages/installer/helpers/filterDirectory.ts b/packages/admin-ui/src/pages/installer/helpers/filterDirectory.ts
index 809d463f77..7b7ef582b6 100644
--- a/packages/admin-ui/src/pages/installer/helpers/filterDirectory.ts
+++ b/packages/admin-ui/src/pages/installer/helpers/filterDirectory.ts
@@ -25,14 +25,19 @@ export default function filterDirectory({
query: string;
selectedCategories: SelectedCategories;
}): DirectoryItem[] {
- const isSomeCategorySelected = Object.values(selectedCategories).reduce((acc, val) => acc || val, false);
+ const selected = Object.keys(selectedCategories).filter((key) => selectedCategories[key]);
+ const isAnyCategorySelected = selected.length > 0;
+
return directory
.filter((dnp) => !query || includesSafe(dnp, query))
- .filter(
- (dnp) =>
- !isSomeCategorySelected ||
- (dnp.status === "ok" && (dnp.categories || []).some((category) => selectedCategories[category]))
- );
+ .filter((dnp) => {
+ if (!isAnyCategorySelected) return true;
+ if (dnp.status !== "ok") return false;
+
+ const dnpCategories = dnp.categories || [];
+ // Must contain *any* selected categories
+ return selected.some((cat) => dnpCategories.includes(cat));
+ });
}
/**