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