UNPKG

@activecollab/components

Version:

ActiveCollab Components

939 lines (915 loc) 36.9 kB
import _extends from "@babel/runtime/helpers/esm/extends"; import React, { useCallback, useMemo, useRef, useState } from "react"; import styled from "styled-components"; import { RoundAvatar } from "./RoundAvatar"; import { AVATAR_COLORS, LG } from "./tokens"; import { Button } from "../../components/Button"; import { Chip } from "../../components/Chip"; import { CompleteCheckbox } from "../../components/CompleteCheckbox"; import { Dialog } from "../../components/Dialog"; import { Filter } from "../../components/Filter"; import { IconButton } from "../../components/IconButton"; import { AccessLogIcon, CancelCrossIcon, CollapseExpandSingleIcon, GitHubIcon, GitIcon, GitLabIcon } from "../../components/Icons"; import { Label } from "../../components/Label"; import { SelectTrigger } from "../../components/SelectTrigger"; import { Sheet } from "../../components/Sheet"; import { Tooltip } from "../../components/Tooltip"; import { Body2, Caption1, Caption2, Header2, Header3 } from "../../components/Typography"; /** * "Webhook Logs" — the VCS webhook delivery log, shown as a wide dialog with a * filterable, expandable list. Extracted into `shared/` so it can be opened * from more than one surface (the Apps & Integrations Repositories page and the * project "..." menu on the Project Repos story). * * Mirrors the app's webhook log model: * `vcs_webhook_logs` → connection_id, event_type, processing_status * (received / unhandled / skipped / processed / * failed / duplicate), delivery_identifier, payload, * created_on * `vcs_webhook_outcomes` → one row per outcome, each with a localized summary * * The status is derived from the outcomes server-side; here we render the * stored value and map it to a palette colour. The connection glyph is the * provider icon (GitHub / GitLab, Git for anything else). */ /** * Normalized event kind. The processor layer maps each provider's raw event * header (GitHub `issues` / GitLab `Issue Hook`, GitHub `pull_request` / GitLab * `Merge Request Hook`, …) onto one provider-agnostic kind, so the same logical * event reads — and filters — identically across connections instead of * appearing once per provider. The set below mirrors the app's * `EVENT_TYPE_OPTIONS` constant. */ /** * The task an outcome created or referenced. In the app each * `vcs_webhook_outcomes` row carries a nullable `task_id`; when it is set the * outcome offers an "Open Task" link that opens the task in a sheet on top of * the log dialog. */ export const WEBHOOK_CONNECTIONS = [{ id: "gh-activecollab", name: "ActiveCollab", service: "github" }, { id: "gh-abstergo", name: "Abstergo Ltd.", service: "github" }, { id: "gh-binford", name: "Binford & Co.", service: "github" }, { id: "gl-platform", name: "Platform", service: "gitlab" }, { id: "gl-content", name: "Content", service: "gitlab" }, { id: "gl-acme-ce", name: "Acme Community", service: "gitlab" }]; /** * Repositories that belong to the connections above. A webhook delivery is * always scoped to one repository, and a repository belongs to exactly one * connection — so the Repository filter can be narrowed to the connection(s) * currently selected in the Connection filter. */ export const WEBHOOK_REPOSITORIES = [{ id: "ac-feather", path: "activecollab/feather", connectionId: "gh-activecollab" }, { id: "ac-backend", path: "activecollab/backend", connectionId: "gh-activecollab" }, { id: "ab-animus", path: "abstergo/animus", connectionId: "gh-abstergo" }, { id: "bi-tool-time", path: "binford/tool-time", connectionId: "gh-binford" }, { id: "pl-gateway", path: "platform/api-gateway", connectionId: "gl-platform" }, { id: "co-handbook", path: "content/handbook", connectionId: "gl-content" }, { id: "co-marketing", path: "content/marketing-site", connectionId: "gl-content" }, { id: "acme-ce", path: "acme/community-edition", connectionId: "gl-acme-ce" }]; const EVENT_KIND_LABELS = { push: "Push", issue: "Issue", pull_request: "Pull Request", pull_request_review: "Pull Request Review", repository: "Repository", other: "Other" }; /** * Fixed Event Type filter options, mirroring the app's `EVENT_TYPE_OPTIONS`. * The app offers the full list regardless of which kinds are present in the * current result set, so we do the same instead of deriving it from the logs. */ const EVENT_TYPE_OPTIONS = [{ id: "push", label: "Push" }, { id: "issue", label: "Issue" }, { id: "pull_request", label: "Pull Request" }, { id: "pull_request_review", label: "Pull Request Review" }, { id: "repository", label: "Repository" }, { id: "other", label: "Other" }]; const STATUS_LABELS = { received: "Received", unhandled: "Unhandled", skipped: "Skipped", processed: "Processed", failed: "Failed", duplicate: "Duplicate" }; /** * Status → palette colour, chosen so the colour matches the meaning: * processed → green (success), failed → coral (error), * skipped → orange (warning), received → sky-blue (in progress), * unhandled → silver (neutral / no handler matched), * duplicate → purple (replayed delivery, dropped before processing). */ const STATUS_COLORS = { processed: { bg: "var(--color-green-pale)", fg: "var(--color-green-pale-darker)" }, failed: { bg: "var(--color-coral)", fg: "var(--color-coral-darker)" }, skipped: { bg: "var(--color-orange)", fg: "var(--color-orange-darker)" }, received: { bg: "var(--color-blue-sky)", fg: "var(--color-blue-sky-darker)" }, unhandled: { bg: "var(--color-gray-silver)", fg: "var(--color-gray-silver-darker)" }, duplicate: { bg: "var(--color-purple)", fg: "var(--color-purple-darker)" } }; /** Provider glyph for a connection: GitHub / GitLab, Git for anything else. */ const connectionIcon = service => { if (service === "github") { return GitHubIcon; } if (service === "gitlab") { return GitLabIcon; } return GitIcon; }; const StatusChip = _ref => { let status = _ref.status; const _STATUS_COLORS$status = STATUS_COLORS[status], bg = _STATUS_COLORS$status.bg, fg = _STATUS_COLORS$status.fg; return /*#__PURE__*/React.createElement(Chip, { label: STATUS_LABELS[status], backgroundColor: bg, color: fg, typographyProps: { variant: "Caption 1", weight: "medium" } }); }; /* --- Relative "ago" time -------------------------------------------------- */ /* The Time column reads relative to a fixed "now" so the mock's examples stay */ /* stable across renders — a live clock would drift the buckets on every reload */ /* and the crafted "3 minutes ago" / "Yesterday" rows would lose their meaning. */ const NOW = new Date(2026, 7, 11, 15, 0, 0); // Tue, 11 Aug 2026, 15:00 const NOW_TS = Math.floor(NOW.getTime() / 1000); const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; const pad2 = value => String(value).padStart(2, "0"); const startOfDay = date => new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime(); /** * When a webhook was received, in the app's relative style: * < 1 min → "Just now" * < 1 hour → "N minutes ago" * earlier today → "Today, HH:MM" * yesterday → "Yesterday, HH:MM" * older, this year → "MMM D" * older, earlier years → "MMM D, YYYY" */ const formatReceivedAt = function (receivedAt, now) { if (now === void 0) { now = NOW; } const then = new Date(receivedAt * 1000); const diffMinutes = Math.floor((now.getTime() - then.getTime()) / 60000); if (diffMinutes < 1) { return "Just now"; } if (diffMinutes < 60) { return diffMinutes + " " + (diffMinutes === 1 ? "minute" : "minutes") + " ago"; } const time = pad2(then.getHours()) + ":" + pad2(then.getMinutes()); const dayDiff = Math.round((startOfDay(now) - startOfDay(then)) / 86400000); if (dayDiff === 0) { return "Today, " + time; } if (dayDiff === 1) { return "Yesterday, " + time; } const date = MONTHS[then.getMonth()] + " " + then.getDate(); return then.getFullYear() === now.getFullYear() ? date : date + ", " + then.getFullYear(); }; /* Timestamp helpers for the mock data below. */ const secondsAgo = seconds => NOW_TS - seconds; const at = (year, month, day, hour, minute) => Math.floor(new Date(year, month, day, hour, minute, 0).getTime() / 1000); /* --- Mock log data (newest first is enforced by sorting on receivedAt) --- */ const PAYLOAD_ISSUE_OPENED = JSON.stringify({ action: "opened", issue: { id: 2002934201, number: 42, title: "Webhook delivery retries are not logged", state: "open", html_url: "https://github.com/activecollab/feather/issues/42", labels: [{ name: "bug" }, { name: "vcs" }], user: { login: "octocat", id: 583231 } }, repository: { id: 776611234, full_name: "activecollab/feather", private: true }, sender: { login: "octocat", id: 583231 } }, null, 2); const PAYLOAD_MERGE_REQUEST = JSON.stringify({ object_kind: "merge_request", event_type: "merge_request", user: { id: 41, name: "Jane Doe", username: "jdoe" }, project: { id: 1801, path_with_namespace: "content/handbook" }, object_attributes: { iid: 318, title: "Add changelog for 8.2", state: "merged", action: "merge", source_branch: "changelog-8-2", target_branch: "main" } }, null, 2); const PAYLOAD_PUSH = JSON.stringify({ ref: "refs/heads/main", before: "9b2c1f0e", after: "f4a7d3c1", commits: [{ id: "f4a7d3c1", message: "Fix typo in webhook log repository", author: { name: "Marko", email: "marko@example.com" } }], repository: { full_name: "activecollab/feather" } }, null, 2); const WEBHOOK_LOGS = [ // Just now (< 1 minute) { id: "log-just-now", connectionId: "gh-activecollab", repositoryId: "ac-feather", eventKind: "issue", status: "processed", deliveryIdentifier: "d3b07384-d9a0-4c9b-8f1e-2a6f1b9c4e21", outcomes: [{ summary: "Created task #1842 from issue #42", task: { id: 1842, name: "Webhook delivery retries are not logged", projectName: "Feather", description: "Retries are not written to the webhook log, so a delivery that only succeeds on a retry still shows as failed. Log each attempt with its outcome." } }], payload: PAYLOAD_ISSUE_OPENED, receivedAt: secondsAgo(25) }, // Minutes ago { id: "log-3m", connectionId: "gl-content", repositoryId: "co-handbook", eventKind: "pull_request", status: "skipped", deliveryIdentifier: "b1946ac9-2f3a-4c0d-9b7e-1d2c3e4f5a6b", outcomes: [{ summary: "Skipped: merge request has no linked issue" }], payload: PAYLOAD_MERGE_REQUEST, receivedAt: secondsAgo(3 * 60) }, { id: "log-12m", connectionId: "gh-abstergo", repositoryId: "ab-animus", eventKind: "issue", status: "failed", deliveryIdentifier: "9c1185a5-c5e9-4e3f-8a1b-7d6e5f4c3b2a", outcomes: [{ summary: "Failed at task creation: project not found" }], payload: PAYLOAD_ISSUE_OPENED, receivedAt: secondsAgo(12 * 60) }, { id: "log-47m", connectionId: "gl-platform", repositoryId: "pl-gateway", eventKind: "push", status: "processed", deliveryIdentifier: "f7c3bc1d-8c5a-4b2e-9f0a-1b2c3d4e5f60", outcomes: [{ summary: "Created task #1839 from issue #311", task: { id: 1839, name: "Refresh the onboarding handbook", projectName: "Handbook", description: "The onboarding handbook is out of date. Review each section and update the steps that changed in the last release." } }, { summary: "Created task #1840 from issue #312", task: { id: 1840, name: "Document webhook scopes", projectName: "Handbook", description: "Explain the connection and repository webhook scopes, and when each one applies." } }], payload: PAYLOAD_PUSH, receivedAt: secondsAgo(47 * 60) }, // Earlier today { id: "log-today-1", connectionId: "gh-activecollab", repositoryId: "ac-feather", eventKind: "other", status: "unhandled", deliveryIdentifier: "1574bddb-75c3-4b1e-8a9c-0d1e2f3a4b5c", outcomes: [], payload: JSON.stringify({ zen: "Keep it logically awesome.", hook_id: 552012 }, null, 2), receivedAt: at(2026, 7, 11, 10, 0) }, { id: "log-today-2", connectionId: "gh-binford", repositoryId: "bi-tool-time", eventKind: "pull_request_review", status: "processed", deliveryIdentifier: "5f2b1c8e-7a3d-4e6f-9b0c-1d2e3f4a5b6c", outcomes: [{ summary: "Referenced task #1699 from pull request #58", task: { id: 1699, name: "Refresh the Tool Time build badge", projectName: "Tool Time", description: "The build badge shows an old status. Update it when the pipeline finishes." } }], payload: PAYLOAD_MERGE_REQUEST, receivedAt: at(2026, 7, 11, 7, 12) }, // Yesterday { id: "log-yesterday-1", connectionId: "gl-content", repositoryId: "co-marketing", eventKind: "issue", status: "received", deliveryIdentifier: "0716d970-2b9e-4a1d-9c3f-5e6a7b8c9d01", outcomes: [], receivedAt: at(2026, 7, 10, 13, 20) }, { id: "log-yesterday-2", connectionId: "gh-activecollab", repositoryId: "ac-feather", eventKind: "issue", status: "duplicate", deliveryIdentifier: "fa3ebd6742c360b2d9652b7f78d9bd7d", outcomes: [], payload: PAYLOAD_ISSUE_OPENED, receivedAt: at(2026, 7, 10, 9, 5) }, // Older, this year — date only { id: "log-aug-8", connectionId: "gh-activecollab", repositoryId: "ac-backend", eventKind: "repository", status: "skipped", deliveryIdentifier: "2c9d0e1f-3a4b-4c5d-8e9f-0a1b2c3d4e5f", outcomes: [{ summary: "Skipped: repository events are not processed" }], receivedAt: at(2026, 7, 8, 16, 42) }, { id: "log-jul-30", connectionId: "gl-acme-ce", repositoryId: "acme-ce", eventKind: "push", status: "failed", deliveryIdentifier: "7d8e9f0a-1b2c-4d3e-9f0a-2b3c4d5e6f70", outcomes: [{ summary: "Failed to match a repository for this push" }], payload: PAYLOAD_PUSH, receivedAt: at(2026, 6, 30, 11, 9) }, { id: "log-jun-12", connectionId: "gh-abstergo", repositoryId: "ab-animus", eventKind: "issue", status: "processed", deliveryIdentifier: "aa11bb22-cc33-4d44-9e55-6f7788990011", outcomes: [{ summary: "Created task #1720 from issue #204", task: { id: 1720, name: "Animus sync drops issue labels", projectName: "Animus", description: "Issue labels are lost when Animus syncs from GitHub. Map the incoming labels onto task labels." } }], payload: PAYLOAD_ISSUE_OPENED, receivedAt: at(2026, 5, 12, 8, 30) }, // Older, earlier year — date with year { id: "log-mar-2025", connectionId: "gl-platform", repositoryId: "pl-gateway", eventKind: "issue", status: "processed", deliveryIdentifier: "bb22cc33-dd44-4e55-9f66-7a8899001122", outcomes: [{ summary: "Created task #1533 from issue #88", task: { id: 1533, name: "Gateway times out on large payloads", projectName: "API Gateway", description: "Large webhook payloads time out at the gateway. Raise the body limit or stream the request." } }], payload: PAYLOAD_ISSUE_OPENED, receivedAt: at(2025, 2, 3, 10, 15) }]; /* --- Lightweight, Prism-style JSON syntax highlighter -------------------- */ /* design-system has no `prismjs` dependency (it's an app dependency), so for */ /* this throwaway mock we tokenize JSON ourselves and colour the tokens with */ /* the same token classes Prism uses, styled via PayloadPre below. */ const escapeHtml = value => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); const highlightJson = json => escapeHtml(json).replace(/("(?:\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(?:true|false)\b|\bnull\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g, match => { let cls = "tok-number"; if (/^"/.test(match)) { cls = /:$/.test(match) ? "tok-key" : "tok-string"; } else if (/true|false/.test(match)) { cls = "tok-boolean"; } else if (/null/.test(match)) { cls = "tok-null"; } return "<span class=\"" + cls + "\">" + match + "</span>"; }); const PAYLOAD_COLLAPSED_LINES = 6; const PayloadPre = styled.pre.withConfig({ displayName: "WebhookLogsDialog__PayloadPre", componentId: "sc-6ol7f6-0" })(["margin:0;padding:12px 14px;border:1px solid var(--color-theme-400);border-radius:6px;background-color:var(--color-theme-200);color:var(--color-theme-800);font-family:\"SFMono-Regular\",Consolas,\"Liberation Mono\",Menlo,monospace;font-size:12px;line-height:18px;overflow-x:auto;white-space:pre;position:relative;", " .tok-key{color:var(--color-coral-lighter);}.tok-string{color:var(--color-green-pale-lighter);}.tok-number{color:var(--color-orange-lighter);}.tok-boolean{color:var(--color-blue-sky-lighter);}.tok-null{color:var(--color-purple-lighter);}"], _ref2 => { let $collapsed = _ref2.$collapsed; return $collapsed ? "max-height: " + (PAYLOAD_COLLAPSED_LINES * 18 + 24) + "px;\n overflow-y: hidden;" : ""; }); const PayloadWrap = styled.div.withConfig({ displayName: "WebhookLogsDialog__PayloadWrap", componentId: "sc-6ol7f6-1" })([".payload-header{display:flex;align-items:center;justify-content:space-between;min-height:24px;margin-bottom:4px;}"]); const PayloadBlock = _ref3 => { let payload = _ref3.payload; const _useState = useState(true), collapsed = _useState[0], setCollapsed = _useState[1]; const lineCount = useMemo(() => payload.split("\n").length, [payload]); const html = useMemo(() => highlightJson(payload), [payload]); const canToggle = lineCount > PAYLOAD_COLLAPSED_LINES; return /*#__PURE__*/React.createElement(PayloadWrap, null, /*#__PURE__*/React.createElement("div", { className: "payload-header" }, /*#__PURE__*/React.createElement(Caption1, { color: "secondary", weight: "bold" }, "Payload"), canToggle ? /*#__PURE__*/React.createElement(Button, { variant: "text colored", size: "small", onClick: () => setCollapsed(value => !value) }, collapsed ? "Show More" : "Show Less") : null), /*#__PURE__*/React.createElement(PayloadPre, { $collapsed: collapsed && canToggle, dangerouslySetInnerHTML: { __html: html } })); }; /* --- A single, expandable log row ---------------------------------------- */ const LogRowWrap = styled.div.withConfig({ displayName: "WebhookLogsDialog__LogRowWrap", componentId: "sc-6ol7f6-2" })(["border-top:1px solid var(--color-theme-300);.lr-summary{box-sizing:border-box;display:flex;align-items:center;gap:12px;width:100%;min-height:56px;padding:8px 8px 8px 12px;background:none;border:0;text-align:left;cursor:pointer;font-family:inherit;&:hover{background-color:var(--color-theme-200);}}.lr-expand{flex-shrink:0;fill:var(--color-theme-600);color:var(--color-theme-600);transform:rotate(180deg);transition:transform 200ms ease;}.lr-expand.open{transform:rotate(0deg);}.lr-conn{display:flex;align-items:center;gap:8px;width:180px;min-width:0;flex-shrink:0;}.lr-conn svg{width:20px;height:20px;flex-shrink:0;}.lr-conn .lr-conn-name{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;}.lr-event{flex:1 1 0%;min-width:0;}.lr-time{width:120px;flex-shrink:0;text-align:right;white-space:nowrap;}.lr-status{width:110px;flex-shrink:0;display:flex;}.lr-outcomes{width:96px;flex-shrink:0;text-align:right;}.lr-detail{padding:4px 16px 20px 44px;display:flex;flex-direction:column;gap:16px;}.lr-field-label{display:block;margin-bottom:4px;}.lr-id{font-family:\"SFMono-Regular\",Consolas,\"Liberation Mono\",Menlo,monospace;word-break:break-all;}.lr-outcome-list{margin:0;padding-left:16px;display:flex;flex-direction:column;gap:6px;}.lr-outcome{display:flex;align-items:baseline;flex-wrap:wrap;gap:4px 8px;}.lr-open-task{padding:0;border:0;background:none;font:inherit;font-size:13px;line-height:18px;color:var(--color-theme-600);text-decoration:underline;cursor:pointer;white-space:nowrap;}.lr-open-task:hover{color:var(--color-theme-800);}"]); const LogRow = _ref4 => { var _connection$name; let log = _ref4.log, connection = _ref4.connection, onOpenTask = _ref4.onOpenTask; const _useState2 = useState(false), open = _useState2[0], setOpen = _useState2[1]; const ConnIcon = connectionIcon(connection == null ? void 0 : connection.service); return /*#__PURE__*/React.createElement(LogRowWrap, null, /*#__PURE__*/React.createElement("button", { type: "button", className: "lr-summary", "aria-expanded": open, onClick: () => setOpen(value => !value) }, /*#__PURE__*/React.createElement(CollapseExpandSingleIcon, { className: open ? "lr-expand open" : "lr-expand" }), /*#__PURE__*/React.createElement("span", { className: "lr-conn" }, /*#__PURE__*/React.createElement(ConnIcon, null), /*#__PURE__*/React.createElement(Body2, { className: "lr-conn-name", weight: "bold" }, (_connection$name = connection == null ? void 0 : connection.name) != null ? _connection$name : "Unknown")), /*#__PURE__*/React.createElement(Body2, { className: "lr-event", color: "tertiary" }, EVENT_KIND_LABELS[log.eventKind]), /*#__PURE__*/React.createElement(Caption1, { className: "lr-time", color: "tertiary" }, formatReceivedAt(log.receivedAt)), /*#__PURE__*/React.createElement("span", { className: "lr-status" }, /*#__PURE__*/React.createElement(StatusChip, { status: log.status })), /*#__PURE__*/React.createElement(Caption1, { className: "lr-outcomes", color: "tertiary" }, log.outcomes.length > 0 ? log.outcomes.length + " " + (log.outcomes.length === 1 ? "outcome" : "outcomes") : "")), open ? /*#__PURE__*/React.createElement("div", { className: "lr-detail" }, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement(Caption1, { className: "lr-field-label", color: "secondary", weight: "bold" }, "Webhook identifier"), /*#__PURE__*/React.createElement(Body2, { className: "lr-id", color: "tertiary" }, log.deliveryIdentifier)), log.outcomes.length > 0 ? /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement(Caption1, { className: "lr-field-label", color: "secondary", weight: "bold" }, "Outcomes"), /*#__PURE__*/React.createElement("ul", { className: "lr-outcome-list" }, log.outcomes.map((outcome, index) => /*#__PURE__*/React.createElement("li", { key: index, className: "lr-outcome" }, /*#__PURE__*/React.createElement(Body2, { color: "tertiary" }, outcome.summary), outcome.task ? /*#__PURE__*/React.createElement(Tooltip, { title: "See the task without closing the log" }, /*#__PURE__*/React.createElement("button", { type: "button", className: "lr-open-task", onClick: () => onOpenTask(outcome.task) }, "Open Task")) : null)))) : null, log.payload ? /*#__PURE__*/React.createElement(PayloadBlock, { payload: log.payload }) : null) : null); }; /* --- Task preview sheet --------------------------------------------------- */ /* Opens the outcome's task in a right-side sheet, on top of the (still open) */ /* log dialog. Uses the same Sheet setup as the Task sheet stories — position */ /* "right", stretch mode, and a Close control — so it reads as the real task */ /* sheet. The body is a representative stand-in for the app's full task sheet. */ const StyledTaskSheet = styled.div.withConfig({ displayName: "WebhookLogsDialog__StyledTaskSheet", componentId: "sc-6ol7f6-3" })(["display:grid;min-height:100%;padding:8px;grid-template-columns:1fr;@media (min-width:", "){grid-template-columns:1fr 328px;}"], LG); const StyledTaskMain = styled.div.withConfig({ displayName: "WebhookLogsDialog__StyledTaskMain", componentId: "sc-6ol7f6-4" })(["padding:24px 24px 0;min-width:0;@media (min-width:", "){grid-column:1;}.task-header{display:flex;align-items:center;gap:8px;min-width:0;margin-bottom:8px;}.task-title{min-width:0;}.metadata{display:flex;flex-wrap:wrap;align-items:center;margin-bottom:24px;}.metadata .seg:not(:last-child){padding-right:12px;margin-right:12px;border-right:1px solid var(--border-primary);}.description{padding-bottom:16px;margin-bottom:16px;border-bottom:1px solid var(--border-primary);}"], LG); const StyledTaskProperties = styled.div.withConfig({ displayName: "WebhookLogsDialog__StyledTaskProperties", componentId: "sc-6ol7f6-5" })(["padding:0 24px;@media (min-width:", "){grid-column:2;padding:24px;background-color:var(--color-theme-200);border-radius:8px;}.field{margin-bottom:24px;}.field:last-child{margin-bottom:0;}.field-label{display:flex;align-items:center;max-width:280px;margin-bottom:4px;}.field-control{width:100%;}.assignee-row{display:flex;align-items:center;gap:8px;}"], LG); const SheetField = _ref5 => { let label = _ref5.label, children = _ref5.children; return /*#__PURE__*/React.createElement("div", { className: "field" }, /*#__PURE__*/React.createElement("div", { className: "field-label" }, /*#__PURE__*/React.createElement(Label, { size: "small" }, label)), children); }; const TaskPreviewSheet = _ref6 => { let task = _ref6.task, open = _ref6.open, onClose = _ref6.onClose; if (!task) { return null; } const controls = [{ tooltip: "Close", disabled: false, onClick: onClose, icon: /*#__PURE__*/React.createElement(CancelCrossIcon, null), className: "" }]; return /*#__PURE__*/React.createElement(Sheet, { open: open, onClose: onClose, controls: controls, position: "right", animation: "right", mode: "stretch" }, /*#__PURE__*/React.createElement(StyledTaskSheet, null, /*#__PURE__*/React.createElement(StyledTaskMain, null, /*#__PURE__*/React.createElement("div", { className: "task-header" }, /*#__PURE__*/React.createElement(CompleteCheckbox, null), /*#__PURE__*/React.createElement(Header2, { className: "task-title" }, "#", task.id, ": ", task.name)), /*#__PURE__*/React.createElement("div", { className: "metadata" }, /*#__PURE__*/React.createElement(Caption1, { className: "seg", color: "secondary" }, task.projectName), /*#__PURE__*/React.createElement(Caption1, { className: "seg", color: "secondary" }, "Task #", task.id), /*#__PURE__*/React.createElement(Caption1, { className: "seg", color: "secondary" }, "Created from a webhook delivery")), /*#__PURE__*/React.createElement("div", { className: "description" }, /*#__PURE__*/React.createElement(Body2, null, task.description))), /*#__PURE__*/React.createElement(StyledTaskProperties, null, /*#__PURE__*/React.createElement(SheetField, { label: "Task List" }, /*#__PURE__*/React.createElement(SelectTrigger, { className: "field-control" }, "Inbox")), /*#__PURE__*/React.createElement(SheetField, { label: "Assignees" }, /*#__PURE__*/React.createElement("div", { className: "assignee-row" }, /*#__PURE__*/React.createElement(RoundAvatar, { $bg: AVATAR_COLORS.purple, $size: 28, $bordered: false }, "IS"), /*#__PURE__*/React.createElement(Body2, null, "Ilija Studen"))), /*#__PURE__*/React.createElement(SheetField, { label: "Start and Due Date" }, /*#__PURE__*/React.createElement(SelectTrigger, { className: "field-control" }, "Not set"))))); }; /* --- The dialog ---------------------------------------------------------- */ /** Wider than the default 540px dialog so the log columns have room. */ const WebhookLogsDialogStyled = styled(Dialog).withConfig({ displayName: "WebhookLogsDialog__WebhookLogsDialogStyled", componentId: "sc-6ol7f6-6" })(["&&{width:calc(100vw - 64px);max-width:760px;}"]); const DialogTitleBar = styled.div.withConfig({ displayName: "WebhookLogsDialog__DialogTitleBar", componentId: "sc-6ol7f6-7" })(["display:flex;align-items:center;justify-content:space-between;gap:16px;.title-actions{display:flex;align-items:center;gap:8px;flex-shrink:0;}"]); const LogListHeader = styled.div.withConfig({ displayName: "WebhookLogsDialog__LogListHeader", componentId: "sc-6ol7f6-8" })(["box-sizing:border-box;display:flex;align-items:center;gap:12px;padding:0 8px 8px 44px;.lh-conn{width:180px;flex-shrink:0;}.lh-event{flex:1 1 0%;min-width:0;}.lh-time{width:120px;flex-shrink:0;text-align:right;}.lh-status{width:110px;flex-shrink:0;text-align:left;}.lh-outcomes{width:96px;flex-shrink:0;text-align:right;}"]); const EmptyLogs = styled.div.withConfig({ displayName: "WebhookLogsDialog__EmptyLogs", componentId: "sc-6ol7f6-9" })(["display:flex;flex-direction:column;align-items:center;gap:8px;padding:40px 0;text-align:center;svg{fill:var(--color-theme-500);}"]); export const WebhookLogsDialog = _ref7 => { let open = _ref7.open, onClose = _ref7.onClose, initialConnectionId = _ref7.initialConnectionId, connectionIds = _ref7.connectionIds, repositoryIds = _ref7.repositoryIds; const _useState3 = useState(() => { const initial = {}; if (connectionIds && connectionIds.length > 0) { initial.connection = connectionIds; } else if (initialConnectionId) { initial.connection = [initialConnectionId]; } if (repositoryIds && repositoryIds.length > 0) { initial.repository = repositoryIds; } return initial; }), filters = _useState3[0], setFilters = _useState3[1]; // The task opened from an outcome's "Open Task" link. The dialog stays open // underneath; the sheet layers on top. `lastTask` keeps the content mounted // through the sheet's slide-out so it does not blank out mid-animation. const _useState4 = useState(null), activeTask = _useState4[0], setActiveTask = _useState4[1]; const lastTaskRef = useRef(null); if (activeTask) { lastTaskRef.current = activeTask; } const connectionsById = useMemo(() => { const map = {}; WEBHOOK_CONNECTIONS.forEach(connection => { map[connection.id] = connection; }); return map; }, []); const filterData = useMemo(() => { var _filters$connection; // The Repository options narrow to the connection(s) currently selected in // the Connection filter; with no connection selected, every repository is // offered. Displayed as "path (connection name)" (matching the project // "..." menu Repositories list) and sorted by repository path. const selectedConnections = (_filters$connection = filters.connection) != null ? _filters$connection : []; const repositorySubmenu = WEBHOOK_REPOSITORIES.filter(repository => selectedConnections.length === 0 || selectedConnections.includes(repository.connectionId)).slice().sort((a, b) => a.path.localeCompare(b.path)).map(repository => { var _connectionsById$repo, _connectionsById$repo2; return { id: repository.id, name: repository.path + " (" + ((_connectionsById$repo = (_connectionsById$repo2 = connectionsById[repository.connectionId]) == null ? void 0 : _connectionsById$repo2.name) != null ? _connectionsById$repo : "Unknown") + ")" }; }); return [{ id: "connection", title: "Connection", submenu: WEBHOOK_CONNECTIONS.map(connection => ({ id: connection.id, name: connection.name })) }, { id: "repository", title: "Repository", searchPlaceholder: "Search repositories", disableInternalSort: true, submenu: repositorySubmenu }, { id: "event_type", title: "Event Type", submenu: EVENT_TYPE_OPTIONS.map(_ref8 => { let id = _ref8.id, label = _ref8.label; return { id, name: label }; }) }, { id: "status", title: "Processing Status", submenu: Object.keys(STATUS_LABELS).map(status => ({ id: status, name: STATUS_LABELS[status] })) }]; }, [filters.connection, connectionsById]); const visibleLogs = useMemo(() => { var _filters$connection2, _filters$repository, _filters$event_type, _filters$status; const byConnection = (_filters$connection2 = filters.connection) != null ? _filters$connection2 : []; const byRepository = (_filters$repository = filters.repository) != null ? _filters$repository : []; const byEvent = (_filters$event_type = filters.event_type) != null ? _filters$event_type : []; const byStatus = (_filters$status = filters.status) != null ? _filters$status : []; return WEBHOOK_LOGS.filter(log => { if (byConnection.length && !byConnection.includes(log.connectionId)) { return false; } if (byRepository.length && !byRepository.includes(log.repositoryId)) { return false; } if (byEvent.length && !byEvent.includes(log.eventKind)) { return false; } if (byStatus.length && !byStatus.includes(log.status)) { return false; } return true; }).sort((a, b) => b.receivedAt - a.receivedAt); }, [filters]); // When the Connection filter narrows, drop any selected repositories that no // longer belong to a selected connection — otherwise a stale repository // selection would silently filter every log out. const handleFiltersChange = useCallback(next => { var _next$connection, _next$repository; const selectedConnections = (_next$connection = next.connection) != null ? _next$connection : []; const selectedRepositories = (_next$repository = next.repository) != null ? _next$repository : []; if (selectedConnections.length && selectedRepositories.length) { const allowedRepositoryIds = new Set(WEBHOOK_REPOSITORIES.filter(repository => selectedConnections.includes(repository.connectionId)).map(repository => repository.id)); const prunedRepositories = selectedRepositories.filter(id => allowedRepositoryIds.has(id)); if (prunedRepositories.length !== selectedRepositories.length) { const updated = _extends({}, next); if (prunedRepositories.length) { updated.repository = prunedRepositories; } else { delete updated.repository; } setFilters(updated); return; } } setFilters(next); }, []); return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(WebhookLogsDialogStyled, { open: open, onClose: onClose, enableBackgroundClick: true }, /*#__PURE__*/React.createElement(Dialog.Title, { disableDefaultHeading: true }, /*#__PURE__*/React.createElement(DialogTitleBar, null, /*#__PURE__*/React.createElement(Header3, null, "Webhook Logs"), /*#__PURE__*/React.createElement("div", { className: "title-actions" }, /*#__PURE__*/React.createElement(Filter, { data: filterData, selected: filters, label: "Filter", clearAllText: "Clear All", noResultText: "No results", emptyFilterText: "No filters to show", onChange: handleFiltersChange }), /*#__PURE__*/React.createElement(IconButton, { variant: "text gray", onClick: onClose }, /*#__PURE__*/React.createElement(CancelCrossIcon, null))))), /*#__PURE__*/React.createElement(Dialog.ContentDivider, null), /*#__PURE__*/React.createElement(Dialog.Content, null, visibleLogs.length > 0 ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(LogListHeader, null, /*#__PURE__*/React.createElement(Caption2, { className: "lh-conn", color: "secondary", weight: "bold" }, "CONNECTION"), /*#__PURE__*/React.createElement(Caption2, { className: "lh-event", color: "secondary", weight: "bold" }, "EVENT TYPE"), /*#__PURE__*/React.createElement(Caption2, { className: "lh-time", color: "secondary", weight: "bold" }, "TIME"), /*#__PURE__*/React.createElement(Caption2, { className: "lh-status", color: "secondary", weight: "bold" }, "STATUS"), /*#__PURE__*/React.createElement(Caption2, { className: "lh-outcomes", color: "secondary", weight: "bold" }, "OUTCOMES")), visibleLogs.map(log => /*#__PURE__*/React.createElement(LogRow, { key: log.id, log: log, connection: connectionsById[log.connectionId], onOpenTask: setActiveTask }))) : /*#__PURE__*/React.createElement(EmptyLogs, null, /*#__PURE__*/React.createElement(AccessLogIcon, null), /*#__PURE__*/React.createElement(Body2, { color: "tertiary" }, "No webhook logs match the selected filters.")))), /*#__PURE__*/React.createElement(TaskPreviewSheet, { task: activeTask != null ? activeTask : lastTaskRef.current, open: Boolean(activeTask), onClose: () => setActiveTask(null) })); }; //# sourceMappingURL=WebhookLogsDialog.js.map