Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ Section Order:
### Removed
-->

<!-- Your changes go here -->
### Changed

- **Administration Card Styling & Update Sections**: Modernized the character, corporation, and alliance administration cards with a responsive grid layout (`auto-fill`), circular portrait headers, dedicated error banners, detailed listing of individual update sections (e.g. Wallet Journal, Mining Ledger, Planets) with color-coded status dots (`OK`, `Failed`, `Token Error`, `Pending`), structured status badge rows with pulsing indicators for errors/issues, last synchronization timestamp, and aligned action buttons.

## [4.0.2] - 2026-10-11

Expand Down
27 changes: 27 additions & 0 deletions frontend/src/Api/OpenApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,33 @@ export interface components {
status?: string | null;
/** Last Sync */
last_sync?: string | null;
/** Error Message */
error_message?: string | null;
/** Sections */
sections?: components["schemas"]["AdminOwnerSectionSchema"][] | null;
};
/**
* AdminOwnerSectionSchema
* @description Status of an individual update section.
*/
AdminOwnerSectionSchema: {
/** Section */
section: string;
/** Section Name */
section_name: string;
/** Is Success */
is_success?: boolean | null;
/**
* Has Token Error
* @default false
*/
has_token_error: boolean;
/** Error Message */
error_message?: string | null;
/** Last Run At */
last_run_at?: string | null;
/** Last Run Finished At */
last_run_finished_at?: string | null;
};
/**
* AdministrationResponse
Expand Down
1 change: 1 addition & 0 deletions frontend/src/Api/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type AllianceOverview = components["schemas"]["AllianceOverview"];
export type DashboardSchema = components["schemas"]["DashboardSchema"];
export type AdministrationResponse = components["schemas"]["AdministrationResponse"];
export type AdminOwnerSchema = components["schemas"]["AdminOwnerSchema"];
export type AdminOwnerSectionSchema = components["schemas"]["AdminOwnerSectionSchema"];

export type BillboardData = components["schemas"]["BillboardData"];
export type BillboardSchema = components["schemas"]["BillboardSchema"];
Expand Down
186 changes: 129 additions & 57 deletions frontend/src/Components/Administration/AdministrationView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { QueryKey } from "@tanstack/react-query";
import { createColumnHelper } from "@tanstack/react-table";
import type { ColumnDef } from "@tanstack/react-table";
import { CheckCircle2, Clock, LogIn, RotateCw, Trash2, UserCheck, UserMinus, UserX, Users, XCircle } from "lucide-react";
import { AlertCircle, AlertTriangle, CheckCircle2, Clock, LogIn, RotateCw, Trash2, UserCheck, UserMinus, UserX, Users, XCircle } from "lucide-react";
import { Alert, Badge, Button, Col, Row, Spinner } from "react-bootstrap";
import { useTranslation } from "react-i18next";

Expand Down Expand Up @@ -229,7 +229,7 @@ function AdministrationView({
</Alert>
)}

<div className="d-flex flex-wrap gap-3">
<div className="lg-entries-grid">
{data.registered.map((entry) => {
const normStatus = entry.status?.toLowerCase();
const isError = normStatus === "error" || Boolean(data.dashboard.issues?.includes(entry.name));
Expand All @@ -243,27 +243,85 @@ function AdministrationView({
? "lg-entry-warning"
: "";

let badgeClass = "lg-badge-secondary";
let StatusIcon = AlertCircle;
let statusLabel = entry.status ?? t("Unknown");

if (isOk) {
badgeClass = "lg-badge-ok";
StatusIcon = CheckCircle2;
statusLabel = t("OK");
} else if (isError) {
badgeClass = "lg-badge-error";
StatusIcon = AlertCircle;
statusLabel = t("Error");
} else if (isTokenError) {
badgeClass = "lg-badge-token-error";
StatusIcon = AlertTriangle;
statusLabel = t("Token Error");
}

return (
<div
key={entry.owner_id}
className={`aa-panel lg-entry d-flex flex-column align-items-center gap-2 ${cardClass}`}
className={`aa-panel lg-entry ${cardClass}`}
>
<div className="fw-bold d-flex flex-column align-items-center gap-1">
<span>{entry.name}</span>
<div className="lg-entry-header">
{entry.icon && (
<img
src={entry.icon}
alt={entry.name}
className="lg-entry-icon"
/>
)}
<div className="lg-entry-info">
<h4 className="lg-entry-name">{entry.name}</h4>
</div>
</div>

{entry.error_message && (
<div className="lg-error-banner" role="alert">
{entry.error_message}
</div>
)}

{entry.sections && entry.sections.length > 0 && (
<div className="lg-sections-list">
{entry.sections.map((sec) => {
let dotClass = "lg-dot-muted";
if (sec.is_success === true) dotClass = "lg-dot-ok";
else if (sec.is_success === false) dotClass = "lg-dot-error";
else if (sec.has_token_error) dotClass = "lg-dot-warning";

return (
<div key={sec.section} className="lg-section-item">
<span className="d-inline-flex align-items-center gap-2">
<span
className={`lg-section-dot ${dotClass}`}
aria-hidden="true"
/>
<span>{sec.section_name}</span>
</span>
<span>
{sec.has_token_error
? t("Token Error")
: sec.is_success === true
? t("OK")
: sec.is_success === false
? t("Failed")
: t("Pending")}
</span>
</div>
);
})}
</div>
)}

<div className="lg-entry-badge-row">
{entry.status && (
<span
className={`badge d-inline-flex align-items-center gap-1 ${
isError
? "bg-danger"
: isTokenError
? "bg-warning text-dark"
: isOk
? "bg-success"
: "bg-secondary"
}`}
>
<span className={`lg-status-badge ${badgeClass}`}>
{hasProblem && (
<span className="aa-pending-dot-container me-1">
<span className="aa-pending-dot-container me-1" aria-hidden="true">
<span
className={
isError
Expand All @@ -280,57 +338,71 @@ function AdministrationView({
/>
</span>
)}
{entry.status}
<StatusIcon size={12} aria-hidden="true" />
<span>{statusLabel}</span>
</span>
)}

{entry.last_sync !== undefined && (
<span
className="lg-entry-last-sync"
title={entry.last_sync ? formatDate(entry.last_sync) : undefined}
>
<Clock size={12} aria-hidden="true" />
<span>{entry.last_sync ? formatRelativeTime(entry.last_sync) : t("Never")}</span>
</span>
)}
</div>
<img src={entry.icon ?? ""} alt="" width={96} height={96} className="rounded" />
{entry.last_sync !== undefined && (
<div
className="small text-muted d-flex align-items-center gap-1"
title={entry.last_sync ? formatDate(entry.last_sync) : undefined}
>
<Clock size={12} />
<span>{entry.last_sync ? formatRelativeTime(entry.last_sync) : t("Never")}</span>

{(showCardLedger || onDelete) && (
<div className="lg-entry-actions">
{showCardLedger &&
renderTooltip(
t("View Ledger"),
<Link
className="aa-btn aa-btn-success aa-btn-sm"
to={ledgerPath(entry.owner_id)}
aria-label={t("View Ledger")}
>
<LogIn size={14} />
</Link>,
)}
{onDelete &&
renderTooltip(
t("Delete"),
<button
type="button"
className="aa-btn aa-btn-danger aa-btn-sm"
aria-label={t("Delete")}
onClick={() => setPending(entry)}
>
<Trash2 size={14} />
</button>,
)}
</div>
)}
<div className="d-flex justify-content-center gap-2">
{showCardLedger &&
renderTooltip(
t("View Ledger"),
<Link
className="aa-btn aa-btn-success aa-btn-sm"
to={ledgerPath(entry.owner_id)}
aria-label={t("View Ledger")}
>
<LogIn size={14} />
</Link>,
)}
{onDelete &&
renderTooltip(
t("Delete"),
<button
type="button"
className="aa-btn aa-btn-danger aa-btn-sm"
aria-label={t("Delete")}
onClick={() => setPending(entry)}
>
<Trash2 size={14} />
</button>,
)}
</div>
</div>
);
})}
);
})}

{data.missing.map((entry) => (
<div
key={entry.owner_id}
className="aa-panel lg-entry lg-entry-missing d-flex flex-column align-items-center gap-2"
className="aa-panel lg-entry lg-entry-missing"
>
<div className="fw-bold">{entry.name}</div>
<img src={entry.icon ?? ""} alt="" width={96} height={96} className="rounded" />
<div className="small">
<div className="lg-entry-header">
{entry.icon && (
<img
src={entry.icon}
alt={entry.name}
className="lg-entry-icon"
/>
)}
<div className="lg-entry-info">
<h4 className="lg-entry-name">{entry.name}</h4>
</div>
</div>
<div className="small text-muted">
{t("{{entry}} is not registered in Ledger.", { entry: missingLabel ?? entryLabel })}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import AdministrationView from "@/Components/Administration/AdministrationView";
const data = {
owner: { character_id: 1, character_name: "Alice", icon: "a.png" },
dashboard: { auth_count: 3, active_count: 1, inactive_count: 0, missing_count: 2, issues: ["Alice"] },
registered: [{ owner_id: 1, name: "Alice", icon: "a.png", status: "ok", last_sync: "2026-10-10T10:00:00Z" }],
registered: [{ owner_id: 1, name: "Alice", icon: "a.png", status: "ok", last_sync: "2026-10-10T10:00:00Z", sections: [] }],
missing: [{ owner_id: 2, name: "Alice Alt", icon: "b.png" }],
members: [],
last_sync: "2026-10-10T10:00:00Z",
Expand Down Expand Up @@ -138,9 +138,9 @@ describe("AdministrationView", () => {
...data,
dashboard: { ...data.dashboard, issues: ["Bob"] },
registered: [
{ owner_id: 1, name: "Alice", icon: "a.png", status: "ok", last_sync: "2026-10-10T10:00:00Z" },
{ owner_id: 2, name: "Bob", icon: "b.png", status: "error", last_sync: "2026-10-10T10:00:00Z" },
{ owner_id: 3, name: "Charlie", icon: "c.png", status: "token_error", last_sync: "2026-10-10T10:00:00Z" },
{ owner_id: 1, name: "Alice", icon: "a.png", status: "ok", last_sync: "2026-10-10T10:00:00Z", sections: [] },
{ owner_id: 2, name: "Bob", icon: "b.png", status: "error", last_sync: "2026-10-10T10:00:00Z", sections: [] },
{ owner_id: 3, name: "Charlie", icon: "c.png", status: "token_error", last_sync: "2026-10-10T10:00:00Z", sections: [] },
],
},
});
Expand All @@ -152,5 +152,54 @@ describe("AdministrationView", () => {
const charlieCard = screen.getByText("Charlie").closest(".lg-entry");
expect(charlieCard).toHaveClass("lg-entry-warning");
});

it("renders individual update sections and error banner when present", () => {
// Test Data
const customData = {
...data,
registered: [
{
owner_id: 1,
name: "Alice",
icon: "a.png",
status: "error",
last_sync: "2026-10-10T10:00:00Z",
error_message: "ESI connection timed out",
sections: [
{
section: "wallet_journal",
section_name: "Wallet Journal",
is_success: true,
has_token_error: false,
},
{
section: "mining_ledger",
section_name: "Mining Ledger",
is_success: false,
has_token_error: false,
error_message: "ESI connection timed out",
},
{
section: "planets",
section_name: "Planets",
is_success: null,
has_token_error: false,
},
],
},
],
};

// Test Action
renderView({ customData });

// Expected Result
expect(screen.getByText("ESI connection timed out")).toBeInTheDocument();
expect(screen.getByText("Wallet Journal")).toBeInTheDocument();
expect(screen.getByText("Mining Ledger")).toBeInTheDocument();
expect(screen.getByText("Planets")).toBeInTheDocument();
expect(screen.getByText("Failed")).toBeInTheDocument();
expect(screen.getByText("Pending")).toBeInTheDocument();
});
});

Loading
Loading