wip 6
This commit is contained in:
34
AGENTS.md
Normal file
34
AGENTS.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Agent Context Hints
|
||||
|
||||
## Architecture Context
|
||||
- **Backend**: Go with Gin router (legacy), SQLC for database queries, currently migrating to V1 API (oapi-codegen)
|
||||
- **Frontend**: React with Vite, currently migrating from Go templates (using the V1 API)
|
||||
- **API**: OpenAPI 3.0 spec, generates Go server (oapi-codegen) and TS client (orval)
|
||||
|
||||
## Data Flow (CRITICAL for migrations)
|
||||
1. Database schema → SQL queries (`database/query.sql`, `database/query.sql.go`)
|
||||
2. SQLC models → API handlers (`api/v1/*.go`)
|
||||
3. Go templates show **intended UI** structure (`templates/pages/*.tmpl`)
|
||||
4. API spec defines actual API contract (`api/v1/openapi.yaml`)
|
||||
5. Generated TS client → React components
|
||||
|
||||
## When Migrating from Go Templates
|
||||
- Check template AND database query results (Go templates may show fields API doesn't return)
|
||||
- Template columns often map to: document_id, title, author, start_time, duration, start/end_percentage
|
||||
- Go template rendering: `{{ template "component/table" }}` with "Columns" and "Keys"
|
||||
|
||||
## API Regeneration Commands
|
||||
- Go backend: `go generate ./api/v1/generate.go`
|
||||
- TS client: `cd frontend && npm run generate:api`
|
||||
|
||||
## Key Files
|
||||
- Database queries: `database/query.sql` → SQLc Query shows actual fields returned
|
||||
- SQLC models: `database/query.sql.go` → SQLc Generated Go struct definitions
|
||||
- Go templates: `templates/pages/*.tmpl` → Legacy UI reference
|
||||
- API spec: `api/v1/openapi.yaml` → contract definition
|
||||
- Generated TS types: `frontend/src/generated/model/*.ts`
|
||||
|
||||
## Common Gotchas
|
||||
- API implementation may not map all fields from DB query (check `api/v1/activity.go` mapping)
|
||||
- `start_time` is `interface{}` in Go models, needs type assertion
|
||||
- Go templates use `LOCAL_TIME()` SQL function for timezone-aware display
|
||||
@@ -2,8 +2,6 @@ package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"reichard.io/antholume/database"
|
||||
)
|
||||
@@ -48,12 +46,24 @@ func (s *Server) GetActivity(ctx context.Context, request GetActivityRequestObje
|
||||
|
||||
apiActivities := make([]Activity, len(activities))
|
||||
for i, a := range activities {
|
||||
// Convert StartTime from interface{} to string
|
||||
startTimeStr := ""
|
||||
if a.StartTime != nil {
|
||||
if str, ok := a.StartTime.(string); ok {
|
||||
startTimeStr = str
|
||||
}
|
||||
}
|
||||
|
||||
apiActivities[i] = Activity{
|
||||
ActivityType: a.DeviceID,
|
||||
DocumentId: a.DocumentID,
|
||||
Id: strconv.Itoa(i),
|
||||
Timestamp: time.Now(),
|
||||
UserId: auth.UserName,
|
||||
DocumentId: a.DocumentID,
|
||||
DeviceId: a.DeviceID,
|
||||
StartTime: startTimeStr,
|
||||
Title: a.Title,
|
||||
Author: a.Author,
|
||||
Duration: a.Duration,
|
||||
StartPercentage: float32(a.StartPercentage),
|
||||
EndPercentage: float32(a.EndPercentage),
|
||||
ReadPercentage: float32(a.ReadPercentage),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
140
api/v1/admin.go
Normal file
140
api/v1/admin.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GET /admin
|
||||
func (s *Server) GetAdmin(ctx context.Context, request GetAdminRequestObject) (GetAdminResponseObject, error) {
|
||||
_, ok := s.getSessionFromContext(ctx)
|
||||
if !ok {
|
||||
return GetAdmin401JSONResponse{Code: 401, Message: "Unauthorized"}, nil
|
||||
}
|
||||
|
||||
// Get database info from the main API
|
||||
// This is a placeholder - you'll need to implement this in the main API or database
|
||||
// For now, return empty data
|
||||
response := GetAdmin200JSONResponse{
|
||||
DatabaseInfo: &DatabaseInfo{
|
||||
DocumentsSize: 0,
|
||||
ActivitySize: 0,
|
||||
ProgressSize: 0,
|
||||
DevicesSize: 0,
|
||||
},
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// POST /admin
|
||||
func (s *Server) PostAdminAction(ctx context.Context, request PostAdminActionRequestObject) (PostAdminActionResponseObject, error) {
|
||||
_, ok := s.getSessionFromContext(ctx)
|
||||
if !ok {
|
||||
return PostAdminAction401JSONResponse{Code: 401, Message: "Unauthorized"}, nil
|
||||
}
|
||||
|
||||
// TODO: Implement admin actions (backup, restore, etc.)
|
||||
// For now, this is a placeholder
|
||||
return PostAdminAction200ApplicationoctetStreamResponse{}, nil
|
||||
}
|
||||
|
||||
// GET /admin/users
|
||||
func (s *Server) GetUsers(ctx context.Context, request GetUsersRequestObject) (GetUsersResponseObject, error) {
|
||||
_, ok := s.getSessionFromContext(ctx)
|
||||
if !ok {
|
||||
return GetUsers401JSONResponse{Code: 401, Message: "Unauthorized"}, nil
|
||||
}
|
||||
|
||||
// Get users from database
|
||||
users, err := s.db.Queries.GetUsers(ctx)
|
||||
if err != nil {
|
||||
return GetUsers500JSONResponse{Code: 500, Message: err.Error()}, nil
|
||||
}
|
||||
|
||||
apiUsers := make([]User, len(users))
|
||||
for i, user := range users {
|
||||
createdAt, _ := time.Parse("2006-01-02T15:04:05", user.CreatedAt)
|
||||
apiUsers[i] = User{
|
||||
Id: user.ID,
|
||||
Admin: user.Admin,
|
||||
CreatedAt: createdAt,
|
||||
}
|
||||
}
|
||||
|
||||
response := GetUsers200JSONResponse{
|
||||
Users: &apiUsers,
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// POST /admin/users
|
||||
func (s *Server) UpdateUser(ctx context.Context, request UpdateUserRequestObject) (UpdateUserResponseObject, error) {
|
||||
_, ok := s.getSessionFromContext(ctx)
|
||||
if !ok {
|
||||
return UpdateUser401JSONResponse{Code: 401, Message: "Unauthorized"}, nil
|
||||
}
|
||||
|
||||
// TODO: Implement user creation, update, deletion
|
||||
// For now, this is a placeholder
|
||||
return UpdateUser200JSONResponse{
|
||||
Users: &[]User{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GET /admin/import
|
||||
func (s *Server) GetImportDirectory(ctx context.Context, request GetImportDirectoryRequestObject) (GetImportDirectoryResponseObject, error) {
|
||||
_, ok := s.getSessionFromContext(ctx)
|
||||
if !ok {
|
||||
return GetImportDirectory401JSONResponse{Code: 401, Message: "Unauthorized"}, nil
|
||||
}
|
||||
|
||||
// TODO: Implement directory listing
|
||||
// For now, this is a placeholder
|
||||
return GetImportDirectory200JSONResponse{
|
||||
CurrentPath: ptrOf("/data"),
|
||||
Items: &[]DirectoryItem{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// POST /admin/import
|
||||
func (s *Server) PostImport(ctx context.Context, request PostImportRequestObject) (PostImportResponseObject, error) {
|
||||
_, ok := s.getSessionFromContext(ctx)
|
||||
if !ok {
|
||||
return PostImport401JSONResponse{Code: 401, Message: "Unauthorized"}, nil
|
||||
}
|
||||
|
||||
// TODO: Implement import functionality
|
||||
// For now, this is a placeholder
|
||||
return PostImport200JSONResponse{
|
||||
Results: &[]ImportResult{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GET /admin/import-results
|
||||
func (s *Server) GetImportResults(ctx context.Context, request GetImportResultsRequestObject) (GetImportResultsResponseObject, error) {
|
||||
_, ok := s.getSessionFromContext(ctx)
|
||||
if !ok {
|
||||
return GetImportResults401JSONResponse{Code: 401, Message: "Unauthorized"}, nil
|
||||
}
|
||||
|
||||
// TODO: Implement import results retrieval
|
||||
// For now, this is a placeholder
|
||||
return GetImportResults200JSONResponse{
|
||||
Results: &[]ImportResult{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GET /admin/logs
|
||||
func (s *Server) GetLogs(ctx context.Context, request GetLogsRequestObject) (GetLogsResponseObject, error) {
|
||||
_, ok := s.getSessionFromContext(ctx)
|
||||
if !ok {
|
||||
return GetLogs401JSONResponse{Code: 401, Message: "Unauthorized"}, nil
|
||||
}
|
||||
|
||||
// TODO: Implement log retrieval
|
||||
// For now, this is a placeholder
|
||||
return GetLogs200JSONResponse{
|
||||
Logs: &[]string{},
|
||||
Filter: request.Params.Filter,
|
||||
}, nil
|
||||
}
|
||||
1006
api/v1/api.gen.go
1006
api/v1/api.gen.go
File diff suppressed because it is too large
Load Diff
@@ -91,23 +91,36 @@ components:
|
||||
Activity:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
user_id:
|
||||
type: string
|
||||
document_id:
|
||||
type: string
|
||||
activity_type:
|
||||
device_id:
|
||||
type: string
|
||||
timestamp:
|
||||
start_time:
|
||||
type: string
|
||||
format: date-time
|
||||
title:
|
||||
type: string
|
||||
author:
|
||||
type: string
|
||||
duration:
|
||||
type: integer
|
||||
format: int64
|
||||
start_percentage:
|
||||
type: number
|
||||
format: float
|
||||
end_percentage:
|
||||
type: number
|
||||
format: float
|
||||
read_percentage:
|
||||
type: number
|
||||
format: float
|
||||
required:
|
||||
- id
|
||||
- user_id
|
||||
- document_id
|
||||
- activity_type
|
||||
- timestamp
|
||||
- device_id
|
||||
- start_time
|
||||
- duration
|
||||
- start_percentage
|
||||
- end_percentage
|
||||
- read_percentage
|
||||
|
||||
SearchItem:
|
||||
type: object
|
||||
@@ -482,6 +495,95 @@ components:
|
||||
- user_statistics
|
||||
- user
|
||||
|
||||
BackupType:
|
||||
type: string
|
||||
enum: [COVERS, DOCUMENTS]
|
||||
|
||||
ImportType:
|
||||
type: string
|
||||
enum: [DIRECT, COPY]
|
||||
|
||||
OperationType:
|
||||
type: string
|
||||
enum: [CREATE, UPDATE, DELETE]
|
||||
|
||||
User:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
admin:
|
||||
type: boolean
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
required:
|
||||
- id
|
||||
- admin
|
||||
- created_at
|
||||
|
||||
UsersResponse:
|
||||
type: object
|
||||
properties:
|
||||
users:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/User'
|
||||
|
||||
ImportResult:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
path:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
enum: [FAILED, SUCCESS, EXISTS]
|
||||
error:
|
||||
type: string
|
||||
|
||||
ImportResultsResponse:
|
||||
type: object
|
||||
properties:
|
||||
results:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ImportResult'
|
||||
|
||||
DirectoryItem:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
path:
|
||||
type: string
|
||||
|
||||
DirectoryListResponse:
|
||||
type: object
|
||||
properties:
|
||||
current_path:
|
||||
type: string
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/DirectoryItem'
|
||||
|
||||
LogEntry:
|
||||
type: string
|
||||
|
||||
LogsResponse:
|
||||
type: object
|
||||
properties:
|
||||
logs:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/LogEntry'
|
||||
filter:
|
||||
type: string
|
||||
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
type: http
|
||||
@@ -1057,4 +1159,307 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
|
||||
/admin:
|
||||
get:
|
||||
summary: Get admin page data
|
||||
operationId: getAdmin
|
||||
tags:
|
||||
- Admin
|
||||
security:
|
||||
- BearerAuth: []
|
||||
responses:
|
||||
200:
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
database_info:
|
||||
$ref: '#/components/schemas/DatabaseInfo'
|
||||
401:
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
|
||||
post:
|
||||
summary: Perform admin action (backup, restore, etc.)
|
||||
operationId: postAdminAction
|
||||
tags:
|
||||
- Admin
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
action:
|
||||
type: string
|
||||
enum: [BACKUP, RESTORE, METADATA_MATCH, CACHE_TABLES]
|
||||
backup_types:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/BackupType'
|
||||
restore_file:
|
||||
type: string
|
||||
format: binary
|
||||
required:
|
||||
- action
|
||||
security:
|
||||
- BearerAuth: []
|
||||
responses:
|
||||
200:
|
||||
description: Action completed successfully
|
||||
content:
|
||||
application/octet-stream:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
400:
|
||||
description: Bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
401:
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
500:
|
||||
description: Internal server error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
|
||||
/admin/users:
|
||||
get:
|
||||
summary: Get all users
|
||||
operationId: getUsers
|
||||
tags:
|
||||
- Admin
|
||||
security:
|
||||
- BearerAuth: []
|
||||
responses:
|
||||
200:
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UsersResponse'
|
||||
401:
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
500:
|
||||
description: Internal server error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
|
||||
post:
|
||||
summary: Create, update, or delete user
|
||||
operationId: updateUser
|
||||
tags:
|
||||
- Admin
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
operation:
|
||||
$ref: '#/components/schemas/OperationType'
|
||||
user:
|
||||
type: string
|
||||
password:
|
||||
type: string
|
||||
is_admin:
|
||||
type: boolean
|
||||
required:
|
||||
- operation
|
||||
- user
|
||||
security:
|
||||
- BearerAuth: []
|
||||
responses:
|
||||
200:
|
||||
description: User updated successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UsersResponse'
|
||||
400:
|
||||
description: Bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
401:
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
500:
|
||||
description: Internal server error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
|
||||
/admin/import:
|
||||
get:
|
||||
summary: Get import directory list
|
||||
operationId: getImportDirectory
|
||||
tags:
|
||||
- Admin
|
||||
parameters:
|
||||
- name: directory
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
- name: select
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
security:
|
||||
- BearerAuth: []
|
||||
responses:
|
||||
200:
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DirectoryListResponse'
|
||||
401:
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
500:
|
||||
description: Internal server error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
|
||||
post:
|
||||
summary: Perform import
|
||||
operationId: postImport
|
||||
tags:
|
||||
- Admin
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
directory:
|
||||
type: string
|
||||
type:
|
||||
$ref: '#/components/schemas/ImportType'
|
||||
required:
|
||||
- directory
|
||||
- type
|
||||
security:
|
||||
- BearerAuth: []
|
||||
responses:
|
||||
200:
|
||||
description: Import completed
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ImportResultsResponse'
|
||||
400:
|
||||
description: Bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
401:
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
500:
|
||||
description: Internal server error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
|
||||
/admin/import-results:
|
||||
get:
|
||||
summary: Get import results
|
||||
operationId: getImportResults
|
||||
tags:
|
||||
- Admin
|
||||
security:
|
||||
- BearerAuth: []
|
||||
responses:
|
||||
200:
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ImportResultsResponse'
|
||||
401:
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
500:
|
||||
description: Internal server error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
|
||||
/admin/logs:
|
||||
get:
|
||||
summary: Get logs with optional filter
|
||||
operationId: getLogs
|
||||
tags:
|
||||
- Admin
|
||||
parameters:
|
||||
- name: filter
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
security:
|
||||
- BearerAuth: []
|
||||
responses:
|
||||
200:
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LogsResponse'
|
||||
401:
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
500:
|
||||
description: Internal server error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import SettingsPage from './pages/SettingsPage';
|
||||
import LoginPage from './pages/LoginPage';
|
||||
import AdminPage from './pages/AdminPage';
|
||||
import AdminImportPage from './pages/AdminImportPage';
|
||||
import AdminImportResultsPage from './pages/AdminImportResultsPage';
|
||||
import AdminUsersPage from './pages/AdminUsersPage';
|
||||
import AdminLogsPage from './pages/AdminLogsPage';
|
||||
import { ProtectedRoute } from './auth/ProtectedRoute';
|
||||
@@ -91,6 +92,14 @@ export function Routes() {
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="admin/import-results"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<AdminImportResultsPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="admin/users"
|
||||
element={
|
||||
|
||||
91
frontend/src/components/Table.tsx
Normal file
91
frontend/src/components/Table.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import React from 'react';
|
||||
|
||||
export interface Column<T> {
|
||||
key: keyof T;
|
||||
header: string;
|
||||
render?: (value: any, row: T, index: number) => React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface TableProps<T> {
|
||||
columns: Column<T>[];
|
||||
data: T[];
|
||||
loading?: boolean;
|
||||
emptyMessage?: string;
|
||||
rowKey?: keyof T | ((row: T) => string);
|
||||
}
|
||||
|
||||
export function Table<T extends Record<string, any>>({
|
||||
columns,
|
||||
data,
|
||||
loading = false,
|
||||
emptyMessage = 'No Results',
|
||||
rowKey,
|
||||
}: TableProps<T>) {
|
||||
const getRowKey = (row: T, index: number): string => {
|
||||
if (typeof rowKey === 'function') {
|
||||
return rowKey(row);
|
||||
}
|
||||
if (rowKey) {
|
||||
return String(row[rowKey] ?? index);
|
||||
}
|
||||
return `row-${index}`;
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="text-gray-500 dark:text-white p-4">Loading...</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<div className="inline-block min-w-full overflow-hidden rounded shadow">
|
||||
<table className="min-w-full bg-white dark:bg-gray-700">
|
||||
<thead>
|
||||
<tr className="border-b dark:border-gray-600">
|
||||
{columns.map((column) => (
|
||||
<th
|
||||
key={String(column.key)}
|
||||
className={`text-left p-3 text-gray-500 dark:text-white ${column.className || ''}`}
|
||||
>
|
||||
{column.header}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.length === 0 ? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={columns.length}
|
||||
className="text-center p-3 text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{emptyMessage}
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
data.map((row, index) => (
|
||||
<tr
|
||||
key={getRowKey(row, index)}
|
||||
className="border-b dark:border-gray-600"
|
||||
>
|
||||
{columns.map((column) => (
|
||||
<td
|
||||
key={`${getRowKey(row, index)}-${String(column.key)}`}
|
||||
className={`p-3 text-gray-700 dark:text-gray-300 ${column.className || ''}`}
|
||||
>
|
||||
{column.render
|
||||
? column.render(row[column.key], row, index)
|
||||
: row[column.key]}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -34,24 +34,34 @@ import type {
|
||||
import type {
|
||||
ActivityResponse,
|
||||
CreateDocumentBody,
|
||||
DirectoryListResponse,
|
||||
DocumentResponse,
|
||||
DocumentsResponse,
|
||||
ErrorResponse,
|
||||
GetActivityParams,
|
||||
GetAdmin200,
|
||||
GetDocumentsParams,
|
||||
GetImportDirectoryParams,
|
||||
GetLogsParams,
|
||||
GetProgressListParams,
|
||||
GetSearchParams,
|
||||
GraphDataResponse,
|
||||
HomeResponse,
|
||||
ImportResultsResponse,
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
LogsResponse,
|
||||
PostAdminActionBody,
|
||||
PostImportBody,
|
||||
PostSearchBody,
|
||||
ProgressListResponse,
|
||||
ProgressResponse,
|
||||
SearchResponse,
|
||||
SettingsResponse,
|
||||
StreaksResponse,
|
||||
UserStatisticsResponse
|
||||
UpdateUserBody,
|
||||
UserStatisticsResponse,
|
||||
UsersResponse
|
||||
} from './model';
|
||||
|
||||
|
||||
@@ -1412,3 +1422,670 @@ export const usePostSearch = <TError = AxiosError<ErrorResponse>,
|
||||
return useMutation(mutationOptions, queryClient);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get admin page data
|
||||
*/
|
||||
export const getAdmin = (
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<AxiosResponse<GetAdmin200>> => {
|
||||
|
||||
|
||||
return axios.default.get(
|
||||
`/api/v1/admin`,options
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export const getGetAdminQueryKey = () => {
|
||||
return [
|
||||
`/api/v1/admin`
|
||||
] as const;
|
||||
}
|
||||
|
||||
|
||||
export const getGetAdminQueryOptions = <TData = Awaited<ReturnType<typeof getAdmin>>, TError = AxiosError<ErrorResponse>>( options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getAdmin>>, TError, TData>>, axios?: AxiosRequestConfig}
|
||||
) => {
|
||||
|
||||
const {query: queryOptions, axios: axiosOptions} = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getGetAdminQueryKey();
|
||||
|
||||
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof getAdmin>>> = ({ signal }) => getAdmin({ signal, ...axiosOptions });
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getAdmin>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
}
|
||||
|
||||
export type GetAdminQueryResult = NonNullable<Awaited<ReturnType<typeof getAdmin>>>
|
||||
export type GetAdminQueryError = AxiosError<ErrorResponse>
|
||||
|
||||
|
||||
export function useGetAdmin<TData = Awaited<ReturnType<typeof getAdmin>>, TError = AxiosError<ErrorResponse>>(
|
||||
options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof getAdmin>>, TError, TData>> & Pick<
|
||||
DefinedInitialDataOptions<
|
||||
Awaited<ReturnType<typeof getAdmin>>,
|
||||
TError,
|
||||
Awaited<ReturnType<typeof getAdmin>>
|
||||
> , 'initialData'
|
||||
>, axios?: AxiosRequestConfig}
|
||||
, queryClient?: QueryClient
|
||||
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
export function useGetAdmin<TData = Awaited<ReturnType<typeof getAdmin>>, TError = AxiosError<ErrorResponse>>(
|
||||
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getAdmin>>, TError, TData>> & Pick<
|
||||
UndefinedInitialDataOptions<
|
||||
Awaited<ReturnType<typeof getAdmin>>,
|
||||
TError,
|
||||
Awaited<ReturnType<typeof getAdmin>>
|
||||
> , 'initialData'
|
||||
>, axios?: AxiosRequestConfig}
|
||||
, queryClient?: QueryClient
|
||||
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
export function useGetAdmin<TData = Awaited<ReturnType<typeof getAdmin>>, TError = AxiosError<ErrorResponse>>(
|
||||
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getAdmin>>, TError, TData>>, axios?: AxiosRequestConfig}
|
||||
, queryClient?: QueryClient
|
||||
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
/**
|
||||
* @summary Get admin page data
|
||||
*/
|
||||
|
||||
export function useGetAdmin<TData = Awaited<ReturnType<typeof getAdmin>>, TError = AxiosError<ErrorResponse>>(
|
||||
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getAdmin>>, TError, TData>>, axios?: AxiosRequestConfig}
|
||||
, queryClient?: QueryClient
|
||||
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
|
||||
|
||||
const queryOptions = getGetAdminQueryOptions(options)
|
||||
|
||||
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
|
||||
|
||||
query.queryKey = queryOptions.queryKey ;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @summary Perform admin action (backup, restore, etc.)
|
||||
*/
|
||||
export const postAdminAction = (
|
||||
postAdminActionBody: PostAdminActionBody, options?: AxiosRequestConfig
|
||||
): Promise<AxiosResponse<Blob>> => {
|
||||
|
||||
const formUrlEncoded = new URLSearchParams();
|
||||
formUrlEncoded.append(`action`, postAdminActionBody.action)
|
||||
if(postAdminActionBody.backup_types !== undefined) {
|
||||
postAdminActionBody.backup_types.forEach(value => formUrlEncoded.append(`backup_types`, value));
|
||||
}
|
||||
if(postAdminActionBody.restore_file !== undefined) {
|
||||
formUrlEncoded.append(`restore_file`, postAdminActionBody.restore_file)
|
||||
}
|
||||
|
||||
return axios.default.post(
|
||||
`/api/v1/admin`,
|
||||
formUrlEncoded,{
|
||||
responseType: 'blob',
|
||||
...options,}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const getPostAdminActionMutationOptions = <TError = AxiosError<ErrorResponse>,
|
||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof postAdminAction>>, TError,{data: PostAdminActionBody}, TContext>, axios?: AxiosRequestConfig}
|
||||
): UseMutationOptions<Awaited<ReturnType<typeof postAdminAction>>, TError,{data: PostAdminActionBody}, TContext> => {
|
||||
|
||||
const mutationKey = ['postAdminAction'];
|
||||
const {mutation: mutationOptions, axios: axiosOptions} = options ?
|
||||
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
||||
options
|
||||
: {...options, mutation: {...options.mutation, mutationKey}}
|
||||
: {mutation: { mutationKey, }, axios: undefined};
|
||||
|
||||
|
||||
|
||||
|
||||
const mutationFn: MutationFunction<Awaited<ReturnType<typeof postAdminAction>>, {data: PostAdminActionBody}> = (props) => {
|
||||
const {data} = props ?? {};
|
||||
|
||||
return postAdminAction(data,axiosOptions)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
return { mutationFn, ...mutationOptions }}
|
||||
|
||||
export type PostAdminActionMutationResult = NonNullable<Awaited<ReturnType<typeof postAdminAction>>>
|
||||
export type PostAdminActionMutationBody = PostAdminActionBody
|
||||
export type PostAdminActionMutationError = AxiosError<ErrorResponse>
|
||||
|
||||
/**
|
||||
* @summary Perform admin action (backup, restore, etc.)
|
||||
*/
|
||||
export const usePostAdminAction = <TError = AxiosError<ErrorResponse>,
|
||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof postAdminAction>>, TError,{data: PostAdminActionBody}, TContext>, axios?: AxiosRequestConfig}
|
||||
, queryClient?: QueryClient): UseMutationResult<
|
||||
Awaited<ReturnType<typeof postAdminAction>>,
|
||||
TError,
|
||||
{data: PostAdminActionBody},
|
||||
TContext
|
||||
> => {
|
||||
|
||||
const mutationOptions = getPostAdminActionMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions, queryClient);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get all users
|
||||
*/
|
||||
export const getUsers = (
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<AxiosResponse<UsersResponse>> => {
|
||||
|
||||
|
||||
return axios.default.get(
|
||||
`/api/v1/admin/users`,options
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export const getGetUsersQueryKey = () => {
|
||||
return [
|
||||
`/api/v1/admin/users`
|
||||
] as const;
|
||||
}
|
||||
|
||||
|
||||
export const getGetUsersQueryOptions = <TData = Awaited<ReturnType<typeof getUsers>>, TError = AxiosError<ErrorResponse>>( options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getUsers>>, TError, TData>>, axios?: AxiosRequestConfig}
|
||||
) => {
|
||||
|
||||
const {query: queryOptions, axios: axiosOptions} = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getGetUsersQueryKey();
|
||||
|
||||
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof getUsers>>> = ({ signal }) => getUsers({ signal, ...axiosOptions });
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getUsers>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
}
|
||||
|
||||
export type GetUsersQueryResult = NonNullable<Awaited<ReturnType<typeof getUsers>>>
|
||||
export type GetUsersQueryError = AxiosError<ErrorResponse>
|
||||
|
||||
|
||||
export function useGetUsers<TData = Awaited<ReturnType<typeof getUsers>>, TError = AxiosError<ErrorResponse>>(
|
||||
options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof getUsers>>, TError, TData>> & Pick<
|
||||
DefinedInitialDataOptions<
|
||||
Awaited<ReturnType<typeof getUsers>>,
|
||||
TError,
|
||||
Awaited<ReturnType<typeof getUsers>>
|
||||
> , 'initialData'
|
||||
>, axios?: AxiosRequestConfig}
|
||||
, queryClient?: QueryClient
|
||||
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
export function useGetUsers<TData = Awaited<ReturnType<typeof getUsers>>, TError = AxiosError<ErrorResponse>>(
|
||||
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getUsers>>, TError, TData>> & Pick<
|
||||
UndefinedInitialDataOptions<
|
||||
Awaited<ReturnType<typeof getUsers>>,
|
||||
TError,
|
||||
Awaited<ReturnType<typeof getUsers>>
|
||||
> , 'initialData'
|
||||
>, axios?: AxiosRequestConfig}
|
||||
, queryClient?: QueryClient
|
||||
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
export function useGetUsers<TData = Awaited<ReturnType<typeof getUsers>>, TError = AxiosError<ErrorResponse>>(
|
||||
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getUsers>>, TError, TData>>, axios?: AxiosRequestConfig}
|
||||
, queryClient?: QueryClient
|
||||
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
/**
|
||||
* @summary Get all users
|
||||
*/
|
||||
|
||||
export function useGetUsers<TData = Awaited<ReturnType<typeof getUsers>>, TError = AxiosError<ErrorResponse>>(
|
||||
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getUsers>>, TError, TData>>, axios?: AxiosRequestConfig}
|
||||
, queryClient?: QueryClient
|
||||
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
|
||||
|
||||
const queryOptions = getGetUsersQueryOptions(options)
|
||||
|
||||
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
|
||||
|
||||
query.queryKey = queryOptions.queryKey ;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @summary Create, update, or delete user
|
||||
*/
|
||||
export const updateUser = (
|
||||
updateUserBody: UpdateUserBody, options?: AxiosRequestConfig
|
||||
): Promise<AxiosResponse<UsersResponse>> => {
|
||||
|
||||
const formUrlEncoded = new URLSearchParams();
|
||||
formUrlEncoded.append(`operation`, updateUserBody.operation)
|
||||
formUrlEncoded.append(`user`, updateUserBody.user)
|
||||
if(updateUserBody.password !== undefined) {
|
||||
formUrlEncoded.append(`password`, updateUserBody.password)
|
||||
}
|
||||
if(updateUserBody.is_admin !== undefined) {
|
||||
formUrlEncoded.append(`is_admin`, updateUserBody.is_admin.toString())
|
||||
}
|
||||
|
||||
return axios.default.post(
|
||||
`/api/v1/admin/users`,
|
||||
formUrlEncoded,options
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const getUpdateUserMutationOptions = <TError = AxiosError<ErrorResponse>,
|
||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof updateUser>>, TError,{data: UpdateUserBody}, TContext>, axios?: AxiosRequestConfig}
|
||||
): UseMutationOptions<Awaited<ReturnType<typeof updateUser>>, TError,{data: UpdateUserBody}, TContext> => {
|
||||
|
||||
const mutationKey = ['updateUser'];
|
||||
const {mutation: mutationOptions, axios: axiosOptions} = options ?
|
||||
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
||||
options
|
||||
: {...options, mutation: {...options.mutation, mutationKey}}
|
||||
: {mutation: { mutationKey, }, axios: undefined};
|
||||
|
||||
|
||||
|
||||
|
||||
const mutationFn: MutationFunction<Awaited<ReturnType<typeof updateUser>>, {data: UpdateUserBody}> = (props) => {
|
||||
const {data} = props ?? {};
|
||||
|
||||
return updateUser(data,axiosOptions)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
return { mutationFn, ...mutationOptions }}
|
||||
|
||||
export type UpdateUserMutationResult = NonNullable<Awaited<ReturnType<typeof updateUser>>>
|
||||
export type UpdateUserMutationBody = UpdateUserBody
|
||||
export type UpdateUserMutationError = AxiosError<ErrorResponse>
|
||||
|
||||
/**
|
||||
* @summary Create, update, or delete user
|
||||
*/
|
||||
export const useUpdateUser = <TError = AxiosError<ErrorResponse>,
|
||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof updateUser>>, TError,{data: UpdateUserBody}, TContext>, axios?: AxiosRequestConfig}
|
||||
, queryClient?: QueryClient): UseMutationResult<
|
||||
Awaited<ReturnType<typeof updateUser>>,
|
||||
TError,
|
||||
{data: UpdateUserBody},
|
||||
TContext
|
||||
> => {
|
||||
|
||||
const mutationOptions = getUpdateUserMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions, queryClient);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get import directory list
|
||||
*/
|
||||
export const getImportDirectory = (
|
||||
params?: GetImportDirectoryParams, options?: AxiosRequestConfig
|
||||
): Promise<AxiosResponse<DirectoryListResponse>> => {
|
||||
|
||||
|
||||
return axios.default.get(
|
||||
`/api/v1/admin/import`,{
|
||||
...options,
|
||||
params: {...params, ...options?.params},}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export const getGetImportDirectoryQueryKey = (params?: GetImportDirectoryParams,) => {
|
||||
return [
|
||||
`/api/v1/admin/import`, ...(params ? [params]: [])
|
||||
] as const;
|
||||
}
|
||||
|
||||
|
||||
export const getGetImportDirectoryQueryOptions = <TData = Awaited<ReturnType<typeof getImportDirectory>>, TError = AxiosError<ErrorResponse>>(params?: GetImportDirectoryParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getImportDirectory>>, TError, TData>>, axios?: AxiosRequestConfig}
|
||||
) => {
|
||||
|
||||
const {query: queryOptions, axios: axiosOptions} = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getGetImportDirectoryQueryKey(params);
|
||||
|
||||
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof getImportDirectory>>> = ({ signal }) => getImportDirectory(params, { signal, ...axiosOptions });
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getImportDirectory>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
}
|
||||
|
||||
export type GetImportDirectoryQueryResult = NonNullable<Awaited<ReturnType<typeof getImportDirectory>>>
|
||||
export type GetImportDirectoryQueryError = AxiosError<ErrorResponse>
|
||||
|
||||
|
||||
export function useGetImportDirectory<TData = Awaited<ReturnType<typeof getImportDirectory>>, TError = AxiosError<ErrorResponse>>(
|
||||
params: undefined | GetImportDirectoryParams, options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof getImportDirectory>>, TError, TData>> & Pick<
|
||||
DefinedInitialDataOptions<
|
||||
Awaited<ReturnType<typeof getImportDirectory>>,
|
||||
TError,
|
||||
Awaited<ReturnType<typeof getImportDirectory>>
|
||||
> , 'initialData'
|
||||
>, axios?: AxiosRequestConfig}
|
||||
, queryClient?: QueryClient
|
||||
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
export function useGetImportDirectory<TData = Awaited<ReturnType<typeof getImportDirectory>>, TError = AxiosError<ErrorResponse>>(
|
||||
params?: GetImportDirectoryParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getImportDirectory>>, TError, TData>> & Pick<
|
||||
UndefinedInitialDataOptions<
|
||||
Awaited<ReturnType<typeof getImportDirectory>>,
|
||||
TError,
|
||||
Awaited<ReturnType<typeof getImportDirectory>>
|
||||
> , 'initialData'
|
||||
>, axios?: AxiosRequestConfig}
|
||||
, queryClient?: QueryClient
|
||||
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
export function useGetImportDirectory<TData = Awaited<ReturnType<typeof getImportDirectory>>, TError = AxiosError<ErrorResponse>>(
|
||||
params?: GetImportDirectoryParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getImportDirectory>>, TError, TData>>, axios?: AxiosRequestConfig}
|
||||
, queryClient?: QueryClient
|
||||
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
/**
|
||||
* @summary Get import directory list
|
||||
*/
|
||||
|
||||
export function useGetImportDirectory<TData = Awaited<ReturnType<typeof getImportDirectory>>, TError = AxiosError<ErrorResponse>>(
|
||||
params?: GetImportDirectoryParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getImportDirectory>>, TError, TData>>, axios?: AxiosRequestConfig}
|
||||
, queryClient?: QueryClient
|
||||
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
|
||||
|
||||
const queryOptions = getGetImportDirectoryQueryOptions(params,options)
|
||||
|
||||
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
|
||||
|
||||
query.queryKey = queryOptions.queryKey ;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @summary Perform import
|
||||
*/
|
||||
export const postImport = (
|
||||
postImportBody: PostImportBody, options?: AxiosRequestConfig
|
||||
): Promise<AxiosResponse<ImportResultsResponse>> => {
|
||||
|
||||
const formUrlEncoded = new URLSearchParams();
|
||||
formUrlEncoded.append(`directory`, postImportBody.directory)
|
||||
formUrlEncoded.append(`type`, postImportBody.type)
|
||||
|
||||
return axios.default.post(
|
||||
`/api/v1/admin/import`,
|
||||
formUrlEncoded,options
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const getPostImportMutationOptions = <TError = AxiosError<ErrorResponse>,
|
||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof postImport>>, TError,{data: PostImportBody}, TContext>, axios?: AxiosRequestConfig}
|
||||
): UseMutationOptions<Awaited<ReturnType<typeof postImport>>, TError,{data: PostImportBody}, TContext> => {
|
||||
|
||||
const mutationKey = ['postImport'];
|
||||
const {mutation: mutationOptions, axios: axiosOptions} = options ?
|
||||
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
||||
options
|
||||
: {...options, mutation: {...options.mutation, mutationKey}}
|
||||
: {mutation: { mutationKey, }, axios: undefined};
|
||||
|
||||
|
||||
|
||||
|
||||
const mutationFn: MutationFunction<Awaited<ReturnType<typeof postImport>>, {data: PostImportBody}> = (props) => {
|
||||
const {data} = props ?? {};
|
||||
|
||||
return postImport(data,axiosOptions)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
return { mutationFn, ...mutationOptions }}
|
||||
|
||||
export type PostImportMutationResult = NonNullable<Awaited<ReturnType<typeof postImport>>>
|
||||
export type PostImportMutationBody = PostImportBody
|
||||
export type PostImportMutationError = AxiosError<ErrorResponse>
|
||||
|
||||
/**
|
||||
* @summary Perform import
|
||||
*/
|
||||
export const usePostImport = <TError = AxiosError<ErrorResponse>,
|
||||
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof postImport>>, TError,{data: PostImportBody}, TContext>, axios?: AxiosRequestConfig}
|
||||
, queryClient?: QueryClient): UseMutationResult<
|
||||
Awaited<ReturnType<typeof postImport>>,
|
||||
TError,
|
||||
{data: PostImportBody},
|
||||
TContext
|
||||
> => {
|
||||
|
||||
const mutationOptions = getPostImportMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions, queryClient);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get import results
|
||||
*/
|
||||
export const getImportResults = (
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<AxiosResponse<ImportResultsResponse>> => {
|
||||
|
||||
|
||||
return axios.default.get(
|
||||
`/api/v1/admin/import-results`,options
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export const getGetImportResultsQueryKey = () => {
|
||||
return [
|
||||
`/api/v1/admin/import-results`
|
||||
] as const;
|
||||
}
|
||||
|
||||
|
||||
export const getGetImportResultsQueryOptions = <TData = Awaited<ReturnType<typeof getImportResults>>, TError = AxiosError<ErrorResponse>>( options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getImportResults>>, TError, TData>>, axios?: AxiosRequestConfig}
|
||||
) => {
|
||||
|
||||
const {query: queryOptions, axios: axiosOptions} = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getGetImportResultsQueryKey();
|
||||
|
||||
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof getImportResults>>> = ({ signal }) => getImportResults({ signal, ...axiosOptions });
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getImportResults>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
}
|
||||
|
||||
export type GetImportResultsQueryResult = NonNullable<Awaited<ReturnType<typeof getImportResults>>>
|
||||
export type GetImportResultsQueryError = AxiosError<ErrorResponse>
|
||||
|
||||
|
||||
export function useGetImportResults<TData = Awaited<ReturnType<typeof getImportResults>>, TError = AxiosError<ErrorResponse>>(
|
||||
options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof getImportResults>>, TError, TData>> & Pick<
|
||||
DefinedInitialDataOptions<
|
||||
Awaited<ReturnType<typeof getImportResults>>,
|
||||
TError,
|
||||
Awaited<ReturnType<typeof getImportResults>>
|
||||
> , 'initialData'
|
||||
>, axios?: AxiosRequestConfig}
|
||||
, queryClient?: QueryClient
|
||||
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
export function useGetImportResults<TData = Awaited<ReturnType<typeof getImportResults>>, TError = AxiosError<ErrorResponse>>(
|
||||
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getImportResults>>, TError, TData>> & Pick<
|
||||
UndefinedInitialDataOptions<
|
||||
Awaited<ReturnType<typeof getImportResults>>,
|
||||
TError,
|
||||
Awaited<ReturnType<typeof getImportResults>>
|
||||
> , 'initialData'
|
||||
>, axios?: AxiosRequestConfig}
|
||||
, queryClient?: QueryClient
|
||||
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
export function useGetImportResults<TData = Awaited<ReturnType<typeof getImportResults>>, TError = AxiosError<ErrorResponse>>(
|
||||
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getImportResults>>, TError, TData>>, axios?: AxiosRequestConfig}
|
||||
, queryClient?: QueryClient
|
||||
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
/**
|
||||
* @summary Get import results
|
||||
*/
|
||||
|
||||
export function useGetImportResults<TData = Awaited<ReturnType<typeof getImportResults>>, TError = AxiosError<ErrorResponse>>(
|
||||
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getImportResults>>, TError, TData>>, axios?: AxiosRequestConfig}
|
||||
, queryClient?: QueryClient
|
||||
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
|
||||
|
||||
const queryOptions = getGetImportResultsQueryOptions(options)
|
||||
|
||||
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
|
||||
|
||||
query.queryKey = queryOptions.queryKey ;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @summary Get logs with optional filter
|
||||
*/
|
||||
export const getLogs = (
|
||||
params?: GetLogsParams, options?: AxiosRequestConfig
|
||||
): Promise<AxiosResponse<LogsResponse>> => {
|
||||
|
||||
|
||||
return axios.default.get(
|
||||
`/api/v1/admin/logs`,{
|
||||
...options,
|
||||
params: {...params, ...options?.params},}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export const getGetLogsQueryKey = (params?: GetLogsParams,) => {
|
||||
return [
|
||||
`/api/v1/admin/logs`, ...(params ? [params]: [])
|
||||
] as const;
|
||||
}
|
||||
|
||||
|
||||
export const getGetLogsQueryOptions = <TData = Awaited<ReturnType<typeof getLogs>>, TError = AxiosError<ErrorResponse>>(params?: GetLogsParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getLogs>>, TError, TData>>, axios?: AxiosRequestConfig}
|
||||
) => {
|
||||
|
||||
const {query: queryOptions, axios: axiosOptions} = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getGetLogsQueryKey(params);
|
||||
|
||||
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof getLogs>>> = ({ signal }) => getLogs(params, { signal, ...axiosOptions });
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getLogs>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
}
|
||||
|
||||
export type GetLogsQueryResult = NonNullable<Awaited<ReturnType<typeof getLogs>>>
|
||||
export type GetLogsQueryError = AxiosError<ErrorResponse>
|
||||
|
||||
|
||||
export function useGetLogs<TData = Awaited<ReturnType<typeof getLogs>>, TError = AxiosError<ErrorResponse>>(
|
||||
params: undefined | GetLogsParams, options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof getLogs>>, TError, TData>> & Pick<
|
||||
DefinedInitialDataOptions<
|
||||
Awaited<ReturnType<typeof getLogs>>,
|
||||
TError,
|
||||
Awaited<ReturnType<typeof getLogs>>
|
||||
> , 'initialData'
|
||||
>, axios?: AxiosRequestConfig}
|
||||
, queryClient?: QueryClient
|
||||
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
export function useGetLogs<TData = Awaited<ReturnType<typeof getLogs>>, TError = AxiosError<ErrorResponse>>(
|
||||
params?: GetLogsParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getLogs>>, TError, TData>> & Pick<
|
||||
UndefinedInitialDataOptions<
|
||||
Awaited<ReturnType<typeof getLogs>>,
|
||||
TError,
|
||||
Awaited<ReturnType<typeof getLogs>>
|
||||
> , 'initialData'
|
||||
>, axios?: AxiosRequestConfig}
|
||||
, queryClient?: QueryClient
|
||||
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
export function useGetLogs<TData = Awaited<ReturnType<typeof getLogs>>, TError = AxiosError<ErrorResponse>>(
|
||||
params?: GetLogsParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getLogs>>, TError, TData>>, axios?: AxiosRequestConfig}
|
||||
, queryClient?: QueryClient
|
||||
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
||||
/**
|
||||
* @summary Get logs with optional filter
|
||||
*/
|
||||
|
||||
export function useGetLogs<TData = Awaited<ReturnType<typeof getLogs>>, TError = AxiosError<ErrorResponse>>(
|
||||
params?: GetLogsParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getLogs>>, TError, TData>>, axios?: AxiosRequestConfig}
|
||||
, queryClient?: QueryClient
|
||||
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
|
||||
|
||||
const queryOptions = getGetLogsQueryOptions(params,options)
|
||||
|
||||
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
|
||||
|
||||
query.queryKey = queryOptions.queryKey ;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -7,9 +7,13 @@
|
||||
*/
|
||||
|
||||
export interface Activity {
|
||||
id: string;
|
||||
user_id: string;
|
||||
document_id: string;
|
||||
activity_type: string;
|
||||
timestamp: string;
|
||||
device_id: string;
|
||||
start_time: string;
|
||||
title?: string;
|
||||
author?: string;
|
||||
duration: number;
|
||||
start_percentage: number;
|
||||
end_percentage: number;
|
||||
read_percentage: number;
|
||||
}
|
||||
|
||||
16
frontend/src/generated/model/backupType.ts
Normal file
16
frontend/src/generated/model/backupType.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Generated by orval v7.21.0 🍺
|
||||
* Do not edit manually.
|
||||
* AnthoLume API v1
|
||||
* REST API for AnthoLume document management system
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
|
||||
export type BackupType = typeof BackupType[keyof typeof BackupType];
|
||||
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const BackupType = {
|
||||
COVERS: 'COVERS',
|
||||
DOCUMENTS: 'DOCUMENTS',
|
||||
} as const;
|
||||
12
frontend/src/generated/model/directoryItem.ts
Normal file
12
frontend/src/generated/model/directoryItem.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Generated by orval v7.21.0 🍺
|
||||
* Do not edit manually.
|
||||
* AnthoLume API v1
|
||||
* REST API for AnthoLume document management system
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
|
||||
export interface DirectoryItem {
|
||||
name?: string;
|
||||
path?: string;
|
||||
}
|
||||
13
frontend/src/generated/model/directoryListResponse.ts
Normal file
13
frontend/src/generated/model/directoryListResponse.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Generated by orval v7.21.0 🍺
|
||||
* Do not edit manually.
|
||||
* AnthoLume API v1
|
||||
* REST API for AnthoLume document management system
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
import type { DirectoryItem } from './directoryItem';
|
||||
|
||||
export interface DirectoryListResponse {
|
||||
current_path?: string;
|
||||
items?: DirectoryItem[];
|
||||
}
|
||||
12
frontend/src/generated/model/getAdmin200.ts
Normal file
12
frontend/src/generated/model/getAdmin200.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Generated by orval v7.21.0 🍺
|
||||
* Do not edit manually.
|
||||
* AnthoLume API v1
|
||||
* REST API for AnthoLume document management system
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
import type { DatabaseInfo } from './databaseInfo';
|
||||
|
||||
export type GetAdmin200 = {
|
||||
database_info?: DatabaseInfo;
|
||||
};
|
||||
12
frontend/src/generated/model/getImportDirectoryParams.ts
Normal file
12
frontend/src/generated/model/getImportDirectoryParams.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Generated by orval v7.21.0 🍺
|
||||
* Do not edit manually.
|
||||
* AnthoLume API v1
|
||||
* REST API for AnthoLume document management system
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
|
||||
export type GetImportDirectoryParams = {
|
||||
directory?: string;
|
||||
select?: string;
|
||||
};
|
||||
11
frontend/src/generated/model/getLogsParams.ts
Normal file
11
frontend/src/generated/model/getLogsParams.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Generated by orval v7.21.0 🍺
|
||||
* Do not edit manually.
|
||||
* AnthoLume API v1
|
||||
* REST API for AnthoLume document management system
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
|
||||
export type GetLogsParams = {
|
||||
filter?: string;
|
||||
};
|
||||
16
frontend/src/generated/model/importResult.ts
Normal file
16
frontend/src/generated/model/importResult.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Generated by orval v7.21.0 🍺
|
||||
* Do not edit manually.
|
||||
* AnthoLume API v1
|
||||
* REST API for AnthoLume document management system
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
import type { ImportResultStatus } from './importResultStatus';
|
||||
|
||||
export interface ImportResult {
|
||||
id?: string;
|
||||
name?: string;
|
||||
path?: string;
|
||||
status?: ImportResultStatus;
|
||||
error?: string;
|
||||
}
|
||||
17
frontend/src/generated/model/importResultStatus.ts
Normal file
17
frontend/src/generated/model/importResultStatus.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Generated by orval v7.21.0 🍺
|
||||
* Do not edit manually.
|
||||
* AnthoLume API v1
|
||||
* REST API for AnthoLume document management system
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
|
||||
export type ImportResultStatus = typeof ImportResultStatus[keyof typeof ImportResultStatus];
|
||||
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const ImportResultStatus = {
|
||||
FAILED: 'FAILED',
|
||||
SUCCESS: 'SUCCESS',
|
||||
EXISTS: 'EXISTS',
|
||||
} as const;
|
||||
12
frontend/src/generated/model/importResultsResponse.ts
Normal file
12
frontend/src/generated/model/importResultsResponse.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Generated by orval v7.21.0 🍺
|
||||
* Do not edit manually.
|
||||
* AnthoLume API v1
|
||||
* REST API for AnthoLume document management system
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
import type { ImportResult } from './importResult';
|
||||
|
||||
export interface ImportResultsResponse {
|
||||
results?: ImportResult[];
|
||||
}
|
||||
16
frontend/src/generated/model/importType.ts
Normal file
16
frontend/src/generated/model/importType.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Generated by orval v7.21.0 🍺
|
||||
* Do not edit manually.
|
||||
* AnthoLume API v1
|
||||
* REST API for AnthoLume document management system
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
|
||||
export type ImportType = typeof ImportType[keyof typeof ImportType];
|
||||
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const ImportType = {
|
||||
DIRECT: 'DIRECT',
|
||||
COPY: 'COPY',
|
||||
} as const;
|
||||
@@ -8,25 +8,41 @@
|
||||
|
||||
export * from './activity';
|
||||
export * from './activityResponse';
|
||||
export * from './backupType';
|
||||
export * from './createDocumentBody';
|
||||
export * from './databaseInfo';
|
||||
export * from './device';
|
||||
export * from './directoryItem';
|
||||
export * from './directoryListResponse';
|
||||
export * from './document';
|
||||
export * from './documentResponse';
|
||||
export * from './documentsResponse';
|
||||
export * from './errorResponse';
|
||||
export * from './getActivityParams';
|
||||
export * from './getAdmin200';
|
||||
export * from './getDocumentsParams';
|
||||
export * from './getImportDirectoryParams';
|
||||
export * from './getLogsParams';
|
||||
export * from './getProgressListParams';
|
||||
export * from './getSearchParams';
|
||||
export * from './getSearchSource';
|
||||
export * from './graphDataPoint';
|
||||
export * from './graphDataResponse';
|
||||
export * from './homeResponse';
|
||||
export * from './importResult';
|
||||
export * from './importResultStatus';
|
||||
export * from './importResultsResponse';
|
||||
export * from './importType';
|
||||
export * from './leaderboardData';
|
||||
export * from './leaderboardEntry';
|
||||
export * from './logEntry';
|
||||
export * from './loginRequest';
|
||||
export * from './loginResponse';
|
||||
export * from './logsResponse';
|
||||
export * from './operationType';
|
||||
export * from './postAdminActionBody';
|
||||
export * from './postAdminActionBodyAction';
|
||||
export * from './postImportBody';
|
||||
export * from './postSearchBody';
|
||||
export * from './progress';
|
||||
export * from './progressListResponse';
|
||||
@@ -36,7 +52,10 @@ export * from './searchResponse';
|
||||
export * from './setting';
|
||||
export * from './settingsResponse';
|
||||
export * from './streaksResponse';
|
||||
export * from './updateUserBody';
|
||||
export * from './user';
|
||||
export * from './userData';
|
||||
export * from './userStatisticsResponse';
|
||||
export * from './userStreak';
|
||||
export * from './usersResponse';
|
||||
export * from './wordCount';
|
||||
9
frontend/src/generated/model/logEntry.ts
Normal file
9
frontend/src/generated/model/logEntry.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Generated by orval v7.21.0 🍺
|
||||
* Do not edit manually.
|
||||
* AnthoLume API v1
|
||||
* REST API for AnthoLume document management system
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
|
||||
export type LogEntry = string;
|
||||
13
frontend/src/generated/model/logsResponse.ts
Normal file
13
frontend/src/generated/model/logsResponse.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Generated by orval v7.21.0 🍺
|
||||
* Do not edit manually.
|
||||
* AnthoLume API v1
|
||||
* REST API for AnthoLume document management system
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
import type { LogEntry } from './logEntry';
|
||||
|
||||
export interface LogsResponse {
|
||||
logs?: LogEntry[];
|
||||
filter?: string;
|
||||
}
|
||||
17
frontend/src/generated/model/operationType.ts
Normal file
17
frontend/src/generated/model/operationType.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Generated by orval v7.21.0 🍺
|
||||
* Do not edit manually.
|
||||
* AnthoLume API v1
|
||||
* REST API for AnthoLume document management system
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
|
||||
export type OperationType = typeof OperationType[keyof typeof OperationType];
|
||||
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const OperationType = {
|
||||
CREATE: 'CREATE',
|
||||
UPDATE: 'UPDATE',
|
||||
DELETE: 'DELETE',
|
||||
} as const;
|
||||
15
frontend/src/generated/model/postAdminActionBody.ts
Normal file
15
frontend/src/generated/model/postAdminActionBody.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Generated by orval v7.21.0 🍺
|
||||
* Do not edit manually.
|
||||
* AnthoLume API v1
|
||||
* REST API for AnthoLume document management system
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
import type { PostAdminActionBodyAction } from './postAdminActionBodyAction';
|
||||
import type { BackupType } from './backupType';
|
||||
|
||||
export type PostAdminActionBody = {
|
||||
action: PostAdminActionBodyAction;
|
||||
backup_types?: BackupType[];
|
||||
restore_file?: Blob;
|
||||
};
|
||||
18
frontend/src/generated/model/postAdminActionBodyAction.ts
Normal file
18
frontend/src/generated/model/postAdminActionBodyAction.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Generated by orval v7.21.0 🍺
|
||||
* Do not edit manually.
|
||||
* AnthoLume API v1
|
||||
* REST API for AnthoLume document management system
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
|
||||
export type PostAdminActionBodyAction = typeof PostAdminActionBodyAction[keyof typeof PostAdminActionBodyAction];
|
||||
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
||||
export const PostAdminActionBodyAction = {
|
||||
BACKUP: 'BACKUP',
|
||||
RESTORE: 'RESTORE',
|
||||
METADATA_MATCH: 'METADATA_MATCH',
|
||||
CACHE_TABLES: 'CACHE_TABLES',
|
||||
} as const;
|
||||
13
frontend/src/generated/model/postImportBody.ts
Normal file
13
frontend/src/generated/model/postImportBody.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Generated by orval v7.21.0 🍺
|
||||
* Do not edit manually.
|
||||
* AnthoLume API v1
|
||||
* REST API for AnthoLume document management system
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
import type { ImportType } from './importType';
|
||||
|
||||
export type PostImportBody = {
|
||||
directory: string;
|
||||
type: ImportType;
|
||||
};
|
||||
15
frontend/src/generated/model/updateUserBody.ts
Normal file
15
frontend/src/generated/model/updateUserBody.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Generated by orval v7.21.0 🍺
|
||||
* Do not edit manually.
|
||||
* AnthoLume API v1
|
||||
* REST API for AnthoLume document management system
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
import type { OperationType } from './operationType';
|
||||
|
||||
export type UpdateUserBody = {
|
||||
operation: OperationType;
|
||||
user: string;
|
||||
password?: string;
|
||||
is_admin?: boolean;
|
||||
};
|
||||
13
frontend/src/generated/model/user.ts
Normal file
13
frontend/src/generated/model/user.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Generated by orval v7.21.0 🍺
|
||||
* Do not edit manually.
|
||||
* AnthoLume API v1
|
||||
* REST API for AnthoLume document management system
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
admin: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
12
frontend/src/generated/model/usersResponse.ts
Normal file
12
frontend/src/generated/model/usersResponse.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Generated by orval v7.21.0 🍺
|
||||
* Do not edit manually.
|
||||
* AnthoLume API v1
|
||||
* REST API for AnthoLume document management system
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
import type { User } from './user';
|
||||
|
||||
export interface UsersResponse {
|
||||
users?: User[];
|
||||
}
|
||||
@@ -1,43 +1,53 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useGetActivity } from '../generated/anthoLumeAPIV1';
|
||||
import { Table } from '../components/Table';
|
||||
|
||||
export default function ActivityPage() {
|
||||
const { data, isLoading } = useGetActivity({ offset: 0, limit: 100 });
|
||||
const activities = data?.data?.activities;
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="text-gray-500 dark:text-white">Loading...</div>;
|
||||
}
|
||||
const columns = [
|
||||
{
|
||||
key: 'document_id' as const,
|
||||
header: 'Document',
|
||||
render: (_: any, row: any) => (
|
||||
<Link
|
||||
to={`/documents/${row.document_id}`}
|
||||
className="text-blue-600 dark:text-blue-400 hover:underline"
|
||||
>
|
||||
{row.author || 'Unknown'} - {row.title || 'Unknown'}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'start_time' as const,
|
||||
header: 'Time',
|
||||
render: (value: any) => value || 'N/A',
|
||||
},
|
||||
{
|
||||
key: 'duration' as const,
|
||||
header: 'Duration',
|
||||
render: (value: any) => {
|
||||
if (!value) return 'N/A';
|
||||
// Format duration (in seconds) to readable format
|
||||
const hours = Math.floor(value / 3600);
|
||||
const minutes = Math.floor((value % 3600) / 60);
|
||||
const seconds = value % 60;
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m ${seconds}s`;
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}m ${seconds}s`;
|
||||
} else {
|
||||
return `${seconds}s`;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'end_percentage' as const,
|
||||
header: 'Percent',
|
||||
render: (value: any) => (value != null ? `${value}%` : '0%'),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<div className="inline-block min-w-full overflow-hidden rounded shadow">
|
||||
<table className="min-w-full bg-white dark:bg-gray-700">
|
||||
<thead>
|
||||
<tr className="border-b dark:border-gray-600">
|
||||
<th className="text-left p-3 text-gray-500 dark:text-white">Activity Type</th>
|
||||
<th className="text-left p-3 text-gray-500 dark:text-white">Document</th>
|
||||
<th className="text-left p-3 text-gray-500 dark:text-white">Timestamp</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{activities?.map((activity: any) => (
|
||||
<tr key={activity.id} className="border-b dark:border-gray-600">
|
||||
<td className="p-3 text-gray-700 dark:text-gray-300">
|
||||
{activity.activity_type}
|
||||
</td>
|
||||
<td className="p-3">
|
||||
<a href={`/documents/${activity.document_id}`} className="text-blue-600 dark:text-blue-400">
|
||||
{activity.document_id}
|
||||
</a>
|
||||
</td>
|
||||
<td className="p-3 text-gray-700 dark:text-gray-300">
|
||||
{new Date(activity.timestamp).toLocaleString()}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <Table columns={columns} data={activities || []} loading={isLoading} />;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,181 @@
|
||||
import { useState } from 'react';
|
||||
import { useGetImportDirectory, usePostImport } from '../generated/anthoLumeAPIV1';
|
||||
import { Button } from '../components/Button';
|
||||
import { FolderOpen } from 'lucide-react';
|
||||
|
||||
export default function AdminImportPage() {
|
||||
const [currentPath, setCurrentPath] = useState<string>('');
|
||||
const [selectedDirectory, setSelectedDirectory] = useState<string>('');
|
||||
const [importType, setImportType] = useState<'DIRECT' | 'COPY'>('DIRECT');
|
||||
|
||||
const { data: directoryData, isLoading } = useGetImportDirectory(
|
||||
currentPath ? { directory: currentPath } : {}
|
||||
);
|
||||
|
||||
const postImport = usePostImport();
|
||||
|
||||
const directories = directoryData?.data?.items || [];
|
||||
const currentPathDisplay = directoryData?.data?.current_path ?? currentPath ?? '/data';
|
||||
|
||||
const handleSelectDirectory = (directory: string) => {
|
||||
setSelectedDirectory(`${currentPath}/${directory}`);
|
||||
};
|
||||
|
||||
const handleNavigateUp = () => {
|
||||
if (currentPathDisplay !== '/') {
|
||||
const parts = currentPathDisplay.split('/');
|
||||
parts.pop();
|
||||
setCurrentPath(parts.join('/') || '');
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = () => {
|
||||
if (!selectedDirectory) return;
|
||||
|
||||
postImport.mutate(
|
||||
{
|
||||
data: {
|
||||
directory: selectedDirectory,
|
||||
type: importType,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: (response) => {
|
||||
console.log('Import completed:', response.data);
|
||||
// Redirect to import results page
|
||||
window.location.href = '/admin/import-results';
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Import failed:', error);
|
||||
alert('Import failed: ' + (error as any).message);
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setSelectedDirectory('');
|
||||
};
|
||||
|
||||
if (isLoading && !currentPath) {
|
||||
return <div className="text-gray-500 dark:text-white">Loading...</div>;
|
||||
}
|
||||
|
||||
if (selectedDirectory) {
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<div className="inline-block min-w-full overflow-hidden rounded shadow">
|
||||
<div
|
||||
className="flex flex-col grow gap-2 p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
|
||||
>
|
||||
<p className="text-lg font-semibold text-gray-500">
|
||||
Selected Import Directory
|
||||
</p>
|
||||
<form className="flex gap-4 flex-col" onSubmit={handleImport}>
|
||||
<div className="flex justify-between gap-4 w-full">
|
||||
<div className="flex gap-4 items-center">
|
||||
<FolderOpen size={20} />
|
||||
<p className="font-medium text-lg break-all">
|
||||
{selectedDirectory}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col justify-around gap-2 mr-4">
|
||||
<div className="inline-flex gap-2 items-center">
|
||||
<input
|
||||
type="radio"
|
||||
id="direct"
|
||||
checked={importType === 'DIRECT'}
|
||||
onChange={() => setImportType('DIRECT')}
|
||||
/>
|
||||
<label htmlFor="direct">Direct</label>
|
||||
</div>
|
||||
<div className="inline-flex gap-2 items-center">
|
||||
<input
|
||||
type="radio"
|
||||
id="copy"
|
||||
checked={importType === 'COPY'}
|
||||
onChange={() => setImportType('COPY')}
|
||||
/>
|
||||
<label htmlFor="copy">Copy</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<Button type="submit" className="px-10 py-2 text-base">
|
||||
Import Directory
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={handleCancel}
|
||||
className="px-10 py-2 text-base"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-xl font-bold dark:text-white">Admin - Import</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">Document import page</p>
|
||||
<div className="overflow-x-auto">
|
||||
<div className="inline-block min-w-full overflow-hidden rounded shadow">
|
||||
<table
|
||||
className="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm"
|
||||
>
|
||||
<thead className="text-gray-800 dark:text-gray-400">
|
||||
<tr>
|
||||
<th
|
||||
className="p-3 font-normal text-left border-b border-gray-200 dark:border-gray-800 w-12"
|
||||
></th>
|
||||
<th
|
||||
className="p-3 font-normal text-left border-b border-gray-200 dark:border-gray-800 break-all"
|
||||
>
|
||||
{currentPath}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-black dark:text-white">
|
||||
{currentPath !== '/' && (
|
||||
<tr>
|
||||
<td
|
||||
className="p-3 border-b border-gray-200 text-gray-800 dark:text-gray-400"
|
||||
></td>
|
||||
<td className="p-3 border-b border-gray-200">
|
||||
<button onClick={handleNavigateUp}>
|
||||
<p>../</p>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{directories.length === 0 ? (
|
||||
<tr>
|
||||
<td className="text-center p-3" colSpan={2}>No Folders</td>
|
||||
</tr>
|
||||
) : (
|
||||
directories.map((item) => (
|
||||
<tr key={item.name}>
|
||||
<td
|
||||
className="p-3 border-b border-gray-200 text-gray-800 dark:text-gray-400"
|
||||
>
|
||||
<button onClick={() => item.name && handleSelectDirectory(item.name)}>
|
||||
<FolderOpen size={20} />
|
||||
</button>
|
||||
</td>
|
||||
<td className="p-3 border-b border-gray-200">
|
||||
<button onClick={() => item.name && handleSelectDirectory(item.name)}>
|
||||
<p>{item.name ?? ''}</p>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
73
frontend/src/pages/AdminImportResultsPage.tsx
Normal file
73
frontend/src/pages/AdminImportResultsPage.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { useGetImportResults } from '../generated/anthoLumeAPIV1';
|
||||
import type { ImportResult } from '../generated/model/importResult';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export default function AdminImportResultsPage() {
|
||||
const { data: resultsData, isLoading } = useGetImportResults();
|
||||
const results = resultsData?.data?.results || [];
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="text-gray-500 dark:text-white">Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<div className="inline-block min-w-full overflow-hidden rounded shadow">
|
||||
<table
|
||||
className="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm"
|
||||
>
|
||||
<thead className="text-gray-800 dark:text-gray-400">
|
||||
<tr>
|
||||
<th
|
||||
className="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
||||
>
|
||||
Document
|
||||
</th>
|
||||
<th
|
||||
className="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
<th
|
||||
className="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
|
||||
>
|
||||
Error
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-black dark:text-white">
|
||||
{results.length === 0 ? (
|
||||
<tr>
|
||||
<td className="text-center p-3" colSpan={3}>No Results</td>
|
||||
</tr>
|
||||
) : (
|
||||
results.map((result: ImportResult, index: number) => (
|
||||
<tr key={index}>
|
||||
<td
|
||||
className="p-3 border-b border-gray-200 grid"
|
||||
style={{ gridTemplateColumns: '4rem auto' }}
|
||||
>
|
||||
<span className="text-gray-800 dark:text-gray-400">Name:</span>
|
||||
{result.id ? (
|
||||
<Link to={`/documents/${result.id}`}>{result.name}</Link>
|
||||
) : (
|
||||
<span>N/A</span>
|
||||
)}
|
||||
<span className="text-gray-800 dark:text-gray-400">File:</span>
|
||||
<span>{result.path}</span>
|
||||
</td>
|
||||
<td className="p-3 border-b border-gray-200">
|
||||
<p>{result.status}</p>
|
||||
</td>
|
||||
<td className="p-3 border-b border-gray-200">
|
||||
<p>{result.error || ''}</p>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,66 @@
|
||||
import { useState, FormEvent } from 'react';
|
||||
import { useGetLogs } from '../generated/anthoLumeAPIV1';
|
||||
import { Button } from '../components/Button';
|
||||
import { Search } from 'lucide-react';
|
||||
|
||||
export default function AdminLogsPage() {
|
||||
const [filter, setFilter] = useState('');
|
||||
|
||||
const { data: logsData, isLoading, refetch } = useGetLogs(
|
||||
filter ? { filter } : {}
|
||||
);
|
||||
|
||||
const logs = logsData?.data?.logs || [];
|
||||
|
||||
const handleFilterSubmit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
refetch();
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="text-gray-500 dark:text-white">Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-xl font-bold dark:text-white">Admin - Logs</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">System logs page</p>
|
||||
{/* Filter Form */}
|
||||
<div
|
||||
className="flex flex-col gap-2 grow p-4 mb-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
|
||||
>
|
||||
<form className="flex gap-4 flex-col lg:flex-row" onSubmit={handleFilterSubmit}>
|
||||
<div className="flex flex-col w-full grow">
|
||||
<div className="flex relative">
|
||||
<span
|
||||
className="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
|
||||
>
|
||||
<Search size={15} />
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
className="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-2 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
||||
placeholder="JQ Filter"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="lg:w-60">
|
||||
<Button variant="secondary" type="submit">Filter</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Log Display */}
|
||||
<div
|
||||
className="flex flex-col-reverse text-black dark:text-white w-full overflow-scroll"
|
||||
style={{ fontFamily: 'monospace' }}
|
||||
>
|
||||
{logs.map((log: string, index: number) => (
|
||||
<span key={index} className="whitespace-nowrap hover:whitespace-pre">
|
||||
{log}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,218 @@
|
||||
import { useState, FormEvent } from 'react';
|
||||
import { useGetAdmin, usePostAdminAction } from '../generated/anthoLumeAPIV1';
|
||||
import { Button } from '../components/Button';
|
||||
|
||||
interface BackupTypes {
|
||||
covers: boolean;
|
||||
documents: boolean;
|
||||
}
|
||||
|
||||
export default function AdminPage() {
|
||||
const { isLoading } = useGetAdmin();
|
||||
const postAdminAction = usePostAdminAction();
|
||||
|
||||
const [backupTypes, setBackupTypes] = useState<BackupTypes>({
|
||||
covers: false,
|
||||
documents: false,
|
||||
});
|
||||
const [restoreFile, setRestoreFile] = useState<File | null>(null);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const handleBackupSubmit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
const backupTypesList: string[] = [];
|
||||
if (backupTypes.covers) backupTypesList.push('COVERS');
|
||||
if (backupTypes.documents) backupTypesList.push('DOCUMENTS');
|
||||
|
||||
postAdminAction.mutate(
|
||||
{
|
||||
data: {
|
||||
action: 'BACKUP',
|
||||
backup_types: backupTypesList as any,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: (response) => {
|
||||
// Handle file download
|
||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', `AnthoLumeBackup_${new Date().toISOString().replace(/[:.]/g, '')}.zip`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
setMessage('Backup completed successfully');
|
||||
setErrorMessage(null);
|
||||
},
|
||||
onError: (error) => {
|
||||
setErrorMessage('Backup failed: ' + (error as any).message);
|
||||
setMessage(null);
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleRestoreSubmit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!restoreFile) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('restore_file', restoreFile);
|
||||
formData.append('action', 'RESTORE');
|
||||
|
||||
postAdminAction.mutate(
|
||||
{
|
||||
data: formData as any,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setMessage('Restore completed successfully');
|
||||
setErrorMessage(null);
|
||||
},
|
||||
onError: (error) => {
|
||||
setErrorMessage('Restore failed: ' + (error as any).message);
|
||||
setMessage(null);
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleMetadataMatch = () => {
|
||||
postAdminAction.mutate(
|
||||
{
|
||||
data: {
|
||||
action: 'METADATA_MATCH',
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setMessage('Metadata matching started');
|
||||
setErrorMessage(null);
|
||||
},
|
||||
onError: (error) => {
|
||||
setErrorMessage('Metadata matching failed: ' + (error as any).message);
|
||||
setMessage(null);
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleCacheTables = () => {
|
||||
postAdminAction.mutate(
|
||||
{
|
||||
data: {
|
||||
action: 'CACHE_TABLES',
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setMessage('Cache tables started');
|
||||
setErrorMessage(null);
|
||||
},
|
||||
onError: (error) => {
|
||||
setErrorMessage('Cache tables failed: ' + (error as any).message);
|
||||
setMessage(null);
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="text-gray-500 dark:text-white">Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-xl font-bold dark:text-white">Admin - General</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">Admin general settings page</p>
|
||||
<div className="w-full flex flex-col gap-4 grow">
|
||||
{/* Backup & Restore Card */}
|
||||
<div
|
||||
className="flex flex-col gap-2 grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
|
||||
>
|
||||
<p className="text-lg font-semibold mb-2">Backup & Restore</p>
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Backup Form */}
|
||||
<form className="flex justify-between" onSubmit={handleBackupSubmit}>
|
||||
<div className="flex gap-8 items-center">
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="backup_covers"
|
||||
checked={backupTypes.covers}
|
||||
onChange={(e) => setBackupTypes({ ...backupTypes, covers: e.target.checked })}
|
||||
/>
|
||||
<label htmlFor="backup_covers">Covers</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="backup_documents"
|
||||
checked={backupTypes.documents}
|
||||
onChange={(e) => setBackupTypes({ ...backupTypes, documents: e.target.checked })}
|
||||
/>
|
||||
<label htmlFor="backup_documents">Documents</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-40 h-10">
|
||||
<Button variant="secondary" type="submit">Backup</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Restore Form */}
|
||||
<form
|
||||
onSubmit={handleRestoreSubmit}
|
||||
className="flex justify-between grow"
|
||||
>
|
||||
<div className="flex items-center w-1/2">
|
||||
<input
|
||||
type="file"
|
||||
accept=".zip"
|
||||
onChange={(e) => setRestoreFile(e.target.files?.[0] || null)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-40 h-10">
|
||||
<Button variant="secondary" type="submit">Restore</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{errorMessage && (
|
||||
<span className="text-red-400 text-xs">{errorMessage}</span>
|
||||
)}
|
||||
{message && (
|
||||
<span className="text-green-400 text-xs">{message}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tasks Card */}
|
||||
<div
|
||||
className="flex flex-col grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
|
||||
>
|
||||
<p className="text-lg font-semibold">Tasks</p>
|
||||
<table className="min-w-full bg-white dark:bg-gray-700 text-sm">
|
||||
<tbody className="text-black dark:text-white">
|
||||
<tr>
|
||||
<td className="pl-0">
|
||||
<p>Metadata Matching</p>
|
||||
</td>
|
||||
<td className="py-2 float-right">
|
||||
<div className="w-40 h-10 text-base">
|
||||
<Button variant="secondary" onClick={handleMetadataMatch}>Run</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<p>Cache Tables</p>
|
||||
</td>
|
||||
<td className="py-2 float-right">
|
||||
<div className="w-40 h-10 text-base">
|
||||
<Button variant="secondary" onClick={handleCacheTables}>Run</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,234 @@
|
||||
import { useState, FormEvent } from 'react';
|
||||
import { useGetUsers, useUpdateUser } from '../generated/anthoLumeAPIV1';
|
||||
import { Plus, Trash2 } from 'lucide-react';
|
||||
|
||||
export default function AdminUsersPage() {
|
||||
const { data: usersData, isLoading, refetch } = useGetUsers({});
|
||||
const updateUser = useUpdateUser();
|
||||
|
||||
const [showAddForm, setShowAddForm] = useState(false);
|
||||
const [newUsername, setNewUsername] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [newIsAdmin, setNewIsAdmin] = useState(false);
|
||||
|
||||
const users = usersData?.data?.users || [];
|
||||
|
||||
const handleCreateUser = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!newUsername || !newPassword) return;
|
||||
|
||||
updateUser.mutate(
|
||||
{
|
||||
data: {
|
||||
operation: 'CREATE',
|
||||
user: newUsername,
|
||||
password: newPassword,
|
||||
is_admin: newIsAdmin,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setShowAddForm(false);
|
||||
setNewUsername('');
|
||||
setNewPassword('');
|
||||
setNewIsAdmin(false);
|
||||
refetch();
|
||||
},
|
||||
onError: (error: any) => {
|
||||
alert('Failed to create user: ' + error.message);
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleDeleteUser = (userId: string) => {
|
||||
updateUser.mutate(
|
||||
{
|
||||
data: {
|
||||
operation: 'DELETE',
|
||||
user: userId,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
refetch();
|
||||
},
|
||||
onError: (error: any) => {
|
||||
alert('Failed to delete user: ' + error.message);
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleUpdatePassword = (userId: string, password: string) => {
|
||||
if (!password) return;
|
||||
|
||||
updateUser.mutate(
|
||||
{
|
||||
data: {
|
||||
operation: 'UPDATE',
|
||||
user: userId,
|
||||
password: password,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
refetch();
|
||||
},
|
||||
onError: (error: any) => {
|
||||
alert('Failed to update password: ' + error.message);
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleToggleAdmin = (userId: string, isAdmin: boolean) => {
|
||||
updateUser.mutate(
|
||||
{
|
||||
data: {
|
||||
operation: 'UPDATE',
|
||||
user: userId,
|
||||
is_admin: isAdmin,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
refetch();
|
||||
},
|
||||
onError: (error: any) => {
|
||||
alert('Failed to update admin status: ' + error.message);
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="text-gray-500 dark:text-white">Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-xl font-bold dark:text-white">Admin - Users</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">User management page</p>
|
||||
<div className="relative h-full overflow-x-auto">
|
||||
{/* Add User Form */}
|
||||
{showAddForm && (
|
||||
<div className="absolute top-10 left-10 p-3 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600">
|
||||
<form onSubmit={handleCreateUser}
|
||||
className="flex flex-col gap-2 text-black dark:text-white text-sm">
|
||||
<input
|
||||
type="text"
|
||||
value={newUsername}
|
||||
onChange={(e) => setNewUsername(e.target.value)}
|
||||
placeholder="Username"
|
||||
className="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
placeholder="Password"
|
||||
className="p-2 bg-gray-300 text-black dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="new_is_admin"
|
||||
checked={newIsAdmin}
|
||||
onChange={(e) => setNewIsAdmin(e.target.checked)}
|
||||
/>
|
||||
<label htmlFor="new_is_admin">Admin</label>
|
||||
</div>
|
||||
<button
|
||||
className="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
type="submit"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Users Table */}
|
||||
<div className="min-w-full overflow-scroll rounded shadow">
|
||||
<table className="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm">
|
||||
<thead className="text-gray-800 dark:text-gray-400">
|
||||
<tr>
|
||||
<th className="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800 w-12">
|
||||
<button onClick={() => setShowAddForm(!showAddForm)}>
|
||||
<Plus size={20} />
|
||||
</button>
|
||||
</th>
|
||||
<th className="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">User</th>
|
||||
<th className="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800">Password</th>
|
||||
<th className="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800 text-center">
|
||||
Permissions
|
||||
</th>
|
||||
<th className="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800 w-48">Created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-black dark:text-white">
|
||||
{users.length === 0 ? (
|
||||
<tr>
|
||||
<td className="text-center p-3" colSpan={5}>No Results</td>
|
||||
</tr>
|
||||
) : (
|
||||
users.map((user) => (
|
||||
<tr key={user.id}>
|
||||
{/* Delete Button */}
|
||||
<td className="p-3 border-b border-gray-200 text-gray-800 dark:text-gray-400 cursor-pointer relative">
|
||||
<button onClick={() => handleDeleteUser(user.id)}>
|
||||
<Trash2 size={20} />
|
||||
</button>
|
||||
</td>
|
||||
{/* User ID */}
|
||||
<td className="p-3 border-b border-gray-200">
|
||||
<p>{user.id}</p>
|
||||
</td>
|
||||
{/* Password Reset */}
|
||||
<td className="border-b border-gray-200 px-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
const password = prompt(`Enter new password for ${user.id}`);
|
||||
if (password) handleUpdatePassword(user.id, password);
|
||||
}}
|
||||
className="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</td>
|
||||
{/* Admin Toggle */}
|
||||
<td className="flex gap-2 justify-center p-3 border-b border-gray-200 text-center min-w-40">
|
||||
<button
|
||||
onClick={() => handleToggleAdmin(user.id, true)}
|
||||
disabled={user.admin}
|
||||
className={`px-2 py-1 rounded-md text-white dark:text-black ${
|
||||
user.admin
|
||||
? 'bg-gray-800 dark:bg-gray-100 cursor-default'
|
||||
: 'bg-gray-400 dark:bg-gray-600 cursor-pointer'
|
||||
}`}
|
||||
>
|
||||
admin
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleToggleAdmin(user.id, false)}
|
||||
disabled={!user.admin}
|
||||
className={`px-2 py-1 rounded-md text-white dark:text-black ${
|
||||
!user.admin
|
||||
? 'bg-gray-800 dark:bg-gray-100 cursor-default'
|
||||
: 'bg-gray-400 dark:bg-gray-600 cursor-pointer'
|
||||
}`}
|
||||
>
|
||||
user
|
||||
</button>
|
||||
</td>
|
||||
{/* Created Date */}
|
||||
<td className="p-3 border-b border-gray-200">
|
||||
<p>{user.created_at}</p>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,51 +1,40 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useGetProgressList } from '../generated/anthoLumeAPIV1';
|
||||
import { Table } from '../components/Table';
|
||||
|
||||
export default function ProgressPage() {
|
||||
const { data, isLoading } = useGetProgressList({ page: 1, limit: 15 });
|
||||
const progress = data?.data?.progress;
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="text-gray-500 dark:text-white">Loading...</div>;
|
||||
}
|
||||
const columns = [
|
||||
{
|
||||
key: 'document_id' as const,
|
||||
header: 'Document',
|
||||
render: (_: any, row: any) => (
|
||||
<Link
|
||||
to={`/documents/${row.document_id}`}
|
||||
className="text-blue-600 dark:text-blue-400 hover:underline"
|
||||
>
|
||||
{row.author || 'Unknown'} - {row.title || 'Unknown'}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'device_name' as const,
|
||||
header: 'Device Name',
|
||||
render: (value: any) => value || 'Unknown',
|
||||
},
|
||||
{
|
||||
key: 'percentage' as const,
|
||||
header: 'Percentage',
|
||||
render: (value: any) => (value ? `${Math.round(value)}%` : '0%'),
|
||||
},
|
||||
{
|
||||
key: 'created_at' as const,
|
||||
header: 'Created At',
|
||||
render: (value: any) => (value ? new Date(value).toLocaleDateString() : 'N/A'),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<div className="inline-block min-w-full overflow-hidden rounded shadow">
|
||||
<table className="min-w-full bg-white dark:bg-gray-700">
|
||||
<thead>
|
||||
<tr className="border-b dark:border-gray-600">
|
||||
<th className="text-left p-3 text-gray-500 dark:text-white">Document</th>
|
||||
<th className="text-left p-3 text-gray-500 dark:text-white">Device Name</th>
|
||||
<th className="text-left p-3 text-gray-500 dark:text-white">Percentage</th>
|
||||
<th className="text-left p-3 text-gray-500 dark:text-white">Created At</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{progress?.map((row: any) => (
|
||||
<tr key={row.document_id} className="border-b dark:border-gray-600">
|
||||
<td className="p-3">
|
||||
<Link
|
||||
to={`/documents/${row.document_id}`}
|
||||
className="text-blue-600 dark:text-blue-400 hover:underline"
|
||||
>
|
||||
{row.author || 'Unknown'} - {row.title || 'Unknown'}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="p-3 text-gray-700 dark:text-gray-300">
|
||||
{row.device_name || 'Unknown'}
|
||||
</td>
|
||||
<td className="p-3 text-gray-700 dark:text-gray-300">
|
||||
{row.percentage ? Math.round(row.percentage) : 0}%
|
||||
</td>
|
||||
<td className="p-3 text-gray-700 dark:text-gray-300">
|
||||
{row.created_at ? new Date(row.created_at).toLocaleDateString() : 'N/A'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <Table columns={columns} data={progress || []} loading={isLoading} />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user