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 DAppNode Installer Banner +
+

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)); + }); } /**