Skip to content
57 changes: 30 additions & 27 deletions packages/admin-ui/src/components/NotificationsMain.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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 });
Expand All @@ -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 (
<div
role="button"
Expand All @@ -133,36 +137,36 @@ export function CollapsableBannerNotification({
}
}}
>
<div className="banner-header">
<h5 className="banner-title">
{title}
{isOpen ? <IoIosArrowUp /> : <IoIosArrowDown />}
</h5>
<button
className="close-btn"
onClick={(e) => {
e.stopPropagation();
handleClose();
}}
>
<MdClose />
</button>
</div>
{children}
</div>
);
};

if (hasClosed) return null;

// open by default if critical
const defaultKey = notification.priority === Priority.critical ? "0" : undefined;

return (
<Accordion activeKey={isOpen ? "0" : undefined} defaultActiveKey={defaultKey}>
<Accordion defaultActiveKey={defaultKey}>
<Accordion.Item eventKey="0">
<BannerToggle eventKey="0" className={`banner-card ${notification.priority}-priority`}>
<div className="banner-header">
<h5 className="banner-title">
{notification.title}
{isOpen ? <IoIosArrowUp /> : <IoIosArrowDown />}
</h5>
<button
className="close-btn"
onClick={(e) => {
e.stopPropagation(); // don't toggle when closing
handleClose();
}}
>
<MdClose />
</button>
</div>

<BannerToggle
eventKey="0"
className={`banner-card ${notification.priority}-priority`}
title={notification.title}
>
<Accordion.Body className="banner-body">
<RenderMarkdown source={notification.body} />
{notification.callToAction && (
Expand All @@ -181,4 +185,3 @@ export function CollapsableBannerNotification({
</Accordion>
);
}
// ...existing code...
2 changes: 1 addition & 1 deletion packages/admin-ui/src/components/Title.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ interface TitleProps {
}

const Title: React.FC<TitleProps> = ({ title, children }) => {
return <div className="section-title">{children ? children.toUpperCase() : title.toLocaleUpperCase()}</div>;
return <div className="section-title">{children ? children : title}</div>;
};

export default Title;
1 change: 1 addition & 0 deletions packages/admin-ui/src/components/notificationsMain.scss
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
flex-direction: row;
align-items: center;
gap: 2px;
font-weight: normal;
}

.close-btn {
Expand Down
2 changes: 1 addition & 1 deletion packages/admin-ui/src/components/sidebar/sidebar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
&.selectable:hover,
&.selectable.active {
color: black;
font-weight: 800;
font-weight: 600;
text-decoration: none;
}
&.selectable:hover {
Expand Down
Binary file added packages/admin-ui/src/img/ai_stars.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ function UpdateCard({ update }: { update: UpdatesInterface }) {
}}
>
<div>
<strong>{prettyDnpName(update.dnpName)}</strong> v{update.newVersion}{" "}
<span className="dnp-name">{prettyDnpName(update.dnpName)}</span> v{update.newVersion}{" "}
{isOpen ? <IoIosArrowUp /> : <IoIosArrowDown />}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -51,7 +54,7 @@
.chain-card .name,
.stats-card .id {
text-transform: capitalize;
font-weight: 800;
font-weight: 600;

display: flex;
.text {
Expand Down Expand Up @@ -95,6 +98,10 @@
align-items: stretch;
height: 100%;

.dnp-name {
font-weight: 600;
}

.package-update-accordion {
min-width: max-content;
display: flex;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,6 @@

header,
.data > span:nth-child(2n-1) {
font-weight: 800;
font-weight: 600;
}
}
Original file line number Diff line number Diff line change
@@ -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 (
<div
className="installer-ai-banner group"
onClick={() => {
onCategoryChange("AI");
}}
>
<Card>
<div className="ai-banner-row">
<img src={aiStars} alt="AI DAppNode Installer Banner" width={50} height={50} />
<div>
<h2>AI Toolkit</h2>
<div className="description">
Explore the new AI-powered Dappnode packages, running locally, privately, and securely on your node.
</div>
</div>
</div>
</Card>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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");

Expand All @@ -146,17 +158,20 @@ export const InstallerDnp: React.FC = () => {
!directoryFiltered.length ? (
<NoPackageFound query={query} />
) : (
<div className="dnps-container">
<DnpStore directory={dnpsFeatured} openDnp={openDnp} featured />
<DnpStore directory={dnpsNormal} openDnp={openDnp} />
{dnpsError.length ? (
showErrorDnps ? (
<DnpStore directory={dnpsError} openDnp={openDnp} />
) : (
<Button onClick={() => setShowErrorDnps(true)}>Show packages still propagating</Button>
)
) : null}
</div>
<>
{!selectedCategories.AI && <InstallerAIBanner onCategoryChange={onCategoryChange} />}
<div className="dnps-container">
{/* <DnpStore directory={dnpsFeatured} openDnp={openDnp} featured /> */}
<DnpStore directory={dnpsNormal} openDnp={openDnp} />
{dnpsError.length ? (
showErrorDnps ? (
<DnpStore directory={dnpsError} openDnp={openDnp} />
) : (
<Button onClick={() => setShowErrorDnps(true)}>Show packages still propagating</Button>
)
) : null}
</div>
</>
)
) : requestStatus.error ? (
<ErrorView error={requestStatus.error} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -147,6 +147,7 @@
/* Type filter */

.type-filter {
margin-top: 10px;
// Hide categories in mobile view
@media (max-width: 40rem) {
display: none;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
});
}

/**
Expand Down
Loading