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.

255 lines (254 loc) 9.66 kB
import { jsxs, jsx } from "react/jsx-runtime"; import { MultiSelect, Button } from "@mantine/core"; import { useQuery, useQueries } from "@tanstack/react-query"; import * as React from "react"; import { useState, useRef } from "react"; import { RUNBOOKS_GET, TASKS_GET_TASK_REVIEWERS, ENTITIES_SEARCH, USERS_GET, GROUPS_GET, REQUESTS_CREATE } from "../../client/endpoints.js"; import { Fetcher } from "../../client/fetcher.js"; import { Avatar, getInitials } from "../avatar/Avatar.js"; import { DialogComponent } from "../dialog/Dialog.js"; import { UserGroupIcon } from "@airplane/views/icons/index.js"; import { Loader } from "../loader/Loader.js"; import { showNotification } from "../notification/showNotification.js"; import { Stack } from "../stack/Stack.js"; import { useAsyncDebounce } from "../table/useAsyncDebounce.js"; import { Text } from "../text/Text.js"; import { Textarea } from "../textarea/Textarea.js"; const ResultRow = /* @__PURE__ */ React.forwardRef(({ label, ug, ...others }, ref) => { return /* @__PURE__ */ jsxs("div", { ref, ...others, children: [ ug.user && /* @__PURE__ */ jsxs(Stack, { spacing: "sm", direction: "row", align: "center", children: [ /* @__PURE__ */ jsx(Avatar, { size: "sm", src: ug.user.avatarURL, children: getInitials(label) }), /* @__PURE__ */ jsx(Text, { disableMarkdown: true, children: label }) ] }), ug.group && /* @__PURE__ */ jsxs(Stack, { spacing: "sm", direction: "row", align: "center", children: [ /* @__PURE__ */ jsx(Avatar, { size: "sm", children: /* @__PURE__ */ jsx(UserGroupIcon, {}) }), /* @__PURE__ */ jsx(Text, { disableMarkdown: true, children: label }) ] }) ] }); }); ResultRow.displayName = "ResultRow"; function RequestRunnableDialog(props) { var _a, _b; const [selections, setSelections] = useState([]); const [reason, setReason] = useState(""); const [searchQuery, setSearchQuery] = useState(""); const [submitting, setSubmitting] = useState(false); const reviewersForSelect = useRef([]); if (props.runbookSlug && props.taskSlug) { throw new Error("cannot specify both runbookSlug and taskSlug"); } const slug = props.runbookSlug ?? props.taskSlug; if (slug === void 0) { throw new Error("must specify runbookSlug or taskSlug"); } const getRunnableEndpoint = props.runbookSlug ? RUNBOOKS_GET : TASKS_GET_TASK_REVIEWERS; const { data: runnableData, isLoading: runnableDataIsLoading } = useQuery([getRunnableEndpoint, slug], async () => { const fetcher = new Fetcher(); return await fetcher.get(getRunnableEndpoint, { taskSlug: props.taskSlug, runbookSlug: props.runbookSlug }); }); const { data: entities } = useQuery([ENTITIES_SEARCH, searchQuery], async () => { const fetcher = new Fetcher(); return await fetcher.get(ENTITIES_SEARCH, { q: searchQuery, scope: "all" }); }, { staleTime: Infinity, enabled: !hasExplicitPermissions(runnableData) }); const hydratedReviewers = useQueries({ queries: ((runnableData == null ? void 0 : runnableData.reviewers) || []).map((reviewer) => { if (reviewer.userID) { return { queryKey: [USERS_GET, reviewer.userID], queryFn: async () => { const fetcher = new Fetcher(); return await fetcher.get(USERS_GET, { userID: reviewer.userID }); } }; } if (reviewer.groupID) { return { queryKey: [GROUPS_GET, reviewer.groupID], queryFn: async () => { const fetcher = new Fetcher(); return await fetcher.get(GROUPS_GET, { groupID: reviewer.groupID }); } }; } throw new Error("expected userID or groupID"); }) }); const reviewersLoading = runnableDataIsLoading || hydratedReviewers.some((res) => res.isLoading); const { triggerID, reviewersForSelect: newReviewersForSelect } = processQueryOutputs(hydratedReviewers, runnableData, entities); const reviewersToAdd = newReviewersForSelect.filter((newReviewer) => !reviewersForSelect.current.some((reviewer) => reviewer.value === newReviewer.value)); reviewersForSelect.current.push(...reviewersToAdd); const onSubmit = async (e) => { e.preventDefault(); if (!triggerID) { throw new Error("no trigger to execute"); } setSubmitting(true); const reviewers = selections.map(getUserOrGroupFromValue); try { const fetcher = new Fetcher(); await fetcher.post(REQUESTS_CREATE, { triggerID, requestData: { ...props.taskSlug && { taskData: { paramValues: props.paramValues } }, ...props.runbookSlug && { runbookData: { paramValues: props.paramValues } } }, reason, reviewers }); showNotification({ message: "Request successful", type: "success" }); props.onSubmit(); setSelections([]); setReason(""); } catch (e2) { showNotification({ title: "Request unsuccessful", message: e2 instanceof Error ? e2.message : "", type: "error" }); } finally { setSubmitting(false); } }; const onSearchChange = useAsyncDebounce((value) => { setSearchQuery(value); }, 200); return /* @__PURE__ */ jsx(DialogComponent, { title: "Request execution of " + (((_a = runnableData == null ? void 0 : runnableData.task) == null ? void 0 : _a.name) || ((_b = runnableData == null ? void 0 : runnableData.runbook) == null ? void 0 : _b.name) || props.taskSlug || props.runbookSlug), padding: 20, opened: props.opened, onClose: props.onClose, children: /* @__PURE__ */ jsx("form", { onSubmit, children: /* @__PURE__ */ jsxs(Stack, { children: [ /* @__PURE__ */ jsx(Textarea, { label: "Reason", value: reason, onChange: (event) => setReason(event.currentTarget.value), description: "Extra context sent to the reviewers below" }), !reviewersLoading && /* @__PURE__ */ jsx(MultiSelect, { data: reviewersForSelect.current, placeholder: "Select users or groups", label: "Reviewers", itemComponent: ResultRow, searchable: true, value: selections, onChange: setSelections, onSearchChange, description: "Request approval from users or groups" }), reviewersLoading && /* @__PURE__ */ jsx(Loader, { variant: "dots" }), /* @__PURE__ */ jsx(Stack, { direction: "row", justify: "end", children: /* @__PURE__ */ jsx(Button, { type: "submit", disabled: reviewersLoading, loading: submitting, children: "Request" }) }) ] }) }) }); } function processQueryOutputs(hydratedReviewers, runnableData, entities) { var _a, _b; let triggerID = ""; let reviewersForSelect = []; if (runnableData != null) { const allUsersAndGroupsFetched = hydratedReviewers.every((res) => res.status === "success"); const triggers = ((_a = runnableData.task) == null ? void 0 : _a.triggers) ?? ((_b = runnableData.runbook) == null ? void 0 : _b.triggers); const formTrigger = triggers == null ? void 0 : triggers.find((t) => t.kind === "form"); if (!formTrigger) { throw new Error("unexpected api response: missing form trigger"); } triggerID = formTrigger.triggerID; if (hasExplicitPermissions(runnableData)) { if (allUsersAndGroupsFetched) { reviewersForSelect = hydratedReviewers.map((queryResult, i) => { const ret = { label: "", value: "", ug: {} }; if ("user" in queryResult.data) { ret.ug.user = queryResult.data.user; ret.label = getUserLabel(queryResult.data.user); ret.value = getUserValue(queryResult.data.user); } if ("group" in queryResult.data) { ret.ug.group = queryResult.data.group; ret.label = getGroupLabel(queryResult.data.group); ret.value = getGroupValue(queryResult.data.group); } return ret; }); } } else { if (entities != null) { reviewersForSelect = entities.results.map((ug, i) => { if (ug.user) { return { label: getUserLabel(ug.user), value: getUserValue(ug.user), ug }; } if (ug.group) { return { label: getGroupLabel(ug.group), value: getGroupValue(ug.group), ug }; } return { label: "", value: "", ug: {} }; }); } } } return { triggerID, reviewersForSelect }; } function getUserLabel(user) { return user.name || user.email || user.userID; } function getGroupLabel(group) { return group.name || group.id; } function getUserValue(user) { return `usr_${user.userID}`; } function getGroupValue(group) { return `grp_${group.id}`; } function getUserOrGroupFromValue(value) { if (value.startsWith("usr_")) { return { userID: value.substring(4) }; } if (value.startsWith("grp_")) { return { groupID: value.substring(4) }; } throw new Error("unexpected value: " + value); } function hasExplicitPermissions(taskOrRunbook) { var _a, _b; return !!(((_a = taskOrRunbook == null ? void 0 : taskOrRunbook.task) == null ? void 0 : _a.requireExplicitPermissions) || ((_b = taskOrRunbook == null ? void 0 : taskOrRunbook.runbook) == null ? void 0 : _b.isPrivate)); } export { RequestRunnableDialog }; //# sourceMappingURL=RequestRunnableDialog.js.map