@amiable-dev/docusaurus-plugin-stentorosaur 0.19.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/context/StatusDataProvider.d.ts +101 -0
- package/lib/context/StatusDataProvider.d.ts.map +1 -0
- package/lib/context/StatusDataProvider.js +266 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +128 -1
- package/lib/options.d.ts.map +1 -1
- package/lib/options.js +3 -0
- package/lib/theme/StatusBadge/index.d.ts +54 -0
- package/lib/theme/StatusBadge/index.d.ts.map +1 -0
- package/lib/theme/StatusBadge/index.js +63 -0
- package/lib/theme/StatusBadge/styles.module.css +120 -0
- package/lib/theme/SystemCard/index.d.ts +98 -0
- package/lib/theme/SystemCard/index.d.ts.map +1 -0
- package/lib/theme/SystemCard/index.js +125 -0
- package/lib/theme/SystemCard/styles.module.css +208 -0
- package/lib/theme/SystemCardGroup/index.d.ts +47 -0
- package/lib/theme/SystemCardGroup/index.d.ts.map +1 -0
- package/lib/theme/SystemCardGroup/index.js +113 -0
- package/lib/theme/SystemCardGroup/styles.module.css +180 -0
- package/lib/theme/UptimeBar/index.d.ts +69 -0
- package/lib/theme/UptimeBar/index.d.ts.map +1 -0
- package/lib/theme/UptimeBar/index.js +177 -0
- package/lib/theme/UptimeBar/styles.module.css +217 -0
- package/lib/types.d.ts +20 -0
- package/lib/types.d.ts.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
- package/scripts/config.js +148 -13
- package/templates/Makefile.status +40 -11
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.SystemCardGroup = SystemCardGroup;
|
|
7
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
8
|
+
/**
|
|
9
|
+
* Copyright (c) Your Organization
|
|
10
|
+
*
|
|
11
|
+
* This source code is licensed under the MIT license found in the
|
|
12
|
+
* LICENSE file in the root directory of this source tree.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* ADR-004: SystemCardGroup Component
|
|
16
|
+
*
|
|
17
|
+
* Container for grouping multiple SystemCards with collapsible behavior.
|
|
18
|
+
* Shows aggregate status (worst-of) for the group.
|
|
19
|
+
*
|
|
20
|
+
* @see docs/adrs/ADR-004-simplified-status-card-ux.md
|
|
21
|
+
*/
|
|
22
|
+
const react_1 = require("react");
|
|
23
|
+
const styles_module_css_1 = __importDefault(require("./styles.module.css"));
|
|
24
|
+
/**
|
|
25
|
+
* Status priority for aggregation (higher = worse)
|
|
26
|
+
*/
|
|
27
|
+
const STATUS_PRIORITY = {
|
|
28
|
+
up: 0,
|
|
29
|
+
maintenance: 1,
|
|
30
|
+
degraded: 2,
|
|
31
|
+
down: 3,
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Status labels for group header
|
|
35
|
+
*/
|
|
36
|
+
const GROUP_STATUS_LABELS = {
|
|
37
|
+
up: 'All Operational',
|
|
38
|
+
degraded: 'Partial Issues',
|
|
39
|
+
down: 'Service Outage',
|
|
40
|
+
maintenance: 'Maintenance',
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Status class mapping
|
|
44
|
+
*/
|
|
45
|
+
const GROUP_STATUS_CLASS_MAP = {
|
|
46
|
+
up: styles_module_css_1.default.groupStatusUp,
|
|
47
|
+
degraded: styles_module_css_1.default.groupStatusDegraded,
|
|
48
|
+
down: styles_module_css_1.default.groupStatusDown,
|
|
49
|
+
maintenance: styles_module_css_1.default.groupStatusMaintenance,
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Extract status from SystemCard children
|
|
53
|
+
*/
|
|
54
|
+
function getWorstStatus(children) {
|
|
55
|
+
let worstStatus = 'up';
|
|
56
|
+
let worstPriority = STATUS_PRIORITY.up;
|
|
57
|
+
react_1.Children.forEach(children, (child) => {
|
|
58
|
+
if ((0, react_1.isValidElement)(child) && child.props.status) {
|
|
59
|
+
const childStatus = child.props.status;
|
|
60
|
+
const priority = STATUS_PRIORITY[childStatus] ?? 0;
|
|
61
|
+
if (priority > worstPriority) {
|
|
62
|
+
worstPriority = priority;
|
|
63
|
+
worstStatus = childStatus;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
return worstStatus;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Count valid children
|
|
71
|
+
*/
|
|
72
|
+
function countChildren(children) {
|
|
73
|
+
let count = 0;
|
|
74
|
+
react_1.Children.forEach(children, (child) => {
|
|
75
|
+
if ((0, react_1.isValidElement)(child)) {
|
|
76
|
+
count++;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
return count;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* SystemCardGroup Component
|
|
83
|
+
*
|
|
84
|
+
* Groups multiple SystemCards together with a collapsible header.
|
|
85
|
+
* Displays aggregate status based on worst child status.
|
|
86
|
+
*/
|
|
87
|
+
function SystemCardGroup({ name, displayName, defaultCollapsed = false, status: explicitStatus, headingLevel = 2, children, className, }) {
|
|
88
|
+
const [collapsed, setCollapsed] = (0, react_1.useState)(defaultCollapsed);
|
|
89
|
+
// Derive status from children or use explicit override
|
|
90
|
+
const aggregatedStatus = (0, react_1.useMemo)(() => {
|
|
91
|
+
if (explicitStatus)
|
|
92
|
+
return explicitStatus;
|
|
93
|
+
return getWorstStatus(children);
|
|
94
|
+
}, [explicitStatus, children]);
|
|
95
|
+
// Count children
|
|
96
|
+
const childCount = (0, react_1.useMemo)(() => countChildren(children), [children]);
|
|
97
|
+
const handleToggle = (0, react_1.useCallback)(() => {
|
|
98
|
+
setCollapsed((prev) => !prev);
|
|
99
|
+
}, []);
|
|
100
|
+
const handleKeyDown = (0, react_1.useCallback)((event) => {
|
|
101
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
102
|
+
event.preventDefault();
|
|
103
|
+
handleToggle();
|
|
104
|
+
}
|
|
105
|
+
}, [handleToggle]);
|
|
106
|
+
const Heading = `h${headingLevel}`;
|
|
107
|
+
const statusLabel = GROUP_STATUS_LABELS[aggregatedStatus];
|
|
108
|
+
const statusClass = GROUP_STATUS_CLASS_MAP[aggregatedStatus];
|
|
109
|
+
const serviceWord = childCount === 1 ? 'service' : 'services';
|
|
110
|
+
return ((0, jsx_runtime_1.jsxs)("section", { className: `${styles_module_css_1.default.systemCardGroup} ${collapsed ? styles_module_css_1.default.collapsed : ''} ${statusClass} ${className || ''}`, "data-group": name, children: [(0, jsx_runtime_1.jsxs)("div", { className: styles_module_css_1.default.groupHeader, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: styles_module_css_1.default.toggleButton, onClick: handleToggle, onKeyDown: handleKeyDown, "aria-expanded": !collapsed, "aria-label": `Toggle ${displayName} group`, children: (0, jsx_runtime_1.jsx)("span", { className: `${styles_module_css_1.default.expandIcon} ${!collapsed ? styles_module_css_1.default.expandIconOpen : ''}`, children: "\u25BC" }) }), (0, jsx_runtime_1.jsxs)("div", { className: styles_module_css_1.default.groupInfo, children: [(0, jsx_runtime_1.jsx)(Heading, { className: styles_module_css_1.default.groupTitle, children: displayName }), (0, jsx_runtime_1.jsxs)("span", { className: styles_module_css_1.default.groupMeta, children: [(0, jsx_runtime_1.jsxs)("span", { className: styles_module_css_1.default.serviceCount, children: [childCount, " ", serviceWord] }), (0, jsx_runtime_1.jsx)("span", { className: styles_module_css_1.default.statusDot }), (0, jsx_runtime_1.jsx)("span", { className: styles_module_css_1.default.statusLabel, children: statusLabel })] })] })] }), (0, jsx_runtime_1.jsx)("div", { className: `${styles_module_css_1.default.groupContent} ${collapsed ? styles_module_css_1.default.contentHidden : ''}`, "aria-hidden": collapsed, children: children })] }));
|
|
111
|
+
}
|
|
112
|
+
// Default export
|
|
113
|
+
exports.default = SystemCardGroup;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-004: SystemCardGroup Styles
|
|
3
|
+
*
|
|
4
|
+
* Container for grouping multiple SystemCards.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/* Group container */
|
|
8
|
+
.systemCardGroup {
|
|
9
|
+
margin-bottom: 1.5rem;
|
|
10
|
+
border: 1px solid var(--ifm-color-emphasis-200);
|
|
11
|
+
border-radius: 8px;
|
|
12
|
+
overflow: hidden;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* Group header */
|
|
16
|
+
.groupHeader {
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
gap: 0.75rem;
|
|
20
|
+
padding: 0.75rem 1rem;
|
|
21
|
+
background: var(--ifm-color-emphasis-100);
|
|
22
|
+
border-bottom: 1px solid var(--ifm-color-emphasis-200);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.collapsed .groupHeader {
|
|
26
|
+
border-bottom: none;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* Toggle button */
|
|
30
|
+
.toggleButton {
|
|
31
|
+
display: flex;
|
|
32
|
+
align-items: center;
|
|
33
|
+
justify-content: center;
|
|
34
|
+
width: 28px;
|
|
35
|
+
height: 28px;
|
|
36
|
+
background: var(--ifm-color-emphasis-200);
|
|
37
|
+
border: none;
|
|
38
|
+
border-radius: 4px;
|
|
39
|
+
cursor: pointer;
|
|
40
|
+
transition: background-color 0.15s ease;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.toggleButton:hover {
|
|
44
|
+
background: var(--ifm-color-emphasis-300);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.toggleButton:focus-visible {
|
|
48
|
+
outline: 2px solid var(--ifm-color-primary);
|
|
49
|
+
outline-offset: 2px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.expandIcon {
|
|
53
|
+
font-size: 0.625rem;
|
|
54
|
+
color: var(--ifm-color-content-secondary);
|
|
55
|
+
transition: transform 0.2s ease;
|
|
56
|
+
transform: rotate(-90deg);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.expandIconOpen {
|
|
60
|
+
transform: rotate(0deg);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* Group info */
|
|
64
|
+
.groupInfo {
|
|
65
|
+
display: flex;
|
|
66
|
+
flex-direction: column;
|
|
67
|
+
gap: 0.125rem;
|
|
68
|
+
flex: 1;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.groupTitle {
|
|
72
|
+
margin: 0;
|
|
73
|
+
font-size: 1rem;
|
|
74
|
+
font-weight: 600;
|
|
75
|
+
color: var(--ifm-color-content);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.groupMeta {
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
gap: 0.5rem;
|
|
82
|
+
font-size: 0.75rem;
|
|
83
|
+
color: var(--ifm-color-content-secondary);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.serviceCount {
|
|
87
|
+
color: var(--ifm-color-content-secondary);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.statusDot {
|
|
91
|
+
width: 6px;
|
|
92
|
+
height: 6px;
|
|
93
|
+
border-radius: 50%;
|
|
94
|
+
background: var(--status-operational, #22c55e);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.groupStatusDegraded .statusDot {
|
|
98
|
+
background: var(--status-degraded, #eab308);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.groupStatusDown .statusDot {
|
|
102
|
+
background: var(--status-outage, #ef4444);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.groupStatusMaintenance .statusDot {
|
|
106
|
+
background: var(--status-maintenance, #3b82f6);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.statusLabel {
|
|
110
|
+
font-weight: 500;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* Status colors for labels */
|
|
114
|
+
.groupStatusUp .statusLabel {
|
|
115
|
+
color: var(--status-operational, #22c55e);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.groupStatusDegraded .statusLabel {
|
|
119
|
+
color: var(--status-degraded, #eab308);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.groupStatusDown .statusLabel {
|
|
123
|
+
color: var(--status-outage, #ef4444);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.groupStatusMaintenance .statusLabel {
|
|
127
|
+
color: var(--status-maintenance, #3b82f6);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* Group content */
|
|
131
|
+
.groupContent {
|
|
132
|
+
display: flex;
|
|
133
|
+
flex-direction: column;
|
|
134
|
+
gap: 0.75rem;
|
|
135
|
+
padding: 1rem;
|
|
136
|
+
transition: max-height 0.3s ease, opacity 0.2s ease, padding 0.3s ease;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.contentHidden {
|
|
140
|
+
max-height: 0;
|
|
141
|
+
opacity: 0;
|
|
142
|
+
padding: 0 1rem;
|
|
143
|
+
overflow: hidden;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* Dark mode */
|
|
147
|
+
[data-theme='dark'] .groupHeader {
|
|
148
|
+
background: var(--ifm-color-emphasis-200);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
[data-theme='dark'] .toggleButton {
|
|
152
|
+
background: var(--ifm-color-emphasis-300);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
[data-theme='dark'] .toggleButton:hover {
|
|
156
|
+
background: var(--ifm-color-emphasis-400);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* Reduced motion */
|
|
160
|
+
@media (prefers-reduced-motion: reduce) {
|
|
161
|
+
.expandIcon,
|
|
162
|
+
.groupContent {
|
|
163
|
+
transition: none;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.contentHidden {
|
|
167
|
+
display: none;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/* Mobile responsiveness */
|
|
172
|
+
@media (max-width: 600px) {
|
|
173
|
+
.groupHeader {
|
|
174
|
+
padding: 0.5rem 0.75rem;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.groupContent {
|
|
178
|
+
padding: 0.75rem;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Your Organization
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* ADR-004: UptimeBar Component
|
|
9
|
+
*
|
|
10
|
+
* 90-day horizontal uptime bar visualization.
|
|
11
|
+
* Displays daily uptime status as colored cells.
|
|
12
|
+
*
|
|
13
|
+
* @see docs/adrs/ADR-004-simplified-status-card-ux.md
|
|
14
|
+
*/
|
|
15
|
+
import React from 'react';
|
|
16
|
+
import { type DayStatus } from '../../context/StatusDataProvider';
|
|
17
|
+
/**
|
|
18
|
+
* Custom thresholds for status determination
|
|
19
|
+
*/
|
|
20
|
+
export interface UptimeThresholds {
|
|
21
|
+
/** Uptime >= this is operational (default: 99) */
|
|
22
|
+
operational: number;
|
|
23
|
+
/** Uptime >= this is degraded, < is outage (default: 95) */
|
|
24
|
+
degraded: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Props for UptimeBar component
|
|
28
|
+
*/
|
|
29
|
+
export interface UptimeBarProps {
|
|
30
|
+
/** Service name for data lookup (uses context) */
|
|
31
|
+
serviceName: string;
|
|
32
|
+
/** Override data for testing/Storybook */
|
|
33
|
+
data?: DayStatus[];
|
|
34
|
+
/** Number of days to display (default: 90) */
|
|
35
|
+
days?: number;
|
|
36
|
+
/** Height of the bar in pixels (default: 34) */
|
|
37
|
+
height?: number;
|
|
38
|
+
/** Gap between bars in pixels (default: 2) */
|
|
39
|
+
gap?: number;
|
|
40
|
+
/** Show uptime percentage text (default: true) */
|
|
41
|
+
showPercentage?: boolean;
|
|
42
|
+
/** Show date labels (default: true) */
|
|
43
|
+
showDateLabels?: boolean;
|
|
44
|
+
/** Customizable thresholds */
|
|
45
|
+
thresholds?: UptimeThresholds;
|
|
46
|
+
/** Loading state */
|
|
47
|
+
loading?: boolean;
|
|
48
|
+
/** Error state */
|
|
49
|
+
error?: Error | null;
|
|
50
|
+
/** Retry callback */
|
|
51
|
+
onRetry?: () => void;
|
|
52
|
+
/** Accessibility label */
|
|
53
|
+
ariaLabel?: string;
|
|
54
|
+
/** Interaction callbacks */
|
|
55
|
+
onDayClick?: (date: string, status: DayStatus) => void;
|
|
56
|
+
onDayHover?: (date: string, status: DayStatus | null) => void;
|
|
57
|
+
/** Optional additional className */
|
|
58
|
+
className?: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* UptimeBar Component
|
|
62
|
+
*
|
|
63
|
+
* Displays a horizontal bar of daily uptime status cells.
|
|
64
|
+
* Each cell represents one day, colored by uptime percentage.
|
|
65
|
+
*/
|
|
66
|
+
export declare function UptimeBar({ serviceName, data: providedData, days, height, gap, showPercentage, showDateLabels, thresholds, loading, error, onRetry, ariaLabel, onDayClick, onDayHover, className, }: UptimeBarProps): React.ReactElement;
|
|
67
|
+
export default UptimeBar;
|
|
68
|
+
export type { DayStatus };
|
|
69
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/theme/UptimeBar/index.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;;;;GAOG;AAEH,OAAO,KAAqE,MAAM,OAAO,CAAC;AAC1F,OAAO,EAAiB,KAAK,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAGjF;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,kDAAkD;IAClD,WAAW,EAAE,MAAM,CAAC;IACpB,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,kDAAkD;IAClD,WAAW,EAAE,MAAM,CAAC;IACpB,0CAA0C;IAC1C,IAAI,CAAC,EAAE,SAAS,EAAE,CAAC;IACnB,8CAA8C;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8CAA8C;IAC9C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,uCAAuC;IACvC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,8BAA8B;IAC9B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,oBAAoB;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kBAAkB;IAClB,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC;IACrB,qBAAqB;IACrB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,0BAA0B;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4BAA4B;IAC5B,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,KAAK,IAAI,CAAC;IACvD,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,IAAI,KAAK,IAAI,CAAC;IAC9D,oCAAoC;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAqED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,EACxB,WAAW,EACX,IAAI,EAAE,YAAY,EAClB,IAAS,EACT,MAAW,EACX,GAAO,EACP,cAAqB,EACrB,cAAqB,EACrB,UAA+B,EAC/B,OAAe,EACf,KAAK,EACL,OAAO,EACP,SAAS,EACT,UAAU,EACV,UAAU,EACV,SAAS,GACV,EAAE,cAAc,GAAG,KAAK,CAAC,YAAY,CA8LrC;AAGD,eAAe,SAAS,CAAC;AAGzB,YAAY,EAAE,SAAS,EAAE,CAAC"}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.UptimeBar = UptimeBar;
|
|
7
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
8
|
+
/**
|
|
9
|
+
* Copyright (c) Your Organization
|
|
10
|
+
*
|
|
11
|
+
* This source code is licensed under the MIT license found in the
|
|
12
|
+
* LICENSE file in the root directory of this source tree.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* ADR-004: UptimeBar Component
|
|
16
|
+
*
|
|
17
|
+
* 90-day horizontal uptime bar visualization.
|
|
18
|
+
* Displays daily uptime status as colored cells.
|
|
19
|
+
*
|
|
20
|
+
* @see docs/adrs/ADR-004-simplified-status-card-ux.md
|
|
21
|
+
*/
|
|
22
|
+
const react_1 = require("react");
|
|
23
|
+
const StatusDataProvider_1 = require("../../context/StatusDataProvider");
|
|
24
|
+
const styles_module_css_1 = __importDefault(require("./styles.module.css"));
|
|
25
|
+
/**
|
|
26
|
+
* Default thresholds per ADR-004
|
|
27
|
+
*/
|
|
28
|
+
const DEFAULT_THRESHOLDS = {
|
|
29
|
+
operational: 99,
|
|
30
|
+
degraded: 95,
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Get status class based on uptime and thresholds
|
|
34
|
+
*/
|
|
35
|
+
function getStatusClass(uptimePercent, thresholds, originalStatus) {
|
|
36
|
+
// If original status is no-data, keep it
|
|
37
|
+
if (originalStatus === 'no-data') {
|
|
38
|
+
return styles_module_css_1.default.statusNoData;
|
|
39
|
+
}
|
|
40
|
+
// Apply threshold-based status
|
|
41
|
+
if (uptimePercent >= thresholds.operational) {
|
|
42
|
+
return styles_module_css_1.default.statusOperational;
|
|
43
|
+
}
|
|
44
|
+
if (uptimePercent >= thresholds.degraded) {
|
|
45
|
+
return styles_module_css_1.default.statusDegraded;
|
|
46
|
+
}
|
|
47
|
+
return styles_module_css_1.default.statusOutage;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Calculate average uptime percentage
|
|
51
|
+
*/
|
|
52
|
+
function calculateAverageUptime(days) {
|
|
53
|
+
if (days.length === 0)
|
|
54
|
+
return 0;
|
|
55
|
+
const sum = days.reduce((acc, day) => acc + day.uptimePercent, 0);
|
|
56
|
+
return sum / days.length;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Generate summary for screen readers
|
|
60
|
+
*/
|
|
61
|
+
function generateAriaLabel(serviceName, days, averageUptime) {
|
|
62
|
+
const operationalDays = days.filter((d) => d.status === 'operational').length;
|
|
63
|
+
const degradedDays = days.filter((d) => d.status === 'degraded').length;
|
|
64
|
+
const outageDays = days.filter((d) => d.status === 'outage').length;
|
|
65
|
+
let summary = `${days.length}-day uptime for ${serviceName}: ${averageUptime.toFixed(1)}%.`;
|
|
66
|
+
if (operationalDays > 0) {
|
|
67
|
+
summary += ` ${operationalDays} days operational.`;
|
|
68
|
+
}
|
|
69
|
+
if (degradedDays > 0) {
|
|
70
|
+
summary += ` ${degradedDays} days degraded.`;
|
|
71
|
+
}
|
|
72
|
+
if (outageDays > 0) {
|
|
73
|
+
summary += ` ${outageDays} days with outage.`;
|
|
74
|
+
}
|
|
75
|
+
return summary;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* UptimeBar Component
|
|
79
|
+
*
|
|
80
|
+
* Displays a horizontal bar of daily uptime status cells.
|
|
81
|
+
* Each cell represents one day, colored by uptime percentage.
|
|
82
|
+
*/
|
|
83
|
+
function UptimeBar({ serviceName, data: providedData, days = 90, height = 34, gap = 2, showPercentage = true, showDateLabels = true, thresholds = DEFAULT_THRESHOLDS, loading = false, error, onRetry, ariaLabel, onDayClick, onDayHover, className, }) {
|
|
84
|
+
// Get data from context or use provided data
|
|
85
|
+
const { getMerged90Days } = (0, StatusDataProvider_1.useStatusData)();
|
|
86
|
+
// Roving tabindex state - track which day has focus
|
|
87
|
+
const [focusedIndex, setFocusedIndex] = (0, react_1.useState)(0);
|
|
88
|
+
const dayCellRefs = (0, react_1.useRef)([]);
|
|
89
|
+
const dayData = (0, react_1.useMemo)(() => {
|
|
90
|
+
if (providedData) {
|
|
91
|
+
return providedData.slice(0, days);
|
|
92
|
+
}
|
|
93
|
+
return getMerged90Days(serviceName).slice(0, days);
|
|
94
|
+
}, [providedData, serviceName, days, getMerged90Days]);
|
|
95
|
+
// Calculate average uptime
|
|
96
|
+
const averageUptime = (0, react_1.useMemo)(() => calculateAverageUptime(dayData), [dayData]);
|
|
97
|
+
// Generate aria label
|
|
98
|
+
const computedAriaLabel = (0, react_1.useMemo)(() => {
|
|
99
|
+
if (ariaLabel)
|
|
100
|
+
return ariaLabel;
|
|
101
|
+
return generateAriaLabel(serviceName, dayData, averageUptime);
|
|
102
|
+
}, [ariaLabel, serviceName, dayData, averageUptime]);
|
|
103
|
+
// Event handlers
|
|
104
|
+
const handleDayClick = (0, react_1.useCallback)((day, index) => {
|
|
105
|
+
setFocusedIndex(index);
|
|
106
|
+
if (onDayClick) {
|
|
107
|
+
onDayClick(day.date, day);
|
|
108
|
+
}
|
|
109
|
+
}, [onDayClick]);
|
|
110
|
+
// Roving tabindex keyboard navigation
|
|
111
|
+
const handleBarKeyDown = (0, react_1.useCallback)((event) => {
|
|
112
|
+
const displayDays = [...dayData].reverse();
|
|
113
|
+
const maxIndex = displayDays.length - 1;
|
|
114
|
+
switch (event.key) {
|
|
115
|
+
case 'ArrowRight':
|
|
116
|
+
event.preventDefault();
|
|
117
|
+
setFocusedIndex((prev) => {
|
|
118
|
+
const newIndex = Math.min(prev + 1, maxIndex);
|
|
119
|
+
dayCellRefs.current[newIndex]?.focus();
|
|
120
|
+
return newIndex;
|
|
121
|
+
});
|
|
122
|
+
break;
|
|
123
|
+
case 'ArrowLeft':
|
|
124
|
+
event.preventDefault();
|
|
125
|
+
setFocusedIndex((prev) => {
|
|
126
|
+
const newIndex = Math.max(prev - 1, 0);
|
|
127
|
+
dayCellRefs.current[newIndex]?.focus();
|
|
128
|
+
return newIndex;
|
|
129
|
+
});
|
|
130
|
+
break;
|
|
131
|
+
case 'Home':
|
|
132
|
+
event.preventDefault();
|
|
133
|
+
setFocusedIndex(0);
|
|
134
|
+
dayCellRefs.current[0]?.focus();
|
|
135
|
+
break;
|
|
136
|
+
case 'End':
|
|
137
|
+
event.preventDefault();
|
|
138
|
+
setFocusedIndex(maxIndex);
|
|
139
|
+
dayCellRefs.current[maxIndex]?.focus();
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}, [dayData]);
|
|
143
|
+
const handleDayKeyDown = (0, react_1.useCallback)((event, day, index) => {
|
|
144
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
145
|
+
event.preventDefault();
|
|
146
|
+
if (onDayClick) {
|
|
147
|
+
onDayClick(day.date, day);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}, [onDayClick]);
|
|
151
|
+
const handleDayHover = (0, react_1.useCallback)((day) => {
|
|
152
|
+
if (onDayHover && day) {
|
|
153
|
+
onDayHover(day.date, day);
|
|
154
|
+
}
|
|
155
|
+
else if (onDayHover && !day) {
|
|
156
|
+
// Pass the last hovered date (or empty string) with null
|
|
157
|
+
onDayHover('', null);
|
|
158
|
+
}
|
|
159
|
+
}, [onDayHover]);
|
|
160
|
+
// Loading state
|
|
161
|
+
if (loading) {
|
|
162
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: `${styles_module_css_1.default.uptimeBarContainer} ${styles_module_css_1.default.loading} ${className || ''}`, children: (0, jsx_runtime_1.jsx)("div", { className: styles_module_css_1.default.skeleton, style: { height: `${height}px` } }) }));
|
|
163
|
+
}
|
|
164
|
+
// Error state
|
|
165
|
+
if (error) {
|
|
166
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: `${styles_module_css_1.default.uptimeBarContainer} ${styles_module_css_1.default.error} ${className || ''}`, children: [(0, jsx_runtime_1.jsx)("span", { className: styles_module_css_1.default.errorMessage, children: "Failed to load uptime data" }), onRetry && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: styles_module_css_1.default.retryButton, onClick: onRetry, "aria-label": "Retry loading uptime data", children: "Retry" }))] }));
|
|
167
|
+
}
|
|
168
|
+
// Empty state
|
|
169
|
+
if (dayData.length === 0) {
|
|
170
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: `${styles_module_css_1.default.uptimeBarContainer} ${styles_module_css_1.default.empty} ${className || ''}`, role: "img", "aria-label": `No uptime data available for ${serviceName}`, children: (0, jsx_runtime_1.jsx)("span", { className: styles_module_css_1.default.emptyMessage, children: "No data available" }) }));
|
|
171
|
+
}
|
|
172
|
+
// Reverse for display (oldest first, newest last)
|
|
173
|
+
const displayDays = [...dayData].reverse();
|
|
174
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: `${styles_module_css_1.default.uptimeBarContainer} ${className || ''}`, children: [showPercentage && ((0, jsx_runtime_1.jsxs)("div", { className: styles_module_css_1.default.uptimePercentage, children: [(0, jsx_runtime_1.jsxs)("span", { className: styles_module_css_1.default.percentageValue, children: [averageUptime.toFixed(2), "%"] }), (0, jsx_runtime_1.jsx)("span", { className: styles_module_css_1.default.percentageLabel, children: "uptime" })] })), (0, jsx_runtime_1.jsx)("div", { className: styles_module_css_1.default.uptimeBar, role: "group", "aria-label": computedAriaLabel, style: { height: `${height}px`, gap: `${gap}px` }, onKeyDown: handleBarKeyDown, children: displayDays.map((day, index) => ((0, jsx_runtime_1.jsx)("button", { ref: (el) => { dayCellRefs.current[index] = el; }, type: "button", className: `${styles_module_css_1.default.dayCell} ${getStatusClass(day.uptimePercent, thresholds, day.status)}`, tabIndex: index === focusedIndex ? 0 : -1, "aria-label": `${day.date}: ${day.uptimePercent.toFixed(1)}% uptime, ${day.incidents} incidents`, onClick: () => handleDayClick(day, index), onKeyDown: (e) => handleDayKeyDown(e, day, index), onMouseEnter: () => handleDayHover(day), onMouseLeave: () => handleDayHover(null), onFocus: () => setFocusedIndex(index) }, day.date))) }), showDateLabels && ((0, jsx_runtime_1.jsxs)("div", { className: styles_module_css_1.default.dateLabels, children: [(0, jsx_runtime_1.jsxs)("span", { className: styles_module_css_1.default.dateLabelStart, children: [days, " days ago"] }), (0, jsx_runtime_1.jsx)("span", { className: styles_module_css_1.default.dateLabelEnd, children: "Today" })] }))] }));
|
|
175
|
+
}
|
|
176
|
+
// Default export for easier imports
|
|
177
|
+
exports.default = UptimeBar;
|