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.

300 lines (299 loc) 12 kB
import { jsx, jsxs } from "react/jsx-runtime"; import { useQuery } from "@tanstack/react-query"; import json5 from "json5"; import { useMemo, useContext, useState } from "react"; import { PERMISSIONS_GET, TASKS_GET_TASK_REVIEWERS, RUNBOOKS_GET } from "../../client/endpoints.js"; import { executeRunbook } from "../../client/executeRunbook.js"; import { executeTask } from "../../client/executeTask.js"; import { Fetcher } from "../../client/fetcher.js"; import { Button } from "../button/Button.js"; import { ComponentErrorBoundary } from "../errorBoundary/ComponentErrorBoundary.js"; import { useCommonLayoutStyle } from "../layout/useCommonLayoutStyle.js"; import { Loader } from "../loader/Loader.js"; import { showNotification } from "../notification/showNotification.js"; import { showRunnableErrorNotification } from "../notification/showRunnableErrorNotification.js"; import { getSlug, getFullQuery } from "../query.js"; import { RequestDialogContext } from "../requestDialog/RequestDialogProvider.js"; import { Stack } from "../stack/Stack.js"; import { Text } from "../text/Text.js"; import { Tooltip } from "../tooltip/Tooltip.js"; import { FormProvider } from "../../state/components/form/FormProvider.js"; import { useFormInputs } from "../../state/components/form/useFormInputs.js"; import { useFormState } from "../../state/components/form/useFormState.js"; import { useComponentId } from "../../state/components/useId.js"; import { useRefetchTasks } from "../../state/tasks/useRefetchTask.js"; import { ParameterInput, validateParameterOptions } from "./parameters.js"; const Form = ({ id: propId, children, ...props }) => { const id = useComponentId(propId); return /* @__PURE__ */ jsx(ComponentErrorBoundary, { componentName: Form.displayName, children: /* @__PURE__ */ jsx(FormProvider, { children: "task" in props || "runbook" in props ? /* @__PURE__ */ jsx(FormWithRunnable, { id, ...props, children }) : /* @__PURE__ */ jsx(InnerForm, { id, ...props, children }) }) }); }; Form.displayName = "Form"; const FormWithRunnable = ({ children, onSubmit, beforeSubmitTransform, disabled, task, runbook, ...props }) => { var _a, _b; if (task && runbook) { throw new Error("form cannot be backed by both task and runbook"); } const runnableDef = task || runbook; const opts = useMemo(() => typeof runnableDef === "string" ? { slug: runnableDef } : typeof runnableDef === "function" ? { slug: getSlug(getFullQuery(runnableDef)) } : "fn" in runnableDef ? { ...runnableDef, slug: getSlug(getFullQuery(runnableDef.fn)) } : runnableDef, [runnableDef]); const prefix = `${props.id}.`; const requestDialogContext = useContext(RequestDialogContext); const getRunnableEndpoint = task ? TASKS_GET_TASK_REVIEWERS : RUNBOOKS_GET; const [loading, setLoading] = useState(false); const refetchTasks = useRefetchTasks(); const { data: permissionsData, status: permissionsStatus } = useQuery([PERMISSIONS_GET, opts.slug], async () => { const fetcher = new Fetcher(); return await fetcher.get(PERMISSIONS_GET, { task_slug: task && opts.slug, runbook_slug: runbook && opts.slug, actions: task ? ["tasks.execute", "tasks.request_run"] : ["runbooks.execute", "trigger_requests.create"] }); }); const { data: runnableData, isLoading: runnableDataIsLoading, error: runnableDataError } = useQuery([getRunnableEndpoint, opts.slug], async () => { const fetcher = new Fetcher(); return await fetcher.get(getRunnableEndpoint, { taskSlug: task && opts.slug, runbookSlug: runbook && opts.slug }); }); const params = ((_a = runnableData == null ? void 0 : runnableData.task) == null ? void 0 : _a.parameters.parameters) || ((_b = runnableData == null ? void 0 : runnableData.runbook) == null ? void 0 : _b.parameters.parameters); const { values: formValues, setValues: setFormValues } = useFormState(props.id); const paramTypes = useMemo(() => Object.fromEntries((params ?? []).map((v) => { return [v.slug, v.type]; })), [params]); const components = useMemo(() => { const visibleParams = params == null ? void 0 : params.filter((v) => (!opts.shownFields || opts.shownFields.includes(v.slug)) && (!opts.hiddenFields || !opts.hiddenFields.includes(v.slug))); const formValuesForRunnable = getParamValues(formValues, paramTypes, opts.fieldOptions, prefix); return visibleParams == null ? void 0 : visibleParams.map((param, index) => { var _a2; const inputID = `${prefix}${param.slug}`; return /* @__PURE__ */ jsx(ParameterInput, { idPrefix: prefix, param, paramValues: formValuesForRunnable, onChange: (value) => { setFormValues({ ...formValues, [inputID]: value }); }, value: formValues[inputID], opt: (_a2 = opts.fieldOptions) == null ? void 0 : _a2.find((opt) => param.slug === opt.slug) }, index); }); }, [params, formValues, setFormValues, opts.fieldOptions, opts.shownFields, opts.hiddenFields, prefix, paramTypes]); if (runnableDataError) { return /* @__PURE__ */ jsxs(InnerForm, { ...props, disabled: true, children: [ /* @__PURE__ */ jsx(Text, { color: "error", children: runnableDataError.message }), children ] }); } if (runnableDataIsLoading || !runnableData || permissionsStatus === "loading" || !params) { return /* @__PURE__ */ jsxs(InnerForm, { ...props, disabled: true, children: [ /* @__PURE__ */ jsx(Loader, {}), children ] }); } const error = validateParameterOptions(params, opts); if (error) { return /* @__PURE__ */ jsxs(InnerForm, { ...props, children: [ /* @__PURE__ */ jsx(Text, { color: "error", children: error }), children ] }); } const { canExecute, canRequest } = task ? processPermissionsQueryResult(permissionsStatus, permissionsData.resource["tasks.execute"], permissionsData.resource["tasks.request_run"]) : processPermissionsQueryResult(permissionsStatus, permissionsData.resource["runbooks.execute"], permissionsData.resource["trigger_requests.create"]); const newOnSubmit = (values) => { const valuesWithDefaults = { ...values }; for (const option of opts.fieldOptions ?? []) { if (option.value !== void 0) { valuesWithDefaults[option.slug] = option.value; } } const paramValues = getParamValues(values, paramTypes, opts.fieldOptions, prefix); const executeRunnable = async () => { var _a2, _b2; setLoading(true); const executeResult = task ? await executeTask(opts.slug, "mutation", paramValues) : await executeRunbook(opts.slug, "mutation", paramValues); if (executeResult.error) { showRunnableErrorNotification({ ...executeResult, error: executeResult.error, slug: opts.slug }); setLoading(false); if ("onError" in opts) { (_a2 = opts.onError) == null ? void 0 : _a2.call(opts, executeResult.output, executeResult.error, executeResult.runID); } } else { showNotification({ title: `Successful ${task ? "run" : "session"}`, message: opts.slug, type: "success" }); setLoading(false); if ("refetchTasks" in opts && opts.refetchTasks) { refetchTasks(opts.refetchTasks); } if ("onSuccess" in opts && executeResult.runID && executeResult.output !== void 0) { (_b2 = opts.onSuccess) == null ? void 0 : _b2.call(opts, executeResult.output, executeResult.runID); } } }; if (canExecute) { executeRunnable(); } else if (canRequest) { requestDialogContext.setState({ params: paramValues, taskSlug: task && opts.slug, runbookSlug: runbook && opts.slug, opened: true }); } onSubmit == null ? void 0 : onSubmit(valuesWithDefaults); }; const newBeforeSubmitTransform = (rawFormValues) => { const valuesWithoutPrefix = getValuesWithoutPrefix(rawFormValues, prefix); return beforeSubmitTransform ? beforeSubmitTransform(valuesWithoutPrefix) : valuesWithoutPrefix; }; const disabledBecauseOfPermissions = !canRequest && !canExecute; const isDisabled = disabled || disabledBecauseOfPermissions; return /* @__PURE__ */ jsxs(InnerForm, { submitting: loading, disabled: isDisabled, disabledMessage: disabledBecauseOfPermissions ? "Missing request and execute permissions" : void 0, onSubmit: newOnSubmit, beforeSubmitTransform: newBeforeSubmitTransform, ...props, children: [ components, children ] }); }; const InnerForm = ({ id, children, submitText = "Submit", onSubmit, beforeSubmitTransform, resetOnSubmit = true, disabled, disabledMessage, submitting, className, style, width, height, grow }) => { const formInputs = useFormInputs(); const { values } = useFormState(id); const { classes: layoutClasses, cx } = useCommonLayoutStyle({ width, height, grow }); return /* @__PURE__ */ jsx("form", { style, className: cx(layoutClasses.style, className), onSubmit: (e) => { if (!hasErrors(formInputs)) { if (resetOnSubmit) { resetInputs(formInputs); } setShowErrors(formInputs, false); onSubmit == null ? void 0 : onSubmit(beforeSubmitTransform ? beforeSubmitTransform(values) : values); } else { setShowErrors(formInputs, true); } e.preventDefault(); }, noValidate: true, children: /* @__PURE__ */ jsxs(Stack, { children: [ children, /* @__PURE__ */ jsx(Stack, { direction: "row", justify: "end", children: /* @__PURE__ */ jsx(Tooltip, { label: disabledMessage, wrapper: "div", disabled: !disabledMessage, children: /* @__PURE__ */ jsx(Button, { type: "submit", loading: submitting, disabled: disabled || !!disabledMessage || isButtonDisabled(formInputs), children: submitText }) }) }) ] }) }); }; const setShowErrors = (formInputs, showErrors) => { for (const { state } of Object.values(formInputs)) { state.setShowErrors(showErrors); } }; const resetInputs = (formInputs) => { for (const { state } of Object.values(formInputs)) { state.reset(); } }; const hasErrors = (formInputs) => Object.values(formInputs).some((state) => state.state.errors.length > 0); const isButtonDisabled = (formInputs) => Object.values(formInputs).some((state) => state.state.errors.length > 0 && state.state.showErrors); function processPermissionsQueryResult(status, apiCanExecute, apiCanRequest) { let canExecute = true; let canRequest = false; if (status === "success") { canExecute = apiCanExecute; canRequest = apiCanRequest; } return { canExecute, canRequest }; } const getValuesWithoutPrefix = (values, prefix) => { const maybeRemovePrefix = (k) => k.startsWith(prefix) ? k.substring(prefix.length) : k; const valuesWithoutPrefix = Object.fromEntries(Object.entries(values).map(([k, v]) => [maybeRemovePrefix(k), v])); return valuesWithoutPrefix; }; const getParamValues = (values, paramTypes, fieldOptions, prefix) => { const formValues = getValuesWithoutPrefix(values, prefix); for (const option of fieldOptions ?? []) { if (option.value !== void 0) { formValues[option.slug] = option.value; } } const paramValues = Object.fromEntries(Object.entries(formValues).filter(([key, _]) => { return key in paramTypes; }).map(([key, val]) => { if (paramTypes[key] === "upload") { const fileVal = val; return [key, Array.isArray(fileVal) ? fileVal[0] : fileVal]; } else if (paramTypes[key] === "json") { try { if (Array.isArray(val)) { return [key, val.map((v) => json5.parse(v))]; } return [key, json5.parse(val)]; } catch { return [key, val]; } } else { return [key, val]; } })); return paramValues; }; export { Form }; //# sourceMappingURL=Form.js.map