Skip to content
Closed
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
14 changes: 11 additions & 3 deletions hook/bindAdminpanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ export default function bindAdminpanel() {

let ProductCatalog: any;
let ProductMediaManager: any;
let OrderLogsViewerControl: any;
let initializeWidgets: any;
try {
ProductCatalog = require("../libs/adminpanel/ProductCatalog/ProductCatalog").ProductCatalog;
ProductMediaManager = require("../libs/adminpanel/ProductMediaManager/ProductMediaManager").ProductMediaManager;
OrderLogsViewerControl = require("../libs/adminpanel/controls/OrderLogsViewerControl").OrderLogsViewerControl;
initializeWidgets = require("../lib/adminpanel/widgets").initializeWidgets;
} catch (e) {
sails.log.warn("Adminpanel bindings are skipped: failed to load adminpanel modules", e);
Expand All @@ -23,9 +25,15 @@ export default function bindAdminpanel() {
catalogHandler.add(productCatalog);

// Product media manager bind
const mediaManagerHandler = sails.hooks.adminpanel.adminizer.mediaManagerHandler
const productMediaManager = new ProductMediaManager()
mediaManagerHandler.add(productMediaManager)
const mediaManagerHandler = sails.hooks.adminpanel.adminizer.mediaManagerHandler
const productMediaManager = new ProductMediaManager()
mediaManagerHandler.add(productMediaManager)

// Order logs custom viewer control bind
const controlsHandler = sails.hooks.adminpanel.adminizer.controlsHandler;
if (!controlsHandler.get("jsonEditor", "order-logs-viewer")) {
controlsHandler.add(new OrderLogsViewerControl(sails.hooks.adminpanel.adminizer));
}

// Initialize dashboard widgets
initializeWidgets();
Expand Down
147 changes: 147 additions & 0 deletions lib/adminpanel/src/controls/order-logs-viewer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import React, { useMemo, useState } from 'react';

const LEVELS = ['debug', 'info', 'warn', 'error'];
const LEVEL_COLOR = {
debug: '#60a5fa',
info: '#34d399',
warn: '#fbbf24',
error: '#f87171',
};

function safeStringify(value) {
if (value === undefined || value === null) return '';
if (typeof value === 'string') return value;
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}

function normalizeLogs(initialValue) {
if (!Array.isArray(initialValue)) return [];

return initialValue.map((item, index) => {
if (!item || typeof item !== 'object') {
return {
index,
timestamp: 'unknown-time',
level: 'info',
module: 'unknown-module',
message: String(item),
data: undefined,
};
}

const level = LEVELS.includes(item.level) ? item.level : 'info';
return {
index,
timestamp: item.timestamp || 'unknown-time',
level,
module: item.module || 'unknown-module',
message: item.message || '',
data: item.data,
};
});
}

function OrderLogsViewer({ initialValue }) {
const logs = useMemo(() => normalizeLogs(initialValue), [initialValue]);
const [query, setQuery] = useState('');
const [activeLevels, setActiveLevels] = useState(() => new Set(LEVELS));

const counters = useMemo(() => {
return logs.reduce((acc, log) => {
acc[log.level] = (acc[log.level] || 0) + 1;
return acc;
}, { debug: 0, info: 0, warn: 0, error: 0 });
}, [logs]);

const filtered = useMemo(() => {
const normalizedQuery = query.trim().toLowerCase();

return logs.filter((log) => {
if (!activeLevels.has(log.level)) return false;
if (!normalizedQuery) return true;

const searchText = `${log.timestamp} ${log.level} ${log.module} ${log.message} ${safeStringify(log.data)}`.toLowerCase();
return searchText.includes(normalizedQuery);
});
}, [logs, activeLevels, query]);

const toggleLevel = (level) => {
setActiveLevels((prev) => {
const next = new Set(prev);
if (next.has(level)) {
next.delete(level);
} else {
next.add(level);
}
return next;
});
};

return (
<div style={{ background: '#0b1020', color: '#e2e8f0', borderRadius: 8, padding: 12, border: '1px solid #1f2937' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, marginBottom: 10 }}>
{LEVELS.map((level) => {
const active = activeLevels.has(level);
return (
<button
key={level}
type="button"
onClick={() => toggleLevel(level)}
style={{
border: `1px solid ${LEVEL_COLOR[level]}`,
color: active ? '#0b1020' : LEVEL_COLOR[level],
background: active ? LEVEL_COLOR[level] : 'transparent',
borderRadius: 6,
padding: '2px 8px',
cursor: 'pointer',
fontSize: 12,
}}
>
{level} ({counters[level] || 0})
</button>
);
})}
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Поиск по логам"
style={{
marginLeft: 'auto',
minWidth: 220,
background: '#020617',
border: '1px solid #334155',
color: '#e2e8f0',
borderRadius: 6,
padding: '4px 8px',
}}
/>
</div>

<div style={{ maxHeight: 420, overflow: 'auto', fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace', fontSize: 12, lineHeight: 1.4 }}>
{filtered.length === 0 && (
<div style={{ opacity: 0.7, padding: 8 }}>Логи не найдены</div>
)}

{filtered.map((log) => (
<div key={`${log.index}-${log.timestamp}`} style={{ borderBottom: '1px solid #1e293b', padding: '6px 4px' }}>
<div>
<span style={{ color: '#94a3b8' }}>[{log.timestamp}] </span>
<span style={{ color: LEVEL_COLOR[log.level], fontWeight: 700 }}>{log.level.toUpperCase()}</span>
<span style={{ color: '#cbd5e1' }}> {log.module}</span>
{log.message ? <span style={{ color: '#e2e8f0' }}> — {log.message}</span> : null}
</div>
{log.data !== undefined && (
<pre style={{ margin: '4px 0 0 0', color: '#cbd5e1', whiteSpace: 'pre-wrap' }}>{safeStringify(log.data)}</pre>
)}
</div>
))}
</div>
</div>
);
}

export default OrderLogsViewer;
3 changes: 2 additions & 1 deletion lib/adminpanel/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export default defineConfig({
cssCodeSplit: true,
lib: {
entry: {
StockManager: path.resolve(__dirname, 'src/stock-manager.jsx')
StockManager: path.resolve(__dirname, 'src/stock-manager.jsx'),
OrderLogsViewer: path.resolve(__dirname, 'src/controls/order-logs-viewer.jsx')
},
formats: ['es'],
},
Expand Down
37 changes: 37 additions & 0 deletions libs/adminpanel/controls/OrderLogsViewerControl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { AbstractControls, Config, ControlType, Path } from "adminizer";

export class OrderLogsViewerControl extends AbstractControls {
readonly name: string = "order-logs-viewer";
readonly type: ControlType = "jsonEditor";

readonly path: Path = {
cssPath: "",
jsPath: {
dev: `${this.routPrefix}/assets/stockmanager/OrderLogsViewer.js`,
production: `${this.routPrefix}/assets/stockmanager/OrderLogsViewer.js`,
},
};

readonly config: Config = {
theme: "dark",
readOnly: true,
};

getConfig(): Config {
return this.config;
}

getJsPath(): string {
return process.env.VITE_ENV === "dev"
? this.path.jsPath.dev
: this.path.jsPath.production;
}

getCssPath(): string | undefined {
return undefined;
}

getName(): string {
return this.name;
}
}
101 changes: 101 additions & 0 deletions libs/adminpanel/controls/orderLogsViewerHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { OrderLogEntry, OrderLogLevel } from "../../OrderLogHelper";

export type ViewerLogLevel = OrderLogLevel;

export interface ViewerLogRecord {
index: number;
timestamp: string;
level: ViewerLogLevel;
module: string;
message: string;
data?: unknown;
raw: unknown;
}

const LOG_LEVELS: ViewerLogLevel[] = ["debug", "info", "warn", "error"];

export function normalizeOrderLogs(input: unknown): ViewerLogRecord[] {
if (!Array.isArray(input)) {
return [];
}

return input.map((item: unknown, index: number): ViewerLogRecord => {
const fallback: ViewerLogRecord = {
index,
timestamp: "unknown-time",
level: "info",
module: "unknown-module",
message: String(item),
raw: item,
};

if (typeof item !== "object" || item === null) {
return fallback;
}

const candidate = item as Partial<OrderLogEntry> & { [k: string]: unknown };
const level = typeof candidate.level === "string" && LOG_LEVELS.includes(candidate.level as ViewerLogLevel)
? candidate.level as ViewerLogLevel
: "info";

return {
index,
timestamp: typeof candidate.timestamp === "string" ? candidate.timestamp : "unknown-time",
level,
module: typeof candidate.module === "string" ? candidate.module : "unknown-module",
message: typeof candidate.message === "string" ? candidate.message : "",
data: candidate.data,
raw: item,
};
});
}

export function filterOrderLogs(logs: ViewerLogRecord[], levels: Set<ViewerLogLevel>, search: string): ViewerLogRecord[] {
const query = search.trim().toLowerCase();

return logs.filter((item: ViewerLogRecord) => {
if (!levels.has(item.level)) {
return false;
}

if (!query) {
return true;
}

const serialized = `${item.timestamp} ${item.level} ${item.module} ${item.message} ${safeStringify(item.data)}`.toLowerCase();
return serialized.includes(query);
});
}

export function safeStringify(data: unknown): string {
if (data === undefined) {
return "";
}

if (typeof data === "string") {
return data;
}

try {
return JSON.stringify(data, null, 2);
} catch {
return String(data);
}
}

export function summarizeOrderLogs(logs: unknown): string {
const normalized = normalizeOrderLogs(logs);

if (normalized.length === 0) {
return "Нет логов";
}

const counts = normalized.reduce((acc: Record<string, number>, item: ViewerLogRecord) => {
acc[item.level] = (acc[item.level] || 0) + 1;
return acc;
}, {});

return ["debug", "info", "warn", "error"]
.map((level: string) => `${level}:${counts[level] || 0}`)
.join(" ");
}
17 changes: 10 additions & 7 deletions libs/adminpanel/models/bind.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { GroupConfig } from "./lib/group";
import { ProductConfig } from "./lib/product";
import { GroupConfig } from "./lib/group";
import { ProductConfig } from "./lib/product";
import { OrderConfig } from "./lib/order";

export const models = {
user: {
Expand All @@ -23,11 +24,13 @@ export const models = {
edit: GroupConfig.edit(),
add: GroupConfig.add(),
},
order: {
model: 'order',
title: 'Orders',
icon: 'shopping_cart'
},
order: {
model: 'order',
title: 'Orders',
icon: 'shopping_cart',
list: OrderConfig.list(),
edit: OrderConfig.edit(),
},
bonusprogram: {
model: 'bonusprogram',
title: 'Bonus programs',
Expand Down
37 changes: 37 additions & 0 deletions libs/adminpanel/models/lib/order.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { CreateUpdateConfig, FieldsModels } from "adminizer";
import { summarizeOrderLogs } from "../../controls/orderLogsViewerHelper";

export class OrderConfig {
static listFields: FieldsModels = {
logs: {
title: "Logs",
displayModifier(value: unknown) {
return summarizeOrderLogs(value);
},
},
};

static editFields: FieldsModels = {
logs: {
title: "Order logs",
type: "json",
disabled: true,
tooltip: "Просмотр логов заказа: чёрный консольный вьювер с фильтрами по уровням",
options: {
name: "order-logs-viewer",
},
},
};

public static list(): { fields: FieldsModels } {
return {
fields: this.listFields,
};
}

public static edit(): CreateUpdateConfig {
return {
fields: this.editFields,
};
}
}
Loading