// DataLoop Cabinet — view components
const { useState, useMemo, useEffect, useRef } = React;
// ---------- Sparkline ----------
function Sparkline({ data, color, fill = true, h = 32, w = 80 }) {
if (!data || data.length === 0) return null;
const min = Math.min(...data);
const max = Math.max(...data);
const range = max - min || 1;
const step = w / (data.length - 1);
const pts = data.map((v, i) => [i * step, h - ((v - min) / range) * (h - 4) - 2]);
const d = pts.map((p, i) => (i === 0 ? `M${p[0]},${p[1]}` : `L${p[0]},${p[1]}`)).join(" ");
const dFill = `${d} L${w},${h} L0,${h} Z`;
return (
{fill && }
);
}
// ---------- Big area chart ----------
function AreaChart({ data, errors, days = 14 }) {
const W = 1000, H = 200, padL = 38, padR = 12, padT = 14, padB = 26;
const innerW = W - padL - padR;
const innerH = H - padT - padB;
const max = Math.max(...data) * 1.05;
const xStep = innerW / (data.length - 1);
const xy = (v, i) => [padL + i * xStep, padT + innerH - (v / max) * innerH];
const pts = data.map((v, i) => xy(v, i));
const path = pts.map((p, i) => (i === 0 ? `M${p[0]},${p[1]}` : `L${p[0]},${p[1]}`)).join(" ");
const fill = `${path} L${pts[pts.length-1][0]},${padT+innerH} L${pts[0][0]},${padT+innerH} Z`;
// y-axis ticks
const ticks = 4;
const tickVals = Array.from({ length: ticks + 1 }, (_, i) => Math.round((max / ticks) * i));
const fmt = (n) => n >= 1000000 ? (n/1000000).toFixed(1)+"М" : n >= 1000 ? Math.round(n/1000)+"К" : n;
// x-axis labels (dates) — synthetic last N days
const today = new Date();
const xLabels = [];
for (let i = data.length - 1; i >= 0; i -= Math.ceil(data.length / 7)) {
const d = new Date(today);
d.setDate(d.getDate() - (data.length - 1 - i));
xLabels.push({ i, label: `${d.getDate()}.${String(d.getMonth()+1).padStart(2,"0")}` });
}
// errors mini bars at bottom
const errMax = Math.max(...errors);
const errH = 18;
const errBars = errors.map((e, i) => {
const x = padL + i * xStep - 4;
const bh = (e / errMax) * errH;
return ;
});
return (
{/* grid */}
{tickVals.map((tv, i) => {
const y = padT + innerH - (tv / max) * innerH;
return (
{fmt(tv)}
);
})}
{/* area */}
{/* dots */}
{pts.map((p, i) => (
))}
{/* x labels */}
{xLabels.map((xl, i) => (
{xl.label}
))}
);
}
// ---------- Icons ----------
function Icon({ name, size = 18 }) {
const p = { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.7, strokeLinecap: "round", strokeLinejoin: "round" };
switch (name) {
case "home": return ;
case "key": return ;
case "logs": return ;
case "billing": return ;
case "team": return ;
case "docs": return ;
case "settings": return ;
case "search": return ;
case "bell": return ;
case "plus": return ;
case "arrow-up": return ;
case "arrow-down": return ;
case "arrow-right": return ;
case "copy": return ;
case "eye": return ;
case "trash": return ;
case "logout": return ;
case "chevron": return ;
default: return null;
}
}
// ============================================================
// OVERVIEW
// ============================================================
function ViewOverview({ goto }) {
const D = window.__CAB;
const quotaPct = Math.round((D.account.quotaUsedToday / D.account.quotaDaily) * 100);
const requestsToday = D.usage14[D.usage14.length - 1];
const requestsYesterday = D.usage14[D.usage14.length - 2];
const deltaPct = Math.round(((requestsToday - requestsYesterday) / requestsYesterday) * 100);
return (
<>
Здравствуйте, {D.user.name.split(" ")[0]}
Сегодня {new Date().toLocaleDateString("ru-RU", { day: "numeric", month: "long" })} · {D.account.org}
Документация
goto("keys")}> Новый ключ
{/* Stats */}
Запросы сегодня
{(requestsToday / 1000).toFixed(1)}К
{quotaPct}% от {(D.account.quotaDaily/1000)}К
Медиана ответа
27мс
−2 мс к вчера
Ошибки 4xx/5xx
198/24ч
0.06% от запросов
Доступность · 30 дн
99.99%
все системы в норме
{/* Chart */}
Запросы по дням
за последние 14 дней · ошибки выделены красным
24ч
7 дн
14 дн
30 дн
успешные запросы
ошибки 4xx/5xx
{/* Two-column: keys + live feed */}
API ключи
4 активных · 2 в production
goto("keys")}>управление
Имя Префикс Использован За 7 дней
{D.keys.slice(0, 4).map((k) => (
{k.name}
{k.env === "live" ? "live · " : "test · "}{k.scope}
{k.prefix}{k.tail}
{k.lastUsed}
{k.requests7d}
))}
Лента запросов
в реальном времени
goto("logs")}>все логи
{D.logs.slice(0, 10).map((l, i) => (
{l.t}
{l.m}
{l.p}
{l.s}
{l.ms} мс
))}
{/* Quickstart + Plan */}
Быстрый старт
три шага до первого запроса
01
Создать ключ
для test или live окружения
02
Установить SDK
npm · pip · go get
03
Первый запрос
POST /v1/suggest/address
копировать
# Первый запрос — подсказки адресов
curl -X POST https://api.dataloop.ru/v1/suggest/address \
-H "Authorization: Token sk_live_a1b2····8c4f" \
-H "Content-Type: application/json" \
-d '{`{ "query": "тверская 12", "count": 5 }`}'
текущий план
{D.account.plan}
{D.account.planPrice}₽/мес
Продление
{D.account.planRenews}
goto("billing")}>Изменить план
goto("billing")}>Счета
>
);
}
// ============================================================
// API KEYS
// ============================================================
function ViewKeys() {
const D = window.__CAB;
const [keys, setKeys] = useState(D.keys);
const [creating, setCreating] = useState(false);
const [newName, setNewName] = useState("");
const [newScope, setNewScope] = useState("full");
const [newEnv, setNewEnv] = useState("test");
const [revealed, setRevealed] = useState(null);
function createKey() {
if (!newName.trim()) return;
const id = "k" + Math.random().toString(36).slice(2, 6);
const rand = Math.random().toString(36).slice(2, 6) + Math.random().toString(36).slice(2, 6);
const prefix = `sk_${newEnv}_${rand.slice(0, 4)}`;
const full = `${prefix}${rand.slice(4, 8)}f2c8a1b9d4e6${rand.slice(0, 8)}`;
const k = {
id, name: newName, prefix, tail: "····" + rand.slice(4, 8),
created: "только что", lastUsed: "никогда",
scope: newScope, env: newEnv, requests7d: "0",
};
setKeys([k, ...keys]);
setRevealed(full);
setCreating(false);
setNewName("");
}
function revokeKey(id) {
setKeys(keys.filter((k) => k.id !== id));
}
return (
<>
API ключи
{keys.length} активных ключей · ротация раз в 90 дней рекомендуется
Экспорт CSV
setCreating(true)}> Создать ключ
{revealed && (
★ Новый ключ создан · показывается один раз
{revealed}
Скопируйте сейчас. После закрытия этого окна полный ключ будет недоступен — мы храним только префикс.
{ navigator.clipboard?.writeText(revealed); }}> Копировать
setRevealed(null)}>Закрыть
)}
{creating && (
Новый ключ
Название
setNewName(e.target.value)} placeholder="например, Backend · production" autoFocus />
Окружение
setNewEnv("test")}>TEST
setNewEnv("live")}>LIVE
Права доступа
setNewScope("full")}>full
setNewScope("suggest")}>suggest only
setNewScope("readonly")}>read only
setCreating(false)}>Отмена
Создать ключ
)}
Ключ
Окружение
Права
Создан
Использован
За 7 дней
{keys.map((k) => (
{k.name}
{k.prefix}{k.tail}
{k.env}
{k.scope}
{k.created}
{k.lastUsed}
{k.requests7d}
Ротация
revokeKey(k.id)}>
))}
NB
Безопасность ключей. Не публикуйте ключи в публичных репозиториях. Используйте переменные окружения. При компрометации — мгновенная ротация без простоя через API POST /v1/keys/rotate .
>
);
}
// ============================================================
// LOGS
// ============================================================
function ViewLogs() {
const D = window.__CAB;
const [selected, setSelected] = useState(D.logs[0]);
const [filterStatus, setFilterStatus] = useState("all");
const [filterEp, setFilterEp] = useState("all");
const logs = D.logs.filter((l) => {
if (filterStatus === "ok" && l.s >= 400) return false;
if (filterStatus === "err" && l.s < 400) return false;
if (filterEp !== "all" && !l.p.includes(filterEp)) return false;
return true;
});
return (
<>
Логи запросов
Хранятся 30 дней · экспорт в S3 доступен на тарифе Прод
Экспорт JSONL
Webhook
Статус:
setFilterStatus("all")}>все
setFilterStatus("ok")}>2xx
setFilterStatus("err")}>4xx · 5xx
Эндпоинт:
setFilterEp("all")}>все
setFilterEp("suggest")}>/suggest
setFilterEp("geocode")}>/geocode
setFilterEp("parse")}>/parse
setFilterEp("batch")}>/batch
{logs.length} записей
Время Метод Эндпоинт Статус Латентность Ключ IP
{logs.map((l, i) => (
setSelected(l)} style={{ background: selected?.req === l.req ? "var(--bg-2)" : undefined }}>
{l.t}
{l.m}
{l.p}
{l.s}
{l.ms} мс
{l.key}
{l.ip}
))}
{selected && (
{selected.m} {selected.p}
{selected.t}
{selected.s}
Request ID {selected.req}
Латентность {selected.ms} мс
API-ключ {selected.key}
IP клиента {selected.ip}
User-Agent dataloop-sdk/node@2.4.1
Запрос · body
копировать
{selected.body}
Ответ · 200 OK
копировать
{"{"}
"suggestions" : [
{"{"} "value" : "г Москва, ул Тверская, д 12" , ... {"}"},
... 4 ещё
]
{"}"}
)}
>
);
}
// ============================================================
// BILLING
// ============================================================
function ViewBilling() {
const D = window.__CAB;
return (
<>
Биллинг
Оплата помесячно · автосписание с привязанной карты
Реквизиты юр.лица
Изменить план
текущий план
{D.account.plan} {D.account.planPrice} ₽/мес
Поддержка
Telegram · ответ 4 ч
Следующее списание
{D.account.planRenews}
Расход за май
по тарифу Прод
5.8
М запросов
из 15 М
Сверх лимита (0.02 ₽/запрос)
0 ₽
История счетов
PDF и акты доступны после оплаты
показать все →
Счёт Период Дата Сумма Статус
{D.invoices.map((inv) => (
{inv.id}
{inv.period}
{inv.date}
{inv.amount} ₽
{inv.status}
PDF
Акт
))}
Способ оплаты
Visa •••• 4242 · действует до 11/28
Изменить карту
>
);
}
// ============================================================
// TEAM
// ============================================================
function ViewTeam() {
const D = window.__CAB;
return (
<>
Команда
{D.team.length} участника · приглашения отправляются по email
Пригласить
Участник Email Роль Добавлен
{D.team.map((m, i) => (
{m.email}
{m.role}
{m.added}
Изменить
))}
>
);
}
// ============================================================
// SETTINGS
// ============================================================
function ViewSettings() {
const D = window.__CAB;
return (
<>
Настройки
Профиль организации, безопасность, вебхуки
Webhooks
уведомления о превышении лимита и ошибках
Добавить
https://hooks.npd.io/dataloop/alerts
quota.exceeded · key.compromised · error.spike
активен
Опасная зона
действия необратимы
Удалить организацию
все ключи будут отозваны, логи удалены через 30 дней
Удалить
>
);
}
Object.assign(window, { ViewOverview, ViewKeys, ViewLogs, ViewBilling, ViewTeam, ViewSettings, Icon, Sparkline });