@isi-ui7/bos7-shared 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/dist/api.d.ts +4 -0
- package/dist/auth7/client.d.ts +26 -0
- package/dist/auth7/delegated-proxy.d.ts +33 -0
- package/dist/auth7/hooks/useAuth.d.ts +10 -0
- package/dist/auth7/hooks/useSession.d.ts +12 -0
- package/dist/auth7/index.d.ts +12 -0
- package/dist/auth7/provider.d.ts +13 -0
- package/dist/auth7/scope-guard.d.ts +5 -0
- package/dist/auth7/service-account.d.ts +2 -0
- package/dist/auth7/token-exchange.d.ts +11 -0
- package/dist/auth7/types.d.ts +63 -0
- package/dist/backend.d.ts +20 -0
- package/dist/crud-components.d.ts +47 -0
- package/dist/crud-hooks.d.ts +15 -0
- package/dist/crud-types.d.ts +86 -0
- package/dist/data-table/cursor-adapter.d.ts +74 -0
- package/dist/event-bus/index.d.ts +20 -0
- package/dist/event-bus/useEventBus.d.ts +21 -0
- package/dist/event-bus.d.ts +1 -0
- package/dist/export.d.ts +7 -0
- package/dist/form-layout.d.ts +39 -0
- package/dist/form-numeric.d.ts +35 -0
- package/dist/form-renderer-utils.d.ts +10 -0
- package/dist/form-renderer.d.ts +18 -0
- package/dist/form-rules.d.ts +12 -0
- package/dist/form-types.d.ts +130 -0
- package/dist/html2canvas.esm-d2sM-0Wm.js +4870 -0
- package/dist/i18n.d.ts +35 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.es-Bylk5fh-.js +5649 -0
- package/dist/index.js +20779 -0
- package/dist/jspdf.es.min-Dzm5j8wC.js +9977 -0
- package/dist/jspdf.plugin.autotable-DcntYaes.js +1078 -0
- package/dist/modal-layout.d.ts +45 -0
- package/dist/nats/client.d.ts +54 -0
- package/dist/notif7.d.ts +21 -0
- package/dist/notification/routing.d.ts +4 -0
- package/dist/notifications-bff.d.ts +22 -0
- package/dist/observability/otel-init.d.ts +1 -0
- package/dist/page-layout.d.ts +24 -0
- package/dist/purify.es-CiEWEeUM.js +605 -0
- package/dist/shell/app-shell-layout.d.ts +70 -0
- package/dist/shell/branch.d.ts +6 -0
- package/dist/shell/interaction.d.ts +2 -0
- package/dist/shell/provider.d.ts +15 -0
- package/dist/style-contract.d.ts +72 -0
- package/dist/types/core.d.ts +39 -0
- package/dist/types/notification.d.ts +30 -0
- package/dist/workflow/api-client.d.ts +2 -0
- package/dist/workflow/backend.d.ts +9 -0
- package/dist/workflow/crud-client.d.ts +3 -0
- package/dist/workflow/notif7.d.ts +6 -0
- package/dist/workflow/starter.d.ts +44 -0
- package/dist/workflow/use-crud-form.d.ts +27 -0
- package/dist/workflow/use-workflow-tracker.d.ts +40 -0
- package/package.json +92 -0
- package/src/api.ts +40 -0
- package/src/auth7/client.ts +248 -0
- package/src/auth7/delegated-proxy.ts +80 -0
- package/src/auth7/hooks/useAuth.ts +21 -0
- package/src/auth7/hooks/useSession.ts +75 -0
- package/src/auth7/index.ts +14 -0
- package/src/auth7/provider.tsx +256 -0
- package/src/auth7/scope-guard.ts +54 -0
- package/src/auth7/service-account.ts +71 -0
- package/src/auth7/token-exchange.ts +125 -0
- package/src/auth7/types.ts +72 -0
- package/src/backend.ts +96 -0
- package/src/crud-components.tsx +580 -0
- package/src/crud-hooks.test.ts +32 -0
- package/src/crud-hooks.ts +31 -0
- package/src/crud-types.ts +64 -0
- package/src/data-table/cursor-adapter.ts +239 -0
- package/src/event-bus/index.ts +90 -0
- package/src/event-bus/useEventBus.ts +93 -0
- package/src/event-bus.ts +1 -0
- package/src/export.ts +62 -0
- package/src/form-contract.css +52 -0
- package/src/form-layout.tsx +90 -0
- package/src/form-numeric.ts +173 -0
- package/src/form-renderer-utils.ts +139 -0
- package/src/form-renderer.tsx +624 -0
- package/src/form-rules.ts +54 -0
- package/src/form-types.ts +126 -0
- package/src/i18n.tsx +92 -0
- package/src/index.ts +20 -0
- package/src/modal-layout.tsx +60 -0
- package/src/nats/client.ts +130 -0
- package/src/notif7.ts +76 -0
- package/src/notification/routing.ts +64 -0
- package/src/notifications-bff.ts +273 -0
- package/src/observability/otel-init.ts +41 -0
- package/src/page-layout.tsx +69 -0
- package/src/shell/app-shell-layout.tsx +494 -0
- package/src/shell/branch.ts +48 -0
- package/src/shell/interaction.ts +17 -0
- package/src/shell/provider.tsx +34 -0
- package/src/style-contract.test.ts +59 -0
- package/src/style-contract.ts +132 -0
- package/src/types/core.ts +46 -0
- package/src/types/notification.ts +35 -0
- package/src/workflow/api-client.ts +7 -0
- package/src/workflow/backend.ts +53 -0
- package/src/workflow/crud-client.ts +43 -0
- package/src/workflow/notif7.ts +28 -0
- package/src/workflow/starter.ts +153 -0
- package/src/workflow/use-crud-form.ts +176 -0
- package/src/workflow/use-workflow-tracker.ts +109 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { getUserIDFromRequest, NOTIF7_URL, notif7Fetch } from './notif7';
|
|
2
|
+
|
|
3
|
+
type Notif7Raw = {
|
|
4
|
+
ID: string;
|
|
5
|
+
UserID?: string;
|
|
6
|
+
EventType?: string;
|
|
7
|
+
Title?: string;
|
|
8
|
+
Body?: string;
|
|
9
|
+
IsRead: boolean;
|
|
10
|
+
RefURL?: string;
|
|
11
|
+
CreatedAt: string;
|
|
12
|
+
Payload?: Record<string, unknown>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type ReqLike = Request & {
|
|
16
|
+
cookies?: { get?: (name: string) => { value?: string } | undefined };
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const DEFAULT_TASK_EVENT_TYPES = new Set([
|
|
20
|
+
'task.assign',
|
|
21
|
+
'task.assigned',
|
|
22
|
+
'task.delegated_to_you',
|
|
23
|
+
'task.pool_completed',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
function getAccessToken(req: ReqLike | Request): string | undefined {
|
|
27
|
+
return (req as ReqLike).cookies?.get?.('access_token')?.value
|
|
28
|
+
?? (req.headers.get('cookie') ?? '')
|
|
29
|
+
.split(';')
|
|
30
|
+
.find((c) => c.trim().startsWith('access_token='))
|
|
31
|
+
?.split('=').slice(1).join('=')
|
|
32
|
+
.trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function extractTaskId(n: Notif7Raw): string | undefined {
|
|
36
|
+
const payload = n.Payload;
|
|
37
|
+
if (!payload) return undefined;
|
|
38
|
+
const candidates = [payload.task_id, payload.taskId, payload.taskID, payload.id, payload.ref_id];
|
|
39
|
+
for (const value of candidates) {
|
|
40
|
+
if (typeof value === 'string' && value.trim() !== '') return value;
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Informational event types that should render in the bell panel without a
|
|
46
|
+
// link target. `instance.completed`/`instance.cancelled` for auto-approval
|
|
47
|
+
// flows are status updates the user just acknowledges — there's no actionable
|
|
48
|
+
// follow-up screen, so a clickable link only adds noise (and dead-ends at
|
|
49
|
+
// /inbox/history when the user isn't a workflow operator).
|
|
50
|
+
const INFO_ONLY_EVENT_TYPES = new Set([
|
|
51
|
+
'instance.completed',
|
|
52
|
+
'instance.cancelled',
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
function resolveActionHref(
|
|
56
|
+
n: Notif7Raw,
|
|
57
|
+
workflowBaseUrl: string,
|
|
58
|
+
taskEventTypes: Set<string>
|
|
59
|
+
): string | undefined {
|
|
60
|
+
const eventType = (n.EventType || '').toLowerCase();
|
|
61
|
+
if (INFO_ONLY_EVENT_TYPES.has(eventType)) return undefined;
|
|
62
|
+
if (!taskEventTypes.has(eventType)) return n.RefURL || undefined;
|
|
63
|
+
const taskId = extractTaskId(n);
|
|
64
|
+
return taskId ? `${workflowBaseUrl}/inbox/${taskId}` : `${workflowBaseUrl}/inbox`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface NotificationBffOptions {
|
|
68
|
+
workflowBaseUrl?: string;
|
|
69
|
+
taskEventTypes?: Set<string>;
|
|
70
|
+
includeUserID?: boolean;
|
|
71
|
+
graceful?: boolean;
|
|
72
|
+
tableActionField?: 'action_url' | 'ref_url';
|
|
73
|
+
bellTitleMap?: Record<string, string>;
|
|
74
|
+
bellTimestampLocale?: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function handleNotificationsGet(req: Request, options: NotificationBffOptions = {}): Promise<Response> {
|
|
78
|
+
const workflowBaseUrl = options.workflowBaseUrl || process.env.NEXT_PUBLIC_BOS7_WORKFLOW_URL || 'https://workflow.bos7.local';
|
|
79
|
+
const taskEventTypes = options.taskEventTypes || DEFAULT_TASK_EVENT_TYPES;
|
|
80
|
+
const includeUserID = options.includeUserID ?? true;
|
|
81
|
+
const graceful = options.graceful ?? true;
|
|
82
|
+
const tableActionField = options.tableActionField || 'action_url';
|
|
83
|
+
const bellTitleMap = options.bellTitleMap || {};
|
|
84
|
+
const bellTimestampLocale = options.bellTimestampLocale ?? false;
|
|
85
|
+
|
|
86
|
+
const url = new URL(req.url);
|
|
87
|
+
const searchParams = url.searchParams;
|
|
88
|
+
const isOffsetMode = searchParams.has('page') || searchParams.has('per_page');
|
|
89
|
+
const accessToken = getAccessToken(req);
|
|
90
|
+
const userID = includeUserID ? getUserIDFromRequest(req as ReqLike) : undefined;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
if (isOffsetMode) {
|
|
94
|
+
const page = Math.max(1, Number(searchParams.get('page') ?? '1'));
|
|
95
|
+
const perPage = Math.max(1, Number(searchParams.get('per_page') ?? '20'));
|
|
96
|
+
const isReadFilter = searchParams.get('is_read');
|
|
97
|
+
|
|
98
|
+
const query = new URLSearchParams({
|
|
99
|
+
limit: String(perPage),
|
|
100
|
+
offset: String((page - 1) * perPage),
|
|
101
|
+
});
|
|
102
|
+
if (isReadFilter) query.set('is_read', isReadFilter);
|
|
103
|
+
|
|
104
|
+
const listRes = await notif7Fetch('GET', '/api/v1/notifications', { userID, accessToken, query });
|
|
105
|
+
if (!listRes.ok) {
|
|
106
|
+
return Response.json({ data: [], total: 0, page, per_page: perPage, total_pages: 0 });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const { data: raw, total = 0 } = await listRes.json() as { data: Notif7Raw[]; total?: number };
|
|
110
|
+
const data = (raw ?? []).map((n) => {
|
|
111
|
+
const actionHref = resolveActionHref(n, workflowBaseUrl, taskEventTypes);
|
|
112
|
+
if (tableActionField === 'ref_url') {
|
|
113
|
+
return {
|
|
114
|
+
id: n.ID,
|
|
115
|
+
title: n.Title,
|
|
116
|
+
message: n.Body || '',
|
|
117
|
+
event_type: n.EventType || '',
|
|
118
|
+
is_read: n.IsRead,
|
|
119
|
+
created_at: n.CreatedAt,
|
|
120
|
+
ref_url: actionHref,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
id: n.ID,
|
|
125
|
+
user_id: n.UserID,
|
|
126
|
+
event_type: n.EventType,
|
|
127
|
+
title: n.Title,
|
|
128
|
+
message: n.Body,
|
|
129
|
+
is_read: n.IsRead,
|
|
130
|
+
action_url: actionHref,
|
|
131
|
+
created_at: n.CreatedAt,
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
return Response.json({ data, total, page, per_page: perPage, total_pages: Math.ceil(total / perPage) || 1 });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const [listRes, countRes] = await Promise.all([
|
|
138
|
+
notif7Fetch('GET', '/api/v1/notifications', {
|
|
139
|
+
userID,
|
|
140
|
+
accessToken,
|
|
141
|
+
query: new URLSearchParams({ limit: '20' }),
|
|
142
|
+
}),
|
|
143
|
+
notif7Fetch('GET', '/api/v1/notifications/unread-count', { userID, accessToken }),
|
|
144
|
+
]);
|
|
145
|
+
|
|
146
|
+
if (!listRes.ok || !countRes.ok) {
|
|
147
|
+
return Response.json({ notifications: [], badgeCount: 0 });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const { data: rawNotifs } = await listRes.json() as { data: Notif7Raw[] };
|
|
151
|
+
const { unread_count } = await countRes.json() as { unread_count?: number };
|
|
152
|
+
|
|
153
|
+
const notifications = (rawNotifs ?? []).map((n) => {
|
|
154
|
+
const eventType = (n.EventType ?? '').toLowerCase();
|
|
155
|
+
const ts = bellTimestampLocale
|
|
156
|
+
? new Date(n.CreatedAt).toLocaleString('id-ID', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' })
|
|
157
|
+
: n.CreatedAt;
|
|
158
|
+
return {
|
|
159
|
+
id: n.ID,
|
|
160
|
+
title: n.Title ?? bellTitleMap[eventType] ?? n.EventType,
|
|
161
|
+
description: n.Body,
|
|
162
|
+
timestamp: ts,
|
|
163
|
+
read: n.IsRead,
|
|
164
|
+
actionHref: resolveActionHref(n, workflowBaseUrl, taskEventTypes),
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return Response.json({ notifications, badgeCount: unread_count ?? 0 });
|
|
169
|
+
} catch {
|
|
170
|
+
if (!graceful) return Response.json({ error: 'notification backend error' }, { status: 502 });
|
|
171
|
+
if (isOffsetMode) {
|
|
172
|
+
const page = Math.max(1, Number(searchParams.get('page') ?? '1'));
|
|
173
|
+
const perPage = Math.max(1, Number(searchParams.get('per_page') ?? '20'));
|
|
174
|
+
return Response.json({ data: [], total: 0, page, per_page: perPage, total_pages: 0 });
|
|
175
|
+
}
|
|
176
|
+
return Response.json({ notifications: [], badgeCount: 0 });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function handleNotificationsMarkAllRead(req: Request, options: { includeUserID?: boolean; path?: string; graceful?: boolean } = {}): Promise<Response> {
|
|
181
|
+
const includeUserID = options.includeUserID ?? true;
|
|
182
|
+
const path = options.path || '/api/v1/notifications/read-all';
|
|
183
|
+
const graceful = options.graceful ?? false;
|
|
184
|
+
const accessToken = getAccessToken(req);
|
|
185
|
+
const userID = includeUserID ? getUserIDFromRequest(req as ReqLike) : undefined;
|
|
186
|
+
try {
|
|
187
|
+
const res = await notif7Fetch('POST', path, { userID, accessToken, body: '{}' });
|
|
188
|
+
const json = await res.json().catch(() => ({}));
|
|
189
|
+
return Response.json(json, { status: res.status });
|
|
190
|
+
} catch {
|
|
191
|
+
if (graceful) return Response.json({ ok: true });
|
|
192
|
+
return Response.json({ ok: false }, { status: 502 });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function handleNotificationsReadOne(req: Request, id: string, options: { includeUserID?: boolean; graceful?: boolean } = {}): Promise<Response> {
|
|
197
|
+
const includeUserID = options.includeUserID ?? true;
|
|
198
|
+
const graceful = options.graceful ?? false;
|
|
199
|
+
const accessToken = getAccessToken(req);
|
|
200
|
+
const userID = includeUserID ? getUserIDFromRequest(req as ReqLike) : undefined;
|
|
201
|
+
try {
|
|
202
|
+
const res = await notif7Fetch('PATCH', `/api/v1/notifications/${id}/read`, { userID, accessToken });
|
|
203
|
+
const json = await res.json().catch(() => ({}));
|
|
204
|
+
return Response.json(json, { status: res.ok ? 200 : res.status });
|
|
205
|
+
} catch {
|
|
206
|
+
if (graceful) return Response.json({ ok: true });
|
|
207
|
+
return Response.json({ ok: false }, { status: 502 });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export async function handleNotificationsStream(req: Request, options: { preferAccessToken?: boolean } = {}): Promise<Response> {
|
|
212
|
+
void options; // preferAccessToken retained for backward-compat; no longer used
|
|
213
|
+
const accessToken = getAccessToken(req);
|
|
214
|
+
|
|
215
|
+
if (!accessToken) {
|
|
216
|
+
return new Response(
|
|
217
|
+
JSON.stringify({ error: { code: 'MISSING_TOKEN', message: 'access_token cookie required for SSE stream' } }),
|
|
218
|
+
{ status: 401, headers: { 'Content-Type': 'application/json' } },
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
const token = accessToken;
|
|
222
|
+
|
|
223
|
+
const upstreamRes = await fetch(`${NOTIF7_URL}/api/v1/stream`, {
|
|
224
|
+
headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}) },
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
if (!upstreamRes.ok || !upstreamRes.body) {
|
|
228
|
+
return new Response('data: {"type":"error"}\n\n', {
|
|
229
|
+
headers: { 'Content-Type': 'text/event-stream' },
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Pipe upstream notif7 SSE bytes to the client through an explicit
|
|
234
|
+
// TransformStream. Returning upstreamRes.body directly relies on Node's
|
|
235
|
+
// fetch ReadableStream propagating chunks immediately, which doesn't happen
|
|
236
|
+
// reliably under Next.js dev; the TransformStream forces per-chunk flushing.
|
|
237
|
+
//
|
|
238
|
+
// Also injects a synthetic `{type:"new_notification"}` envelope after each
|
|
239
|
+
// notif7 event chunk so `@isi-ui7/realtime` useNotificationBell — which
|
|
240
|
+
// expects {type:"badge_sync"|"new_notification"} — fires its onMessage and
|
|
241
|
+
// refreshes the badge. The original notif7 chunk is forwarded untouched so
|
|
242
|
+
// page-level listeners that read EventType/RefID still work.
|
|
243
|
+
const decoder = new TextDecoder();
|
|
244
|
+
const encoder = new TextEncoder();
|
|
245
|
+
const SYNTHETIC = encoder.encode('data: {"type":"new_notification"}\n\n');
|
|
246
|
+
const transform = new TransformStream<Uint8Array, Uint8Array>({
|
|
247
|
+
transform(chunk, controller) {
|
|
248
|
+
controller.enqueue(chunk);
|
|
249
|
+
// Heuristic: any chunk that begins with `data: {` and contains a notif7
|
|
250
|
+
// `EventType` field is a real notification. Heartbeats start with `:`
|
|
251
|
+
// and the initial `: connected\n\n` is also a comment — skip both.
|
|
252
|
+
const text = decoder.decode(chunk, { stream: true });
|
|
253
|
+
if (text.startsWith('data: {') && text.includes('"EventType"')) {
|
|
254
|
+
controller.enqueue(SYNTHETIC);
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
void upstreamRes.body.pipeTo(transform.writable).catch(() => {
|
|
259
|
+
/* upstream closed — TransformStream will close client side */
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
return new Response(transform.readable, {
|
|
263
|
+
headers: {
|
|
264
|
+
'Content-Type': 'text/event-stream',
|
|
265
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
266
|
+
Connection: 'keep-alive',
|
|
267
|
+
// Without X-Accel-Buffering, some proxies/runtimes buffer SSE chunks until
|
|
268
|
+
// the connection closes — live events never reach the browser, only the
|
|
269
|
+
// catch-up replay on reconnect. Setting `no` forces flush-per-chunk.
|
|
270
|
+
'X-Accel-Buffering': 'no',
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// OTel init untuk Next.js BFF — dipanggil sekali dari instrumentation.ts setiap app.
|
|
2
|
+
// Tidak ada side-effect jika OTEL_EXPORTER_OTLP_ENDPOINT tidak di-set.
|
|
3
|
+
//
|
|
4
|
+
// Usage di instrumentation.ts (root dari Next.js project):
|
|
5
|
+
// export async function register() {
|
|
6
|
+
// if (process.env.NEXT_RUNTIME === 'nodejs') {
|
|
7
|
+
// const { initOtel } = await import('@isi-ui7/bos7-shared')
|
|
8
|
+
// await initOtel(process.env.OTEL_SERVICE_NAME)
|
|
9
|
+
// }
|
|
10
|
+
// }
|
|
11
|
+
let initialized = false
|
|
12
|
+
|
|
13
|
+
export async function initOtel(serviceName?: string): Promise<void> {
|
|
14
|
+
if (initialized) return
|
|
15
|
+
initialized = true
|
|
16
|
+
|
|
17
|
+
const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT
|
|
18
|
+
if (!endpoint) return // no-op jika endpoint tidak dikonfigurasi
|
|
19
|
+
|
|
20
|
+
const [
|
|
21
|
+
{ NodeSDK },
|
|
22
|
+
{ OTLPTraceExporter },
|
|
23
|
+
{ getNodeAutoInstrumentations },
|
|
24
|
+
] = await Promise.all([
|
|
25
|
+
import('@opentelemetry/sdk-node'),
|
|
26
|
+
import('@opentelemetry/exporter-trace-otlp-http'),
|
|
27
|
+
import('@opentelemetry/auto-instrumentations-node'),
|
|
28
|
+
])
|
|
29
|
+
|
|
30
|
+
const sdk = new NodeSDK({
|
|
31
|
+
serviceName: serviceName ?? process.env.OTEL_SERVICE_NAME ?? 'bos7-bff',
|
|
32
|
+
traceExporter: new OTLPTraceExporter({ url: `${endpoint}/v1/traces` }),
|
|
33
|
+
instrumentations: [
|
|
34
|
+
getNodeAutoInstrumentations({
|
|
35
|
+
'@opentelemetry/instrumentation-fs': { enabled: false }, // terlalu verbose
|
|
36
|
+
}),
|
|
37
|
+
],
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
sdk.start()
|
|
41
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
type BreadcrumbItem = {
|
|
4
|
+
label: string;
|
|
5
|
+
href?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function PageLayout({ children }: { children: ReactNode }) {
|
|
9
|
+
return <div className="page">{children}</div>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function PageHeader({
|
|
13
|
+
title,
|
|
14
|
+
subtitle,
|
|
15
|
+
titleExtra,
|
|
16
|
+
breadcrumbs,
|
|
17
|
+
children,
|
|
18
|
+
}: {
|
|
19
|
+
title: string;
|
|
20
|
+
subtitle?: string;
|
|
21
|
+
titleExtra?: ReactNode;
|
|
22
|
+
breadcrumbs?: BreadcrumbItem[];
|
|
23
|
+
children?: ReactNode;
|
|
24
|
+
}) {
|
|
25
|
+
return (
|
|
26
|
+
<div className="page__header">
|
|
27
|
+
<div>
|
|
28
|
+
{breadcrumbs && breadcrumbs.length > 0 ? (
|
|
29
|
+
<div className="page__breadcrumb" aria-label="Breadcrumb">
|
|
30
|
+
{breadcrumbs.map((item, index) => (
|
|
31
|
+
<span key={`${item.label}-${index}`} className="page__breadcrumb-segment">
|
|
32
|
+
{index > 0 ? <span>/</span> : null}
|
|
33
|
+
<span className="page__breadcrumb-current">{item.label}</span>
|
|
34
|
+
</span>
|
|
35
|
+
))}
|
|
36
|
+
</div>
|
|
37
|
+
) : null}
|
|
38
|
+
{titleExtra ? (
|
|
39
|
+
<div className="page__title-row">
|
|
40
|
+
<h2 className="page__title">{title}</h2>
|
|
41
|
+
{titleExtra}
|
|
42
|
+
</div>
|
|
43
|
+
) : (
|
|
44
|
+
<h2 className="page__title">{title}</h2>
|
|
45
|
+
)}
|
|
46
|
+
{subtitle && <p className="page__subtitle">{subtitle}</p>}
|
|
47
|
+
</div>
|
|
48
|
+
{children && <div className="page__actions">{children}</div>}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function PageBody({ children }: { children: ReactNode }) {
|
|
54
|
+
return <div className="page__body">{children}</div>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function PageFooter({
|
|
58
|
+
children,
|
|
59
|
+
spread = false,
|
|
60
|
+
}: {
|
|
61
|
+
children: ReactNode;
|
|
62
|
+
spread?: boolean;
|
|
63
|
+
}) {
|
|
64
|
+
return (
|
|
65
|
+
<div className={`page__footer${spread ? ' page__footer--spread' : ''}`}>
|
|
66
|
+
{children}
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|