UNPKG

@airplane/views

Version:

A React library for building Airplane views. Views components are optimized in style and functionality to produce internal apps that are easy to build and maintain.

424 lines (423 loc) 14.1 kB
import { jsx, jsxs, Fragment } from "react/jsx-runtime"; import { Button as Button$1 } from "@mantine/core"; import { useQuery } from "@tanstack/react-query"; import * as React from "react"; import { forwardRef, useCallback, useState, useEffect } from "react"; import { PERMISSIONS_GET } from "../../client/endpoints.js"; import { Fetcher } from "../../client/fetcher.js"; import { ConfirmationComponent } from "../dialog/Confirmation.js"; import { ComponentErrorBoundary } from "../errorBoundary/ComponentErrorBoundary.js"; import { ArrowTopRightOnSquareIconMini } from "@airplane/views/icons/index.js"; import { useCommonLayoutStyle } from "../layout/useCommonLayoutStyle.js"; import { showNotification } from "../notification/showNotification.js"; import { showRunnableErrorNotification } from "../notification/showRunnableErrorNotification.js"; import { RequestRunnableDialog } from "../requestDialog/RequestRunnableDialog.js"; import { Text } from "../text/Text.js"; import { Tooltip } from "../tooltip/Tooltip.js"; import { useButtonState } from "../../state/components/button/useButtonState.js"; import { useComponentId } from "../../state/components/useId.js"; import { useRunbookMutation } from "../../state/tasks/useRunbookMutation.js"; import { useTaskMutation } from "../../state/tasks/useTaskMutation.js"; import { useStyles } from "./Button.styles.js"; import { getFullMutation, getSlug, getRunbookFullMutation } from "../query.js"; import { useRouter } from "../../routing/useRouter.js"; const ButtonC = (props, ref) => /* @__PURE__ */ jsx(ComponentErrorBoundary, { componentName: "Button", children: /* @__PURE__ */ jsx(ButtonWithoutRef, { ...props, innerRef: ref }) }); const Button = /* @__PURE__ */ forwardRef(ButtonC); function ButtonWithoutRef(props) { const id = useComponentId(props.id); const { setResult } = useButtonState(id); if (doesUseTask(props)) { return /* @__PURE__ */ jsx(ButtonWithTask, { ...props, setResult }); } else if (doesUseRunbook(props)) { return /* @__PURE__ */ jsx(ButtonWithRunbook, { ...props, setResult }); } else { const { innerRef, ...restProps } = props; return /* @__PURE__ */ jsx(ButtonComponent, { ref: innerRef, ...restProps }); } } function UnconnectedButton(props) { if (doesUseTask(props)) { return /* @__PURE__ */ jsx(ButtonWithTask, { ...props }); } else if (doesUseRunbook(props)) { return /* @__PURE__ */ jsx(ButtonWithRunbook, { ...props }); } else { return /* @__PURE__ */ jsx(ButtonComponent, { ...props }); } } const ButtonWithTask = ({ innerRef, setResult, confirm, onClick, style, ...restProps }) => { const fullMutation = getFullMutation(restProps.task); const slug = getSlug(fullMutation); const [dialogOpened, setDialogOpened] = React.useState(false); const { onSuccess: fullMutationOnSuccess, onError: fullMutationOnError } = fullMutation ?? {}; const onSuccess = useCallback( // eslint-disable-next-line @typescript-eslint/no-explicit-any (output, runID) => { setResult == null ? void 0 : setResult({ output, runID }); fullMutationOnSuccess == null ? void 0 : fullMutationOnSuccess(output, runID); showNotification({ title: `Successful run`, message: slug, type: "success" }); }, [setResult, fullMutationOnSuccess, slug] ); const onError = useCallback((output, error, runID, status2) => { setResult == null ? void 0 : setResult({ output, error, runID }); fullMutationOnError == null ? void 0 : fullMutationOnError(output, error, runID); showRunnableErrorNotification({ error, runID, slug }); }, [setResult, fullMutationOnError, slug]); const { mutate, loading } = useTaskMutation({ ...fullMutation, onSuccess, onError }); const { data, status } = useQuery([PERMISSIONS_GET, slug], async () => { const fetcher = new Fetcher(); return await fetcher.get(PERMISSIONS_GET, { task_slug: slug, actions: ["tasks.execute", "tasks.request_run"] }); }); const { canExecute, canRequest, disabled } = processPermissionsQueryResult(status, data == null ? void 0 : data.resource["tasks.execute"], data == null ? void 0 : data.resource["tasks.request_run"]); const confirmOptions = processConfirm(confirm, slug); const newOnClick = useCallback((e) => { if (status === "error" || canExecute) { mutate(); } else if (canRequest) { setDialogOpened(true); } onClick == null ? void 0 : onClick(e); }, [status, canExecute, canRequest, mutate, onClick]); if (disabled) { if (style) { style.pointerEvents = "all"; } else { style = { pointerEvents: "all" }; } } return /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx(Tooltip, { label: status === "success" ? "Missing request and execute permissions" : "Loading permissions...", position: "bottom-start", disabled: !disabled, children: /* @__PURE__ */ jsx(ButtonComponent, { ref: innerRef, disabled, loading, onClick: newOnClick, confirm: canExecute && confirmOptions, style, ...restProps }) }), /* @__PURE__ */ jsx(RequestRunnableDialog, { opened: dialogOpened, onSubmit: () => setDialogOpened(false), onClose: () => setDialogOpened(false), taskSlug: slug, paramValues: fullMutation.params || {} }) ] }); }; const ButtonWithRunbook = ({ innerRef, setResult, confirm, onClick, ...restProps }) => { const fullMutation = getRunbookFullMutation(restProps.runbook); const slug = fullMutation.slug; const [dialogOpened, setDialogOpened] = React.useState(false); const { onSuccess: fullMutationOnSuccess, onError: fullMutationOnError } = fullMutation ?? {}; const onSuccess = useCallback((sessionID) => { setResult == null ? void 0 : setResult({ sessionID }); fullMutationOnSuccess == null ? void 0 : fullMutationOnSuccess(sessionID); showNotification({ title: `Successful session`, message: slug, type: "success" }); }, [setResult, fullMutationOnSuccess, slug]); const onError = useCallback((error, sessionID, status2) => { setResult == null ? void 0 : setResult({ error, sessionID }); fullMutationOnError == null ? void 0 : fullMutationOnError(error, sessionID); showRunnableErrorNotification({ error, sessionID, slug }); }, [setResult, fullMutationOnError, slug]); const { mutate, loading } = useRunbookMutation({ ...fullMutation, onSuccess, onError }); const { data, status } = useQuery([PERMISSIONS_GET, slug], async () => { const fetcher = new Fetcher(); return await fetcher.get(PERMISSIONS_GET, { runbook_slug: slug, actions: ["runbooks.execute", "trigger_requests.create"] }); }); const { canExecute, canRequest, disabled } = processPermissionsQueryResult(status, data == null ? void 0 : data.resource["runbooks.execute"], data == null ? void 0 : data.resource["trigger_requests.create"]); const confirmOptions = processConfirm(confirm, slug); const newOnClick = useCallback((e) => { if (status === "error" || canExecute) { mutate(); } else if (canRequest) { setDialogOpened(true); } onClick == null ? void 0 : onClick(e); }, [status, canExecute, canRequest, mutate, onClick]); return /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx(Tooltip, { label: status === "success" ? "Missing request and execute permissions" : "Loading permissions...", wrapper: "div", position: "bottom-start", disabled: !disabled, children: /* @__PURE__ */ jsx(ButtonComponent, { ref: innerRef, disabled, loading, onClick: newOnClick, confirm: canExecute && confirmOptions, ...restProps }) }), /* @__PURE__ */ jsx(RequestRunnableDialog, { opened: dialogOpened, onSubmit: () => setDialogOpened(false), onClose: () => setDialogOpened(false), runbookSlug: slug, paramValues: fullMutation.params || {} }) ] }); }; const ButtonComponent = /* @__PURE__ */ forwardRef(({ preset = "primary", leftAlign, disableFocusRing, width, height, grow, className, style, ...props }, ref) => { const [showConfirmation, setShowConfirmation] = useState(false); const [clickEvent, setClickEvent] = useState(); const presetStyle = buttonPreset[preset]; const variant = props.variant || presetStyle.variant; const color = props.color || presetStyle.color; const { classes, cx } = useStyles({ color, variant }); const { classes: layoutClasses } = useCommonLayoutStyle({ width, height, grow }); const onClickWithConfirmation = useCallback((e) => { var _a; if (!isButton(props)) { return; } const onClick = props.onClick; if (props.confirm) { setShowConfirmation(true); setClickEvent(e); const confirmOptions = typeof props.confirm === "boolean" ? {} : props.confirm; (_a = confirmOptions.onClick) == null ? void 0 : _a.call(confirmOptions, e); } else { onClick == null ? void 0 : onClick(e); } if (props.stopPropagation) { e.stopPropagation(); } }, [props]); if (isAnchor(props)) { return /* @__PURE__ */ jsx(LinkButtonComponent, { ref, leftAlign, disableFocusRing, width, height, grow, className, style, ...props, color, variant }); } else if (isButton(props)) { const { confirm, ...rest } = props; const confirmOptions = typeof confirm === "boolean" ? {} : confirm; if (confirmOptions && !confirmOptions.body && !confirmOptions.title) { confirmOptions.title = "Are you sure?"; } return /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx(Button$1, { component: "button", ref, ...rest, loaderPosition: "center", onClick: onClickWithConfirmation, color, variant, className: cx(layoutClasses.style, className), style, classNames: { root: cx({ [classes.disableFocusRing]: disableFocusRing, [classes.recolorRoot]: !disableFocusRing }), inner: cx({ [classes.leftAlign]: leftAlign }) } }), /* @__PURE__ */ jsx(ConfirmationComponent, { opened: showConfirmation, onClose: () => setShowConfirmation(false), onConfirm: () => { var _a; setShowConfirmation(false); (_a = props.onClick) == null ? void 0 : _a.call(props, clickEvent); setClickEvent(void 0); }, cancelText: confirmOptions == null ? void 0 : confirmOptions.cancelText, confirmText: confirmOptions == null ? void 0 : confirmOptions.confirmText, title: confirmOptions == null ? void 0 : confirmOptions.title, children: (confirmOptions == null ? void 0 : confirmOptions.body) ?? null }) ] }); } return null; }); ButtonComponent.displayName = "ButtonComponent"; const LinkButtonComponent = /* @__PURE__ */ forwardRef(({ href, newTab, target: userTarget, disabled, color, variant, leftAlign, disableFocusRing, width, height, grow, className, style, ...props }, ref) => { const { classes, cx } = useStyles({ color, variant }); const { classes: layoutClasses } = useCommonLayoutStyle({ width, height, grow }); const [buttonHref, setButtonHref] = useState(() => typeof href === "string" ? href : ""); const { getHref } = useRouter(); useEffect(() => { const getButtonHref = async () => { if (typeof href !== "string") { const rhr = await getHref(href); setButtonHref(rhr); } else { setButtonHref(href); } }; getButtonHref(); }, [href, getHref]); const targetNewTab = newTab == null ? true : newTab; const target = userTarget ?? targetNewTab ? "_blank" : "_top"; return /* @__PURE__ */ jsx(Button$1, { className: cx(layoutClasses.style, className), style, classNames: { root: cx({ [classes.disableFocusRing]: disableFocusRing, [classes.recolorRoot]: !disableFocusRing }), inner: cx({ [classes.leftAlign]: leftAlign }) }, variant, color, component: "a", ref, target, rightIcon: target === "_blank" ? /* @__PURE__ */ jsx(ArrowTopRightOnSquareIconMini, {}) : void 0, href: buttonHref, disabled: !buttonHref || disabled, ...props, loaderPosition: "center" }); }); LinkButtonComponent.displayName = "LinkButtonComponent"; const isAnchor = (props) => { return !!props.href; }; const isButton = (props) => { return !props.href; }; function doesUseTask(props) { return Boolean(props.task); } function doesUseRunbook(props) { return Boolean(props.runbook); } function processConfirm(confirm, slug) { const confirmOptions = typeof confirm === "boolean" ? {} : confirm; if (confirmOptions) { if (confirmOptions.body === void 0) { confirmOptions.body = /* @__PURE__ */ jsxs(Text, { children: [ "Are you sure you want to run ", /* @__PURE__ */ jsxs("b", { children: [ slug, "?" ] }) ] }); } if (confirmOptions.title === void 0) { confirmOptions.title = slug; } confirmOptions.confirmText = (confirmOptions == null ? void 0 : confirmOptions.confirmText) ?? "Run"; } return confirmOptions; } function processPermissionsQueryResult(status, apiCanExecute, apiCanRequest) { let canExecute = false; let canRequest = false; if (status === "success") { canExecute = apiCanExecute; canRequest = apiCanRequest; } const disabled = status === "loading" || status === "success" && !canExecute && !canRequest; return { canExecute, canRequest, disabled }; } const buttonPreset = { primary: { variant: "filled", color: "primary" }, danger: { variant: "filled", color: "error" }, secondary: { variant: "light", color: "primary" }, tertiary: { variant: "outline", color: "secondary" } }; export { Button, ButtonComponent, ButtonWithoutRef, UnconnectedButton }; //# sourceMappingURL=Button.js.map