diff --git a/src/api/functions/auditLog.ts b/src/api/functions/auditLog.ts index c8a4f9b3..add38c72 100644 --- a/src/api/functions/auditLog.ts +++ b/src/api/functions/auditLog.ts @@ -56,10 +56,6 @@ export function buildAuditLogTransactPut({ }: { entry: AuditLogEntry; }): TransactWriteItem { - if (process.env.DISABLE_AUDIT_LOG && process.env.RunEnvironment === "dev") { - console.log(`Audit log entry: ${JSON.stringify(entry)}`); - return {}; - } const item = buildMarshalledAuditLogItem(entry); return { Put: { diff --git a/src/ui/config.ts b/src/ui/config.ts index 0010f1da..08022fdd 100644 --- a/src/ui/config.ts +++ b/src/ui/config.ts @@ -22,6 +22,7 @@ export type KnownGroups = { export type ConfigType = { AadValidClientId: string; + LinkryPublicUrl: string; ServiceConfiguration: Record; KnownGroupMappings: KnownGroups; }; @@ -43,6 +44,7 @@ type EnvironmentConfigType = { const environmentConfig: EnvironmentConfigType = { "local-dev": { AadValidClientId: "d1978c23-6455-426a-be4d-528b2d2e4026", + LinkryPublicUrl: "go.aws.qa.acmuiuc.org", ServiceConfiguration: { core: { friendlyName: "Core Management Service (NonProd)", @@ -75,6 +77,7 @@ const environmentConfig: EnvironmentConfigType = { }, dev: { AadValidClientId: "d1978c23-6455-426a-be4d-528b2d2e4026", + LinkryPublicUrl: "go.aws.qa.acmuiuc.org", ServiceConfiguration: { core: { friendlyName: "Core Management Service (NonProd)", @@ -107,6 +110,7 @@ const environmentConfig: EnvironmentConfigType = { }, prod: { AadValidClientId: "43fee67e-e383-4071-9233-ef33110e9386", + LinkryPublicUrl: "go.acm.illinois.edu", ServiceConfiguration: { core: { friendlyName: "Core Management Service", diff --git a/src/ui/pages/linkry/LinkShortener.page.tsx b/src/ui/pages/linkry/LinkShortener.page.tsx index dc700242..d972e7de 100644 --- a/src/ui/pages/linkry/LinkShortener.page.tsx +++ b/src/ui/pages/linkry/LinkShortener.page.tsx @@ -16,25 +16,16 @@ import { } from "@mantine/core"; import { useDisclosure } from "@mantine/hooks"; import { notifications } from "@mantine/notifications"; -import { - IconCancel, - IconCross, - IconEdit, - IconPlus, - IconTrash, -} from "@tabler/icons-react"; -import dayjs from "dayjs"; +import { IconCancel, IconEdit, IconPlus, IconTrash } from "@tabler/icons-react"; import React, { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { z } from "zod"; -import { capitalizeFirstLetter } from "./ManageLink.page.js"; -import FullScreenLoader from "@ui/components/AuthContext/LoadingScreen"; import { AuthGuard } from "@ui/components/AuthGuard"; import { useApi } from "@ui/util/api"; import { AppRoles } from "@common/roles.js"; -import { wrap } from "module"; import { linkRecord } from "@common/types/linkry.js"; +import { getRunEnvironmentConfig } from "@ui/config.js"; const wrapTextStyle: React.CSSProperties = { wordWrap: "break-word", @@ -82,7 +73,7 @@ export const LinkShortener: React.FC = () => { }} > - https://go.acm.illinois.edu/{link.slug} + {getRunEnvironmentConfig().LinkryPublicUrl}/{link.slug} diff --git a/src/ui/pages/linkry/ManageLink.page.tsx b/src/ui/pages/linkry/ManageLink.page.tsx index 87c1e681..dbdd3727 100644 --- a/src/ui/pages/linkry/ManageLink.page.tsx +++ b/src/ui/pages/linkry/ManageLink.page.tsx @@ -19,12 +19,13 @@ import { IconDeviceFloppy } from "@tabler/icons-react"; import { LinkryGroupUUIDToGroupNameMap } from "@common/config"; import FullScreenLoader from "@ui/components/AuthContext/LoadingScreen"; import { LINKRY_MAX_SLUG_LENGTH } from "@common/types/linkry"; +import { getRunEnvironmentConfig } from "@ui/config"; export function capitalizeFirstLetter(string: string) { return string.charAt(0).toUpperCase() + string.slice(1); } -const baseUrl = "go.acm.illinois.edu"; +const baseUrl = getRunEnvironmentConfig().LinkryPublicUrl; const slugRegex = new RegExp("^(https?://)?[a-zA-Z0-9-._/]*$"); const urlRegex = new RegExp("^https?://[a-zA-Z0-9-._/?=&+:]*$"); @@ -205,7 +206,7 @@ export const ManageLinkPage: React.FC = () => { rightSectionWidth="150px" leftSection={ } rightSection={ @@ -216,7 +217,7 @@ export const ManageLinkPage: React.FC = () => { color="blue" onClick={generateRandomSlug} > - Generate Random URL + Random ) } diff --git a/src/ui/pages/stripe/CurrentLinks.test.tsx b/src/ui/pages/stripe/CurrentLinks.test.tsx index 0c0d91dd..f9aff528 100644 --- a/src/ui/pages/stripe/CurrentLinks.test.tsx +++ b/src/ui/pages/stripe/CurrentLinks.test.tsx @@ -16,6 +16,7 @@ vi.mock("@ui/components/AuthContext", async () => { describe("StripeCurrentLinksPanel Tests", () => { const getLinksMock = vi.fn(); + const deactivateLinkMock = vi.fn(); const renderComponent = async () => { await act(async () => { @@ -26,7 +27,10 @@ describe("StripeCurrentLinksPanel Tests", () => { withCssVariables forceColorScheme="light" > - + , ); @@ -151,10 +155,10 @@ describe("StripeCurrentLinksPanel Tests", () => { expect(checkbox).not.toBeChecked(); }); - it("triggers deactivation when clicking deactivate button", async () => { + it("triggers deactivation when clicking deactivate button with all active", async () => { getLinksMock.mockResolvedValue([ { - id: "1", + id: "abc123", active: true, invoiceId: "INV-001", invoiceAmountUsd: 5000, @@ -163,7 +167,6 @@ describe("StripeCurrentLinksPanel Tests", () => { link: "http://example.com", }, ]); - const notificationsMock = vi.spyOn(notifications, "show"); await renderComponent(); const checkbox = screen.getByLabelText("Select row"); @@ -172,14 +175,64 @@ describe("StripeCurrentLinksPanel Tests", () => { const deactivateButton = await screen.findByText(/Deactivate 1 link/); await userEvent.click(deactivateButton); - expect(notificationsMock).toHaveBeenCalledWith( - expect.objectContaining({ - title: "Feature not available", - message: "Coming soon!", - color: "yellow", - }), + expect(deactivateLinkMock).toHaveBeenCalledWith("abc123"); + }); + + it("doesn't show deactivation when clicking deactivate button on non-active", async () => { + getLinksMock.mockResolvedValue([ + { + id: "abc123", + active: false, + invoiceId: "INV-001", + invoiceAmountUsd: 5000, + userId: "user@example.com", + createdAt: "2024-02-01", + link: "http://example.com", + }, + ]); + await renderComponent(); + + const checkbox = screen.getByLabelText("Select row"); + await userEvent.click(checkbox); + + const deactivateButton = screen.queryByText(/Deactivate 1 link/); + expect(deactivateButton).not.toBeInTheDocument(); + + expect(deactivateLinkMock).not.toHaveBeenCalled(); + }); + + it("only offers to deactivate one link when there is one active and one non-active", async () => { + getLinksMock.mockResolvedValue([ + { + id: "abc123", + active: false, + invoiceId: "INV-001", + invoiceAmountUsd: 5000, + userId: "user@example.com", + createdAt: "2024-02-01", + link: "http://example.com", + }, + { + id: "def456", + active: true, + invoiceId: "INV-002", + invoiceAmountUsd: 5000, + userId: "user@example.com", + createdAt: "2024-02-01", + link: "http://example.com", + }, + ]); + await renderComponent(); + + const checkboxes = screen.getAllByLabelText("Select row"); + checkboxes.map(async (x) => await userEvent.click(x)); + + const deactivateButton = await screen.findByText( + /Deactivate 1 active link/, ); + await userEvent.click(deactivateButton); - notificationsMock.mockRestore(); + expect(deactivateLinkMock).toHaveBeenCalledTimes(1); + expect(deactivateLinkMock).toHaveBeenNthCalledWith(1, "def456"); }); }); diff --git a/src/ui/pages/stripe/CurrentLinks.tsx b/src/ui/pages/stripe/CurrentLinks.tsx index a56a25d0..a10a90f6 100644 --- a/src/ui/pages/stripe/CurrentLinks.tsx +++ b/src/ui/pages/stripe/CurrentLinks.tsx @@ -25,34 +25,51 @@ const HumanFriendlyDate = ({ date }: { date: string | Date }) => { interface StripeCurrentLinksPanelProps { getLinks: () => Promise; + deactivateLink: (linkId: string) => Promise; } export const StripeCurrentLinksPanel: React.FC< StripeCurrentLinksPanelProps -> = ({ getLinks }) => { +> = ({ getLinks, deactivateLink }) => { const [links, setLinks] = useState(null); const [isLoading, setIsLoading] = useState(true); - const [selectedRows, setSelectedRows] = useState([]); + const [selectedRows, setSelectedRows] = useState< + { id: string; active: boolean }[] + >([]); const { userData } = useAuth(); - useEffect(() => { - const getLinksOnLoad = async () => { - try { - setIsLoading(true); - const data = await getLinks(); - setLinks(data); - setIsLoading(false); - } catch (e) { - setIsLoading(false); - notifications.show({ - title: "Error", - message: - "Failed to get payment links. Please try again or contact support.", - color: "red", - icon: , - }); - console.error(e); + const deleteLinks = async (linkIds: string[]) => { + const promises = linkIds.map((x) => deactivateLink(x)); + const results = await Promise.allSettled(promises); + let success = 0; + let fail = 0; + for (const item of results) { + if (item.status === "rejected") { + fail++; + } else { + success++; } - }; + } + return { fail, success }; + }; + const getLinksOnLoad = async () => { + try { + setIsLoading(true); + const data = await getLinks(); + setLinks(data); + setIsLoading(false); + } catch (e) { + setIsLoading(false); + notifications.show({ + title: "Error", + message: + "Failed to get payment links. Please try again or contact support.", + color: "red", + icon: , + }); + console.error(e); + } + }; + useEffect(() => { getLinksOnLoad(); }, []); const createTableRow = (data: GetInvoiceLinksResponse[number]) => { @@ -60,7 +77,7 @@ export const StripeCurrentLinksPanel: React.FC< x.id).includes(data.id) ? "var(--mantine-color-blue-light)" : undefined } @@ -68,12 +85,12 @@ export const StripeCurrentLinksPanel: React.FC< x.id).includes(data.id)} onChange={(event) => setSelectedRows( event.currentTarget.checked - ? [...selectedRows, data.id] - : selectedRows.filter((id) => id !== data.id), + ? [...selectedRows, { id: data.id, active: data.active }] + : selectedRows.filter(({ id }) => id !== data.id), ) } /> @@ -116,13 +133,27 @@ export const StripeCurrentLinksPanel: React.FC< ); }; - const deactivateLinks = (linkIds: string[]) => { - notifications.show({ - title: "Feature not available", - message: "Coming soon!", - color: "yellow", - icon: , - }); + const deactivateLinks = async (linkIds: string[]) => { + setIsLoading(true); + try { + const result = await deleteLinks(linkIds); + if (result.fail > 0) { + notifications.show({ + title: `Failed to deactivate ${pluralize("link", result.fail, true)}.`, + message: "Please try again later.", + color: "red", + }); + } + if (result.success > 0) { + notifications.show({ + message: `Deactivated ${pluralize("link", result.success, true)}!`, + color: "green", + }); + } + getLinksOnLoad(); + } finally { + setIsLoading(false); + } }; return ( @@ -131,14 +162,23 @@ export const StripeCurrentLinksPanel: React.FC< Current Links - {selectedRows.length > 0 && ( + {selectedRows.filter((x) => x.active).length > 0 && ( )} @@ -150,16 +190,16 @@ export const StripeCurrentLinksPanel: React.FC< = links.length : false} onChange={(event) => setSelectedRows(() => { if (!links) { return []; } - if (selectedRows.length === links.length) { + if (selectedRows.length >= links.length) { return []; } - return links.map((x) => x.id); + return links.map((x) => ({ id: x.id, active: x.active })); }) } /> diff --git a/src/ui/pages/stripe/ViewLinks.page.tsx b/src/ui/pages/stripe/ViewLinks.page.tsx index f9f767a3..66ce813c 100644 --- a/src/ui/pages/stripe/ViewLinks.page.tsx +++ b/src/ui/pages/stripe/ViewLinks.page.tsx @@ -33,6 +33,10 @@ export const ManageStripeLinksPage: React.FC = () => { return response.data; }; + const deactivateLink = async (linkId: string): Promise => { + await api.delete(`/api/v1/stripe/paymentLinks/${linkId}`); + }; + return ( { Create a Stripe Payment Link to accept credit card payments. - + );