@triagepilot/ui 1.1.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/LICENSE +105 -0
- package/dist/api.d.ts +155 -0
- package/dist/api.js +1 -0
- package/dist/components/EffectiveConfiguration.d.ts +10 -0
- package/dist/components/EffectiveConfiguration.js +48 -0
- package/dist/components/OperationsDashboard.d.ts +13 -0
- package/dist/components/OperationsDashboard.js +168 -0
- package/dist/components/ReviewerAvailability.d.ts +11 -0
- package/dist/components/ReviewerAvailability.js +158 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/styles.css +146 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Functional Source License, Version 1.1, Apache 2.0 Future License
|
|
2
|
+
|
|
3
|
+
## Abbreviation
|
|
4
|
+
|
|
5
|
+
FSL-1.1-Apache-2.0
|
|
6
|
+
|
|
7
|
+
## Notice
|
|
8
|
+
|
|
9
|
+
Copyright 2026 Miroslav Babjak
|
|
10
|
+
|
|
11
|
+
## Terms and Conditions
|
|
12
|
+
|
|
13
|
+
### Licensor ("We")
|
|
14
|
+
|
|
15
|
+
The party offering the Software under these Terms and Conditions.
|
|
16
|
+
|
|
17
|
+
### The Software
|
|
18
|
+
|
|
19
|
+
The "Software" is each version of the software that we make available under
|
|
20
|
+
these Terms and Conditions, as indicated by our inclusion of these Terms and
|
|
21
|
+
Conditions with the Software.
|
|
22
|
+
|
|
23
|
+
### License Grant
|
|
24
|
+
|
|
25
|
+
Subject to your compliance with this License Grant and the Patents,
|
|
26
|
+
Redistribution and Trademark clauses below, we hereby grant you the right to
|
|
27
|
+
use, copy, modify, create derivative works, publicly perform, publicly display
|
|
28
|
+
and redistribute the Software for any Permitted Purpose identified below.
|
|
29
|
+
|
|
30
|
+
### Permitted Purpose
|
|
31
|
+
|
|
32
|
+
A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
|
|
33
|
+
means making the Software available to others in a commercial product or
|
|
34
|
+
service that:
|
|
35
|
+
|
|
36
|
+
1. substitutes for the Software;
|
|
37
|
+
|
|
38
|
+
2. substitutes for any other product or service we offer using the Software
|
|
39
|
+
that exists as of the date we make the Software available; or
|
|
40
|
+
|
|
41
|
+
3. offers the same or substantially similar functionality as the Software.
|
|
42
|
+
|
|
43
|
+
Permitted Purposes specifically include using the Software:
|
|
44
|
+
|
|
45
|
+
1. for your internal use and access;
|
|
46
|
+
|
|
47
|
+
2. for non-commercial education;
|
|
48
|
+
|
|
49
|
+
3. for non-commercial research; and
|
|
50
|
+
|
|
51
|
+
4. in connection with professional services that you provide to a licensee
|
|
52
|
+
using the Software in accordance with these Terms and Conditions.
|
|
53
|
+
|
|
54
|
+
### Patents
|
|
55
|
+
|
|
56
|
+
To the extent your use for a Permitted Purpose would necessarily infringe our
|
|
57
|
+
patents, the license grant above includes a license under our patents. If you
|
|
58
|
+
make a claim against any party that the Software infringes or contributes to
|
|
59
|
+
the infringement of any patent, then your patent license to the Software ends
|
|
60
|
+
immediately.
|
|
61
|
+
|
|
62
|
+
### Redistribution
|
|
63
|
+
|
|
64
|
+
The Terms and Conditions apply to all copies, modifications and derivatives of
|
|
65
|
+
the Software.
|
|
66
|
+
|
|
67
|
+
If you redistribute any copies, modifications or derivatives of the Software,
|
|
68
|
+
you must include a copy of or a link to these Terms and Conditions and not
|
|
69
|
+
remove any copyright notices provided in or with the Software.
|
|
70
|
+
|
|
71
|
+
### Disclaimer
|
|
72
|
+
|
|
73
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
|
74
|
+
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
|
|
75
|
+
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
|
|
76
|
+
|
|
77
|
+
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
|
|
78
|
+
SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
|
|
79
|
+
EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
|
|
80
|
+
|
|
81
|
+
### Trademarks
|
|
82
|
+
|
|
83
|
+
Except for displaying the License Details and identifying us as the origin of
|
|
84
|
+
the Software, you have no right under these Terms and Conditions to use our
|
|
85
|
+
trademarks, trade names, service marks or product names.
|
|
86
|
+
|
|
87
|
+
## Grant of Future License
|
|
88
|
+
|
|
89
|
+
We hereby irrevocably grant you an additional license to use the Software under
|
|
90
|
+
the Apache License, Version 2.0 that is effective on the second anniversary of
|
|
91
|
+
the date we make the Software available. On or after that date, you may use the
|
|
92
|
+
Software under the Apache License, Version 2.0, in which case the following
|
|
93
|
+
will apply:
|
|
94
|
+
|
|
95
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
96
|
+
this file except in compliance with the License.
|
|
97
|
+
|
|
98
|
+
You may obtain a copy of the License at
|
|
99
|
+
|
|
100
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
101
|
+
|
|
102
|
+
Unless required by applicable law or agreed to in writing, software distributed
|
|
103
|
+
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
104
|
+
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
105
|
+
specific language governing permissions and limitations under the License.
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { ActionStatus, RepositoryMode, ReviewerReplacementOutcome, RiskTier, RoutingAction } from "@triagepilot/contracts";
|
|
2
|
+
export interface WorkspaceContext {
|
|
3
|
+
id: string;
|
|
4
|
+
displayName: string;
|
|
5
|
+
}
|
|
6
|
+
export interface AuthorizationCapabilities {
|
|
7
|
+
canViewOperations: boolean;
|
|
8
|
+
canManageConfiguration: boolean;
|
|
9
|
+
canManageReviewerAvailability: boolean;
|
|
10
|
+
canRunRoutingRecovery: boolean;
|
|
11
|
+
}
|
|
12
|
+
export type NavigationTarget = "configuration";
|
|
13
|
+
export interface NavigationHost {
|
|
14
|
+
hrefFor(target: NavigationTarget): string;
|
|
15
|
+
}
|
|
16
|
+
export interface OperationsApiClient {
|
|
17
|
+
readOperationsOverview(workspace: WorkspaceContext): Promise<OperationsOverview>;
|
|
18
|
+
readEffectiveConfiguration(workspace: WorkspaceContext, repository: RepositoryContext): Promise<EffectiveConfigurationOverview>;
|
|
19
|
+
readAvailabilitySettings(workspace: WorkspaceContext): Promise<AvailabilitySettingsOverview>;
|
|
20
|
+
updateAvailabilityTimezone(workspace: WorkspaceContext, timezone: string): Promise<AvailabilitySettingsOverview>;
|
|
21
|
+
listReviewerAbsences(workspace: WorkspaceContext): Promise<ReviewerAbsenceOverview[]>;
|
|
22
|
+
scheduleReviewerAbsence(workspace: WorkspaceContext, input: ReviewerAbsenceMutation): Promise<ReviewerAbsenceOverview>;
|
|
23
|
+
reviseReviewerAbsence(workspace: WorkspaceContext, absenceId: string, input: ReviewerAbsenceMutation & {
|
|
24
|
+
expectedRevision: number;
|
|
25
|
+
}): Promise<ReviewerAbsenceOverview>;
|
|
26
|
+
cancelReviewerAbsence(workspace: WorkspaceContext, absenceId: string, expectedRevision: number): Promise<ReviewerAbsenceOverview>;
|
|
27
|
+
listReviewerReplacementHistory(workspace: WorkspaceContext, absenceId?: string): Promise<ReviewerReplacementOverview[]>;
|
|
28
|
+
queueRoutingRecovery(workspace: WorkspaceContext, request: {
|
|
29
|
+
decisionId: string;
|
|
30
|
+
} | {
|
|
31
|
+
changeRequestUrl: string;
|
|
32
|
+
}): Promise<{
|
|
33
|
+
jobId: string;
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
36
|
+
export interface AvailabilitySettingsOverview {
|
|
37
|
+
timezone: string;
|
|
38
|
+
updatedAt: string;
|
|
39
|
+
}
|
|
40
|
+
export interface ReviewerAbsenceMutation {
|
|
41
|
+
externalActorId: string;
|
|
42
|
+
startLocal: string;
|
|
43
|
+
endLocal: string;
|
|
44
|
+
startUtcOffset?: string;
|
|
45
|
+
endUtcOffset?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface ReviewerAbsenceOverview {
|
|
48
|
+
id: string;
|
|
49
|
+
externalActorId: string;
|
|
50
|
+
startAt: string;
|
|
51
|
+
endAt: string;
|
|
52
|
+
status: "active" | "upcoming" | "ended" | "cancelled";
|
|
53
|
+
revision: number;
|
|
54
|
+
cancelledAt: string | null;
|
|
55
|
+
createdAt: string;
|
|
56
|
+
updatedAt: string;
|
|
57
|
+
}
|
|
58
|
+
export interface ReviewerReplacementOverview {
|
|
59
|
+
id: string;
|
|
60
|
+
absenceId: string;
|
|
61
|
+
absenceRevision: number;
|
|
62
|
+
decisionId: string;
|
|
63
|
+
unavailableActorId: string;
|
|
64
|
+
replacementActorId: string | null;
|
|
65
|
+
outcome: ReviewerReplacementOutcome;
|
|
66
|
+
reason: string;
|
|
67
|
+
state: "finalizer_pending" | "completed" | "permanent_failure";
|
|
68
|
+
lastError: string | null;
|
|
69
|
+
completedAt: string;
|
|
70
|
+
}
|
|
71
|
+
export interface ProviderLink {
|
|
72
|
+
label: string;
|
|
73
|
+
href: string | null;
|
|
74
|
+
}
|
|
75
|
+
export interface RepositoryContext {
|
|
76
|
+
id: string;
|
|
77
|
+
repository: ProviderLink;
|
|
78
|
+
}
|
|
79
|
+
export interface ProviderStatusOverview {
|
|
80
|
+
id: string;
|
|
81
|
+
label: string;
|
|
82
|
+
value: string;
|
|
83
|
+
detail?: string;
|
|
84
|
+
state?: "neutral" | "healthy" | "failed";
|
|
85
|
+
}
|
|
86
|
+
export interface RepositoryOverview extends RepositoryContext {
|
|
87
|
+
configState: string;
|
|
88
|
+
mode: RepositoryMode;
|
|
89
|
+
}
|
|
90
|
+
export interface DecisionOverview {
|
|
91
|
+
id: string;
|
|
92
|
+
repository: ProviderLink;
|
|
93
|
+
changeRequest: ProviderLink | null;
|
|
94
|
+
mode: RepositoryMode;
|
|
95
|
+
action: RoutingAction;
|
|
96
|
+
actionStatus: ActionStatus;
|
|
97
|
+
actionError: string | null;
|
|
98
|
+
policyCheckState: "not_started" | "in_progress" | "success" | "failure";
|
|
99
|
+
riskScore: number;
|
|
100
|
+
riskBreakdown: RiskBreakdown | null;
|
|
101
|
+
requestedReviewerCount: number | null;
|
|
102
|
+
reviewerShortfall: number | null;
|
|
103
|
+
selectedReviewer: string | null;
|
|
104
|
+
selectedReviewers: string[];
|
|
105
|
+
createdAt: string;
|
|
106
|
+
}
|
|
107
|
+
export interface RiskBreakdown {
|
|
108
|
+
classifierVersion: string;
|
|
109
|
+
tier: RiskTier;
|
|
110
|
+
components: Array<{
|
|
111
|
+
reason: string;
|
|
112
|
+
score: number;
|
|
113
|
+
detail: string;
|
|
114
|
+
}>;
|
|
115
|
+
}
|
|
116
|
+
export interface JobFailureOverview {
|
|
117
|
+
id: string;
|
|
118
|
+
error: string;
|
|
119
|
+
failedAt: string;
|
|
120
|
+
}
|
|
121
|
+
export interface ActionFailureOverview {
|
|
122
|
+
decisionId: string;
|
|
123
|
+
repository: ProviderLink;
|
|
124
|
+
error: string;
|
|
125
|
+
failedAt: string;
|
|
126
|
+
}
|
|
127
|
+
export interface OperationsOverview {
|
|
128
|
+
statuses: ProviderStatusOverview[];
|
|
129
|
+
repositories: RepositoryOverview[];
|
|
130
|
+
decisions: DecisionOverview[];
|
|
131
|
+
failures: {
|
|
132
|
+
jobs: JobFailureOverview[];
|
|
133
|
+
actions: ActionFailureOverview[];
|
|
134
|
+
};
|
|
135
|
+
worker: {
|
|
136
|
+
available: boolean;
|
|
137
|
+
workerId: string | null;
|
|
138
|
+
lastHeartbeatAt: string | null;
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
export interface EffectiveConfigurationOverview {
|
|
142
|
+
repository: ProviderLink;
|
|
143
|
+
trustedPath: string | null;
|
|
144
|
+
trustedRevision: string;
|
|
145
|
+
repositoryRevision: string | null;
|
|
146
|
+
inheritanceMode: "defaults" | "organization" | "replace" | "inherit" | "legacy";
|
|
147
|
+
effectiveHash: string | null;
|
|
148
|
+
values: EffectiveConfigurationValue[];
|
|
149
|
+
}
|
|
150
|
+
export interface EffectiveConfigurationValue {
|
|
151
|
+
path: string;
|
|
152
|
+
label: string;
|
|
153
|
+
value: unknown;
|
|
154
|
+
source: "default" | "organization" | "repository";
|
|
155
|
+
}
|
package/dist/api.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AuthorizationCapabilities, EffectiveConfigurationOverview, NavigationHost, OperationsApiClient, RepositoryContext, WorkspaceContext } from "../api.js";
|
|
2
|
+
export interface EffectiveConfigurationProps {
|
|
3
|
+
api: OperationsApiClient;
|
|
4
|
+
workspace: WorkspaceContext;
|
|
5
|
+
repository: RepositoryContext;
|
|
6
|
+
authorization: AuthorizationCapabilities;
|
|
7
|
+
navigation: NavigationHost;
|
|
8
|
+
initialConfiguration?: EffectiveConfigurationOverview;
|
|
9
|
+
}
|
|
10
|
+
export declare function EffectiveConfiguration({ api, workspace, repository, authorization, navigation, initialConfiguration, }: EffectiveConfigurationProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
export function EffectiveConfiguration({ api, workspace, repository, authorization, navigation, initialConfiguration, }) {
|
|
4
|
+
const [state, setState] = useState(initialConfiguration ? { status: "ready", configuration: initialConfiguration } : { status: "loading" });
|
|
5
|
+
async function loadConfiguration() {
|
|
6
|
+
if (!authorization.canViewOperations) {
|
|
7
|
+
setState({ status: "failed", message: "Configuration is not available for this workspace." });
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
setState({ status: "loading" });
|
|
11
|
+
try {
|
|
12
|
+
setState({ status: "ready", configuration: await api.readEffectiveConfiguration(workspace, repository) });
|
|
13
|
+
}
|
|
14
|
+
catch (caught) {
|
|
15
|
+
setState({ status: "failed", message: messageFrom(caught, "Could not load the effective configuration.") });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (initialConfiguration) {
|
|
20
|
+
setState({ status: "ready", configuration: initialConfiguration });
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
void loadConfiguration();
|
|
24
|
+
}, [api, workspace.id, repository.id, authorization.canViewOperations, initialConfiguration]);
|
|
25
|
+
if (state.status === "loading") {
|
|
26
|
+
return (_jsxs("section", { className: "data-section", role: "status", "aria-live": "polite", children: [_jsx("div", { className: "section-heading", children: _jsx("h2", { children: "Effective configuration" }) }), _jsx("p", { className: "empty-cell", children: "Reading configuration provenance." })] }));
|
|
27
|
+
}
|
|
28
|
+
if (state.status === "failed") {
|
|
29
|
+
return (_jsxs("section", { className: "data-section data-section--danger", "aria-labelledby": "configuration-error-title", children: [_jsx("div", { className: "section-heading", children: _jsx("h2", { id: "configuration-error-title", children: "Effective configuration" }) }), _jsx("p", { role: "alert", className: "notice notice--danger", children: state.message })] }));
|
|
30
|
+
}
|
|
31
|
+
const configuration = state.configuration;
|
|
32
|
+
return (_jsxs("section", { className: "effective-configuration data-section", id: "effective-configuration", "aria-labelledby": "effective-configuration-heading", children: [_jsxs("div", { className: "section-heading", children: [_jsx("h2", { id: "effective-configuration-heading", children: "Effective configuration" }), authorization.canManageConfiguration ? (_jsx("a", { className: "button-link button-link--quiet", href: navigation.hrefFor("configuration"), children: "Edit configuration" })) : null] }), _jsxs("dl", { className: "configuration-provenance", children: [_jsxs("div", { children: [_jsx("dt", { children: "Workspace" }), _jsx("dd", { children: workspace.displayName })] }), _jsxs("div", { children: [_jsx("dt", { children: "Repository" }), _jsx("dd", { children: _jsx(ProviderLinkValue, { link: configuration.repository }) })] }), _jsxs("div", { children: [_jsx("dt", { children: "Trusted path" }), _jsx("dd", { children: configuration.trustedPath ?? "No repository configuration" })] }), _jsxs("div", { children: [_jsx("dt", { children: "Trusted revision" }), _jsx("dd", { children: configuration.repositoryRevision ?? configuration.trustedRevision })] }), _jsxs("div", { children: [_jsx("dt", { children: "Inheritance" }), _jsx("dd", { children: configuration.inheritanceMode })] }), _jsxs("div", { children: [_jsx("dt", { children: "Effective hash" }), _jsx("dd", { children: configuration.effectiveHash ?? "invalid" })] })] }), _jsx("div", { className: "table-scroll", role: "region", tabIndex: 0, "aria-labelledby": "effective-configuration-heading", children: _jsxs("table", { children: [_jsx("caption", { className: "sr-only", children: "Effective configuration values" }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Value" }), _jsx("th", { scope: "col", children: "Setting" }), _jsx("th", { scope: "col", children: "Source" })] }) }), _jsx("tbody", { children: configuration.values.length === 0 ? (_jsx("tr", { children: _jsx("td", { className: "empty-cell", colSpan: 3, children: "No effective configuration values are available." }) })) : (configuration.values.map((entry) => (_jsxs("tr", { children: [_jsx("td", { className: "data-text", children: formatValue(entry.value) }), _jsxs("th", { scope: "row", children: [_jsx("span", { className: "data-text", children: entry.label }), _jsx("span", { className: "cell-detail", children: entry.path })] }), _jsx("td", { children: _jsxs("span", { className: `chip chip--${entry.source}`, children: [entry.source, " source"] }) })] }, entry.path)))) })] }) })] }));
|
|
33
|
+
}
|
|
34
|
+
function ProviderLinkValue({ link }) {
|
|
35
|
+
return link.href === null ? link.label : (_jsx("a", { href: link.href, target: "_blank", rel: "noreferrer", children: link.label }));
|
|
36
|
+
}
|
|
37
|
+
function formatValue(value) {
|
|
38
|
+
if (typeof value === "string")
|
|
39
|
+
return value;
|
|
40
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
41
|
+
return String(value);
|
|
42
|
+
if (value === null)
|
|
43
|
+
return "null";
|
|
44
|
+
return JSON.stringify(value);
|
|
45
|
+
}
|
|
46
|
+
function messageFrom(caught, fallback) {
|
|
47
|
+
return caught instanceof Error ? caught.message : fallback;
|
|
48
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { AuthorizationCapabilities, NavigationHost, OperationsApiClient, OperationsOverview, WorkspaceContext } from "../api.js";
|
|
3
|
+
export interface OperationsDashboardProps {
|
|
4
|
+
api: OperationsApiClient;
|
|
5
|
+
workspace: WorkspaceContext;
|
|
6
|
+
authorization: AuthorizationCapabilities;
|
|
7
|
+
navigation: NavigationHost;
|
|
8
|
+
initialOverview?: OperationsOverview;
|
|
9
|
+
headerActions?: ReactNode;
|
|
10
|
+
onUnauthorized?(message: string): void;
|
|
11
|
+
onOverviewChange?(overview: OperationsOverview): void;
|
|
12
|
+
}
|
|
13
|
+
export declare function OperationsDashboard({ ...props }: OperationsDashboardProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
export function OperationsDashboard({ ...props }) {
|
|
4
|
+
return _jsx(OperationsDashboardWorkspace, { ...props }, props.workspace.id);
|
|
5
|
+
}
|
|
6
|
+
function OperationsDashboardWorkspace({ api, workspace, authorization, navigation, initialOverview, headerActions, onUnauthorized, onOverviewChange, }) {
|
|
7
|
+
const [state, setState] = useState(() => !authorization.canViewOperations
|
|
8
|
+
? { status: "failed", message: "Operations are not available for this workspace." }
|
|
9
|
+
: initialOverview
|
|
10
|
+
? { status: "ready", overview: initialOverview }
|
|
11
|
+
: { status: "loading" });
|
|
12
|
+
const [recoveryUrl, setRecoveryUrl] = useState("");
|
|
13
|
+
const [pendingRecovery, setPendingRecovery] = useState(null);
|
|
14
|
+
const [recoveryNotice, setRecoveryNotice] = useState(null);
|
|
15
|
+
const recoveryInFlight = useRef(false);
|
|
16
|
+
async function loadOverview() {
|
|
17
|
+
if (!authorization.canViewOperations) {
|
|
18
|
+
setState({ status: "failed", message: "Operations are not available for this workspace." });
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
setState({ status: "loading" });
|
|
22
|
+
try {
|
|
23
|
+
const overview = await api.readOperationsOverview(workspace);
|
|
24
|
+
setState({ status: "ready", overview });
|
|
25
|
+
onOverviewChange?.(overview);
|
|
26
|
+
}
|
|
27
|
+
catch (caught) {
|
|
28
|
+
if (isUnauthorized(caught)) {
|
|
29
|
+
onUnauthorized?.(messageFrom(caught, "The operations session has expired."));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
setState({ status: "failed", message: messageFrom(caught, "Could not load the operations overview.") });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!authorization.canViewOperations) {
|
|
37
|
+
setState({ status: "failed", message: "Operations are not available for this workspace." });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (initialOverview) {
|
|
41
|
+
setState({ status: "ready", overview: initialOverview });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
void loadOverview();
|
|
45
|
+
}, [api, workspace.id, authorization.canViewOperations, initialOverview]);
|
|
46
|
+
async function queueRecovery(request, key) {
|
|
47
|
+
if (recoveryInFlight.current || !authorization.canRunRoutingRecovery)
|
|
48
|
+
return;
|
|
49
|
+
recoveryInFlight.current = true;
|
|
50
|
+
setPendingRecovery(key);
|
|
51
|
+
setRecoveryNotice(null);
|
|
52
|
+
try {
|
|
53
|
+
await api.queueRoutingRecovery(workspace, request);
|
|
54
|
+
if ("changeRequestUrl" in request)
|
|
55
|
+
setRecoveryUrl("");
|
|
56
|
+
setRecoveryNotice({
|
|
57
|
+
tone: "success",
|
|
58
|
+
message: "Routing run queued. The new revision will appear after the worker processes it.",
|
|
59
|
+
});
|
|
60
|
+
await loadOverview();
|
|
61
|
+
}
|
|
62
|
+
catch (caught) {
|
|
63
|
+
const message = messageFrom(caught, "Could not queue the routing run.");
|
|
64
|
+
if (isUnauthorized(caught)) {
|
|
65
|
+
onUnauthorized?.(message);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
setRecoveryNotice({ tone: "danger", message });
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
recoveryInFlight.current = false;
|
|
72
|
+
setPendingRecovery(null);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function submitMissingRequest(event) {
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
await queueRecovery({ changeRequestUrl: recoveryUrl }, "missing-request");
|
|
78
|
+
}
|
|
79
|
+
if (state.status === "loading") {
|
|
80
|
+
return (_jsxs("section", { className: "operations-state", role: "status", "aria-live": "polite", children: [headerActions ? _jsx("div", { className: "operations-actions operations-actions--state", children: headerActions }) : null, _jsx("p", { className: "eyebrow", children: workspace.displayName }), _jsx("h1", { children: "Loading operational data" }), _jsx("p", { className: "lede", children: "Reading the current installation state." })] }));
|
|
81
|
+
}
|
|
82
|
+
if (state.status === "failed") {
|
|
83
|
+
return (_jsxs("section", { className: "operations-state", "aria-labelledby": "overview-error-title", children: [headerActions ? _jsx("div", { className: "operations-actions operations-actions--state", children: headerActions }) : null, _jsx("p", { className: "eyebrow", children: "Operations unavailable" }), _jsx("h1", { id: "overview-error-title", children: "The dashboard could not load" }), _jsx("p", { role: "alert", className: "notice notice--danger", children: state.message }), authorization.canViewOperations ? (_jsx("div", { className: "recovery-actions", children: _jsx("button", { type: "button", onClick: () => void loadOverview(), children: "Retry overview" }) })) : null] }));
|
|
84
|
+
}
|
|
85
|
+
const overview = state.overview;
|
|
86
|
+
return (_jsxs("section", { className: "operations-dashboard", id: "overview", children: [_jsxs("div", { className: "operations-heading", children: [_jsxs("div", { children: [_jsx("h1", { id: "dashboard-title", children: "Operations ledger" }), _jsx("p", { className: "lede", children: "Monitor routing health and review the decisions made by this installation." })] }), headerActions || authorization.canManageConfiguration ? (_jsxs("div", { className: "operations-actions", children: [headerActions, authorization.canManageConfiguration ? (_jsx("a", { className: "button-link button-link--quiet", href: navigation.hrefFor("configuration"), children: "Edit configuration" })) : null] })) : null] }), _jsxs("section", { className: "status-ledger", id: "system-health", "aria-label": "Installation status", children: [overview.statuses.map((status) => (_jsx(StatusNode, { label: status.label, value: status.value, ...(status.detail === undefined ? {} : { detail: status.detail }), ...(status.state === undefined ? {} : { state: status.state }) }, status.id))), _jsx(StatusNode, { label: "Worker", value: overview.worker.available ? "Worker available" : "Worker unavailable", detail: overview.worker.lastHeartbeatAt
|
|
87
|
+
? `Last heartbeat ${formatDate(overview.worker.lastHeartbeatAt)}`
|
|
88
|
+
: "No heartbeat recorded", state: overview.worker.available ? "healthy" : "failed" })] }), _jsx(DataSection, { id: "repositories", title: "Connected repositories", count: overview.repositories.length, children: _jsx(TableRegion, { labelledBy: "repositories-heading", children: _jsxs("table", { children: [_jsx("caption", { className: "sr-only", children: "Connected repositories" }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Repository" }), _jsx("th", { scope: "col", children: "Configuration" }), _jsx("th", { scope: "col", children: "Mode" })] }) }), _jsx("tbody", { children: overview.repositories.length === 0 ? (_jsx(EmptyRow, { columns: 3, children: "No repositories are connected to this provider connection." })) : (overview.repositories.map((repository) => (_jsxs("tr", { children: [_jsx("th", { scope: "row", className: "data-text", children: _jsx(ProviderLinkValue, { link: repository.repository }) }), _jsx("td", { children: _jsx(StatusChip, { value: repository.configState }) }), _jsx("td", { children: _jsx(StatusChip, { value: repository.mode }) })] }, repository.id)))) })] }) }) }), _jsxs(DataSection, { id: "decisions", title: "Recent routing decisions", count: overview.decisions.length, children: [authorization.canRunRoutingRecovery ? (_jsxs("form", { className: "routing-recovery-form", onSubmit: (event) => void submitMissingRequest(event), children: [_jsxs("div", { children: [_jsx("label", { htmlFor: "routing-recovery-url", children: "Run missing change request" }), _jsx("span", { className: "cell-detail", children: "Use its provider URL or reference when no routing decision exists yet." })] }), _jsx("input", { id: "routing-recovery-url", type: "url", required: true, placeholder: "https://provider.example/owner/repository/change/123", value: recoveryUrl, onChange: (event) => setRecoveryUrl(event.target.value), disabled: pendingRecovery !== null }), _jsx("button", { type: "submit", disabled: pendingRecovery !== null, children: pendingRecovery === "missing-request" ? "Queueing…" : "Run routing" })] })) : null, recoveryNotice ? (_jsxs("div", { role: recoveryNotice.tone === "danger" ? "alert" : "status", className: `notice notice--${recoveryNotice.tone} routing-notice`, children: [_jsx("span", { children: recoveryNotice.message }), recoveryNotice.tone === "success" ? (_jsx("button", { className: "button--quiet button--compact", type: "button", disabled: pendingRecovery !== null, onClick: () => void loadOverview(), children: "Refresh ledger" })) : null] })) : null, _jsx(TableRegion, { labelledBy: "decisions-heading", children: _jsxs("table", { children: [_jsx("caption", { className: "sr-only", children: "Recent routing decisions" }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Change request" }), _jsx("th", { scope: "col", children: "Risk" }), _jsx("th", { scope: "col", children: "Route" }), _jsx("th", { scope: "col", children: "Human review" }), _jsx("th", { scope: "col", children: "Outcome" }), _jsx("th", { scope: "col", children: "Reviewers" }), _jsx("th", { scope: "col", children: "Recorded" }), authorization.canRunRoutingRecovery ? _jsx("th", { scope: "col", children: "Action" }) : null] }) }), _jsx("tbody", { children: overview.decisions.length === 0 ? (_jsx(EmptyRow, { columns: authorization.canRunRoutingRecovery ? 8 : 7, children: "No routing decisions have been recorded yet." })) : (overview.decisions.map((decision) => (_jsxs("tr", { children: [_jsxs("th", { scope: "row", children: [_jsx("span", { className: "data-text", children: _jsx(ProviderLinkValue, { link: decision.repository }) }), _jsx("span", { className: "cell-detail", children: decision.changeRequest === null ? ("—") : (_jsx(ProviderLinkValue, { link: decision.changeRequest })) })] }), _jsxs("td", { children: [_jsx("span", { className: "data-text", children: decision.riskScore }), _jsx(RiskBreakdown, { breakdown: decision.riskBreakdown })] }), _jsxs("td", { children: [_jsx("span", { className: "data-text", children: labelFor(decision.action) }), _jsx("span", { className: "cell-detail", children: decision.mode })] }), _jsx("td", { children: _jsx(HumanReviewStatusChip, { state: decision.policyCheckState }) }), _jsxs("td", { children: [_jsx(StatusChip, { value: decision.actionStatus }), decision.actionError ? _jsx("span", { className: "cell-error", children: decision.actionError }) : null] }), _jsxs("td", { children: [_jsx("span", { className: "data-text", children: decision.selectedReviewers.join(", ") || "—" }), _jsx(ReviewerRequirement, { decision: decision })] }), _jsx("td", { children: _jsx("time", { dateTime: decision.createdAt, children: formatDate(decision.createdAt) }) }), authorization.canRunRoutingRecovery ? (_jsx("td", { children: decision.changeRequest === null ? (_jsx("span", { className: "cell-detail", children: "\u2014" })) : (_jsx("button", { className: "button--compact", type: "button", disabled: pendingRecovery !== null, onClick: () => void queueRecovery({ decisionId: decision.id }, decision.id), children: pendingRecovery === decision.id ? "Queueing…" : "Re-run routing" })) })) : null] }, decision.id)))) })] }) })] }), _jsxs("div", { className: "failure-grid", children: [_jsx(DataSection, { id: "job-failures", title: "Permanent job failures", count: overview.failures.jobs.length, tone: "danger", children: _jsx(TableRegion, { labelledBy: "job-failures-heading", children: _jsxs("table", { children: [_jsx("caption", { className: "sr-only", children: "Permanent job failures" }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Job" }), _jsx("th", { scope: "col", children: "Error" }), _jsx("th", { scope: "col", children: "Failed" })] }) }), _jsx("tbody", { children: overview.failures.jobs.length === 0 ? (_jsx(EmptyRow, { columns: 3, children: "No permanent job failures." })) : (overview.failures.jobs.map((failure) => (_jsxs("tr", { children: [_jsx("th", { scope: "row", className: "data-text id-cell", children: failure.id }), _jsx("td", { children: failure.error }), _jsx("td", { children: _jsx("time", { dateTime: failure.failedAt, children: formatDate(failure.failedAt) }) })] }, failure.id)))) })] }) }) }), _jsx(DataSection, { id: "action-failures", title: "Action failures", count: overview.failures.actions.length, tone: "danger", children: _jsx(TableRegion, { labelledBy: "action-failures-heading", children: _jsxs("table", { children: [_jsx("caption", { className: "sr-only", children: "Action failures" }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Repository" }), _jsx("th", { scope: "col", children: "Error" }), _jsx("th", { scope: "col", children: "Failed" })] }) }), _jsx("tbody", { children: overview.failures.actions.length === 0 ? (_jsx(EmptyRow, { columns: 3, children: "No action failures." })) : (overview.failures.actions.map((failure) => (_jsxs("tr", { children: [_jsx("th", { scope: "row", className: "data-text", children: _jsx(ProviderLinkValue, { link: failure.repository }) }), _jsx("td", { children: failure.error }), _jsx("td", { children: _jsx("time", { dateTime: failure.failedAt, children: formatDate(failure.failedAt) }) })] }, failure.decisionId)))) })] }) }) })] })] }));
|
|
89
|
+
}
|
|
90
|
+
function ProviderLinkValue({ link }) {
|
|
91
|
+
return link.href === null ? link.label : (_jsx("a", { href: link.href, target: "_blank", rel: "noreferrer", children: link.label }));
|
|
92
|
+
}
|
|
93
|
+
function RiskBreakdown({ breakdown }) {
|
|
94
|
+
if (!breakdown)
|
|
95
|
+
return _jsx("span", { className: "cell-detail", children: "Breakdown unavailable" });
|
|
96
|
+
return (_jsxs("details", { className: "risk-breakdown", children: [_jsx("summary", { children: "Score breakdown" }), _jsxs("p", { className: "risk-breakdown__meta", children: [breakdown.classifierVersion, " \u00B7 ", breakdown.tier] }), _jsx("ul", { children: breakdown.components.map((component, index) => (_jsxs("li", { children: [_jsx("span", { className: "data-text risk-breakdown__score", children: formatComponentScore(component) }), _jsxs("span", { children: [_jsx("strong", { children: labelForRiskComponent(component.reason) }), _jsx("span", { className: "cell-detail", children: component.detail })] })] }, `${component.reason}-${index}`))) })] }));
|
|
97
|
+
}
|
|
98
|
+
function ReviewerRequirement({ decision }) {
|
|
99
|
+
if (decision.requestedReviewerCount === null || decision.requestedReviewerCount <= 0)
|
|
100
|
+
return null;
|
|
101
|
+
const reviewerShortfall = decision.reviewerShortfall ?? Math.max(0, decision.requestedReviewerCount - decision.selectedReviewers.length);
|
|
102
|
+
return (_jsxs("span", { className: "cell-detail", children: [decision.selectedReviewers.length, " of ", decision.requestedReviewerCount, " required", reviewerShortfall > 0 ? ` · shortfall ${reviewerShortfall}` : ""] }));
|
|
103
|
+
}
|
|
104
|
+
function TableRegion({ labelledBy, children }) {
|
|
105
|
+
return (_jsx("div", { className: "table-scroll", role: "region", tabIndex: 0, "aria-labelledby": labelledBy, children: children }));
|
|
106
|
+
}
|
|
107
|
+
function StatusNode({ label, value, detail, state = "neutral", }) {
|
|
108
|
+
return (_jsxs("div", { className: `status-node status-node--${state}`, children: [_jsx("span", { className: "status-label", children: label }), _jsx("strong", { children: value }), detail ? _jsx("small", { children: detail }) : null] }));
|
|
109
|
+
}
|
|
110
|
+
function DataSection({ id, title, count, tone = "neutral", children, }) {
|
|
111
|
+
return (_jsxs("section", { className: `data-section data-section--${tone}`, id: id, children: [_jsxs("div", { className: "section-heading", children: [_jsx("h2", { id: `${id}-heading`, children: title }), _jsx("span", { className: "count", "aria-label": `${count} records`, children: count })] }), children] }));
|
|
112
|
+
}
|
|
113
|
+
function EmptyRow({ columns, children }) {
|
|
114
|
+
return (_jsx("tr", { children: _jsx("td", { className: "empty-cell", colSpan: columns, children: children }) }));
|
|
115
|
+
}
|
|
116
|
+
function StatusChip({ value }) {
|
|
117
|
+
return _jsx("span", { className: `chip chip--${value.replaceAll("_", "-")}`, children: labelFor(value) });
|
|
118
|
+
}
|
|
119
|
+
function HumanReviewStatusChip({ state }) {
|
|
120
|
+
const tone = state === "success" ? "succeeded" : state === "failure" ? "failed" : "neutral";
|
|
121
|
+
return _jsx("span", { className: `chip chip--${tone}`, children: humanReviewLabelFor(state) });
|
|
122
|
+
}
|
|
123
|
+
function humanReviewLabelFor(state) {
|
|
124
|
+
switch (state) {
|
|
125
|
+
case "in_progress":
|
|
126
|
+
return "Waiting for approval";
|
|
127
|
+
case "success":
|
|
128
|
+
return "Approved";
|
|
129
|
+
case "failure":
|
|
130
|
+
return "Failed";
|
|
131
|
+
case "not_started":
|
|
132
|
+
return "Not started";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function formatComponentScore(component) {
|
|
136
|
+
if (component.reason === "docs_or_test_suppressor")
|
|
137
|
+
return "cap";
|
|
138
|
+
return component.score >= 0 ? `+${component.score}` : String(component.score);
|
|
139
|
+
}
|
|
140
|
+
function labelForRiskComponent(reason) {
|
|
141
|
+
const labels = {
|
|
142
|
+
changed_file_count: "Changed file count",
|
|
143
|
+
large_line_delta: "Large line delta",
|
|
144
|
+
dependency_lockfile_change: "Dependency lockfile change",
|
|
145
|
+
migration_or_schema_change: "Migration or schema change",
|
|
146
|
+
ai_authorship_signal: "AI authorship signal",
|
|
147
|
+
docs_or_test_suppressor: "Documentation or test-only cap",
|
|
148
|
+
};
|
|
149
|
+
if (reason.startsWith("high_risk_path:")) {
|
|
150
|
+
return `High-risk path: ${reason.slice("high_risk_path:".length)}`;
|
|
151
|
+
}
|
|
152
|
+
return labels[reason] ?? reason.replaceAll("_", " ");
|
|
153
|
+
}
|
|
154
|
+
function labelFor(value) {
|
|
155
|
+
return value.replaceAll("_", " ");
|
|
156
|
+
}
|
|
157
|
+
function formatDate(value) {
|
|
158
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
159
|
+
dateStyle: "medium",
|
|
160
|
+
timeStyle: "short",
|
|
161
|
+
}).format(new Date(value));
|
|
162
|
+
}
|
|
163
|
+
function messageFrom(caught, fallback) {
|
|
164
|
+
return caught instanceof Error ? caught.message : fallback;
|
|
165
|
+
}
|
|
166
|
+
function isUnauthorized(caught) {
|
|
167
|
+
return typeof caught === "object" && caught !== null && "status" in caught && caught.status === 401;
|
|
168
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AuthorizationCapabilities, AvailabilitySettingsOverview, OperationsApiClient, ReviewerAbsenceOverview, ReviewerReplacementOverview, WorkspaceContext } from "../api.js";
|
|
2
|
+
export interface ReviewerAvailabilityProps {
|
|
3
|
+
api: OperationsApiClient;
|
|
4
|
+
workspace: WorkspaceContext;
|
|
5
|
+
authorization: AuthorizationCapabilities;
|
|
6
|
+
initialSettings?: AvailabilitySettingsOverview;
|
|
7
|
+
initialAbsences?: ReviewerAbsenceOverview[];
|
|
8
|
+
initialReplacementHistory?: ReviewerReplacementOverview[];
|
|
9
|
+
onUnauthorized?(message: string): void;
|
|
10
|
+
}
|
|
11
|
+
export declare function ReviewerAvailability({ ...props }: ReviewerAvailabilityProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
const emptyForm = { externalActorId: "", startLocal: "", endLocal: "" };
|
|
4
|
+
export function ReviewerAvailability({ ...props }) {
|
|
5
|
+
return _jsx(ReviewerAvailabilityWorkspace, { ...props }, props.workspace.id);
|
|
6
|
+
}
|
|
7
|
+
function ReviewerAvailabilityWorkspace({ api, workspace, authorization, initialSettings, initialAbsences, initialReplacementHistory, onUnauthorized, }) {
|
|
8
|
+
const [settings, setSettings] = useState(initialSettings ?? null);
|
|
9
|
+
const [timezone, setTimezone] = useState(initialSettings?.timezone ?? "UTC");
|
|
10
|
+
const [absences, setAbsences] = useState(initialAbsences ?? []);
|
|
11
|
+
const [history, setHistory] = useState(initialReplacementHistory ?? []);
|
|
12
|
+
const [form, setForm] = useState(emptyForm);
|
|
13
|
+
const [editing, setEditing] = useState(null);
|
|
14
|
+
const [cancelling, setCancelling] = useState(null);
|
|
15
|
+
const [loading, setLoading] = useState(initialSettings === undefined || initialAbsences === undefined || initialReplacementHistory === undefined);
|
|
16
|
+
const [pending, setPending] = useState(null);
|
|
17
|
+
const [error, setError] = useState(null);
|
|
18
|
+
const mutationInFlight = useRef(false);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!authorization.canViewOperations)
|
|
21
|
+
return;
|
|
22
|
+
if (initialSettings !== undefined && initialAbsences !== undefined && initialReplacementHistory !== undefined)
|
|
23
|
+
return;
|
|
24
|
+
let current = true;
|
|
25
|
+
setLoading(true);
|
|
26
|
+
void Promise.all([
|
|
27
|
+
api.readAvailabilitySettings(workspace),
|
|
28
|
+
api.listReviewerAbsences(workspace),
|
|
29
|
+
api.listReviewerReplacementHistory(workspace),
|
|
30
|
+
]).then(([nextSettings, nextAbsences, nextHistory]) => {
|
|
31
|
+
if (!current)
|
|
32
|
+
return;
|
|
33
|
+
setSettings(nextSettings);
|
|
34
|
+
setTimezone(nextSettings.timezone);
|
|
35
|
+
setAbsences(nextAbsences);
|
|
36
|
+
setHistory(nextHistory);
|
|
37
|
+
setError(null);
|
|
38
|
+
}).catch((caught) => handleError(caught, onUnauthorized, setError)).finally(() => {
|
|
39
|
+
if (current)
|
|
40
|
+
setLoading(false);
|
|
41
|
+
});
|
|
42
|
+
return () => { current = false; };
|
|
43
|
+
}, [api, workspace, authorization.canViewOperations, initialSettings, initialAbsences, initialReplacementHistory, onUnauthorized]);
|
|
44
|
+
if (!authorization.canViewOperations) {
|
|
45
|
+
return _jsx("section", { className: "availability-grid", id: "reviewer-availability", children: _jsx("p", { children: "You are not authorized to view reviewer availability." }) });
|
|
46
|
+
}
|
|
47
|
+
if (loading || settings === null) {
|
|
48
|
+
return _jsx("section", { className: "availability-grid", id: "reviewer-availability", "aria-live": "polite", children: _jsx("p", { children: "Loading reviewer availability\u2026" }) });
|
|
49
|
+
}
|
|
50
|
+
const busy = pending !== null;
|
|
51
|
+
async function mutate(key, action) {
|
|
52
|
+
if (mutationInFlight.current)
|
|
53
|
+
return false;
|
|
54
|
+
mutationInFlight.current = true;
|
|
55
|
+
setPending(key);
|
|
56
|
+
setError(null);
|
|
57
|
+
try {
|
|
58
|
+
await action();
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
catch (caught) {
|
|
62
|
+
handleError(caught, onUnauthorized, setError);
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
mutationInFlight.current = false;
|
|
67
|
+
setPending(null);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function submitTimezone(event) {
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
if (editing !== null)
|
|
73
|
+
return;
|
|
74
|
+
await mutate("timezone", async () => {
|
|
75
|
+
const next = await api.updateAvailabilityTimezone(workspace, timezone);
|
|
76
|
+
setSettings(next);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
async function submitAbsence(event) {
|
|
80
|
+
event.preventDefault();
|
|
81
|
+
const current = editing;
|
|
82
|
+
const saved = await mutate("absence", async () => {
|
|
83
|
+
const next = current
|
|
84
|
+
? await api.reviseReviewerAbsence(workspace, current.id, { ...form, expectedRevision: current.revision })
|
|
85
|
+
: await api.scheduleReviewerAbsence(workspace, form);
|
|
86
|
+
setAbsences((items) => [next, ...items.filter((item) => item.id !== next.id)]);
|
|
87
|
+
});
|
|
88
|
+
if (saved) {
|
|
89
|
+
setEditing(null);
|
|
90
|
+
setForm(emptyForm);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function confirmCancellation(absence) {
|
|
94
|
+
const saved = await mutate(absence.id, async () => {
|
|
95
|
+
const next = await api.cancelReviewerAbsence(workspace, absence.id, absence.revision);
|
|
96
|
+
setAbsences((items) => items.map((item) => item.id === next.id ? next : item));
|
|
97
|
+
});
|
|
98
|
+
if (saved)
|
|
99
|
+
setCancelling(null);
|
|
100
|
+
}
|
|
101
|
+
function beginEdit(absence) {
|
|
102
|
+
setEditing(absence);
|
|
103
|
+
setCancelling(null);
|
|
104
|
+
setError(null);
|
|
105
|
+
setForm({
|
|
106
|
+
externalActorId: absence.externalActorId,
|
|
107
|
+
startLocal: toLocalDateTime(absence.startAt, settings.timezone),
|
|
108
|
+
endLocal: toLocalDateTime(absence.endAt, settings.timezone),
|
|
109
|
+
startUtcOffset: toUtcOffset(absence.startAt, settings.timezone),
|
|
110
|
+
endUtcOffset: toUtcOffset(absence.endAt, settings.timezone),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return (_jsxs("section", { className: "availability-grid", id: "reviewer-availability", children: [_jsxs("div", { className: "availability-intro", children: [_jsx("p", { className: "eyebrow", children: "Reviewer routing" }), _jsx("h2", { id: "availability-heading", children: "Reviewer availability" }), _jsx("p", { children: "Schedule individual reviewer absences in the workspace timezone." })] }), error ? _jsx("p", { role: "alert", className: "notice notice--danger", children: error }) : null, authorization.canManageReviewerAvailability ? (_jsxs(_Fragment, { children: [_jsxs("form", { className: "availability-form", onSubmit: (event) => void submitTimezone(event), children: [_jsx("label", { htmlFor: "availability-timezone", children: "Workspace timezone" }), _jsx("input", { id: "availability-timezone", value: timezone, onChange: (event) => setTimezone(event.target.value), disabled: busy || editing !== null, required: true }), _jsx("small", { children: editing ? "Finish or discard the active edit before changing timezone." : "Use an IANA timezone, for example Europe/Bratislava." }), _jsx("button", { type: "submit", disabled: busy || editing !== null, children: pending === "timezone" ? "Saving…" : "Save timezone" })] }), _jsxs("form", { className: "availability-form", onSubmit: (event) => void submitAbsence(event), children: [_jsx("h3", { children: editing ? `Edit absence for ${editing.externalActorId}` : "Record reviewer absence" }), _jsx("label", { htmlFor: "absence-actor", children: "External actor ID" }), _jsx("input", { id: "absence-actor", value: form.externalActorId, onChange: (event) => setForm({ ...form, externalActorId: event.target.value }), disabled: busy, required: true }), _jsxs("label", { htmlFor: "absence-start", children: ["Start (", settings.timezone, ")"] }), _jsx("input", { id: "absence-start", type: "datetime-local", value: form.startLocal, onChange: (event) => setForm({ ...form, startLocal: event.target.value }), disabled: busy, required: true }), _jsx("label", { htmlFor: "absence-start-offset", children: "Start UTC offset (only for ambiguous times)" }), _jsx("input", { id: "absence-start-offset", placeholder: "+02:00", value: form.startUtcOffset ?? "", onChange: (event) => setForm(withOptionalOffset(form, "startUtcOffset", event.target.value)), disabled: busy }), _jsxs("label", { htmlFor: "absence-end", children: ["End (", settings.timezone, ")"] }), _jsx("input", { id: "absence-end", type: "datetime-local", value: form.endLocal, onChange: (event) => setForm({ ...form, endLocal: event.target.value }), disabled: busy, required: true }), _jsx("label", { htmlFor: "absence-end-offset", children: "End UTC offset (only for ambiguous times)" }), _jsx("input", { id: "absence-end-offset", placeholder: "+01:00", value: form.endUtcOffset ?? "", onChange: (event) => setForm(withOptionalOffset(form, "endUtcOffset", event.target.value)), disabled: busy }), _jsxs("div", { className: "availability-form__actions", children: [_jsx("button", { type: "submit", disabled: busy, children: pending === "absence" ? "Saving…" : editing ? "Save absence" : "Add absence" }), editing ? _jsx("button", { type: "button", className: "button--quiet", disabled: busy, onClick: () => { setEditing(null); setForm(emptyForm); }, children: "Discard edit" }) : null] })] })] })) : _jsxs("p", { className: "cell-detail", children: ["Timezone: ", settings.timezone, ". Availability is read-only."] }), _jsxs("div", { className: "availability-history table-scroll", role: "region", tabIndex: 0, "aria-labelledby": "availability-history-heading", children: [_jsx("h3", { id: "availability-history-heading", children: "Absence history" }), _jsxs("table", { children: [_jsx("caption", { className: "sr-only", children: "Reviewer absences" }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Reviewer" }), _jsx("th", { scope: "col", children: "Window" }), _jsx("th", { scope: "col", children: "Status" }), _jsx("th", { scope: "col", children: "Replacement history" }), _jsx("th", { scope: "col", children: "Actions" })] }) }), _jsx("tbody", { children: absences.length === 0 ? _jsx("tr", { children: _jsx("td", { colSpan: 5, className: "empty-cell", children: "No reviewer absences are recorded." }) }) : absences.map((absence) => {
|
|
114
|
+
const replacements = history.filter((item) => item.absenceId === absence.id);
|
|
115
|
+
const mutable = absence.status === "active" || absence.status === "upcoming";
|
|
116
|
+
return _jsxs("tr", { children: [_jsx("th", { scope: "row", className: "data-text", children: absence.externalActorId }), _jsxs("td", { children: [_jsx("time", { dateTime: absence.startAt, children: formatDate(absence.startAt, settings.timezone) }), _jsxs("span", { className: "cell-detail", children: ["to ", _jsx("time", { dateTime: absence.endAt, children: formatDate(absence.endAt, settings.timezone) })] })] }), _jsx("td", { children: _jsx("span", { className: `chip chip--${absence.status}`, children: capitalize(absence.status) }) }), _jsx("td", { children: replacements.length === 0 ? _jsx("span", { className: "cell-detail", children: "No replacement outcomes recorded." }) : _jsx("ul", { className: "replacement-list", children: replacements.map((item) => _jsxs("li", { children: [_jsx("span", { className: "data-text", children: item.replacementActorId ?? "No replacement" }), _jsxs("span", { className: "cell-detail", children: [item.outcome.replaceAll("_", " "), " \u00B7 ", item.reason] }), item.lastError ? _jsx("span", { className: "cell-error", children: item.lastError }) : null] }, item.id)) }) }), _jsx("td", { children: authorization.canManageReviewerAvailability && mutable ? _jsxs("div", { className: "availability-actions", children: [_jsx("button", { type: "button", className: "button--quiet", disabled: busy, onClick: () => beginEdit(absence), children: "Edit" }), cancelling === absence.id ? _jsxs(_Fragment, { children: [_jsx("span", { className: "cell-detail", children: "Cancel this absence?" }), _jsx("button", { type: "button", disabled: busy, onClick: () => void confirmCancellation(absence), children: "Confirm cancel" }), _jsx("button", { type: "button", className: "button--quiet", disabled: busy, onClick: () => setCancelling(null), children: "Keep" })] }) : _jsx("button", { type: "button", disabled: busy, onClick: () => setCancelling(absence.id), children: "Cancel" })] }) : _jsx("span", { className: "cell-detail", children: "\u2014" }) })] }, absence.id);
|
|
117
|
+
}) })] })] })] }));
|
|
118
|
+
}
|
|
119
|
+
function handleError(caught, onUnauthorized, setError) {
|
|
120
|
+
const message = caught instanceof Error ? caught.message : "Could not update reviewer availability.";
|
|
121
|
+
if (typeof caught === "object" && caught !== null && "status" in caught && caught.status === 401) {
|
|
122
|
+
onUnauthorized?.(message);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
setError(message);
|
|
126
|
+
}
|
|
127
|
+
function formatDate(value, timezone) {
|
|
128
|
+
return new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short", timeZone: timezone }).format(new Date(value));
|
|
129
|
+
}
|
|
130
|
+
function toLocalDateTime(value, timezone) {
|
|
131
|
+
const fields = Object.fromEntries(new Intl.DateTimeFormat("en-CA", {
|
|
132
|
+
timeZone: timezone, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hourCycle: "h23",
|
|
133
|
+
}).formatToParts(new Date(value)).filter((part) => part.type !== "literal").map((part) => [part.type, part.value]));
|
|
134
|
+
return `${fields.year}-${fields.month}-${fields.day}T${fields.hour}:${fields.minute}`;
|
|
135
|
+
}
|
|
136
|
+
function toUtcOffset(value, timezone) {
|
|
137
|
+
const instant = new Date(value);
|
|
138
|
+
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
139
|
+
timeZone: timezone, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hourCycle: "h23",
|
|
140
|
+
}).formatToParts(instant);
|
|
141
|
+
const read = (type) => Number(parts.find((part) => part.type === type)?.value);
|
|
142
|
+
const localAsUtc = Date.UTC(read("year"), read("month") - 1, read("day"), read("hour"), read("minute"));
|
|
143
|
+
const offsetMinutes = Math.round((localAsUtc - instant.getTime()) / 60_000);
|
|
144
|
+
const sign = offsetMinutes < 0 ? "-" : "+";
|
|
145
|
+
const absolute = Math.abs(offsetMinutes);
|
|
146
|
+
return `${sign}${String(Math.floor(absolute / 60)).padStart(2, "0")}:${String(absolute % 60).padStart(2, "0")}`;
|
|
147
|
+
}
|
|
148
|
+
function capitalize(value) {
|
|
149
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
150
|
+
}
|
|
151
|
+
function withOptionalOffset(form, field, value) {
|
|
152
|
+
const next = { ...form };
|
|
153
|
+
if (value === "")
|
|
154
|
+
delete next[field];
|
|
155
|
+
else
|
|
156
|
+
next[field] = value;
|
|
157
|
+
return next;
|
|
158
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { OperationsDashboard, type OperationsDashboardProps, } from "./components/OperationsDashboard.js";
|
|
2
|
+
export { EffectiveConfiguration, type EffectiveConfigurationProps, } from "./components/EffectiveConfiguration.js";
|
|
3
|
+
export { ReviewerAvailability, type ReviewerAvailabilityProps, } from "./components/ReviewerAvailability.js";
|
|
4
|
+
export type { ActionFailureOverview, AvailabilitySettingsOverview, AuthorizationCapabilities, DecisionOverview, EffectiveConfigurationOverview, EffectiveConfigurationValue, JobFailureOverview, NavigationHost, NavigationTarget, OperationsApiClient, OperationsOverview, ProviderLink, ProviderStatusOverview, RepositoryContext, RepositoryOverview, RiskBreakdown, ReviewerAbsenceMutation, ReviewerAbsenceOverview, ReviewerReplacementOverview, WorkspaceContext, } from "./api.js";
|
package/dist/index.js
ADDED
package/dist/styles.css
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
.operations-state {
|
|
2
|
+
width: min(100%, 440px);
|
|
3
|
+
border: 1px solid var(--rule);
|
|
4
|
+
border-radius: 14px;
|
|
5
|
+
padding: 32px;
|
|
6
|
+
background: var(--surface);
|
|
7
|
+
box-shadow: 0 12px 30px #0b395914;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.operations-dashboard { display: grid; gap: 18px; scroll-margin-top: 20px; }
|
|
11
|
+
.operations-heading { display: flex; align-items: end; justify-content: space-between; gap: 28px; margin-bottom: 2px; }
|
|
12
|
+
.operations-heading .lede { margin-top: 7px; }
|
|
13
|
+
.operations-actions { display: flex; align-items: center; gap: 12px; }
|
|
14
|
+
.operations-actions--state { justify-content: flex-end; margin-bottom: 22px; }
|
|
15
|
+
.button-link {
|
|
16
|
+
display: inline-block;
|
|
17
|
+
border: 1px solid var(--navy);
|
|
18
|
+
border-radius: 7px;
|
|
19
|
+
padding: 10px 16px;
|
|
20
|
+
color: white;
|
|
21
|
+
background: var(--navy);
|
|
22
|
+
font-size: .78rem;
|
|
23
|
+
font-weight: 700;
|
|
24
|
+
text-decoration: none;
|
|
25
|
+
}
|
|
26
|
+
.button-link--quiet { border-color: var(--rule); color: var(--navy); background: white; }
|
|
27
|
+
|
|
28
|
+
.status-ledger {
|
|
29
|
+
display: grid;
|
|
30
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
31
|
+
gap: 12px;
|
|
32
|
+
scroll-margin-top: 20px;
|
|
33
|
+
}
|
|
34
|
+
.status-node {
|
|
35
|
+
position: relative;
|
|
36
|
+
display: grid;
|
|
37
|
+
gap: 5px;
|
|
38
|
+
min-width: 0;
|
|
39
|
+
border: 1px solid var(--rule);
|
|
40
|
+
border-radius: 8px;
|
|
41
|
+
padding: 15px 16px 16px;
|
|
42
|
+
background: var(--surface);
|
|
43
|
+
}
|
|
44
|
+
.status-node::after { position: absolute; top: 16px; right: 16px; width: 7px; height: 7px; border-radius: 50%; background: #a8bac4; content: ""; }
|
|
45
|
+
.status-node--healthy::after { background: var(--teal); box-shadow: 0 0 0 4px #08b7b616; }
|
|
46
|
+
.status-node--failed::after { background: var(--caution); box-shadow: 0 0 0 4px #c6494915; }
|
|
47
|
+
.status-node strong { overflow: hidden; padding-right: 18px; color: var(--navy); font-family: "Geist Mono", ui-monospace, monospace; font-size: .85rem; text-overflow: ellipsis; white-space: nowrap; }
|
|
48
|
+
.status-node small { overflow: hidden; color: var(--muted); font-size: .69rem; text-overflow: ellipsis; white-space: nowrap; }
|
|
49
|
+
|
|
50
|
+
.data-section {
|
|
51
|
+
scroll-margin-top: 20px;
|
|
52
|
+
overflow: hidden;
|
|
53
|
+
border: 1px solid var(--rule);
|
|
54
|
+
border-radius: 8px;
|
|
55
|
+
background: var(--surface);
|
|
56
|
+
}
|
|
57
|
+
.data-section--danger { border-color: #ecdada; }
|
|
58
|
+
.section-heading { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-height: 56px; border-bottom: 1px solid var(--rule); padding: 12px 18px; }
|
|
59
|
+
.count { min-width: 30px; border-radius: 999px; padding: 4px 8px; color: var(--teal-dark); background: var(--surface-soft); font-family: "Geist Mono", ui-monospace, monospace; font-size: .66rem; font-weight: 700; text-align: center; }
|
|
60
|
+
.table-scroll { max-width: 100%; overflow-x: auto; }
|
|
61
|
+
.table-scroll:focus-visible, .button-link:focus-visible { outline: 3px solid var(--teal); outline-offset: -3px; }
|
|
62
|
+
.button-link:focus-visible { outline-offset: 3px; }
|
|
63
|
+
table { width: 100%; border-collapse: collapse; font-size: .77rem; text-align: left; }
|
|
64
|
+
th, td { border-bottom: 1px solid var(--rule); padding: 13px 18px; vertical-align: top; }
|
|
65
|
+
thead th { color: var(--muted); background: #f8fbfb; font-size: .61rem; font-weight: 750; letter-spacing: .07em; text-transform: uppercase; white-space: nowrap; }
|
|
66
|
+
tbody tr:last-child > * { border-bottom: 0; }
|
|
67
|
+
tbody tr:hover > * { background: #fbfdfd; }
|
|
68
|
+
tbody th { font-weight: 650; }
|
|
69
|
+
a { color: var(--teal-dark); text-underline-offset: 2px; }
|
|
70
|
+
.data-text, time, .chip { font-family: "Geist Mono", ui-monospace, monospace; }
|
|
71
|
+
.cell-detail, .cell-error { display: block; margin-top: 5px; color: var(--muted); font-size: .67rem; line-height: 1.4; }
|
|
72
|
+
.cell-error { max-width: 32ch; color: var(--caution); }
|
|
73
|
+
.risk-breakdown { margin-top: 6px; color: var(--ink); font-size: .68rem; }
|
|
74
|
+
.risk-breakdown summary { cursor: pointer; color: var(--teal-dark); font-weight: 700; }
|
|
75
|
+
.risk-breakdown__meta { margin: 7px 0; color: var(--muted); }
|
|
76
|
+
.risk-breakdown ul { display: grid; gap: 7px; min-width: 29ch; margin: 0; padding: 0; list-style: none; }
|
|
77
|
+
.risk-breakdown li { display: grid; grid-template-columns: 3.5ch 1fr; gap: 8px; }
|
|
78
|
+
.risk-breakdown__score { color: var(--teal-dark); font-weight: 700; }
|
|
79
|
+
.chip { display: inline-block; border: 1px solid var(--rule); border-radius: 999px; padding: 3px 7px; color: var(--muted); background: #f8fbfb; font-size: .61rem; font-weight: 700; white-space: nowrap; }
|
|
80
|
+
.chip--valid, .chip--succeeded, .chip--enforce, .chip--repository, .chip--active, .chip--upcoming {
|
|
81
|
+
border-color: #b8e7e3;
|
|
82
|
+
color: var(--teal-dark);
|
|
83
|
+
background: #effbfa;
|
|
84
|
+
}
|
|
85
|
+
.chip--invalid, .chip--failed, .chip--cancelled { border-color: #eccaca; color: var(--caution); background: #fff6f6; }
|
|
86
|
+
.id-cell { max-width: 15ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
87
|
+
.empty-cell { height: 76px; color: var(--muted); text-align: center; vertical-align: middle; }
|
|
88
|
+
.failure-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; }
|
|
89
|
+
|
|
90
|
+
.routing-recovery-form {
|
|
91
|
+
display: grid;
|
|
92
|
+
grid-template-columns: minmax(15rem, 1fr) minmax(18rem, 1.8fr) auto;
|
|
93
|
+
align-items: end;
|
|
94
|
+
gap: 12px;
|
|
95
|
+
border-bottom: 1px solid var(--rule);
|
|
96
|
+
padding: 16px 18px;
|
|
97
|
+
background: var(--surface-soft);
|
|
98
|
+
}
|
|
99
|
+
.routing-recovery-form label { display: block; color: var(--navy); font-size: .75rem; font-weight: 700; }
|
|
100
|
+
.routing-recovery-form input, .availability-form input {
|
|
101
|
+
width: 100%;
|
|
102
|
+
border: 1px solid #b9d4dd;
|
|
103
|
+
border-radius: 7px;
|
|
104
|
+
padding: 9px 10px;
|
|
105
|
+
color: var(--ink);
|
|
106
|
+
background: white;
|
|
107
|
+
}
|
|
108
|
+
.routing-notice { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 0; border-right: 0; border-left: 0; border-radius: 0; }
|
|
109
|
+
.button--compact { padding: 7px 10px; font-size: .67rem; white-space: nowrap; }
|
|
110
|
+
|
|
111
|
+
.availability-grid {
|
|
112
|
+
display: grid;
|
|
113
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
114
|
+
gap: 18px;
|
|
115
|
+
margin-top: 18px;
|
|
116
|
+
scroll-margin-top: 20px;
|
|
117
|
+
border: 1px solid var(--rule);
|
|
118
|
+
border-radius: 8px;
|
|
119
|
+
padding: 20px;
|
|
120
|
+
background: var(--surface);
|
|
121
|
+
}
|
|
122
|
+
.availability-intro, .availability-history, .availability-grid > .notice { grid-column: 1 / -1; }
|
|
123
|
+
.availability-intro { border-bottom: 1px solid var(--rule); padding-bottom: 15px; }
|
|
124
|
+
.availability-intro p:last-child { margin: 7px 0 0; color: var(--muted); font-size: .76rem; }
|
|
125
|
+
.availability-form { display: grid; align-content: start; gap: 8px; border: 1px solid var(--rule); border-radius: 8px; padding: 16px; background: #fbfdfd; }
|
|
126
|
+
.availability-form label { margin-top: 8px; color: var(--ink); font-size: .72rem; font-weight: 700; }
|
|
127
|
+
.availability-form small { color: var(--muted); font-size: .67rem; line-height: 1.4; }
|
|
128
|
+
.availability-form__actions, .availability-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 8px; }
|
|
129
|
+
.availability-history { border: 1px solid var(--rule); border-radius: 8px; }
|
|
130
|
+
.availability-history h3 { margin: 0; border-bottom: 1px solid var(--rule); padding: 14px 16px; }
|
|
131
|
+
.replacement-list { display: grid; gap: 8px; margin: 0; padding-left: 18px; }
|
|
132
|
+
|
|
133
|
+
.effective-configuration { margin-top: 18px; }
|
|
134
|
+
.configuration-provenance { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1px; margin: 0; border-bottom: 1px solid var(--rule); background: var(--rule); }
|
|
135
|
+
.configuration-provenance div { min-width: 0; padding: 13px 16px; background: var(--surface); }
|
|
136
|
+
.configuration-provenance dt { margin-bottom: 5px; color: var(--muted); font-size: .62rem; font-weight: 750; letter-spacing: .06em; text-transform: uppercase; }
|
|
137
|
+
.configuration-provenance dd { margin: 0; overflow-wrap: anywhere; font-family: "Geist Mono", ui-monospace, monospace; font-size: .72rem; }
|
|
138
|
+
.sr-only { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0, 0, 0, 0); border: 0; white-space: nowrap; }
|
|
139
|
+
|
|
140
|
+
@media (max-width: 760px) {
|
|
141
|
+
.operations-heading, .operations-actions { align-items: flex-start; flex-direction: column; gap: 14px; }
|
|
142
|
+
.status-ledger, .failure-grid, .availability-grid, .routing-recovery-form, .configuration-provenance { grid-template-columns: 1fr; }
|
|
143
|
+
.availability-grid { padding: 15px; }
|
|
144
|
+
table { min-width: 680px; }
|
|
145
|
+
.failure-grid table { min-width: 520px; }
|
|
146
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@triagepilot/ui",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"license": "FSL-1.1-Apache-2.0",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/TriagePilot/triage-pilot.git",
|
|
8
|
+
"directory": "packages/ui"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./styles.css": "./dist/styles.css"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@triagepilot/contracts": "1.1.0"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"react": "^18.3.1",
|
|
29
|
+
"react-dom": "^18.3.1"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/react": "^18.3.18",
|
|
33
|
+
"@types/react-dom": "^18.3.5",
|
|
34
|
+
"react": "^18.3.1",
|
|
35
|
+
"react-dom": "^18.3.1"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"publishedAt": "2026-09-22T09:41:47.000Z",
|
|
41
|
+
"futureLicenseEffectiveAt": "2028-09-22T09:41:47.000Z",
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "rm -rf dist && tsc -p tsconfig.json && node ../../scripts/prepare-package.mjs --package . --asset src/styles.css=dist/styles.css",
|
|
44
|
+
"check": "tsc -p tsconfig.json --noEmit"
|
|
45
|
+
}
|
|
46
|
+
}
|