wip 20
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getSVGGraphData } from './ReadingHistoryGraph';
|
||||
|
||||
// Test data matching Go test exactly
|
||||
|
||||
@@ -9,9 +9,6 @@ export interface SVGPoint {
|
||||
y: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates bezier control points for smooth curves
|
||||
*/
|
||||
function getSVGBezierOpposedLine(
|
||||
pointA: SVGPoint,
|
||||
pointB: SVGPoint
|
||||
@@ -19,7 +16,6 @@ function getSVGBezierOpposedLine(
|
||||
const lengthX = pointB.x - pointA.x;
|
||||
const lengthY = pointB.y - pointA.y;
|
||||
|
||||
// Go uses int() which truncates toward zero, JavaScript Math.trunc matches this
|
||||
return {
|
||||
Length: Math.floor(Math.sqrt(lengthX * lengthX + lengthY * lengthY)),
|
||||
Angle: Math.trunc(Math.atan2(lengthY, lengthX)),
|
||||
@@ -32,7 +28,6 @@ function getBezierControlPoint(
|
||||
nextPoint: SVGPoint | null,
|
||||
isReverse: boolean
|
||||
): SVGPoint {
|
||||
// First / Last Point
|
||||
let pPrev = prevPoint;
|
||||
let pNext = nextPoint;
|
||||
if (!pPrev) {
|
||||
@@ -42,57 +37,49 @@ function getBezierControlPoint(
|
||||
pNext = currentPoint;
|
||||
}
|
||||
|
||||
// Modifiers
|
||||
const smoothingRatio: number = 0.2;
|
||||
const directionModifier: number = isReverse ? Math.PI : 0;
|
||||
const smoothingRatio = 0.2;
|
||||
const directionModifier = isReverse ? Math.PI : 0;
|
||||
|
||||
const opposingLine = getSVGBezierOpposedLine(pPrev, pNext);
|
||||
const lineAngle: number = opposingLine.Angle + directionModifier;
|
||||
const lineLength: number = opposingLine.Length * smoothingRatio;
|
||||
const lineAngle = opposingLine.Angle + directionModifier;
|
||||
const lineLength = opposingLine.Length * smoothingRatio;
|
||||
|
||||
// Calculate Control Point - Go converts everything to int
|
||||
// Note: int(math.Cos(...) * lineLength) means truncate product, not truncate then multiply
|
||||
return {
|
||||
x: Math.floor(currentPoint.x + Math.trunc(Math.cos(lineAngle) * lineLength)),
|
||||
y: Math.floor(currentPoint.y + Math.trunc(Math.sin(lineAngle) * lineLength)),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the bezier path for the graph
|
||||
*/
|
||||
function getSVGBezierPath(points: SVGPoint[]): string {
|
||||
if (points.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let bezierSVGPath: string = '';
|
||||
let bezierSVGPath = '';
|
||||
|
||||
for (let index = 0; index < points.length; index++) {
|
||||
const point = points[index];
|
||||
if (!point) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (index === 0) {
|
||||
bezierSVGPath += `M ${point.x},${point.y}`;
|
||||
} else {
|
||||
const pointPlusOne = points[index + 1];
|
||||
const pointMinusOne = points[index - 1];
|
||||
const pointMinusTwo: SVGPoint | null = index - 2 >= 0 ? points[index - 2] : null;
|
||||
|
||||
const startControlPoint: SVGPoint = getBezierControlPoint(
|
||||
pointMinusOne,
|
||||
pointMinusTwo,
|
||||
point,
|
||||
false
|
||||
);
|
||||
const endControlPoint: SVGPoint = getBezierControlPoint(
|
||||
point,
|
||||
pointMinusOne,
|
||||
pointPlusOne || point,
|
||||
true
|
||||
);
|
||||
|
||||
// Go converts all coordinates to int
|
||||
bezierSVGPath += ` C${startControlPoint.x},${startControlPoint.y} ${endControlPoint.x},${endControlPoint.y} ${point.x},${point.y}`;
|
||||
continue;
|
||||
}
|
||||
|
||||
const pointMinusOne = points[index - 1];
|
||||
if (!pointMinusOne) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const pointPlusOne = points[index + 1] ?? point;
|
||||
const pointMinusTwo = index - 2 >= 0 ? (points[index - 2] ?? null) : null;
|
||||
|
||||
const startControlPoint = getBezierControlPoint(pointMinusOne, pointMinusTwo, point, false);
|
||||
const endControlPoint = getBezierControlPoint(point, pointMinusOne, pointPlusOne, true);
|
||||
|
||||
bezierSVGPath += ` C${startControlPoint.x},${startControlPoint.y} ${endControlPoint.x},${endControlPoint.y} ${point.x},${point.y}`;
|
||||
}
|
||||
|
||||
return bezierSVGPath;
|
||||
@@ -105,42 +92,35 @@ export interface SVGGraphData {
|
||||
Offset: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get SVG Graph Data
|
||||
*/
|
||||
export function getSVGGraphData(
|
||||
inputData: GraphDataPoint[],
|
||||
svgWidth: number,
|
||||
svgHeight: number
|
||||
): SVGGraphData {
|
||||
// Derive Height
|
||||
let maxHeight: number = 0;
|
||||
let maxHeight = 0;
|
||||
for (const item of inputData) {
|
||||
if (item.minutes_read > maxHeight) {
|
||||
maxHeight = item.minutes_read;
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical Graph Real Estate
|
||||
const sizePercentage: number = 0.5;
|
||||
const sizePercentage = 0.5;
|
||||
const sizeRatio = maxHeight > 0 ? (svgHeight * sizePercentage) / maxHeight : 0;
|
||||
const blockOffset = inputData.length > 0 ? Math.floor(svgWidth / inputData.length) : 0;
|
||||
|
||||
// Scale Ratio -> Desired Height
|
||||
const sizeRatio: number = (svgHeight * sizePercentage) / maxHeight;
|
||||
|
||||
// Point Block Offset
|
||||
const blockOffset: number = Math.floor(svgWidth / inputData.length);
|
||||
|
||||
// Line & Bar Points
|
||||
const linePoints: SVGPoint[] = [];
|
||||
|
||||
// Bezier Fill Coordinates (Max X, Min X, Max Y)
|
||||
let maxBX: number = 0;
|
||||
let maxBY: number = 0;
|
||||
let minBX: number = 0;
|
||||
let maxBX = 0;
|
||||
let maxBY = 0;
|
||||
let minBX = 0;
|
||||
|
||||
for (let idx = 0; idx < inputData.length; idx++) {
|
||||
// Go uses int conversion
|
||||
const itemSize = Math.floor(inputData[idx].minutes_read * sizeRatio);
|
||||
const item = inputData[idx];
|
||||
if (!item) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const itemSize = Math.floor(item.minutes_read * sizeRatio);
|
||||
const itemY = svgHeight - itemSize;
|
||||
const lineX = (idx + 1) * blockOffset;
|
||||
|
||||
@@ -162,7 +142,6 @@ export function getSVGGraphData(
|
||||
}
|
||||
}
|
||||
|
||||
// Return Data
|
||||
return {
|
||||
LinePoints: linePoints,
|
||||
BezierPath: getSVGBezierPath(linePoints),
|
||||
@@ -171,27 +150,14 @@ export function getSVGGraphData(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a date string to YYYY-MM-DD format (ISO-like)
|
||||
* Note: The date string from the API is already in YYYY-MM-DD format,
|
||||
* but since JavaScript Date parsing can add timezone offsets, we use UTC
|
||||
* methods to ensure we get the correct date.
|
||||
*/
|
||||
function formatDate(dateString: string): string {
|
||||
const date = new Date(dateString);
|
||||
// Use UTC methods to avoid timezone offset issues
|
||||
const year = date.getUTCFullYear();
|
||||
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getUTCDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* ReadingHistoryGraph component
|
||||
*
|
||||
* Displays a bezier curve graph of daily reading totals with hover tooltips.
|
||||
* Exact copy of Go template implementation.
|
||||
*/
|
||||
export default function ReadingHistoryGraph({ data }: ReadingHistoryGraphProps) {
|
||||
const svgWidth = 800;
|
||||
const svgHeight = 70;
|
||||
@@ -204,11 +170,7 @@ export default function ReadingHistoryGraph({ data }: ReadingHistoryGraphProps)
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
BezierPath,
|
||||
BezierFill,
|
||||
LinePoints: _linePoints,
|
||||
} = getSVGGraphData(data, svgWidth, svgHeight);
|
||||
const { BezierPath, BezierFill } = getSVGGraphData(data, svgWidth, svgHeight);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
@@ -227,7 +189,6 @@ export default function ReadingHistoryGraph({ data }: ReadingHistoryGraphProps)
|
||||
{data.map((point, i) => (
|
||||
<div
|
||||
key={i}
|
||||
onClick
|
||||
className="w-full opacity-0 hover:opacity-100"
|
||||
style={{
|
||||
background:
|
||||
|
||||
@@ -2,14 +2,14 @@ import React from 'react';
|
||||
import { Skeleton } from './Skeleton';
|
||||
import { cn } from '../utils/cn';
|
||||
|
||||
export interface Column<T extends Record<string, unknown>> {
|
||||
export interface Column<T extends object> {
|
||||
key: keyof T;
|
||||
header: string;
|
||||
render?: (value: T[keyof T], _row: T, _index: number) => React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface TableProps<T extends Record<string, unknown>> {
|
||||
export interface TableProps<T extends object> {
|
||||
columns: Column<T>[];
|
||||
data: T[];
|
||||
loading?: boolean;
|
||||
@@ -17,7 +17,6 @@ export interface TableProps<T extends Record<string, unknown>> {
|
||||
rowKey?: keyof T | ((row: T) => string);
|
||||
}
|
||||
|
||||
// Skeleton table component for loading state
|
||||
function SkeletonTable({
|
||||
rows = 5,
|
||||
columns = 4,
|
||||
@@ -28,7 +27,7 @@ function SkeletonTable({
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn('bg-white dark:bg-gray-700 rounded-lg overflow-hidden', className)}>
|
||||
<div className={cn('overflow-hidden rounded-lg bg-white dark:bg-gray-700', className)}>
|
||||
<table className="min-w-full">
|
||||
<thead>
|
||||
<tr className="border-b dark:border-gray-600">
|
||||
@@ -58,19 +57,19 @@ function SkeletonTable({
|
||||
);
|
||||
}
|
||||
|
||||
export function Table<T extends Record<string, unknown>>({
|
||||
export function Table<T extends object>({
|
||||
columns,
|
||||
data,
|
||||
loading = false,
|
||||
emptyMessage = 'No Results',
|
||||
rowKey,
|
||||
}: TableProps<T>) {
|
||||
const getRowKey = (_row: T, index: number): string => {
|
||||
const getRowKey = (row: T, index: number): string => {
|
||||
if (typeof rowKey === 'function') {
|
||||
return rowKey(_row);
|
||||
return rowKey(row);
|
||||
}
|
||||
if (rowKey) {
|
||||
return String(_row[rowKey] ?? index);
|
||||
return String(row[rowKey] ?? index);
|
||||
}
|
||||
return `row-${index}`;
|
||||
};
|
||||
@@ -113,7 +112,9 @@ export function Table<T extends Record<string, unknown>>({
|
||||
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]}
|
||||
{column.render
|
||||
? column.render(row[column.key], row, index)
|
||||
: (row[column.key] as React.ReactNode)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
export function GitIcon() {
|
||||
interface GitIconProps {
|
||||
size?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function GitIcon({ size = 20, className = '' }: GitIconProps) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="text-black dark:text-white"
|
||||
height="20"
|
||||
className={`${className} text-black dark:text-white`.trim()}
|
||||
height={size}
|
||||
viewBox="0 0 219 92"
|
||||
fill="currentColor"
|
||||
>
|
||||
@@ -22,19 +27,19 @@ export function GitIcon() {
|
||||
style={{ stroke: 'none', fillRule: 'nonzero', fillOpacity: 1 }}
|
||||
d="M130.871 31.836c-4.785 0-8.351 2.352-8.351 8.008 0 4.261 2.347 7.222 8.093 7.222 4.871 0 8.18-2.867 8.18-7.398 0-5.133-2.961-7.832-7.922-7.832Zm-9.57 39.95c-1.133 1.39-2.262 2.87-2.262 4.612 0 3.48 4.434 4.524 10.527 4.524 5.051 0 11.926-.352 11.926-5.043 0-2.793-3.308-2.965-7.488-3.227Zm25.761-39.688c1.563 2.004 3.22 4.789 3.22 8.793 0 9.656-7.571 15.316-18.536 15.316-2.789 0-5.312-.348-6.879-.785l-2.87 4.613 8.526.52c15.059.96 23.934 1.398 23.934 12.968 0 10.008-8.789 15.665-23.934 15.665-15.75 0-21.757-4.004-21.757-10.88 0-3.917 1.742-6 4.789-8.878-2.875-1.211-3.828-3.387-3.828-5.739 0-1.914.953-3.656 2.523-5.312 1.566-1.652 3.305-3.305 5.395-5.219-4.262-2.09-7.485-6.617-7.485-13.058 0-10.008 6.613-16.88 19.93-16.88 3.742 0 6.004.344 8.008.872h16.972v7.394l-8.007.61"
|
||||
/>
|
||||
<g clip-path="url(#a)">
|
||||
<g clipPath="url(#a)">
|
||||
<path
|
||||
style={{ stroke: 'none', fillRule: 'nonzero', fillOpacity: 1 }}
|
||||
d="M170.379 16.281c-4.961 0-7.832-2.87-7.832-7.836 0-4.957 2.871-7.656 7.832-7.656 5.05 0 7.922 2.7 7.922 7.656 0 4.965-2.871 7.836-7.922 7.836Zm-11.227 52.305V61.71l4.438-.606c1.219-.175 1.394-.437 1.394-1.746V33.773c0-.953-.261-1.566-1.132-1.824l-4.7-1.656.957-7.047h18.016V59.36c0 1.399.086 1.57 1.395 1.746l4.437.606v6.875h-24.805"
|
||||
/>
|
||||
</g>
|
||||
<g clip-path="url(#b)">
|
||||
<g clipPath="url(#b)">
|
||||
<path
|
||||
style={{ stroke: 'none', fillRule: 'nonzero', fillOpacity: 1 }}
|
||||
d="M218.371 65.21c-3.742 1.825-9.223 3.481-14.187 3.481-10.356 0-14.27-4.175-14.27-14.015V31.879c0-.524 0-.871-.7-.871h-6.093v-7.746c7.664-.871 10.707-4.703 11.664-14.188h8.27v12.36c0 .609 0 .87.695.87h12.27v8.704h-12.965v20.797c0 5.136 1.218 7.136 5.918 7.136 2.437 0 4.96-.609 7.047-1.39l2.351 7.66"
|
||||
/>
|
||||
</g>
|
||||
<g clip-path="url(#c)">
|
||||
<g clipPath="url(#c)">
|
||||
<path
|
||||
style={{ stroke: 'none', fillRule: 'nonzero', fillOpacity: 1 }}
|
||||
d="M89.422 42.371 49.629 2.582a5.868 5.868 0 0 0-8.3 0l-8.263 8.262 10.48 10.484a6.965 6.965 0 0 1 7.173 1.668 6.98 6.98 0 0 1 1.656 7.215l10.102 10.105a6.963 6.963 0 0 1 7.214 1.657 6.976 6.976 0 0 1 0 9.875 6.98 6.98 0 0 1-9.879 0 6.987 6.987 0 0 1-1.519-7.594l-9.422-9.422v24.793a6.979 6.979 0 0 1 1.848 1.32 6.988 6.988 0 0 1 0 9.88c-2.73 2.726-7.153 2.726-9.875 0a6.98 6.98 0 0 1 0-9.88 6.893 6.893 0 0 1 2.285-1.523V34.398a6.893 6.893 0 0 1-2.285-1.523 6.988 6.988 0 0 1-1.508-7.637L29.004 14.902 1.719 42.187a5.868 5.868 0 0 0 0 8.301l39.793 39.793a5.868 5.868 0 0 0 8.3 0l39.61-39.605a5.873 5.873 0 0 0 0-8.305"
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useGetActivity } from '../generated/anthoLumeAPIV1';
|
||||
import type { Activity } from '../generated/model';
|
||||
import { Table } from '../components/Table';
|
||||
import { Table, type Column } from '../components/Table';
|
||||
import { formatDuration } from '../utils/formatters';
|
||||
|
||||
export default function ActivityPage() {
|
||||
const { data, isLoading } = useGetActivity({ offset: 0, limit: 100 });
|
||||
const activities = data?.status === 200 ? data.data.activities : [];
|
||||
|
||||
const columns = [
|
||||
const columns: Column<Activity>[] = [
|
||||
{
|
||||
key: 'document_id' as const,
|
||||
header: 'Document',
|
||||
render: (_value: Activity['document_id'], row: Activity) => (
|
||||
render: (_value, row) => (
|
||||
<Link
|
||||
to={`/documents/${row.document_id}`}
|
||||
className="text-blue-600 hover:underline dark:text-blue-400"
|
||||
@@ -24,19 +24,19 @@ export default function ActivityPage() {
|
||||
{
|
||||
key: 'start_time' as const,
|
||||
header: 'Time',
|
||||
render: (value: Activity['start_time']) => value || 'N/A',
|
||||
render: value => String(value || 'N/A'),
|
||||
},
|
||||
{
|
||||
key: 'duration' as const,
|
||||
header: 'Duration',
|
||||
render: (value: Activity['duration']) => {
|
||||
return formatDuration(value || 0);
|
||||
render: value => {
|
||||
return formatDuration(typeof value === 'number' ? value : 0);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'end_percentage' as const,
|
||||
header: 'Percent',
|
||||
render: (value: Activity['end_percentage']) => (value != null ? `${value}%` : '0%'),
|
||||
render: value => (typeof value === 'number' ? `${value}%` : '0%'),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useGetImportDirectory, usePostImport } from '../generated/anthoLumeAPIV1';
|
||||
import type { DirectoryItem, DirectoryListResponse } from '../generated/model';
|
||||
import { getErrorMessage } from '../utils/errors';
|
||||
import { Button } from '../components/Button';
|
||||
import { FolderOpenIcon } from '../icons';
|
||||
@@ -17,8 +18,10 @@ export default function AdminImportPage() {
|
||||
|
||||
const postImport = usePostImport();
|
||||
|
||||
const directories = directoryData?.data?.items || [];
|
||||
const currentPathDisplay = directoryData?.data?.current_path ?? currentPath ?? '/data';
|
||||
const directoryResponse =
|
||||
directoryData?.status === 200 ? (directoryData.data as DirectoryListResponse) : null;
|
||||
const directories = directoryResponse?.items ?? [];
|
||||
const currentPathDisplay = directoryResponse?.current_path ?? currentPath ?? '/data';
|
||||
|
||||
const handleSelectDirectory = (directory: string) => {
|
||||
setSelectedDirectory(`${currentPath}/${directory}`);
|
||||
@@ -148,7 +151,7 @@ export default function AdminImportPage() {
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
directories.map(item => (
|
||||
directories.map((item: DirectoryItem) => (
|
||||
<tr key={item.name}>
|
||||
<td className="border-b border-gray-200 p-3 text-gray-800 dark:text-gray-400">
|
||||
<button onClick={() => item.name && handleSelectDirectory(item.name)}>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useGetImportResults } from '../generated/anthoLumeAPIV1';
|
||||
import type { ImportResult } from '../generated/model/importResult';
|
||||
import type { ImportResult, ImportResultsResponse } from '../generated/model';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export default function AdminImportResultsPage() {
|
||||
const { data: resultsData, isLoading } = useGetImportResults();
|
||||
const results = resultsData?.data?.results || [];
|
||||
const results =
|
||||
resultsData?.status === 200 ? (resultsData.data as ImportResultsResponse).results || [] : [];
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="text-gray-500 dark:text-white">Loading...</div>;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, FormEvent } from 'react';
|
||||
import { useGetLogs } from '../generated/anthoLumeAPIV1';
|
||||
import type { LogsResponse } from '../generated/model';
|
||||
import { Button } from '../components/Button';
|
||||
import { SearchIcon } from '../icons';
|
||||
|
||||
@@ -8,7 +9,7 @@ export default function AdminLogsPage() {
|
||||
|
||||
const { data: logsData, isLoading, refetch } = useGetLogs(filter ? { filter } : {});
|
||||
|
||||
const logs = logsData?.data?.logs || [];
|
||||
const logs = logsData?.status === 200 ? ((logsData.data as LogsResponse).logs ?? []) : [];
|
||||
|
||||
const handleFilterSubmit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -21,7 +22,6 @@ export default function AdminLogsPage() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Filter Form */}
|
||||
<div className="mb-4 flex grow flex-col gap-2 rounded bg-white p-4 text-gray-500 shadow-lg dark:bg-gray-700 dark:text-white">
|
||||
<form className="flex flex-col gap-4 lg:flex-row" onSubmit={handleFilterSubmit}>
|
||||
<div className="flex w-full grow flex-col">
|
||||
@@ -46,14 +46,13 @@ export default function AdminLogsPage() {
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Log Display */}
|
||||
<div
|
||||
className="flex w-full flex-col-reverse overflow-scroll text-black dark:text-white"
|
||||
style={{ fontFamily: 'monospace' }}
|
||||
>
|
||||
{logs.map((log: string, index: number) => (
|
||||
{logs.map((log, index) => (
|
||||
<span key={index} className="whitespace-nowrap hover:whitespace-pre">
|
||||
{log}
|
||||
{typeof log === 'string' ? log : JSON.stringify(log)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, FormEvent } from 'react';
|
||||
import { useGetUsers, useUpdateUser } from '../generated/anthoLumeAPIV1';
|
||||
import type { User, UsersResponse } from '../generated/model';
|
||||
import { AddIcon, DeleteIcon } from '../icons';
|
||||
import { useToasts } from '../components/ToastContext';
|
||||
import { getErrorMessage } from '../utils/errors';
|
||||
@@ -14,7 +15,7 @@ export default function AdminUsersPage() {
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [newIsAdmin, setNewIsAdmin] = useState(false);
|
||||
|
||||
const users = usersData?.data?.users || [];
|
||||
const users = usersData?.status === 200 ? ((usersData.data as UsersResponse).users ?? []) : [];
|
||||
|
||||
const handleCreateUser = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -73,7 +74,7 @@ export default function AdminUsersPage() {
|
||||
data: {
|
||||
operation: 'UPDATE',
|
||||
user: userId,
|
||||
password: password,
|
||||
password,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -116,7 +117,6 @@ export default function AdminUsersPage() {
|
||||
|
||||
return (
|
||||
<div className="relative h-full overflow-x-auto">
|
||||
{/* Add User Form */}
|
||||
{showAddForm && (
|
||||
<div className="absolute left-10 top-10 rounded bg-gray-200 p-3 shadow-lg shadow-gray-500 transition-all duration-200 dark:bg-gray-600 dark:shadow-gray-900">
|
||||
<form
|
||||
@@ -156,7 +156,6 @@ export default function AdminUsersPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Users Table */}
|
||||
<div className="min-w-full overflow-scroll rounded shadow">
|
||||
<table className="min-w-full bg-white text-sm leading-normal dark:bg-gray-700">
|
||||
<thead className="text-gray-800 dark:text-gray-400">
|
||||
@@ -188,19 +187,16 @@ export default function AdminUsersPage() {
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
users.map(user => (
|
||||
users.map((user: User) => (
|
||||
<tr key={user.id}>
|
||||
{/* Delete Button */}
|
||||
<td className="relative cursor-pointer border-b border-gray-200 p-3 text-gray-800 dark:text-gray-400">
|
||||
<button onClick={() => handleDeleteUser(user.id)}>
|
||||
<DeleteIcon size={20} />
|
||||
</button>
|
||||
</td>
|
||||
{/* User ID */}
|
||||
<td className="border-b border-gray-200 p-3">
|
||||
<p>{user.id}</p>
|
||||
</td>
|
||||
{/* Password Reset */}
|
||||
<td className="border-b border-gray-200 px-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
@@ -212,7 +208,6 @@ export default function AdminUsersPage() {
|
||||
Reset
|
||||
</button>
|
||||
</td>
|
||||
{/* Admin Toggle */}
|
||||
<td className="flex min-w-40 justify-center gap-2 border-b border-gray-200 p-3 text-center">
|
||||
<button
|
||||
onClick={() => handleToggleAdmin(user.id, true)}
|
||||
@@ -237,7 +232,6 @@ export default function AdminUsersPage() {
|
||||
user
|
||||
</button>
|
||||
</td>
|
||||
{/* Created Date */}
|
||||
<td className="border-b border-gray-200 p-3">
|
||||
<p>{user.created_at}</p>
|
||||
</td>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useGetProgressList } from '../generated/anthoLumeAPIV1';
|
||||
import type { Progress } from '../generated/model';
|
||||
import { Table } from '../components/Table';
|
||||
import { Table, type Column } from '../components/Table';
|
||||
|
||||
export default function ProgressPage() {
|
||||
const { data, isLoading } = useGetProgressList({ page: 1, limit: 15 });
|
||||
const progress = data?.status === 200 ? (data.data.progress ?? []) : [];
|
||||
|
||||
const columns = [
|
||||
const columns: Column<Progress>[] = [
|
||||
{
|
||||
key: 'document_id' as const,
|
||||
header: 'Document',
|
||||
render: (_value: Progress['document_id'], row: Progress) => (
|
||||
render: (_value, row) => (
|
||||
<Link
|
||||
to={`/documents/${row.document_id}`}
|
||||
className="text-blue-600 hover:underline dark:text-blue-400"
|
||||
@@ -23,18 +23,18 @@ export default function ProgressPage() {
|
||||
{
|
||||
key: 'device_name' as const,
|
||||
header: 'Device Name',
|
||||
render: (value: Progress['device_name']) => value || 'Unknown',
|
||||
render: value => String(value || 'Unknown'),
|
||||
},
|
||||
{
|
||||
key: 'percentage' as const,
|
||||
header: 'Percentage',
|
||||
render: (value: Progress['percentage']) => (value ? `${Math.round(value)}%` : '0%'),
|
||||
render: value => (typeof value === 'number' ? `${Math.round(value)}%` : '0%'),
|
||||
},
|
||||
{
|
||||
key: 'created_at' as const,
|
||||
header: 'Created At',
|
||||
render: (value: Progress['created_at']) =>
|
||||
value ? new Date(value).toLocaleDateString() : 'N/A',
|
||||
render: value =>
|
||||
typeof value === 'string' && value ? new Date(value).toLocaleDateString() : 'N/A',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
22
frontend/src/types/window.d.ts
vendored
Normal file
22
frontend/src/types/window.d.ts
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
interface FilePickerAcceptType {
|
||||
description?: string;
|
||||
accept: Record<string, string[]>;
|
||||
}
|
||||
|
||||
interface SaveFilePickerOptions {
|
||||
suggestedName?: string;
|
||||
types?: FilePickerAcceptType[];
|
||||
}
|
||||
|
||||
interface FileSystemWritableFileStream {
|
||||
write(data: BufferSource | Blob | string): Promise<void>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
interface FileSystemFileHandle {
|
||||
createWritable(): Promise<FileSystemWritableFileStream>;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
showSaveFilePicker?: (options?: SaveFilePickerOptions) => Promise<FileSystemFileHandle>;
|
||||
}
|
||||
Reference in New Issue
Block a user