// 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}
{/* Stats */}
Запросы сегодня
{(requestsToday / 1000).toFixed(1)}К
{quotaPct}% от {(D.account.quotaDaily/1000)}К
Медиана ответа
27мс
−2 мс к вчера
Ошибки 4xx/5xx
198/24ч
0.06% от запросов
Доступность · 30 дн
99.99%
все системы в норме
{/* Chart */}
Запросы по дням
за последние 14 дней · ошибки выделены красным
успешные запросы ошибки 4xx/5xx
{/* Two-column: keys + live feed */}
API ключи
4 активных · 2 в production
goto("keys")}>управление
{D.keys.slice(0, 4).map((k) => ( ))}
ИмяПрефиксИспользованЗа 7 дней
{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}₽/мес
Лимит
500 000 / сутки
Продление
{D.account.planRenews}
SLA
99.95%
Поддержка
Telegram · 4 ч
); } // ============================================================ // 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 дней рекомендуется
{revealed && (
★ Новый ключ создан · показывается один раз
{revealed}
Скопируйте сейчас. После закрытия этого окна полный ключ будет недоступен — мы храним только префикс.
)} {creating && (

Новый ключ

setNewName(e.target.value)} placeholder="например, Backend · production" autoFocus />
)}
{keys.map((k) => ( ))}
Ключ Окружение Права Создан Использован За 7 дней
{k.name}
{k.prefix}{k.tail}
{k.env} {k.scope} {k.created} {k.lastUsed} {k.requests7d}
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 доступен на тарифе Прод
Статус: Эндпоинт: {logs.length} записей
{logs.map((l, i) => ( setSelected(l)} style={{ background: selected?.req === l.req ? "var(--bg-2)" : undefined }}> ))}
ВремяМетодЭндпоинтСтатусЛатентностьКлючIP
{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} ₽/мес
Запросов в сутки
500 000
SLA
99.95% · < 50 мс
Поддержка
Telegram · ответ 4 ч
Следующее списание
{D.account.planRenews}
Расход за май
по тарифу Прод
5.8 М запросов из 15 М
Suggest
4.2 М
Geocode
1.1 М
Batch
0.5 М
Сверх лимита (0.02 ₽/запрос) 0 ₽
История счетов
PDF и акты доступны после оплаты
показать все →
{D.invoices.map((inv) => ( ))}
СчётПериодДатаСуммаСтатус
{inv.id} {inv.period} {inv.date} {inv.amount} ₽ {inv.status}
Способ оплаты
Visa •••• 4242 · действует до 11/28
); } // ============================================================ // TEAM // ============================================================ function ViewTeam() { const D = window.__CAB; return ( <>

Команда

{D.team.length} участника · приглашения отправляются по email
{D.team.map((m, i) => ( ))}
УчастникEmailРольДобавлен
{m.initials}
{m.name}
{m.email} {m.role} {m.added}
); } // ============================================================ // SETTINGS // ============================================================ function ViewSettings() { const D = window.__CAB; return ( <>

Настройки

Профиль организации, безопасность, вебхуки
Организация
отображается в счетах и API
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 });