@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
JavaScript
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