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