Guilherme Silberfarb Costa commited on
Commit
3dc38bc
·
1 Parent(s): 918e0df

correcao de logs

Browse files
Files changed (2) hide show
  1. frontend/src/App.jsx +143 -99
  2. frontend/src/styles.css +52 -2
frontend/src/App.jsx CHANGED
@@ -6,6 +6,8 @@ import PesquisaTab from './components/PesquisaTab'
6
  import RepositorioTab from './components/RepositorioTab'
7
  import VisualizacaoTab from './components/VisualizacaoTab'
8
 
 
 
9
  const TABS = [
10
  { key: 'Início', label: 'Início', hint: 'Visão geral rápida do aplicativo' },
11
  { key: 'Pesquisa', label: 'Pesquisa', hint: 'Busca inicial de modelos .dai' },
@@ -33,11 +35,17 @@ export default function App() {
33
  const [logsError, setLogsError] = useState('')
34
  const [logsScope, setLogsScope] = useState('')
35
  const [logsUsuario, setLogsUsuario] = useState('')
36
- const [logsLimit, setLogsLimit] = useState(200)
37
 
38
  const isAdmin = String(authUser?.perfil || '').toLowerCase() === 'admin'
39
  const logsEnabled = Boolean(logsStatus?.enabled)
40
  const logsDisabledReason = String(logsStatus?.reason || 'Logs indisponíveis')
 
 
 
 
 
 
41
 
42
  function resetToLogin(message = '') {
43
  setAuthToken('')
@@ -50,6 +58,7 @@ export default function App() {
50
  setLogsOpen(false)
51
  setLogsEvents([])
52
  setLogsError('')
 
53
  setActiveTab(TABS[0].key)
54
  setAuthError(message)
55
  }
@@ -131,6 +140,7 @@ export default function App() {
131
  setLogsOpen(false)
132
  setLogsEvents([])
133
  setLogsError('')
 
134
  setLogsStatusLoading(false)
135
  setLogsLoading(false)
136
  return
@@ -138,6 +148,16 @@ export default function App() {
138
  void carregarLogsStatus()
139
  }, [authUser, isAdmin])
140
 
 
 
 
 
 
 
 
 
 
 
141
  async function onSubmitLogin(event) {
142
  event.preventDefault()
143
  setAuthError('')
@@ -191,11 +211,13 @@ export default function App() {
191
  setLogsLoading(true)
192
  setLogsError('')
193
  try {
194
- const resp = await api.logsEvents({ scope: logsScope, usuario: logsUsuario, limit: logsLimit })
195
  setLogsEvents(Array.isArray(resp?.events) ? resp.events : [])
 
196
  } catch (err) {
197
  setLogsError(err.message || 'Falha ao carregar logs.')
198
  setLogsEvents([])
 
199
  } finally {
200
  setLogsLoading(false)
201
  }
@@ -212,6 +234,10 @@ export default function App() {
212
  await carregarEventosLogs()
213
  }
214
 
 
 
 
 
215
  return (
216
  <div className="app-shell">
217
  <header className="app-header app-header-logo-only">
@@ -230,10 +256,10 @@ export default function App() {
230
  <button
231
  type="button"
232
  onClick={() => void onToggleLogs()}
233
- disabled={logsStatusLoading || !logsEnabled}
234
- title={!logsEnabled ? logsDisabledReason : 'Abrir leitura de logs'}
235
  >
236
- Logs
237
  </button>
238
  ) : null}
239
  <button type="button" onClick={() => void onLogout()}>
@@ -243,98 +269,6 @@ export default function App() {
243
  </div>
244
  ) : null}
245
 
246
- {authUser && isAdmin && logsOpen ? (
247
- <section className="logs-panel">
248
- <div className="logs-panel-head">
249
- <h3>Logs</h3>
250
- <div className="logs-panel-meta">
251
- <span>{logsStatus?.backend === 'hf_dataset' ? 'Origem: Dataset HF' : 'Origem: indisponível'}</span>
252
- {logsStatus?.revision ? <span>Revisão: {String(logsStatus.revision).slice(0, 8)}</span> : null}
253
- </div>
254
- </div>
255
-
256
- {!logsEnabled ? (
257
- <div className="section1-empty-hint">{logsDisabledReason}</div>
258
- ) : (
259
- <>
260
- <div className="logs-filters">
261
- <div className="logs-field">
262
- <label htmlFor="logs-scope">Escopo</label>
263
- <select id="logs-scope" value={logsScope} onChange={(event) => setLogsScope(event.target.value)}>
264
- <option value="">Todos</option>
265
- <option value="auth">Auth</option>
266
- <option value="repositorio">Repositório</option>
267
- <option value="elaboracao">Elaboração</option>
268
- <option value="visualizacao">Visualização</option>
269
- </select>
270
- </div>
271
- <div className="logs-field">
272
- <label htmlFor="logs-usuario">Usuário</label>
273
- <input
274
- id="logs-usuario"
275
- type="text"
276
- value={logsUsuario}
277
- onChange={(event) => setLogsUsuario(event.target.value)}
278
- placeholder="filtrar por usuário"
279
- autoComplete="off"
280
- />
281
- </div>
282
- <div className="logs-field logs-field-small">
283
- <label htmlFor="logs-limit">Limite</label>
284
- <input
285
- id="logs-limit"
286
- type="number"
287
- min="1"
288
- max="1000"
289
- value={logsLimit}
290
- onChange={(event) => setLogsLimit(Number(event.target.value || 200))}
291
- />
292
- </div>
293
- <button type="button" onClick={() => void carregarEventosLogs()} disabled={logsLoading}>
294
- Atualizar
295
- </button>
296
- </div>
297
-
298
- {logsError ? <div className="error-line">{logsError}</div> : null}
299
-
300
- <div className="table-container">
301
- <table className="logs-table">
302
- <thead>
303
- <tr>
304
- <th>Timestamp</th>
305
- <th>Usuário</th>
306
- <th>Perfil</th>
307
- <th>Escopo</th>
308
- <th>Ação</th>
309
- <th>Status</th>
310
- <th>Detalhes</th>
311
- </tr>
312
- </thead>
313
- <tbody>
314
- {logsEvents.map((item) => (
315
- <tr key={item.event_id || `${item.ts}-${item.action}`}>
316
- <td>{item.ts || '-'}</td>
317
- <td>{item.usuario || '-'}</td>
318
- <td>{item.perfil || '-'}</td>
319
- <td>{item.scope || '-'}</td>
320
- <td>{item.action || '-'}</td>
321
- <td>{item.status || '-'}</td>
322
- <td className="logs-table-details">{JSON.stringify(item.details || {})}</td>
323
- </tr>
324
- ))}
325
- {!logsEvents.length ? (
326
- <tr>
327
- <td colSpan={7}>{logsLoading ? 'Carregando logs...' : 'Nenhum log encontrado para os filtros.'}</td>
328
- </tr>
329
- ) : null}
330
- </tbody>
331
- </table>
332
- </div>
333
- </>
334
- )}
335
- </section>
336
- ) : null}
337
-
338
  {authLoading ? <div className="status-line">Validando autenticação...</div> : null}
339
 
340
  {!authLoading && !authUser ? (
@@ -377,7 +311,116 @@ export default function App() {
377
  ) : null}
378
 
379
  {authUser ? (
380
- <>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
381
  <nav className="tabs" aria-label="Navegação principal">
382
  {TABS.map((tab) => {
383
  const active = tab.key === activeTab
@@ -416,7 +459,8 @@ export default function App() {
416
  <div className="tab-pane" hidden={activeTab !== 'Repositório'}>
417
  <RepositorioTab authUser={authUser} />
418
  </div>
419
- </>
 
420
  ) : null}
421
  </div>
422
  )
 
6
  import RepositorioTab from './components/RepositorioTab'
7
  import VisualizacaoTab from './components/VisualizacaoTab'
8
 
9
+ const LOGS_PAGE_SIZE = 30
10
+
11
  const TABS = [
12
  { key: 'Início', label: 'Início', hint: 'Visão geral rápida do aplicativo' },
13
  { key: 'Pesquisa', label: 'Pesquisa', hint: 'Busca inicial de modelos .dai' },
 
35
  const [logsError, setLogsError] = useState('')
36
  const [logsScope, setLogsScope] = useState('')
37
  const [logsUsuario, setLogsUsuario] = useState('')
38
+ const [logsPage, setLogsPage] = useState(1)
39
 
40
  const isAdmin = String(authUser?.perfil || '').toLowerCase() === 'admin'
41
  const logsEnabled = Boolean(logsStatus?.enabled)
42
  const logsDisabledReason = String(logsStatus?.reason || 'Logs indisponíveis')
43
+ const logsViewActive = Boolean(authUser && isAdmin && logsOpen)
44
+ const logsTotalPages = Math.max(1, Math.ceil(logsEvents.length / LOGS_PAGE_SIZE))
45
+ const logsCurrentPage = Math.min(logsPage, logsTotalPages)
46
+ const logsStartIndex = logsEvents.length ? (logsCurrentPage - 1) * LOGS_PAGE_SIZE : 0
47
+ const logsVisibleEvents = logsEvents.slice(logsStartIndex, logsStartIndex + LOGS_PAGE_SIZE)
48
+ const logsEndIndex = logsEvents.length ? Math.min(logsStartIndex + LOGS_PAGE_SIZE, logsEvents.length) : 0
49
 
50
  function resetToLogin(message = '') {
51
  setAuthToken('')
 
58
  setLogsOpen(false)
59
  setLogsEvents([])
60
  setLogsError('')
61
+ setLogsPage(1)
62
  setActiveTab(TABS[0].key)
63
  setAuthError(message)
64
  }
 
140
  setLogsOpen(false)
141
  setLogsEvents([])
142
  setLogsError('')
143
+ setLogsPage(1)
144
  setLogsStatusLoading(false)
145
  setLogsLoading(false)
146
  return
 
148
  void carregarLogsStatus()
149
  }, [authUser, isAdmin])
150
 
151
+ useEffect(() => {
152
+ if (!logsEvents.length && logsPage !== 1) {
153
+ setLogsPage(1)
154
+ return
155
+ }
156
+ if (logsPage > logsTotalPages) {
157
+ setLogsPage(logsTotalPages)
158
+ }
159
+ }, [logsEvents.length, logsPage, logsTotalPages])
160
+
161
  async function onSubmitLogin(event) {
162
  event.preventDefault()
163
  setAuthError('')
 
211
  setLogsLoading(true)
212
  setLogsError('')
213
  try {
214
+ const resp = await api.logsEvents({ scope: logsScope, usuario: logsUsuario, limit: 1000 })
215
  setLogsEvents(Array.isArray(resp?.events) ? resp.events : [])
216
+ setLogsPage(1)
217
  } catch (err) {
218
  setLogsError(err.message || 'Falha ao carregar logs.')
219
  setLogsEvents([])
220
+ setLogsPage(1)
221
  } finally {
222
  setLogsLoading(false)
223
  }
 
234
  await carregarEventosLogs()
235
  }
236
 
237
+ function onCloseLogsView() {
238
+ setLogsOpen(false)
239
+ }
240
+
241
  return (
242
  <div className="app-shell">
243
  <header className="app-header app-header-logo-only">
 
256
  <button
257
  type="button"
258
  onClick={() => void onToggleLogs()}
259
+ disabled={logsStatusLoading || (!logsEnabled && !logsOpen)}
260
+ title={logsOpen ? 'Fechar visualização de logs' : !logsEnabled ? logsDisabledReason : 'Abrir leitura de logs'}
261
  >
262
+ {logsOpen ? 'Fechar logs' : 'Logs'}
263
  </button>
264
  ) : null}
265
  <button type="button" onClick={() => void onLogout()}>
 
269
  </div>
270
  ) : null}
271
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  {authLoading ? <div className="status-line">Validando autenticação...</div> : null}
273
 
274
  {!authLoading && !authUser ? (
 
311
  ) : null}
312
 
313
  {authUser ? (
314
+ logsViewActive ? (
315
+ <section className="logs-panel logs-panel-dedicated">
316
+ <div className="logs-panel-head">
317
+ <div className="logs-panel-head-main">
318
+ <h3>Logs</h3>
319
+ <p className="logs-close-hint">Para voltar ao app, clique em "Fechar logs".</p>
320
+ </div>
321
+ <div className="logs-panel-head-actions">
322
+ <div className="logs-panel-meta">
323
+ <span>{logsStatus?.backend === 'hf_dataset' ? 'Origem: Dataset HF' : 'Origem: indisponível'}</span>
324
+ {logsStatus?.revision ? <span>Revisão: {String(logsStatus.revision).slice(0, 8)}</span> : null}
325
+ </div>
326
+ <button type="button" className="logs-close-btn" onClick={onCloseLogsView}>
327
+ Fechar logs
328
+ </button>
329
+ </div>
330
+ </div>
331
+
332
+ {!logsEnabled ? (
333
+ <div className="section1-empty-hint">{logsDisabledReason}</div>
334
+ ) : (
335
+ <>
336
+ <div className="logs-filters">
337
+ <div className="logs-field">
338
+ <label htmlFor="logs-scope">Escopo</label>
339
+ <select id="logs-scope" value={logsScope} onChange={(event) => setLogsScope(event.target.value)}>
340
+ <option value="">Todos</option>
341
+ <option value="auth">Auth</option>
342
+ <option value="repositorio">Repositório</option>
343
+ <option value="elaboracao">Elaboração</option>
344
+ <option value="visualizacao">Visualização</option>
345
+ </select>
346
+ </div>
347
+ <div className="logs-field">
348
+ <label htmlFor="logs-usuario">Usuário</label>
349
+ <input
350
+ id="logs-usuario"
351
+ type="text"
352
+ value={logsUsuario}
353
+ onChange={(event) => setLogsUsuario(event.target.value)}
354
+ placeholder="filtrar por usuário"
355
+ autoComplete="off"
356
+ />
357
+ </div>
358
+ <button type="button" onClick={() => void carregarEventosLogs()} disabled={logsLoading}>
359
+ Atualizar
360
+ </button>
361
+ </div>
362
+
363
+ {logsError ? <div className="error-line">{logsError}</div> : null}
364
+
365
+ <div className="table-container">
366
+ <table className="logs-table">
367
+ <thead>
368
+ <tr>
369
+ <th>Timestamp</th>
370
+ <th>Usuário</th>
371
+ <th>Perfil</th>
372
+ <th>Escopo</th>
373
+ <th>Ação</th>
374
+ <th>Status</th>
375
+ <th>Detalhes</th>
376
+ </tr>
377
+ </thead>
378
+ <tbody>
379
+ {logsVisibleEvents.map((item) => (
380
+ <tr key={item.event_id || `${item.ts}-${item.action}`}>
381
+ <td>{item.ts || '-'}</td>
382
+ <td>{item.usuario || '-'}</td>
383
+ <td>{item.perfil || '-'}</td>
384
+ <td>{item.scope || '-'}</td>
385
+ <td>{item.action || '-'}</td>
386
+ <td>{item.status || '-'}</td>
387
+ <td className="logs-table-details">{JSON.stringify(item.details || {})}</td>
388
+ </tr>
389
+ ))}
390
+ {!logsEvents.length ? (
391
+ <tr>
392
+ <td colSpan={7}>{logsLoading ? 'Carregando logs...' : 'Nenhum log encontrado para os filtros.'}</td>
393
+ </tr>
394
+ ) : null}
395
+ </tbody>
396
+ </table>
397
+ </div>
398
+
399
+ <div className="logs-pagination">
400
+ <span>{logsEvents.length ? `Exibindo ${logsStartIndex + 1}-${logsEndIndex} de ${logsEvents.length}` : 'Exibindo 0 de 0'}</span>
401
+ <div className="logs-pagination-actions">
402
+ <button
403
+ type="button"
404
+ onClick={() => setLogsPage((prev) => Math.max(1, prev - 1))}
405
+ disabled={logsLoading || logsCurrentPage <= 1}
406
+ >
407
+ Anterior
408
+ </button>
409
+ <span>Página {logsCurrentPage} de {logsTotalPages}</span>
410
+ <button
411
+ type="button"
412
+ onClick={() => setLogsPage((prev) => Math.min(logsTotalPages, prev + 1))}
413
+ disabled={logsLoading || logsCurrentPage >= logsTotalPages}
414
+ >
415
+ Próxima
416
+ </button>
417
+ </div>
418
+ </div>
419
+ </>
420
+ )}
421
+ </section>
422
+ ) : (
423
+ <>
424
  <nav className="tabs" aria-label="Navegação principal">
425
  {TABS.map((tab) => {
426
  const active = tab.key === activeTab
 
459
  <div className="tab-pane" hidden={activeTab !== 'Repositório'}>
460
  <RepositorioTab authUser={authUser} />
461
  </div>
462
+ </>
463
+ )
464
  ) : null}
465
  </div>
466
  )
frontend/src/styles.css CHANGED
@@ -328,21 +328,43 @@ textarea {
328
  padding: 12px;
329
  }
330
 
 
 
 
 
331
  .logs-panel-head {
332
  display: flex;
333
  justify-content: space-between;
334
- align-items: baseline;
335
  gap: 10px;
336
  flex-wrap: wrap;
337
  margin-bottom: 10px;
338
  }
339
 
 
 
 
 
 
340
  .logs-panel-head h3 {
341
  margin: 0;
342
  font-family: 'Sora', sans-serif;
343
  color: #2b4258;
344
  }
345
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  .logs-panel-meta {
347
  display: inline-flex;
348
  gap: 10px;
@@ -350,9 +372,16 @@ textarea {
350
  font-size: 0.82rem;
351
  }
352
 
 
 
 
 
 
 
 
353
  .logs-filters {
354
  display: grid;
355
- grid-template-columns: 1.1fr 1.2fr 100px auto;
356
  gap: 10px;
357
  align-items: end;
358
  margin-bottom: 10px;
@@ -398,6 +427,27 @@ textarea {
398
  max-width: 420px;
399
  }
400
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401
  .inner-tabs {
402
  display: flex;
403
  flex-wrap: wrap;
 
328
  padding: 12px;
329
  }
330
 
331
+ .logs-panel-dedicated {
332
+ margin-top: 10px;
333
+ }
334
+
335
  .logs-panel-head {
336
  display: flex;
337
  justify-content: space-between;
338
+ align-items: flex-start;
339
  gap: 10px;
340
  flex-wrap: wrap;
341
  margin-bottom: 10px;
342
  }
343
 
344
+ .logs-panel-head-main {
345
+ display: grid;
346
+ gap: 4px;
347
+ }
348
+
349
  .logs-panel-head h3 {
350
  margin: 0;
351
  font-family: 'Sora', sans-serif;
352
  color: #2b4258;
353
  }
354
 
355
+ .logs-close-hint {
356
+ margin: 0;
357
+ color: #5f7388;
358
+ font-size: 0.82rem;
359
+ }
360
+
361
+ .logs-panel-head-actions {
362
+ display: inline-flex;
363
+ align-items: center;
364
+ gap: 10px;
365
+ flex-wrap: wrap;
366
+ }
367
+
368
  .logs-panel-meta {
369
  display: inline-flex;
370
  gap: 10px;
 
372
  font-size: 0.82rem;
373
  }
374
 
375
+ .logs-close-btn {
376
+ border: 1px solid #adc3d8;
377
+ background: #eef5fb;
378
+ color: #2f4760;
379
+ font-weight: 700;
380
+ }
381
+
382
  .logs-filters {
383
  display: grid;
384
+ grid-template-columns: 1.1fr 1.2fr auto;
385
  gap: 10px;
386
  align-items: end;
387
  margin-bottom: 10px;
 
427
  max-width: 420px;
428
  }
429
 
430
+ .logs-pagination {
431
+ margin-top: 10px;
432
+ display: flex;
433
+ align-items: center;
434
+ justify-content: space-between;
435
+ gap: 10px;
436
+ flex-wrap: wrap;
437
+ color: #4b6278;
438
+ font-size: 0.82rem;
439
+ }
440
+
441
+ .logs-pagination-actions {
442
+ display: inline-flex;
443
+ align-items: center;
444
+ gap: 8px;
445
+ }
446
+
447
+ .logs-pagination-actions button {
448
+ min-width: 96px;
449
+ }
450
+
451
  .inner-tabs {
452
  display: flex;
453
  flex-wrap: wrap;