pankod/refine

View on GitHub
packages/devtools-ui/src/components/add-package-drawer.tsx

Summary

Maintainability
A
2 hrs
Test Coverage
import React from "react";
import clsx from "clsx";
import { AvailablePackageType } from "@refinedev/devtools-shared";

import { CloseIcon } from "./icons/close";
import { SearchIcon } from "./icons/search";
import { AddPackageItem } from "./add-package.item";
import { Modal } from "./modal";
import { Highlight } from "./highlight";
import { PlusCircleIcon } from "./icons/plus-circle";
import { Button } from "./button";
import { getAvailablePackages } from "src/utils/packages";
import { UpdateIcon } from "./icons/update";
import { CheckIcon } from "./icons/check";
import { InfoIcon } from "./icons/info";

type Props = {
  visible: boolean;
  onClose: () => void;
  installedPackages: string[];
  onInstall: (packagesToInstall: string[]) => Promise<boolean>;
  dismissOnOverlayClick?: boolean;
};

export const AddPackageDrawer = ({
  visible,
  onClose,
  installedPackages,
  onInstall,
  dismissOnOverlayClick = true,
}: Props) => {
  const [delayedVisible, setDelayedVisible] = React.useState(visible);
  const [installModal, setInstallModal] = React.useState<string | null>(null);

  const [packages, setPackages] = React.useState<AvailablePackageType[]>([]);

  const [search, setSearch] = React.useState("");

  React.useEffect(() => {
    getAvailablePackages().then((pkgs) => setPackages(pkgs));
  }, []);

  React.useEffect(() => {
    setTimeout(() => {
      setDelayedVisible(visible);
    }, 200);
  }, [visible]);

  const onCloseInternal = React.useCallback(() => {
    setDelayedVisible(false);
    setTimeout(() => {
      onClose();
    }, 200);
  }, [onClose]);

  const [status, setStatus] = React.useState<
    "idle" | "installing" | "error" | "done"
  >("idle");

  const updatePackage = React.useCallback(async () => {
    if (installModal) {
      const installCommand =
        packages.find((el) => el.name === installModal)?.install ?? "";
      const packagesToInstall = installCommand
        .replace("npm install ", "")
        .split(" ");

      setStatus("installing");
      const response = await onInstall(packagesToInstall);
      if (response) {
        setStatus("done");
        setInstallModal(null);
        getAvailablePackages().then((pkgs) => {
          setPackages(pkgs);
        });
      } else {
        setStatus("error");
      }
    }
  }, [installModal]);

  const data = React.useMemo(() => {
    const filtered = packages
      .filter((pkg) => !installedPackages.includes(pkg.name))
      .filter((pkg) => pkg.name.includes(search));

    return filtered;
  }, [packages, installedPackages, search]);

  const icon = React.useMemo(() => {
    switch (status) {
      case "installing":
        return <UpdateIcon className="re-text-gray-0 re-animate-spin" />;
      case "done":
        return <CheckIcon className="re-text-gray-0" />;
      case "error":
        return <InfoIcon className="re-text-gray-0 re-rotate-180" />;
      case "idle":
        return <PlusCircleIcon className="re-text-gray-0" />;
    }
  }, [status]);

  const statusText = React.useMemo(() => {
    switch (status) {
      case "installing":
        return "Installing";
      case "done":
        return "Installed";
      case "error":
        return "Error";
      case "idle":
        return "Install";
    }
  }, [status]);

  return (
    <div
      className={clsx(
        "re-z-10",
        "re-fixed",
        "re-left-0",
        "re-top-0",
        "re-h-full",
        "re-w-full",
        !visible && "re-hidden",
        visible && "re-block",
        !visible && "re-pointer-events-none",
        visible && "re-pointer-events-auto",
      )}
      onClick={dismissOnOverlayClick ? onCloseInternal : undefined}
    >
      <div
        className={clsx(
          "re-absolute",
          "re-w-full",
          "re-h-full",
          "re-backdrop-blur-sm",
          "re-bg-gray-900",
          "re-bg-opacity-50",
          "re-transition-all",
          "re-ease-in-out",
          "re-duration-200",
          !delayedVisible && "re-pointer-events-none",
          delayedVisible && "re-pointer-events-auto",
        )}
        style={{
          transformOrigin: "center right",
          transform: `${
            delayedVisible ? "scale(1)" : "scale(0)"
          } translate3d(0,0,0)`,
        }}
      />
      <div
        onClick={(e) => e.stopPropagation()}
        className={clsx(
          "re-absolute",
          "re-right-0",
          "re-top-0",
          "re-h-auto",
          "re-w-[400px]",
          "re-overflow-auto",
          "re-border-l",
          "re-border-l-gray-700",
          "re-bg-gray-800",
          "re-transition-transform",
          "re-duration-200",
          "re-ease-in-out",
          "re-shadow-2xl",
          delayedVisible && "re-translate-x-0",
          !delayedVisible && "re-translate-x-full",
          "re-flex",
          "re-flex-col",
          "re-h-full",
        )}
        style={{
          transformOrigin: "center right",
          transform: `${
            delayedVisible ? "translateX(0px)" : "translateX(100%)"
          } translateZ(0)`,
        }}
      >
        <div
          className={clsx(
            "re-p-5",
            "re-flex",
            "re-items-center",
            "re-justify-between",
            "re-border-b",
            "re-border-b-gray-700",
            "re-flex-shrink-0",
          )}
        >
          <div
            className={clsx(
              "re-text-gray-300",
              "re-text-sm",
              "re-leading-6",
              "re-font-semibold",
            )}
          >
            Explore refine packages
          </div>
          <button
            type="button"
            onClick={onCloseInternal}
            className={clsx(
              "re-w-6",
              "re-h-6",
              "re-appearance-none",
              "re-bg-none",
              "re-border-none",
              "re-outline-none",
              "re-text-gray-500",
            )}
          >
            <CloseIcon className="re-w-6 re-h-6" />
          </button>
        </div>
        <div
          className={clsx(
            "re-flex",
            "re-flex-col",
            "re-flex-1",
            "re-overflow-hidden",
          )}
        >
          <div
            className={clsx(
              "re-pt-5",
              "re-px-5",
              "re-flex",
              "re-items-center",
              "re-justify-center",
              "re-w-full",
              "re-rounded-lg",
            )}
          >
            <div className={clsx("re-relative", "re-w-full")}>
              <input
                type="text"
                className={clsx(
                  "re-w-full",
                  "re-py-[7px]",
                  "re-pr-2",
                  "re-rounded-lg",
                  "re-border",
                  "re-border-gray-700",
                  "re-bg-gray-900",
                  "re-outline-none",
                  "re-text-gray-300",
                  "re-placeholder-gray-500",
                  "re-text-sm",
                  "re-leading-6",
                  "re-pl-10",
                )}
                placeholder="Search packages"
                value={search}
                onChange={(e) => setSearch(e.target.value)}
              />
              <div
                className={clsx(
                  "re-pointer-events-none",
                  "re-absolute",
                  "re-h-full",
                  "re-top-0",
                  "re-left-0",
                  "re-pl-3",
                  "re-flex",
                  "re-items-center",
                  "re-justify-center",
                )}
              >
                <SearchIcon
                  className={clsx("re-text-gray-500", "re-w-4", "re-h-4")}
                />
              </div>
            </div>
          </div>
          <div
            className={clsx(
              "re-overflow-scroll",
              "re-flex-1",
              "re-px-5",
              "re-pb-5",
              "re-pt-5",
            )}
          >
            <div className={clsx("re-flex", "re-flex-col", "re-gap-6")}>
              {data.map((pkg) => (
                <AddPackageItem
                  key={pkg.name}
                  {...pkg}
                  onInstall={() => setInstallModal(pkg.name)}
                />
              ))}
            </div>
          </div>
        </div>
      </div>
      <Modal
        visible={Boolean(installModal)}
        onClose={() => setInstallModal(null)}
        overlay
        header={
          <div className={clsx("re-flex", "re-flex-col", "re-gap-2")}>
            <div
              className={clsx(
                "re-text-gray-300",
                "re-text-sm",
                "re-leading-6",
                "re-font-semibold",
              )}
            >
              {packages.find((el) => el.name === installModal)?.name}
            </div>
            <div
              className={clsx(
                "re-text-gray-400",
                "re-text-sm",
                "re-leading-6",
                "re-font-normal",
              )}
            >
              {packages.find((el) => el.name === installModal)?.description ??
                ""}
            </div>
          </div>
        }
        footer={
          <div
            className={clsx(
              "re-flex",
              "re-flex-row",
              "re-gap-2",
              "re-items-center",
              "re-justify-end",
            )}
          >
            <Button
              onClick={() => updatePackage()}
              className={clsx(
                "re-gap-2",
                "re-bg-alt-blue",
                "re-flex-nowrap",
                "re-flex",
                "re-items-center",
                "re-justify-between",
                "!re-pl-2",
              )}
            >
              {icon}
              <span className="re-text-gray-0">{statusText}</span>
            </Button>
          </div>
        }
      >
        <div
          className={clsx(
            "re-p-5",
            "re-flex",
            "re-flex-col",
            "re-gap-2",
            "re-border-b",
            "re-border-b-gray-700",
          )}
        >
          <div
            className={clsx(
              "re-text-sm",
              "re-leading-6",
              "re-text-gray-300",
              "re-font-semibold",
            )}
          >
            How to install?
          </div>
          <div
            className={clsx(
              "re-bg-gray-700",
              "re-rounded-lg",
              "re-p-4",
              "re-text-sm",
              "re-overflow-auto",
            )}
          >
            <Highlight
              code={
                packages.find((el) => el.name === installModal)?.install ?? ""
              }
              language="bash"
            />
          </div>
        </div>
        <div className={clsx("re-p-5", "re-flex", "re-flex-col", "re-gap-2")}>
          <div
            className={clsx(
              "re-text-sm",
              "re-leading-6",
              "re-text-gray-300",
              "re-font-semibold",
            )}
          >
            How to use?
          </div>
          <div
            className={clsx(
              "re-bg-gray-700",
              "re-rounded-lg",
              "re-p-4",
              "re-text-sm",
              "re-overflow-auto",
            )}
          >
            <Highlight
              code={
                packages.find((el) => el.name === installModal)?.usage ?? ""
              }
              language="tsx"
            />
          </div>
        </div>
      </Modal>
    </div>
  );
};