mirror of
https://github.com/gethomepage/homepage.git
synced 2026-09-26 14:01:16 -07:00
Finally the widget
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import classNames from "classnames";
|
||||
import { useTranslation } from "next-i18next/pages";
|
||||
import { TbApi } from "react-icons/tb";
|
||||
import useSWR from "swr";
|
||||
|
||||
import Container from "../widget/container";
|
||||
import Error from "../widget/error";
|
||||
import Raw from "../widget/raw";
|
||||
|
||||
import ResolvedIcon from "components/resolvedicon";
|
||||
import { formatValue, getValue } from "widgets/customapi/utils";
|
||||
|
||||
export default function CustomApi({ options }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { index, icon, mappings = [], refreshInterval = 10000 } = options;
|
||||
|
||||
const { data, error } = useSWR(`/api/widgets/customapi?${new URLSearchParams({ index }).toString()}`, {
|
||||
refreshInterval: Math.max(1000, refreshInterval),
|
||||
});
|
||||
|
||||
if (error || (data?.error && !mappings.some((mapping) => mapping.field === "error"))) {
|
||||
return <Error options={options} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container options={options} additionalClassNames="information-widget-customapi">
|
||||
<Raw>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex-none mr-2">
|
||||
{icon ? (
|
||||
<ResolvedIcon icon={icon} width={24} height={24} />
|
||||
) : (
|
||||
<TbApi className="w-6 h-6 text-theme-800 dark:text-theme-200" />
|
||||
)}
|
||||
</div>
|
||||
<div className={classNames("flex flex-wrap items-center gap-0.5", !data && "animate-pulse")}>
|
||||
{mappings.map((mapping) => (
|
||||
<div key={mapping.label} className="flex flex-col items-center justify-center px-1 text-xs">
|
||||
<span className="text-theme-800 dark:text-theme-200">{mapping.label}</span>
|
||||
<span className="text-theme-800/70 dark:text-theme-200/50">
|
||||
{data ? formatValue(t, mapping, getValue(mapping.field, data)) : "-"}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Raw>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
|
||||
const { useSWR } = vi.hoisted(() => ({ useSWR: vi.fn() }));
|
||||
vi.mock("swr", () => ({ default: useSWR }));
|
||||
|
||||
vi.mock("components/resolvedicon", () => ({
|
||||
default: ({ icon }) => <div data-testid="resolved-icon" data-icon={icon} />,
|
||||
}));
|
||||
|
||||
import CustomApi from "./customapi";
|
||||
|
||||
const mappings = [
|
||||
{ field: "count", label: "Count" },
|
||||
{ field: "nested.name", label: "Name" },
|
||||
];
|
||||
|
||||
describe("components/widgets/customapi", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("requests the route by widget index with a minimum refresh interval", () => {
|
||||
useSWR.mockReturnValue({ data: undefined, error: undefined });
|
||||
|
||||
renderWithProviders(<CustomApi options={{ index: 2, mappings, refreshInterval: 10 }} />, {
|
||||
settings: { target: "_self" },
|
||||
});
|
||||
|
||||
expect(useSWR).toHaveBeenCalledWith("/api/widgets/customapi?index=2", { refreshInterval: 1000 });
|
||||
});
|
||||
|
||||
it("renders labels with placeholders while loading", () => {
|
||||
useSWR.mockReturnValue({ data: undefined, error: undefined });
|
||||
|
||||
renderWithProviders(<CustomApi options={{ index: 0, mappings }} />, { settings: { target: "_self" } });
|
||||
|
||||
expect(screen.getByText("Count")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("-")).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("renders mapped values and the configured icon", () => {
|
||||
useSWR.mockReturnValue({ data: { count: 5, nested: { name: "foo" } }, error: undefined });
|
||||
|
||||
renderWithProviders(<CustomApi options={{ index: 0, icon: "mdi-api", mappings }} />, {
|
||||
settings: { target: "_self" },
|
||||
});
|
||||
|
||||
expect(screen.getByTestId("resolved-icon")).toHaveAttribute("data-icon", "mdi-api");
|
||||
expect(screen.getByText("5")).toBeInTheDocument();
|
||||
expect(screen.getByText("foo")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("falls back to a default icon when none is configured", () => {
|
||||
useSWR.mockReturnValue({ data: { count: 5 }, error: undefined });
|
||||
|
||||
const { container } = renderWithProviders(<CustomApi options={{ index: 0, mappings }} />, {
|
||||
settings: { target: "_self" },
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId("resolved-icon")).toBeNull();
|
||||
expect(container.querySelector("svg")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("renders an error widget when the route returns an error", () => {
|
||||
useSWR.mockReturnValue({ data: { error: { message: "HTTP Error" } }, error: undefined });
|
||||
|
||||
renderWithProviders(<CustomApi options={{ index: 0, mappings }} />, { settings: { target: "_self" } });
|
||||
|
||||
expect(screen.getByText("widget.api_error")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("treats an error key as data when a mapping reads it", () => {
|
||||
useSWR.mockReturnValue({ data: { error: "none" }, error: undefined });
|
||||
|
||||
renderWithProviders(<CustomApi options={{ index: 0, mappings: [{ field: "error", label: "Error" }] }} />, {
|
||||
settings: { target: "_self" },
|
||||
});
|
||||
|
||||
expect(screen.queryByText("widget.api_error")).toBeNull();
|
||||
expect(screen.getByText("none")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -16,6 +16,7 @@ const widgetMappings = {
|
||||
longhorn: dynamic(() => import("components/widgets/longhorn/longhorn")),
|
||||
kubernetes: dynamic(() => import("components/widgets/kubernetes/kubernetes")),
|
||||
stocks: dynamic(() => import("components/widgets/stocks/stocks")),
|
||||
customapi: dynamic(() => import("components/widgets/customapi/customapi")),
|
||||
};
|
||||
|
||||
export default function Widget({ widget, style }) {
|
||||
|
||||
Reference in New Issue
Block a user