|
38 | 38 | let abacRuleId = $state<string>(""); |
39 | 39 | let accessCheckDone = $state(false); |
40 | 40 |
|
| 41 | + let usersWithAccess = $state<any>(null); |
| 42 | + let usersWithAccessLoading = $state(false); |
| 43 | + let usersWithAccessError = $state<string | null>(null); |
| 44 | +
|
| 45 | + let usersWithAccessParsedError = $derived.by(() => { |
| 46 | + if (!usersWithAccessError) return null; |
| 47 | + const missingRoleMatch = usersWithAccessError.match( |
| 48 | + /OBP-(\d+):.*missing one or more roles:\s*(.+)/i, |
| 49 | + ); |
| 50 | + if (missingRoleMatch) { |
| 51 | + const roles = missingRoleMatch[2].split(",").map((r: string) => r.trim()); |
| 52 | + return { type: "missing_role" as const, code: missingRoleMatch[1], roles, message: usersWithAccessError }; |
| 53 | + } |
| 54 | + return { type: "general" as const, message: usersWithAccessError }; |
| 55 | + }); |
| 56 | +
|
| 57 | + // Group users by view_id and access_source for display in Views Available |
| 58 | + let usersByView = $derived.by(() => { |
| 59 | + if (!usersWithAccess?.users) return new Map<string, { direct: string[]; abac: string[] }>(); |
| 60 | + const map = new Map<string, { direct: string[]; abac: string[] }>(); |
| 61 | + for (const user of usersWithAccess.users) { |
| 62 | + if (!user.views) continue; |
| 63 | + for (const view of user.views) { |
| 64 | + if (!map.has(view.view_id)) { |
| 65 | + map.set(view.view_id, { direct: [], abac: [] }); |
| 66 | + } |
| 67 | + const entry = map.get(view.view_id)!; |
| 68 | + const name = user.username || user.user_id || "Unknown"; |
| 69 | + if (view.access_source === "ABAC") { |
| 70 | + entry.abac.push(name); |
| 71 | + } else { |
| 72 | + entry.direct.push(name); |
| 73 | + } |
| 74 | + } |
| 75 | + } |
| 76 | + return map; |
| 77 | + }); |
| 78 | +
|
41 | 79 | let bankId = $derived(page.params.bank_id); |
42 | 80 | let accountId = $derived(page.params.account_id); |
43 | 81 | let viewId = $derived(page.params.view_id); |
|
92 | 130 | } |
93 | 131 | } |
94 | 132 |
|
| 133 | + async function fetchUsersWithAccess(bankId: string, accountId: string) { |
| 134 | + usersWithAccessLoading = true; |
| 135 | + usersWithAccessError = null; |
| 136 | + try { |
| 137 | + const res = await trackedFetch( |
| 138 | + `/api/obp/banks/${encodeURIComponent(bankId)}/accounts/${encodeURIComponent(accountId)}/users-with-access` |
| 139 | + ); |
| 140 | + if (!res.ok) { |
| 141 | + const data = await res.json().catch(() => ({})); |
| 142 | + throw new Error(data.error || "Failed to fetch users with account access"); |
| 143 | + } |
| 144 | + usersWithAccess = await res.json(); |
| 145 | + } catch (err) { |
| 146 | + usersWithAccessError = err instanceof Error ? err.message : "Failed to fetch users with account access"; |
| 147 | + usersWithAccess = null; |
| 148 | + } finally { |
| 149 | + usersWithAccessLoading = false; |
| 150 | + } |
| 151 | + } |
| 152 | +
|
95 | 153 | async function loadPage(bankId: string, accountId: string, viewId: string) { |
96 | 154 | account = null; |
97 | 155 | error = null; |
| 156 | + usersWithAccess = null; |
98 | 157 | await checkAccountAccess(bankId, accountId, viewId); |
99 | 158 | if (hasAccountAccess) { |
100 | | - await fetchAccount(bankId, accountId, viewId); |
| 159 | + await Promise.all([ |
| 160 | + fetchAccount(bankId, accountId, viewId), |
| 161 | + fetchUsersWithAccess(bankId, accountId) |
| 162 | + ]); |
101 | 163 | } |
102 | 164 | } |
103 | 165 |
|
|
350 | 412 | <h2 class="section-title"> |
351 | 413 | Views Available ({account.views_available.length}) |
352 | 414 | </h2> |
353 | | - <div class="views-list"> |
| 415 | + {#if usersWithAccessError} |
| 416 | + {#if usersWithAccessParsedError && usersWithAccessParsedError.type === "missing_role"} |
| 417 | + <MissingRoleAlert |
| 418 | + roles={usersWithAccessParsedError.roles} |
| 419 | + errorCode={usersWithAccessParsedError.code} |
| 420 | + message={usersWithAccessParsedError.message} |
| 421 | + bankId={bankId} |
| 422 | + /> |
| 423 | + {:else} |
| 424 | + <div class="users-access-error"> |
| 425 | + {usersWithAccessError} |
| 426 | + </div> |
| 427 | + {/if} |
| 428 | + {/if} |
| 429 | + <div class="views-table"> |
| 430 | + <div class="views-table-header"> |
| 431 | + <div class="views-col-name">View</div> |
| 432 | + <div class="views-col-users">Direct</div> |
| 433 | + <div class="views-col-users">ABAC</div> |
| 434 | + </div> |
354 | 435 | {#each account.views_available as view} |
355 | | - <div class="view-item"> |
356 | | - <span class="view-name">{view.short_name || view.id}</span> |
357 | | - {#if view.description} |
358 | | - <span class="view-description">{view.description}</span> |
359 | | - {/if} |
360 | | - {#if view.is_public} |
361 | | - <span class="view-badge public">PUBLIC</span> |
362 | | - {/if} |
| 436 | + {@const viewUsers = usersByView.get(view.id)} |
| 437 | + <div class="views-table-row"> |
| 438 | + <div class="views-col-name"> |
| 439 | + <span class="view-name">{view.short_name || view.id}</span> |
| 440 | + {#if view.description} |
| 441 | + <span class="view-description">{view.description}</span> |
| 442 | + {/if} |
| 443 | + {#if view.is_public} |
| 444 | + <span class="view-badge public">PUBLIC</span> |
| 445 | + {/if} |
| 446 | + </div> |
| 447 | + <div class="views-col-users"> |
| 448 | + {#if usersWithAccessLoading} |
| 449 | + <Loader2 size={14} class="spinner-icon" /> |
| 450 | + {:else if viewUsers?.direct?.length} |
| 451 | + {#each viewUsers.direct as username} |
| 452 | + <span class="user-chip direct">{username}</span> |
| 453 | + {/each} |
| 454 | + {:else if !usersWithAccessError} |
| 455 | + <span class="no-users">—</span> |
| 456 | + {/if} |
| 457 | + </div> |
| 458 | + <div class="views-col-users"> |
| 459 | + {#if usersWithAccessLoading} |
| 460 | + <Loader2 size={14} class="spinner-icon" /> |
| 461 | + {:else if viewUsers?.abac?.length} |
| 462 | + {#each viewUsers.abac as username} |
| 463 | + <span class="user-chip abac">{username}</span> |
| 464 | + {/each} |
| 465 | + {:else if !usersWithAccessError} |
| 466 | + <span class="no-users">—</span> |
| 467 | + {/if} |
| 468 | + </div> |
363 | 469 | </div> |
364 | 470 | {/each} |
365 | 471 | </div> |
|
810 | 916 | color: var(--color-surface-300); |
811 | 917 | } |
812 | 918 |
|
| 919 | + /* Users with access error */ |
| 920 | + .users-access-error { |
| 921 | + padding: 0.75rem 1rem; |
| 922 | + margin-bottom: 1rem; |
| 923 | + background: #fef2f2; |
| 924 | + border: 1px solid #fca5a5; |
| 925 | + border-radius: 6px; |
| 926 | + color: #991b1b; |
| 927 | + font-size: 0.875rem; |
| 928 | + } |
| 929 | +
|
| 930 | + :global([data-mode="dark"]) .users-access-error { |
| 931 | + background: rgba(220, 38, 38, 0.1); |
| 932 | + border-color: rgba(220, 38, 38, 0.3); |
| 933 | + color: rgb(var(--color-error-300)); |
| 934 | + } |
| 935 | +
|
813 | 936 | /* Routings */ |
814 | 937 | .routings-list { |
815 | 938 | display: flex; |
|
935 | 1058 | color: rgb(var(--color-warning-300)); |
936 | 1059 | } |
937 | 1060 |
|
938 | | - /* Views */ |
939 | | - .views-list { |
| 1061 | + /* Views table */ |
| 1062 | + .views-table { |
| 1063 | + border: 1px solid #e5e7eb; |
| 1064 | + border-radius: 6px; |
| 1065 | + overflow: hidden; |
| 1066 | + } |
| 1067 | +
|
| 1068 | + :global([data-mode="dark"]) .views-table { |
| 1069 | + border-color: rgb(var(--color-surface-700)); |
| 1070 | + } |
| 1071 | +
|
| 1072 | + .views-table-header { |
| 1073 | + display: grid; |
| 1074 | + grid-template-columns: 1fr 1fr 1fr; |
| 1075 | + gap: 0; |
| 1076 | + background: #f3f4f6; |
| 1077 | + border-bottom: 1px solid #e5e7eb; |
| 1078 | + font-size: 0.75rem; |
| 1079 | + font-weight: 600; |
| 1080 | + color: #6b7280; |
| 1081 | + text-transform: uppercase; |
| 1082 | + letter-spacing: 0.05em; |
| 1083 | + } |
| 1084 | +
|
| 1085 | + :global([data-mode="dark"]) .views-table-header { |
| 1086 | + background: rgb(var(--color-surface-900)); |
| 1087 | + border-bottom-color: rgb(var(--color-surface-700)); |
| 1088 | + color: var(--color-surface-400); |
| 1089 | + } |
| 1090 | +
|
| 1091 | + .views-table-header > div { |
| 1092 | + padding: 0.625rem 1rem; |
| 1093 | + } |
| 1094 | +
|
| 1095 | + .views-table-row { |
| 1096 | + display: grid; |
| 1097 | + grid-template-columns: 1fr 1fr 1fr; |
| 1098 | + gap: 0; |
| 1099 | + border-bottom: 1px solid #e5e7eb; |
| 1100 | + } |
| 1101 | +
|
| 1102 | + .views-table-row:last-child { |
| 1103 | + border-bottom: none; |
| 1104 | + } |
| 1105 | +
|
| 1106 | + :global([data-mode="dark"]) .views-table-row { |
| 1107 | + border-bottom-color: rgb(var(--color-surface-700)); |
| 1108 | + } |
| 1109 | +
|
| 1110 | + .views-col-name { |
| 1111 | + padding: 0.75rem 1rem; |
940 | 1112 | display: flex; |
941 | | - flex-direction: column; |
| 1113 | + align-items: center; |
942 | 1114 | gap: 0.5rem; |
| 1115 | + flex-wrap: wrap; |
943 | 1116 | } |
944 | 1117 |
|
945 | | - .view-item { |
| 1118 | + .views-col-users { |
| 1119 | + padding: 0.75rem 1rem; |
946 | 1120 | display: flex; |
947 | 1121 | align-items: center; |
948 | | - gap: 0.75rem; |
949 | | - padding: 0.75rem 1rem; |
950 | | - background: #fafafa; |
951 | | - border: 1px solid #e5e7eb; |
952 | | - border-radius: 6px; |
| 1122 | + flex-wrap: wrap; |
| 1123 | + gap: 0.375rem; |
953 | 1124 | } |
954 | 1125 |
|
955 | | - :global([data-mode="dark"]) .view-item { |
956 | | - background: rgb(var(--color-surface-900)); |
957 | | - border-color: rgb(var(--color-surface-700)); |
| 1126 | + .views-col-users :global(.spinner-icon) { |
| 1127 | + animation: spin 1s linear infinite; |
| 1128 | + color: #9ca3af; |
| 1129 | + } |
| 1130 | +
|
| 1131 | + :global([data-mode="dark"]) .views-col-users :global(.spinner-icon) { |
| 1132 | + color: var(--color-surface-500); |
958 | 1133 | } |
959 | 1134 |
|
960 | 1135 | .view-name { |
|
970 | 1145 | .view-description { |
971 | 1146 | font-size: 0.8rem; |
972 | 1147 | color: #6b7280; |
973 | | - flex: 1; |
974 | 1148 | } |
975 | 1149 |
|
976 | 1150 | :global([data-mode="dark"]) .view-description { |
|
982 | 1156 | padding: 0.125rem 0.5rem; |
983 | 1157 | border-radius: 9999px; |
984 | 1158 | font-weight: 600; |
985 | | - margin-left: auto; |
986 | 1159 | } |
987 | 1160 |
|
988 | 1161 | .view-badge.public { |
|
995 | 1168 | color: rgb(var(--color-success-300)); |
996 | 1169 | } |
997 | 1170 |
|
998 | | - .view-badge.private { |
999 | | - background: #fee2e2; |
1000 | | - color: #991b1b; |
| 1171 | + .user-chip { |
| 1172 | + display: inline-block; |
| 1173 | + font-size: 0.75rem; |
| 1174 | + padding: 0.15rem 0.5rem; |
| 1175 | + border-radius: 4px; |
| 1176 | + font-weight: 500; |
1001 | 1177 | } |
1002 | 1178 |
|
1003 | | - :global([data-mode="dark"]) .view-badge.private { |
1004 | | - background: rgba(239, 68, 68, 0.2); |
1005 | | - color: rgb(var(--color-error-300)); |
| 1179 | + .user-chip.direct { |
| 1180 | + background: #dbeafe; |
| 1181 | + color: #1e40af; |
| 1182 | + } |
| 1183 | +
|
| 1184 | + :global([data-mode="dark"]) .user-chip.direct { |
| 1185 | + background: rgba(59, 130, 246, 0.2); |
| 1186 | + color: rgb(var(--color-primary-300)); |
| 1187 | + } |
| 1188 | +
|
| 1189 | + .user-chip.abac { |
| 1190 | + background: #fef3c7; |
| 1191 | + color: #92400e; |
| 1192 | + } |
| 1193 | +
|
| 1194 | + :global([data-mode="dark"]) .user-chip.abac { |
| 1195 | + background: rgba(245, 158, 11, 0.2); |
| 1196 | + color: rgb(var(--color-warning-300)); |
| 1197 | + } |
| 1198 | +
|
| 1199 | + .no-users { |
| 1200 | + color: #d1d5db; |
| 1201 | + font-size: 0.875rem; |
| 1202 | + } |
| 1203 | +
|
| 1204 | + :global([data-mode="dark"]) .no-users { |
| 1205 | + color: var(--color-surface-600); |
1006 | 1206 | } |
1007 | 1207 |
|
1008 | 1208 | /* Empty / Loading / Error states */ |
|
0 commit comments