@activecollab/components
Version:
ActiveCollab Components
939 lines (915 loc) • 36.9 kB
JavaScript
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, "&").replace(/</g, "<").replace(/>/g, ">");
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