@elinpf/dsh-ops-access-ui 0.1.7 → 0.2.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/client.js CHANGED
@@ -29,7 +29,6 @@ __export(client_exports, {
29
29
  apiFetchResult: () => apiFetchResult,
30
30
  apply: () => apply,
31
31
  defaultTtlChoice: () => defaultTtlChoice,
32
- delegatedAccessCount: () => delegatedAccessCount,
33
32
  deleteEntry: () => deleteEntry,
34
33
  fetchAdminList: () => fetchAdminList,
35
34
  fetchKinds: () => fetchKinds,
@@ -40,7 +39,7 @@ __export(client_exports, {
40
39
  inject: () => inject,
41
40
  liveGrantFor: () => liveGrantFor,
42
41
  name: () => name,
43
- pendingAccessCount: () => pendingAccessCount,
42
+ pendingRequestCount: () => pendingRequestCount,
44
43
  postPanelDecide: () => postPanelDecide,
45
44
  postPanelDeny: () => postPanelDeny,
46
45
  postPanelExtend: () => postPanelExtend,
@@ -109,6 +108,32 @@ async function fetchEntry(kind, name2, tier) {
109
108
  return null;
110
109
  }
111
110
  }
111
+ async function fetchAdminRequests(signal) {
112
+ try {
113
+ const res = await fetch("/ops-access/admin/requests", { signal });
114
+ if (!res.ok) return null;
115
+ const data = await res.json();
116
+ return Array.isArray(data) ? data : null;
117
+ } catch {
118
+ return null;
119
+ }
120
+ }
121
+ async function fetchRequestDetail(id) {
122
+ try {
123
+ const res = await fetch("/ops-access/admin/requests/detail?id=" + encodeURIComponent(id));
124
+ if (!res.ok) return null;
125
+ return await res.json();
126
+ } catch {
127
+ return null;
128
+ }
129
+ }
130
+ function decideRegistrationRequest(id, approved) {
131
+ return apiFetchResult("/ops-access/admin/requests/decide", {
132
+ method: "POST",
133
+ headers: { "content-type": "application/json" },
134
+ body: JSON.stringify({ id, approved })
135
+ }, "decideRegistrationRequest");
136
+ }
112
137
  async function fetchPanelGrants(sessionId, signal) {
113
138
  try {
114
139
  const res = await fetch("/ops-access/grants?session=" + encodeURIComponent(sessionId), { signal });
@@ -177,24 +202,19 @@ function postPanelDecide(id, approved, ttlMinutes) {
177
202
  body: JSON.stringify(approved ? { id, approved, ttlMinutes } : { id, approved })
178
203
  }, "postPanelDecide");
179
204
  }
180
- function pendingAccessCount(session) {
181
- if (session === null || session === void 0 || !Array.isArray(session.runningCalls)) return 0;
182
- return session.runningCalls.filter((c) => c.name === "request_access").length;
183
- }
184
- function delegatedAccessCount(requests, sessionId) {
205
+ function pendingRequestCount(requests) {
185
206
  if (!Array.isArray(requests)) return 0;
186
- return requests.filter((r) => r.parentSession === sessionId).length;
207
+ return requests.length;
187
208
  }
188
209
  function AccessBadge(props) {
189
- const own = pendingAccessCount(props.session);
190
- const [delegated, setDelegated] = (0, import_react.useState)(0);
191
210
  const sid = props.sessionId;
211
+ const [count, setCount] = (0, import_react.useState)(0);
192
212
  (0, import_react.useEffect)(() => {
193
213
  if (sid === void 0) return;
194
214
  let alive = true;
195
215
  const poll = async () => {
196
216
  const data = await fetchPanelRequests(sid);
197
- if (alive && data !== null) setDelegated(delegatedAccessCount(data.requests, sid));
217
+ if (alive && data !== null) setCount(pendingRequestCount(data.requests));
198
218
  };
199
219
  void poll();
200
220
  const timer = setInterval(() => {
@@ -205,13 +225,12 @@ function AccessBadge(props) {
205
225
  clearInterval(timer);
206
226
  };
207
227
  }, [sid]);
208
- const count = own + delegated;
209
228
  if (count === 0 || sid === void 0) return null;
210
229
  return (0, import_react.createElement)(
211
230
  "button",
212
231
  {
213
232
  className: "ops-access-badge",
214
- title: delegated > 0 ? "\u6709\u5F85\u5BA1\u6279\u7684\u63D0\u6743\u7533\u8BF7\uFF08\u542B\u59D4\u6D3E\u5B50\u4F1A\u8BDD\uFF09\u2014 \u70B9\u51FB\u6253\u5F00\u6388\u6743\u9762\u677F" : "\u6709\u5F85\u5BA1\u6279\u7684\u63D0\u6743\u7533\u8BF7 \u2014 \u70B9\u51FB\u6253\u5F00\u6388\u6743\u9762\u677F",
233
+ title: "\u6709\u5F85\u5BA1\u6279\u7684\u63D0\u6743\u7533\u8BF7 \u2014 \u70B9\u51FB\u6253\u5F00\u6388\u6743\u9762\u677F",
215
234
  onClick: () => props.openPanel(sid)
216
235
  },
217
236
  (0, import_react.createElement)("span", { className: "ops-access-badge-dot" }),
@@ -701,7 +720,7 @@ function TierBadge({ status }) {
701
720
  }, (0, import_react.createElement)(CrossIcon));
702
721
  }
703
722
  function AdminListView(props) {
704
- const { entries, loading, onRefresh, onAdd, onEdit, onDelete } = props;
723
+ const { entries, loading, requests, onReview, onRefresh, onAdd, onEdit, onDelete } = props;
705
724
  const kindOrder = [];
706
725
  const byKind = {};
707
726
  for (const e of entries) {
@@ -734,6 +753,46 @@ function AdminListView(props) {
734
753
  }, (0, import_react.createElement)(PlusIcon), "\u65B0\u589E")
735
754
  )
736
755
  ),
756
+ requests && requests.length > 0 && (0, import_react.createElement)(
757
+ "div",
758
+ { className: "ops-access-admin-card" },
759
+ (0, import_react.createElement)(
760
+ "div",
761
+ { className: "ops-access-admin-card-header" },
762
+ (0, import_react.createElement)("span", { className: "ops-access-admin-card-title" }, "\u5F85\u5BA1\u6279\u6CE8\u518C\u7533\u8BF7"),
763
+ (0, import_react.createElement)("span", { className: "ops-access-admin-card-count" }, String(requests.length) + " \u6761")
764
+ ),
765
+ ...requests.map(
766
+ (request) => (0, import_react.createElement)(
767
+ "div",
768
+ { key: request.id, className: "ops-access-admin-card-row" },
769
+ (0, import_react.createElement)(
770
+ "div",
771
+ { className: "ops-access-admin-card-row-main" },
772
+ (0, import_react.createElement)(
773
+ "div",
774
+ { className: "ops-access-admin-card-row-name" },
775
+ request.kind + "/" + request.name,
776
+ (0, import_react.createElement)("span", { className: "ops-access-admin-card-row-id" }, request.tier)
777
+ ),
778
+ (0, import_react.createElement)(
779
+ "div",
780
+ { className: "ops-access-admin-card-row-desc" },
781
+ (request.reason ? request.reason + " \xB7 " : "") + new Date(request.createdAt).toLocaleString()
782
+ )
783
+ ),
784
+ (0, import_react.createElement)(
785
+ "div",
786
+ { className: "ops-access-admin-row-actions" },
787
+ (0, import_react.createElement)("button", {
788
+ type: "button",
789
+ className: "ops-access-admin-btn ops-access-admin-btn-primary",
790
+ onClick: () => onReview?.(request)
791
+ }, "\u5BA1\u67E5")
792
+ )
793
+ )
794
+ )
795
+ ),
737
796
  entries.length === 0 ? (0, import_react.createElement)(
738
797
  "div",
739
798
  { className: "ops-access-admin-empty" },
@@ -1145,6 +1204,88 @@ function AdminFormView(props) {
1145
1204
  )
1146
1205
  );
1147
1206
  }
1207
+ function RequestReviewView(props) {
1208
+ const { detail, deciding, error, onDecide, onCancel } = props;
1209
+ const [confirming, setConfirming] = (0, import_react.useState)(null);
1210
+ const fieldEntries = Object.entries(detail.fields);
1211
+ return (0, import_react.createElement)(
1212
+ "div",
1213
+ { className: "ops-access-admin-root" },
1214
+ (0, import_react.createElement)(
1215
+ "div",
1216
+ { className: "ops-access-admin-header" },
1217
+ (0, import_react.createElement)(
1218
+ "span",
1219
+ { className: "ops-access-admin-title" },
1220
+ "\u5BA1\u6279\u6CE8\u518C\u7533\u8BF7: " + detail.kind + "/" + detail.name + " (" + detail.tier + ")"
1221
+ )
1222
+ ),
1223
+ (0, import_react.createElement)(
1224
+ "div",
1225
+ { className: "ops-access-admin-card-row-desc" },
1226
+ (detail.reason ? "\u7406\u7531: " + detail.reason + " \xB7 " : "") + "\u63D0\u4EA4\u4E8E " + new Date(detail.createdAt).toLocaleString()
1227
+ ),
1228
+ ...fieldEntries.map(([fieldName, value]) => {
1229
+ const text = typeof value === "string" ? value : JSON.stringify(value);
1230
+ return (0, import_react.createElement)(
1231
+ "div",
1232
+ { key: fieldName, style: { margin: "8px 0" } },
1233
+ (0, import_react.createElement)("div", { className: "ops-access-admin-card-row-name" }, fieldName),
1234
+ text.includes("\n") || text.length > 80 ? (0, import_react.createElement)("textarea", {
1235
+ readOnly: true,
1236
+ rows: Math.min(text.split("\n").length + 1, 12),
1237
+ style: { width: "100%", fontFamily: "monospace" },
1238
+ value: text
1239
+ }) : (0, import_react.createElement)("div", { style: { fontFamily: "monospace" } }, text)
1240
+ );
1241
+ }),
1242
+ fieldEntries.length === 0 && (0, import_react.createElement)("div", { className: "ops-access-admin-empty" }, "(\u65E0\u5B57\u6BB5)"),
1243
+ error && (0, import_react.createElement)("div", { className: "ops-access-admin-card-row-desc" }, error),
1244
+ confirming ? (0, import_react.createElement)(
1245
+ "div",
1246
+ { className: "ops-access-admin-confirm" },
1247
+ (0, import_react.createElement)(
1248
+ "div",
1249
+ null,
1250
+ confirming === "approve" ? "\u786E\u8BA4\u6279\u51C6\uFF1F\u8BE5 " + detail.tier + " tier \u5C06\u7ACB\u5373\u5199\u5165\u51ED\u8BC1\u5E93\u5E76\u751F\u6548\u3002" : "\u786E\u8BA4\u62D2\u7EDD\uFF1F\u8BE5\u7533\u8BF7\u5C06\u88AB\u6807\u8BB0\u4E3A\u5DF2\u62D2\u7EDD,\u4E0D\u4F1A\u5199\u5165\u4EFB\u4F55\u5185\u5BB9\u3002"
1251
+ ),
1252
+ (0, import_react.createElement)(
1253
+ "div",
1254
+ { className: "ops-access-admin-actions" },
1255
+ (0, import_react.createElement)("button", {
1256
+ type: "button",
1257
+ className: "ops-access-admin-btn " + (confirming === "approve" ? "ops-access-admin-btn-primary" : "ops-access-admin-btn-danger"),
1258
+ disabled: deciding,
1259
+ onClick: () => onDecide(confirming === "approve")
1260
+ }, deciding ? "\u63D0\u4EA4\u4E2D\u2026" : confirming === "approve" ? "\u786E\u8BA4\u6279\u51C6" : "\u786E\u8BA4\u62D2\u7EDD"),
1261
+ (0, import_react.createElement)("button", {
1262
+ type: "button",
1263
+ className: "ops-access-admin-btn",
1264
+ disabled: deciding,
1265
+ onClick: () => setConfirming(null)
1266
+ }, "\u53D6\u6D88")
1267
+ )
1268
+ ) : (0, import_react.createElement)(
1269
+ "div",
1270
+ { className: "ops-access-admin-actions", style: { marginTop: "12px" } },
1271
+ (0, import_react.createElement)("button", {
1272
+ type: "button",
1273
+ className: "ops-access-admin-btn ops-access-admin-btn-primary",
1274
+ onClick: () => setConfirming("approve")
1275
+ }, "\u6279\u51C6\u5199\u5165"),
1276
+ (0, import_react.createElement)("button", {
1277
+ type: "button",
1278
+ className: "ops-access-admin-btn ops-access-admin-btn-danger",
1279
+ onClick: () => setConfirming("reject")
1280
+ }, "\u62D2\u7EDD"),
1281
+ (0, import_react.createElement)("button", {
1282
+ type: "button",
1283
+ className: "ops-access-admin-btn",
1284
+ onClick: onCancel
1285
+ }, "\u8FD4\u56DE")
1286
+ )
1287
+ );
1288
+ }
1148
1289
  function AdminSection(_props) {
1149
1290
  const [view, setView] = (0, import_react.useState)("list");
1150
1291
  const [entries, setEntries] = (0, import_react.useState)([]);
@@ -1153,10 +1294,15 @@ function AdminSection(_props) {
1153
1294
  const [editEntry, setEditEntry] = (0, import_react.useState)(null);
1154
1295
  const [deleteTarget, setDeleteTarget] = (0, import_react.useState)(null);
1155
1296
  const [deleting, setDeleting] = (0, import_react.useState)(false);
1297
+ const [requests, setRequests] = (0, import_react.useState)(null);
1298
+ const [reviewDetail, setReviewDetail] = (0, import_react.useState)(null);
1299
+ const [reviewError, setReviewError] = (0, import_react.useState)(null);
1300
+ const [deciding, setDeciding] = (0, import_react.useState)(false);
1156
1301
  const refreshList = (0, import_react.useCallback)(async () => {
1157
1302
  setListLoading(true);
1158
- const result = await fetchAdminList();
1159
- setEntries(result);
1303
+ const [list, pending] = await Promise.all([fetchAdminList(), fetchAdminRequests()]);
1304
+ setEntries(list);
1305
+ setRequests(pending);
1160
1306
  setListLoading(false);
1161
1307
  }, []);
1162
1308
  (0, import_react.useEffect)(() => {
@@ -1190,6 +1336,29 @@ function AdminSection(_props) {
1190
1336
  setDeleteTarget(null);
1191
1337
  refreshList();
1192
1338
  }, [deleteTarget, refreshList]);
1339
+ const handleReview = (0, import_react.useCallback)(async (request) => {
1340
+ setReviewError(null);
1341
+ const detail = await fetchRequestDetail(request.id);
1342
+ if (!detail) {
1343
+ setReviewError("\u52A0\u8F7D\u7533\u8BF7\u8BE6\u60C5\u5931\u8D25 \u2014 \u7533\u8BF7\u53EF\u80FD\u5DF2\u88AB\u5904\u7406");
1344
+ refreshList();
1345
+ return;
1346
+ }
1347
+ setReviewDetail(detail);
1348
+ }, [refreshList]);
1349
+ const handleDecide = (0, import_react.useCallback)(async (approved) => {
1350
+ if (!reviewDetail) return;
1351
+ setDeciding(true);
1352
+ setReviewError(null);
1353
+ const result = await decideRegistrationRequest(reviewDetail.id, approved);
1354
+ setDeciding(false);
1355
+ if (!result.ok) {
1356
+ setReviewError(result.error ?? "\u64CD\u4F5C\u5931\u8D25");
1357
+ return;
1358
+ }
1359
+ setReviewDetail(null);
1360
+ refreshList();
1361
+ }, [reviewDetail, refreshList]);
1193
1362
  if (deleteTarget) {
1194
1363
  return (0, import_react.createElement)(
1195
1364
  "div",
@@ -1217,6 +1386,15 @@ function AdminSection(_props) {
1217
1386
  )
1218
1387
  );
1219
1388
  }
1389
+ if (reviewDetail) {
1390
+ return (0, import_react.createElement)(RequestReviewView, {
1391
+ detail: reviewDetail,
1392
+ deciding,
1393
+ error: reviewError,
1394
+ onDecide: handleDecide,
1395
+ onCancel: () => setReviewDetail(null)
1396
+ });
1397
+ }
1220
1398
  if (view === "form") {
1221
1399
  return (0, import_react.createElement)(AdminFormView, {
1222
1400
  error: formError,
@@ -1229,6 +1407,8 @@ function AdminSection(_props) {
1229
1407
  return (0, import_react.createElement)(AdminListView, {
1230
1408
  entries,
1231
1409
  loading: listLoading,
1410
+ requests,
1411
+ onReview: handleReview,
1232
1412
  onRefresh: refreshList,
1233
1413
  onAdd: handleAdd,
1234
1414
  onEdit: handleEdit,
@@ -1722,10 +1902,14 @@ function apply(ctx) {
1722
1902
  () => slots.inject(
1723
1903
  "conversation.input.dock",
1724
1904
  () => slots.register(
1725
- { name: "conversation.input.dock", id: "ops-access-badge", order: 30 },
1905
+ {
1906
+ name: "conversation.input.dock",
1907
+ id: "ops-access-badge",
1908
+ order: 30,
1909
+ inject: (sessionId) => ({ sessionId })
1910
+ },
1726
1911
  (props) => AccessBadge({
1727
1912
  sessionId: props.sessionId,
1728
- session: props.session,
1729
1913
  openPanel: (sid) => opsPanels.open(sid, "access")
1730
1914
  })
1731
1915
  )
package/lib/types.d.ts CHANGED
@@ -110,4 +110,25 @@ export interface PanelDenied {
110
110
  export interface OverviewGrant extends PanelGrant {
111
111
  session: string;
112
112
  }
113
+ /** One pending registration request as reported by GET /ops-access/admin/requests. */
114
+ export interface RegistrationRequestMeta {
115
+ id: string;
116
+ kind: string;
117
+ name: string;
118
+ tier: 'ro' | 'rw';
119
+ envelope: {
120
+ name?: string;
121
+ description?: string;
122
+ environment?: string;
123
+ };
124
+ reason?: string;
125
+ status: 'pending' | 'approved' | 'rejected';
126
+ createdAt: string;
127
+ /** Field names → value sizes; field VALUES never ride the list. */
128
+ fields: Record<string, number>;
129
+ }
130
+ /** The full request incl. field values — GET /ops-access/admin/requests/detail. */
131
+ export interface RegistrationRequestDetail extends Omit<RegistrationRequestMeta, 'fields'> {
132
+ fields: Record<string, unknown>;
133
+ }
113
134
  //# sourceMappingURL=types.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elinpf/dsh-ops-access-ui",
3
- "version": "0.1.7",
3
+ "version": "0.2.0",
4
4
  "description": "Browser half carrier for @-mentioning ops-access profiles: the host row exists only for client-bundle discovery; candidates come from GET /ops-access/list served by @elinpf/dsh-ops-access, and mention expansion (agent/pre-step) lives there too.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",