UserInviteModal.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. import React, {useContext, useEffect, useMemo, useState} from "react";
  2. import axios from "axios";
  3. import styled from "styled-components";
  4. import { z } from "zod";
  5. import target from "assets/target.svg";
  6. import {type Invite, inviteValidator} from "../lib/invites/types";
  7. import InviteRow from "../main/home/app-dashboard/validate-apply/app-settings/EnvGroupRow";
  8. import type { PopulatedEnvGroup } from "../main/home/app-dashboard/validate-apply/app-settings/types";
  9. import Button from "./porter/Button";
  10. import Container from "./porter/Container";
  11. import Modal from "./porter/Modal";
  12. import SelectableList from "./porter/SelectableList";
  13. import Spacer from "./porter/Spacer";
  14. import Text from "./porter/Text";
  15. import {Context} from "../shared/Context";
  16. import type {InviteType} from "../shared/types";
  17. import api from "../shared/api";
  18. import type {Column} from "react-table";
  19. import CopyToClipboard from "./CopyToClipboard";
  20. import Loading from "./Loading";
  21. import Heading from "./form-components/Heading";
  22. import Helper from "./form-components/Helper";
  23. import PermissionGroup from "../main/home/project-settings/PermissionGroup";
  24. import RoleModal from "../main/home/project-settings/RoleModal";
  25. import InputRow from "./form-components/InputRow";
  26. import RadioSelector from "./RadioSelector";
  27. import Table from "./OldTable";
  28. import {Collaborator} from "../main/home/project-settings/InviteList";
  29. import {SubmitButton} from "../main/home/cluster-dashboard/stacks/launch/components/styles";
  30. import {AuthnContext} from "../shared/auth/AuthnContext";
  31. type Props = {
  32. invites: Invite[];
  33. closeModal: () => void;
  34. };
  35. type InviteMap = Record<
  36. number,
  37. {
  38. status: "pending" | "accepted" | "declined" | "expired";
  39. }
  40. >;
  41. const UserInviteModal: React.FC<Props> = ({ invites, closeModal }) => {
  42. const { checkInvites } = useContext(AuthnContext);
  43. const [inviteMap, setInviteMap] = useState<InviteMap>({});
  44. const [errorText, setErrorText] = useState<string>("");
  45. useEffect(() => {
  46. invites.forEach((invite) => {
  47. if (!inviteMap[invite.id] && invite.status === "pending") {
  48. setInviteMap({
  49. ...inviteMap,
  50. [invite.id]: {
  51. status: "pending",
  52. },
  53. });
  54. }
  55. });
  56. }, [invites]);
  57. const acceptInvite = (invite: Invite): void => {
  58. setInviteMap({
  59. ...inviteMap,
  60. [invite.id]: {
  61. status: "accepted",
  62. },
  63. });
  64. };
  65. const declineInvite = (invite: Invite): void => {
  66. setInviteMap({
  67. ...inviteMap,
  68. [invite.id]: {
  69. status: "declined",
  70. },
  71. });
  72. };
  73. const isDeclined = (invite: Invite): boolean => {
  74. return inviteMap[invite.id]?.status === "declined";
  75. };
  76. const isAccepted = (invite: Invite): boolean => {
  77. return inviteMap[invite.id]?.status === "accepted";
  78. };
  79. return (
  80. <Modal>
  81. <Text size={16}>Pending project invites</Text>
  82. <Spacer height="15px" />
  83. <>
  84. <Text color="helper">
  85. Accept or decline all pending project invites to proceed.
  86. </Text>
  87. <Spacer y={1} />
  88. <ScrollableContainer>
  89. <InviteList>
  90. {invites.map((invite, i) => (
  91. <Container row spaced key={i}>
  92. <Container>{invite.project.name}</Container>
  93. <Container>{invite.inviter.email}</Container>
  94. <SelectedIndicator
  95. onClick={() => {
  96. declineInvite(invite);
  97. }}
  98. isSelected={isDeclined(invite)}
  99. >
  100. <Check className="material-icons">close</Check>
  101. </SelectedIndicator>
  102. <SelectedIndicator
  103. onClick={() => {
  104. acceptInvite(invite);
  105. }}
  106. isSelected={isAccepted(invite)}
  107. >
  108. <Check className="material-icons">check</Check>
  109. </SelectedIndicator>
  110. </Container>
  111. ))}
  112. </InviteList>
  113. <Spacer y={1} />
  114. <Button
  115. onClick={() => {
  116. api.respondUserInvites(
  117. "<token>",
  118. {
  119. accepted_invite_ids: Object.entries(inviteMap).filter(([_, invite]) => invite.status === "accepted").map(([id]) => parseInt(id)),
  120. declined_invite_ids: Object.entries(inviteMap).filter(([_, invite]) => invite.status === "declined").map(([id]) => parseInt(id)),
  121. },
  122. {}
  123. )
  124. .then(() => {
  125. setErrorText("");
  126. console.log("here")
  127. checkInvites()
  128. console.log("there")
  129. closeModal()
  130. })
  131. .catch((err) => {
  132. console.log(err)
  133. if (axios.isAxiosError(err) && err.response?.data?.error) {
  134. setErrorText(err.response?.data?.error);
  135. return;
  136. }
  137. setErrorText(
  138. "An error occurred responding to project invites. Please try again."
  139. );
  140. })
  141. }}
  142. errorText={errorText}
  143. disabled={Object.values(inviteMap).filter((invite) => invite.status === "pending").length > 0}
  144. >Respond to invites</Button>
  145. </ScrollableContainer>
  146. </>
  147. </Modal>
  148. );
  149. };
  150. export default UserInviteModal;
  151. const Check = styled.i`
  152. color: #ffffff;
  153. background: #ffffff33;
  154. width: 24px;
  155. height: 23px;
  156. z-index: 0;
  157. display: flex;
  158. align-items: center;
  159. justify-content: center;
  160. border-radius: 50%;
  161. `;
  162. const SelectedIndicator = styled.div<{ isSelected: boolean }>`
  163. width: 25px;
  164. height: 25px;
  165. border: 1px solid ${(props) => (props.isSelected ? "#ffffff" : "#ffffff55")};
  166. border-radius: 50%;
  167. cursor: pointer;
  168. display: flex;
  169. z-index: 1;
  170. align-items: center;
  171. justify-content: center;
  172. :hover {
  173. border-color: #ffffff;
  174. background: #ffffff11;
  175. }
  176. > i {
  177. font-size: 18px;
  178. color: #ffffff;
  179. }
  180. `;
  181. const InviteList = styled.div`
  182. display: flex;
  183. flex-direction: column;
  184. gap: 15px;
  185. `;
  186. const ScrollableContainer = styled.div`
  187. flex: 1;
  188. overflow-y: auto;
  189. max-height: 480px;
  190. `;