Merge pull request #6581 from homarr-labs/feat/docs-integration-dataflow
fix(docs): restore homepage integration flow
This commit is contained in:
4 files changed
+272
-144
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
@@ -1,147 +1,84 @@
|
||||
import React, { useRef } from "react";
|
||||
import styles from "../../../../pages/index.module.css";
|
||||
import { supportedIntegrations } from "../../../../constants/supported-integrations";
|
||||
|
||||
import LeaderLine, { SocketType } from "leader-line-new";
|
||||
const nodePositions = [
|
||||
styles.dataflowNodeTopLeft,
|
||||
styles.dataflowNodeMiddleLeft,
|
||||
styles.dataflowNodeBottomLeft,
|
||||
styles.dataflowNodeTopRight,
|
||||
styles.dataflowNodeMiddleRight,
|
||||
styles.dataflowNodeBottomRight,
|
||||
];
|
||||
const cyclingIntegrations = supportedIntegrations.filter((integration) => integration.name !== "Homarr");
|
||||
|
||||
export const DataflowVisualizationComponent = () => {
|
||||
const homarrRef = useRef<HTMLImageElement>(null);
|
||||
const sonarrRef = useRef<HTMLImageElement>(null);
|
||||
const lidarrRef = useRef<HTMLImageElement>(null);
|
||||
const radarrRef = useRef<HTMLImageElement>(null);
|
||||
const jellyfinRef = useRef<HTMLImageElement>(null);
|
||||
const plexRef = useRef<HTMLImageElement>(null);
|
||||
const sabnzbdRef = useRef<HTMLImageElement>(null);
|
||||
const paths = [
|
||||
"M145 82 C330 82 330 235 480 260",
|
||||
"M145 260 C300 260 360 260 480 260",
|
||||
"M145 438 C330 438 330 285 480 260",
|
||||
"M480 260 C630 235 630 82 815 82",
|
||||
"M480 260 C600 260 660 260 815 260",
|
||||
"M480 260 C630 285 630 438 815 438",
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={"bg-black/[.10] py-20"}>
|
||||
<h2 className={"text-center lg:text-5xl text-3xl font-extrabold mb-24"}>
|
||||
export const DataflowVisualizationComponent = () => (
|
||||
<section className={styles.dataflowSection}>
|
||||
<div className={styles.dataflowHeading}>
|
||||
<h2>
|
||||
No YAML configurations.
|
||||
<br />
|
||||
Easy and quick to manage integrations.
|
||||
</h2>
|
||||
<p>Homarr brings status, controls, and updates together in one dashboard.</p>
|
||||
</div>
|
||||
|
||||
<div className="relative max-w-128 h-80 mx-auto animated-dataflow mx-5">
|
||||
<img
|
||||
ref={homarrRef}
|
||||
className={"absolute left-1/2 top-1/2 -translate-y-1/2 -translate-x-1/2 aspect-square object-contain"}
|
||||
src={"/img/logo.png"}
|
||||
alt={"Homarr Logo"}
|
||||
width={100}
|
||||
height={50}
|
||||
/>
|
||||
<div
|
||||
className={styles.dataflowDiagram}
|
||||
role="img"
|
||||
aria-label={`Information flowing both ways between Homarr and rotating examples from ${cyclingIntegrations.length} supported integrations`}
|
||||
>
|
||||
<svg className={styles.dataflowLines} viewBox="0 0 960 520" preserveAspectRatio="none" aria-hidden="true">
|
||||
{paths.map((path) => (
|
||||
<g key={path}>
|
||||
<path className={styles.dataflowLine} d={path} pathLength="1" />
|
||||
<path className={styles.dataflowSignal} d={path} pathLength="1" />
|
||||
<path className={styles.dataflowSignalReverse} d={path} pathLength="1" />
|
||||
</g>
|
||||
))}
|
||||
</svg>
|
||||
|
||||
<img
|
||||
ref={radarrRef}
|
||||
className={"absolute left-0 top-0"}
|
||||
src={"https://github.com/walkxcode/dashboard-icons/blob/main/png/radarr.png?raw=true"}
|
||||
alt={"Radarr"}
|
||||
width={50}
|
||||
height={50}
|
||||
/>
|
||||
<img
|
||||
ref={sonarrRef}
|
||||
className={"absolute left-0 top-1/2 -translate-y-1/2"}
|
||||
src={"https://github.com/walkxcode/dashboard-icons/blob/main/png/sonarr.png?raw=true"}
|
||||
alt={"Sonarr"}
|
||||
width={50}
|
||||
height={50}
|
||||
/>
|
||||
<img
|
||||
ref={lidarrRef}
|
||||
className={"absolute left-0 bottom-0"}
|
||||
src={"https://github.com/walkxcode/dashboard-icons/blob/main/png/lidarr.png?raw=true"}
|
||||
alt={"Lidarr"}
|
||||
width={50}
|
||||
height={50}
|
||||
/>
|
||||
{nodePositions.map((position, nodeIndex) => {
|
||||
const cycleStepInSeconds = 1.2 + nodeIndex * 0.13;
|
||||
const phaseOffsetInSeconds = nodeIndex * 2.7;
|
||||
|
||||
<img
|
||||
ref={jellyfinRef}
|
||||
className={"absolute right-0 top-1/2 -translate-y-1/2"}
|
||||
src={"https://github.com/walkxcode/dashboard-icons/blob/main/png/jellyfin.png?raw=true"}
|
||||
alt={"Jellyfin"}
|
||||
width={50}
|
||||
height={50}
|
||||
/>
|
||||
return (
|
||||
<div className={`${styles.dataflowNode} ${position}`} key={position}>
|
||||
<div className={styles.dataflowCyclingIcons} aria-hidden="true">
|
||||
{cyclingIntegrations.map((_, integrationIndex) => {
|
||||
const integration =
|
||||
cyclingIntegrations[(integrationIndex + nodeIndex * 4) % cyclingIntegrations.length];
|
||||
|
||||
<img
|
||||
ref={plexRef}
|
||||
className={"absolute right-0 bottom-0"}
|
||||
src={"https://github.com/walkxcode/dashboard-icons/blob/main/png/plex.png?raw=true"}
|
||||
alt={"Plex"}
|
||||
width={50}
|
||||
height={50}
|
||||
/>
|
||||
return (
|
||||
<img
|
||||
src={integration.iconUrl}
|
||||
alt=""
|
||||
width={54}
|
||||
height={54}
|
||||
key={integration.name}
|
||||
style={{
|
||||
animationDelay: `${-(integrationIndex * cycleStepInSeconds + phaseOffsetInSeconds)}s`,
|
||||
animationDuration: `${cyclingIntegrations.length * cycleStepInSeconds}s`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<img
|
||||
ref={sabnzbdRef}
|
||||
className={"absolute right-0 top-0"}
|
||||
src={"https://github.com/walkxcode/dashboard-icons/blob/main/png/sabnzbd.png?raw=true"}
|
||||
alt={"Sabnzbd"}
|
||||
width={50}
|
||||
height={50}
|
||||
/>
|
||||
|
||||
<LineTree start={sonarrRef} end={homarrRef} startSocket={"right"} endSocket={"left"} />
|
||||
<LineTree start={radarrRef} end={homarrRef} startSocket={"right"} endSocket={"left"} />
|
||||
<LineTree start={lidarrRef} end={homarrRef} startSocket={"right"} endSocket={"left"} />
|
||||
<LineTree start={plexRef} end={homarrRef} startSocket={"left"} endSocket={"right"} x={100} />
|
||||
<LineTree start={jellyfinRef} end={homarrRef} startSocket={"left"} endSocket={"right"} x={100} />
|
||||
<LineTree start={sabnzbdRef} end={homarrRef} startSocket={"left"} endSocket={"right"} x={100} />
|
||||
<div className={`${styles.dataflowNode} ${styles.dataflowHub}`}>
|
||||
<img src="/img/logo.png" alt="" width={86} height={86} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const LineTree = ({
|
||||
start,
|
||||
end,
|
||||
startSocket,
|
||||
endSocket,
|
||||
x = 0,
|
||||
}: {
|
||||
start: any;
|
||||
end: any;
|
||||
startSocket: SocketType;
|
||||
endSocket: SocketType;
|
||||
x?: number;
|
||||
}) => {
|
||||
const line = useRef<LeaderLine | null>(null);
|
||||
let leaderLine: LeaderLine;
|
||||
|
||||
React.useEffect(() => {
|
||||
const drawLine = () => {
|
||||
leaderLine = new LeaderLine(start.current, LeaderLine.pointAnchor(end.current, { x: `${x}%` }), {
|
||||
startSocket: startSocket,
|
||||
endSocket: endSocket,
|
||||
color: "var(--ifm-color-primary)",
|
||||
size: 3,
|
||||
dash: {
|
||||
animation: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
const timer = setInterval(() => {
|
||||
if (start.current) {
|
||||
clearInterval(timer);
|
||||
drawLine();
|
||||
}
|
||||
}, 5);
|
||||
return () => {
|
||||
timer && clearInterval(timer);
|
||||
leaderLine.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
// scroll and resize listeners could be assigned here
|
||||
setTimeout(() => {
|
||||
// skip current even loop and wait
|
||||
// the end of parent's render call
|
||||
if (line.current && end?.current) {
|
||||
(line.current as any).position();
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
</section>
|
||||
);
|
||||
@@ -230,6 +230,172 @@ section {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dataflowSection {
|
||||
margin-bottom: 0;
|
||||
padding: clamp(4rem, 8vw, 7rem) clamp(1rem, 4vw, 3rem);
|
||||
overflow: hidden;
|
||||
background: rgb(0 0 0 / 10%);
|
||||
}
|
||||
|
||||
.dataflowHeading {
|
||||
max-width: 58rem;
|
||||
margin: 0 auto clamp(2rem, 5vw, 4rem);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dataflowHeading h2 {
|
||||
margin-bottom: 1rem;
|
||||
font-size: clamp(2rem, 5vw, 4rem);
|
||||
line-height: 1.08;
|
||||
letter-spacing: -0.03em;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.dataflowHeading p {
|
||||
margin: 0 auto;
|
||||
max-width: 42rem;
|
||||
color: color-mix(in srgb, var(--ifm-font-color-base) 76%, transparent);
|
||||
font-size: clamp(1rem, 2vw, 1.25rem);
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.dataflowDiagram {
|
||||
position: relative;
|
||||
width: min(100%, 70rem);
|
||||
aspect-ratio: 960 / 520;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.dataflowLines {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.dataflowLine,
|
||||
.dataflowSignal,
|
||||
.dataflowSignalReverse {
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.dataflowLine {
|
||||
stroke: color-mix(in srgb, var(--ifm-color-primary) 30%, transparent);
|
||||
stroke-width: 3;
|
||||
}
|
||||
|
||||
.dataflowSignal,
|
||||
.dataflowSignalReverse {
|
||||
stroke: var(--ifm-color-primary);
|
||||
stroke-width: 7;
|
||||
stroke-dasharray: 0.035 0.965;
|
||||
animation: dataflow-signal 2.8s linear infinite;
|
||||
}
|
||||
|
||||
.dataflowSignalReverse {
|
||||
animation-direction: reverse;
|
||||
animation-delay: -1.4s;
|
||||
}
|
||||
|
||||
.dataflowNode {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
box-sizing: border-box;
|
||||
width: clamp(4.5rem, 10vw, 7.5rem);
|
||||
aspect-ratio: 1;
|
||||
padding: clamp(0.6rem, 1.5vw, 1rem);
|
||||
border-radius: 0.875rem;
|
||||
color: var(--ifm-font-color-base);
|
||||
background: var(--ifm-background-surface-color);
|
||||
box-shadow: 0 6px 12px rgb(31 41 55 / 14%);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.dataflowNode img {
|
||||
width: clamp(2rem, 4.5vw, 3.4rem);
|
||||
height: clamp(2rem, 4.5vw, 3.4rem);
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.dataflowCyclingIcons {
|
||||
position: relative;
|
||||
width: clamp(2rem, 4.5vw, 3.4rem);
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
.dataflowCyclingIcons img {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
animation: dataflow-icon-cycle linear infinite;
|
||||
}
|
||||
|
||||
.dataflowNodeTopLeft,
|
||||
.dataflowNodeMiddleLeft,
|
||||
.dataflowNodeBottomLeft {
|
||||
left: 15%;
|
||||
}
|
||||
|
||||
.dataflowNodeTopRight,
|
||||
.dataflowNodeMiddleRight,
|
||||
.dataflowNodeBottomRight {
|
||||
left: 85%;
|
||||
}
|
||||
|
||||
.dataflowNodeTopLeft,
|
||||
.dataflowNodeTopRight {
|
||||
top: 16%;
|
||||
}
|
||||
|
||||
.dataflowNodeMiddleLeft,
|
||||
.dataflowNodeMiddleRight,
|
||||
.dataflowHub {
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
.dataflowNodeBottomLeft,
|
||||
.dataflowNodeBottomRight {
|
||||
top: 84%;
|
||||
}
|
||||
|
||||
.dataflowHub {
|
||||
left: 50%;
|
||||
width: clamp(6rem, 13vw, 9rem);
|
||||
}
|
||||
|
||||
.dataflowHub img {
|
||||
width: clamp(3rem, 7vw, 5.4rem);
|
||||
height: clamp(3rem, 7vw, 5.4rem);
|
||||
}
|
||||
|
||||
@keyframes dataflow-signal {
|
||||
to {
|
||||
stroke-dashoffset: -1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dataflow-icon-cycle {
|
||||
0%,
|
||||
3.7% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
4.2%,
|
||||
99% {
|
||||
opacity: 0;
|
||||
transform: scale(0.82);
|
||||
}
|
||||
}
|
||||
|
||||
:global([data-theme="dark"]) .resourceMeterCritical .resourceLabel,
|
||||
:global([data-theme="dark"]) .containerRowFailed {
|
||||
color: #ffe3e3;
|
||||
@@ -280,8 +446,39 @@ section {
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.statusDotFailed {
|
||||
animation: none;
|
||||
@media screen and (max-width: 640px) {
|
||||
.dataflowSection {
|
||||
padding-block: 4rem;
|
||||
}
|
||||
|
||||
.dataflowHeading {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.dataflowDiagram {
|
||||
aspect-ratio: 4 / 5;
|
||||
}
|
||||
|
||||
.dataflowNode {
|
||||
width: 4.5rem;
|
||||
padding: 0.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.statusDotFailed,
|
||||
.dataflowSignal,
|
||||
.dataflowSignalReverse,
|
||||
.dataflowCyclingIcons img {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.dataflowSignal,
|
||||
.dataflowSignalReverse {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dataflowCyclingIcons img:first-child {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,9 @@ import Layout from "@theme/Layout";
|
||||
import HomepageFeatures from "../components/pages/home/features/features";
|
||||
import HomepageUserReviews from "../components/pages/home/review-list/review-list";
|
||||
import HomeHero from "../components/pages/home/hero/hero";
|
||||
import { DataflowVisualizationComponent } from "../components/pages/home/dataflow/dataflow";
|
||||
|
||||
import styles from "./index.module.css";
|
||||
import BrowserOnly from "@docusaurus/BrowserOnly";
|
||||
import { SectionContainer } from "@site/src/components/pages/home/container/section-container";
|
||||
import { DragAndDropShowcase } from "@site/src/components/pages/home/drag-and-drop/drag-and-drop-showcase";
|
||||
import { AvailableIntegrations } from "@site/src/components/pages/home/integrations/available-integrations";
|
||||
@@ -32,13 +32,7 @@ export default function Home() {
|
||||
<SectionContainer>
|
||||
<HomepageFeatures />
|
||||
</SectionContainer>
|
||||
<BrowserOnly fallback={<span>loading...</span>}>
|
||||
{() => {
|
||||
const DataflowVisualizationComponent =
|
||||
require("../components/pages/home/dataflow/dataflow").DataflowVisualizationComponent;
|
||||
return <DataflowVisualizationComponent />;
|
||||
}}
|
||||
</BrowserOnly>
|
||||
<DataflowVisualizationComponent />
|
||||
<DragAndDropShowcase />
|
||||
<AvailableIntegrations />
|
||||
<SectionContainer>
|
||||
|
||||
Reference in new issue
Block a user